|
1
|
|
|
# -*- coding: utf-8 -*- |
|
2
|
|
|
|
|
3
|
|
|
import json |
|
4
|
|
|
import socket |
|
5
|
|
|
import ssl |
|
6
|
|
|
import time |
|
7
|
|
|
import typing |
|
8
|
|
|
from email import message_from_bytes |
|
9
|
|
|
from email.header import decode_header |
|
10
|
|
|
from email.header import make_header |
|
11
|
|
|
from email.message import Message |
|
12
|
|
|
from email.utils import parseaddr |
|
13
|
|
|
|
|
14
|
|
|
import filelock |
|
15
|
|
|
import imapclient |
|
16
|
|
|
import markdown |
|
17
|
|
|
import requests |
|
18
|
|
|
from email_reply_parser import EmailReplyParser |
|
19
|
|
|
|
|
20
|
|
|
from tracim_backend.exceptions import BadStatusCode |
|
21
|
|
|
from tracim_backend.exceptions import EmptyEmailBody |
|
22
|
|
|
from tracim_backend.exceptions import NoSpecialKeyFound |
|
23
|
|
|
from tracim_backend.exceptions import UnsupportedRequestMethod |
|
24
|
|
|
from tracim_backend.lib.mail_fetcher.email_processing.parser import ParsedHTMLMail # nopep8 |
|
25
|
|
|
from tracim_backend.lib.mail_fetcher.email_processing.sanitizer import HtmlSanitizer # nopep8 |
|
26
|
|
|
from tracim_backend.lib.utils.authentification import TRACIM_API_KEY_HEADER |
|
27
|
|
|
from tracim_backend.lib.utils.authentification import TRACIM_API_USER_EMAIL_LOGIN_HEADER # nopep8 |
|
28
|
|
|
from tracim_backend.lib.utils.logger import logger |
|
29
|
|
|
|
|
30
|
|
|
TRACIM_SPECIAL_KEY_HEADER = 'X-Tracim-Key' |
|
31
|
|
|
CONTENT_TYPE_TEXT_PLAIN = 'text/plain' |
|
32
|
|
|
CONTENT_TYPE_TEXT_HTML = 'text/html' |
|
33
|
|
|
|
|
34
|
|
|
IMAP_CHECKED_FLAG = imapclient.FLAGGED |
|
35
|
|
|
IMAP_SEEN_FLAG = imapclient.SEEN |
|
36
|
|
|
|
|
37
|
|
|
MAIL_FETCHER_FILELOCK_TIMEOUT = 10 |
|
38
|
|
|
MAIL_FETCHER_CONNECTION_TIMEOUT = 60*3 |
|
39
|
|
|
MAIL_FETCHER_IDLE_RESPONSE_TIMEOUT = 60*9 # this should be not more |
|
40
|
|
|
# that 29 minutes according to rfc2177.(server wait 30min by default) |
|
41
|
|
|
|
|
42
|
|
|
|
|
43
|
|
|
class MessageContainer(object): |
|
44
|
|
|
def __init__(self, message: Message, uid: int) -> None: |
|
45
|
|
|
self.message = message |
|
46
|
|
|
self.uid = uid |
|
47
|
|
|
|
|
48
|
|
|
|
|
49
|
|
|
class DecodedMail(object): |
|
50
|
|
|
def __init__(self, message: Message, uid: int=None) -> None: |
|
51
|
|
|
self._message = message |
|
52
|
|
|
self.uid = uid |
|
53
|
|
|
|
|
54
|
|
|
def _decode_header(self, header_title: str) -> typing.Optional[str]: |
|
55
|
|
|
# FIXME : Handle exception |
|
56
|
|
|
if header_title in self._message: |
|
57
|
|
|
return str(make_header(decode_header(self._message[header_title]))) |
|
58
|
|
|
else: |
|
59
|
|
|
return None |
|
60
|
|
|
|
|
61
|
|
|
def get_subject(self) -> typing.Optional[str]: |
|
62
|
|
|
return self._decode_header('subject') |
|
63
|
|
|
|
|
64
|
|
|
def get_from_address(self) -> str: |
|
65
|
|
|
return parseaddr(self._message['From'])[1] |
|
66
|
|
|
|
|
67
|
|
|
def get_to_address(self) -> str: |
|
68
|
|
|
return parseaddr(self._message['To'])[1] |
|
69
|
|
|
|
|
70
|
|
|
def get_first_ref(self) -> str: |
|
71
|
|
|
return parseaddr(self._message['References'])[1] |
|
72
|
|
|
|
|
73
|
|
|
def get_special_key(self) -> typing.Optional[str]: |
|
74
|
|
|
return self._decode_header(TRACIM_SPECIAL_KEY_HEADER) |
|
75
|
|
|
|
|
76
|
|
|
def get_body( |
|
77
|
|
|
self, |
|
78
|
|
|
use_html_parsing=True, |
|
79
|
|
|
use_txt_parsing=True, |
|
80
|
|
|
) -> typing.Optional[str]: |
|
81
|
|
|
body_part = self._get_mime_body_message() |
|
82
|
|
|
body = None |
|
83
|
|
|
if body_part: |
|
84
|
|
|
charset = body_part.get_content_charset('iso-8859-1') |
|
85
|
|
|
content_type = body_part.get_content_type() |
|
86
|
|
|
if content_type == CONTENT_TYPE_TEXT_PLAIN: |
|
87
|
|
|
txt_body = body_part.get_payload(decode=True).decode( |
|
88
|
|
|
charset) |
|
89
|
|
|
if use_txt_parsing: |
|
90
|
|
|
txt_body = EmailReplyParser.parse_reply(txt_body) |
|
91
|
|
|
html_body = markdown.markdown(txt_body) |
|
92
|
|
|
body = HtmlSanitizer.sanitize(html_body) |
|
93
|
|
|
|
|
94
|
|
|
elif content_type == CONTENT_TYPE_TEXT_HTML: |
|
95
|
|
|
html_body = body_part.get_payload(decode=True).decode( |
|
96
|
|
|
charset) |
|
97
|
|
|
if use_html_parsing: |
|
98
|
|
|
html_body = str(ParsedHTMLMail(html_body)) |
|
99
|
|
|
body = HtmlSanitizer.sanitize(html_body) |
|
100
|
|
|
if not body: |
|
101
|
|
|
raise EmptyEmailBody() |
|
102
|
|
|
return body |
|
103
|
|
|
|
|
104
|
|
|
def _get_mime_body_message(self) -> typing.Optional[Message]: |
|
105
|
|
|
# TODO - G.M - 2017-11-16 - Use stdlib msg.get_body feature for py3.6+ |
|
106
|
|
|
part = None |
|
107
|
|
|
# Check for html |
|
108
|
|
|
for part in self._message.walk(): |
|
109
|
|
|
content_type = part.get_content_type() |
|
110
|
|
|
content_dispo = str(part.get('Content-Disposition')) |
|
111
|
|
|
if content_type == CONTENT_TYPE_TEXT_HTML \ |
|
112
|
|
|
and 'attachment' not in content_dispo: |
|
113
|
|
|
return part |
|
114
|
|
|
# check for plain text |
|
115
|
|
|
for part in self._message.walk(): |
|
116
|
|
|
content_type = part.get_content_type() |
|
117
|
|
|
content_dispo = str(part.get('Content-Disposition')) |
|
118
|
|
|
if content_type == CONTENT_TYPE_TEXT_PLAIN \ |
|
119
|
|
|
and 'attachment' not in content_dispo: |
|
120
|
|
|
return part |
|
121
|
|
|
return part |
|
122
|
|
|
|
|
123
|
|
|
def get_key(self) -> typing.Optional[str]: |
|
124
|
|
|
|
|
125
|
|
|
""" |
|
126
|
|
|
key is the string contain in some mail header we need to retrieve. |
|
127
|
|
|
First try checking special header, them check 'to' header |
|
128
|
|
|
and finally check first(oldest) mail-id of 'references' header |
|
129
|
|
|
""" |
|
130
|
|
|
first_ref = self.get_first_ref() |
|
131
|
|
|
to_address = self.get_to_address() |
|
132
|
|
|
special_key = self.get_special_key() |
|
133
|
|
|
|
|
134
|
|
|
if special_key: |
|
135
|
|
|
return special_key |
|
136
|
|
|
if to_address: |
|
137
|
|
|
return DecodedMail.find_key_from_mail_address(to_address) |
|
138
|
|
|
if first_ref: |
|
139
|
|
|
return DecodedMail.find_key_from_mail_address(first_ref) |
|
140
|
|
|
|
|
141
|
|
|
raise NoSpecialKeyFound() |
|
142
|
|
|
|
|
143
|
|
|
@classmethod |
|
144
|
|
|
def find_key_from_mail_address( |
|
145
|
|
|
cls, |
|
146
|
|
|
mail_address: str, |
|
147
|
|
|
) -> typing.Optional[str]: |
|
148
|
|
|
""" Parse mail_adress-like string |
|
149
|
|
|
to retrieve key. |
|
150
|
|
|
|
|
151
|
|
|
:param mail_address: user+key@something like string |
|
152
|
|
|
:return: key |
|
153
|
|
|
""" |
|
154
|
|
|
username = mail_address.split('@')[0] |
|
155
|
|
|
username_data = username.split('+') |
|
156
|
|
|
if len(username_data) == 2: |
|
157
|
|
|
return username_data[1] |
|
158
|
|
|
return None |
|
159
|
|
|
|
|
160
|
|
|
|
|
161
|
|
|
class BadIMAPFetchResponse(Exception): |
|
162
|
|
|
pass |
|
163
|
|
|
|
|
164
|
|
|
|
|
165
|
|
|
class MailFetcher(object): |
|
166
|
|
|
def __init__( |
|
167
|
|
|
self, |
|
168
|
|
|
host: str, |
|
169
|
|
|
port: str, |
|
170
|
|
|
user: str, |
|
171
|
|
|
password: str, |
|
172
|
|
|
use_ssl: bool, |
|
173
|
|
|
folder: str, |
|
174
|
|
|
use_idle: bool, |
|
175
|
|
|
connection_max_lifetime: int, |
|
176
|
|
|
heartbeat: int, |
|
177
|
|
|
api_base_url: str, |
|
178
|
|
|
api_key: str, |
|
179
|
|
|
use_html_parsing: bool, |
|
180
|
|
|
use_txt_parsing: bool, |
|
181
|
|
|
lockfile_path: str, |
|
182
|
|
|
burst: bool, |
|
183
|
|
|
) -> None: |
|
184
|
|
|
""" |
|
185
|
|
|
Fetch mail from a mailbox folder through IMAP and add their content to |
|
186
|
|
|
Tracim through http according to mail Headers. |
|
187
|
|
|
Fetch is regular. |
|
188
|
|
|
:param host: imap server hostname |
|
189
|
|
|
:param port: imap connection port |
|
190
|
|
|
:param user: user login of mailbox |
|
191
|
|
|
:param password: user password of mailbox |
|
192
|
|
|
:param use_ssl: use imap over ssl connection |
|
193
|
|
|
:param folder: mail folder where new mail are fetched |
|
194
|
|
|
:param use_idle: use IMAP IDLE(server notification) when available |
|
195
|
|
|
:param heartbeat: seconds to wait before fetching new mail again |
|
196
|
|
|
:param connection_max_lifetime: maximum duration allowed for a |
|
197
|
|
|
connection . connection are automatically renew when their |
|
198
|
|
|
lifetime excess this duration. |
|
199
|
|
|
:param api_base_url: url to get access to tracim api |
|
200
|
|
|
:param api_key: tracim api key |
|
201
|
|
|
:param use_html_parsing: parse html mail |
|
202
|
|
|
:param use_txt_parsing: parse txt mail |
|
203
|
|
|
:param burst: if true, run only one time, |
|
204
|
|
|
if false run as continous daemon. |
|
205
|
|
|
""" |
|
206
|
|
|
self.host = host |
|
207
|
|
|
self.port = port |
|
208
|
|
|
self.user = user |
|
209
|
|
|
self.password = password |
|
210
|
|
|
self.use_ssl = use_ssl |
|
211
|
|
|
self.folder = folder |
|
212
|
|
|
self.heartbeat = heartbeat |
|
213
|
|
|
self.use_idle = use_idle |
|
214
|
|
|
self.connection_max_lifetime = connection_max_lifetime |
|
215
|
|
|
self.api_base_url = api_base_url |
|
216
|
|
|
self.api_key = api_key |
|
217
|
|
|
self.use_html_parsing = use_html_parsing |
|
218
|
|
|
self.use_txt_parsing = use_txt_parsing |
|
219
|
|
|
self.lock = filelock.FileLock(lockfile_path) |
|
220
|
|
|
self._is_active = True |
|
221
|
|
|
self.burst = burst |
|
222
|
|
|
|
|
223
|
|
|
def run(self) -> None: |
|
224
|
|
|
logger.info(self, 'Starting MailFetcher') |
|
225
|
|
|
while self._is_active: |
|
226
|
|
|
imapc = None |
|
227
|
|
|
sleep_after_connection = True |
|
228
|
|
|
try: |
|
229
|
|
|
imapc = imapclient.IMAPClient( |
|
230
|
|
|
self.host, |
|
231
|
|
|
self.port, |
|
232
|
|
|
ssl=self.use_ssl, |
|
233
|
|
|
timeout=MAIL_FETCHER_CONNECTION_TIMEOUT |
|
234
|
|
|
) |
|
235
|
|
|
imapc.login(self.user, self.password) |
|
236
|
|
|
|
|
237
|
|
|
logger.debug(self, 'Select folder {}'.format( |
|
238
|
|
|
self.folder, |
|
239
|
|
|
)) |
|
240
|
|
|
imapc.select_folder(self.folder) |
|
241
|
|
|
|
|
242
|
|
|
# force renew connection when deadline is reached |
|
243
|
|
|
deadline = time.time() + self.connection_max_lifetime |
|
244
|
|
|
while True: |
|
245
|
|
|
if not self._is_active: |
|
246
|
|
|
logger.warning(self, 'Mail Fetcher process aborted') |
|
247
|
|
|
sleep_after_connection = False |
|
248
|
|
|
break |
|
249
|
|
|
|
|
250
|
|
|
if time.time() > deadline: |
|
251
|
|
|
logger.debug( |
|
252
|
|
|
self, |
|
253
|
|
|
"MailFetcher Connection Lifetime limit excess" |
|
254
|
|
|
", Try Re-new connection") |
|
255
|
|
|
sleep_after_connection = False |
|
256
|
|
|
break |
|
257
|
|
|
|
|
258
|
|
|
# check for new mails |
|
259
|
|
|
self._check_mail(imapc) |
|
260
|
|
|
|
|
261
|
|
|
if self.use_idle and imapc.has_capability('IDLE'): |
|
262
|
|
|
# IDLE_mode wait until event from server |
|
263
|
|
|
logger.debug(self, 'wail for event(IDLE)') |
|
264
|
|
|
imapc.idle() |
|
265
|
|
|
imapc.idle_check( |
|
266
|
|
|
timeout=MAIL_FETCHER_IDLE_RESPONSE_TIMEOUT |
|
267
|
|
|
) |
|
268
|
|
|
imapc.idle_done() |
|
269
|
|
|
else: |
|
270
|
|
|
if self.use_idle and not imapc.has_capability('IDLE'): |
|
271
|
|
|
log = 'IDLE mode activated but server do not' \ |
|
272
|
|
|
'support it, use polling instead.' |
|
273
|
|
|
logger.warning(self, log) |
|
274
|
|
|
|
|
275
|
|
|
if self.burst: |
|
276
|
|
|
self.stop() |
|
277
|
|
|
break |
|
278
|
|
|
# normal polling mode : sleep a define duration |
|
279
|
|
|
logger.debug(self, |
|
280
|
|
|
'sleep for {}'.format(self.heartbeat)) |
|
281
|
|
|
time.sleep(self.heartbeat) |
|
282
|
|
|
|
|
283
|
|
|
if self.burst: |
|
284
|
|
|
self.stop() |
|
285
|
|
|
break |
|
286
|
|
|
# Socket |
|
287
|
|
|
except (socket.error, |
|
288
|
|
|
socket.gaierror, |
|
289
|
|
|
socket.herror) as e: |
|
290
|
|
|
log = 'Socket fail with IMAP connection {}' |
|
291
|
|
|
logger.error(self, log.format(e.__str__())) |
|
292
|
|
|
|
|
293
|
|
|
except socket.timeout as e: |
|
294
|
|
|
log = 'Socket timeout on IMAP connection {}' |
|
295
|
|
|
logger.error(self, log.format(e.__str__())) |
|
296
|
|
|
|
|
297
|
|
|
# SSL |
|
298
|
|
|
except ssl.SSLError as e: |
|
299
|
|
|
log = 'SSL error on IMAP connection' |
|
300
|
|
|
logger.error(self, log.format(e.__str__())) |
|
301
|
|
|
|
|
302
|
|
|
except ssl.CertificateError as e: |
|
303
|
|
|
log = 'SSL Certificate verification failed on IMAP connection' |
|
304
|
|
|
logger.error(self, log.format(e.__str__())) |
|
305
|
|
|
|
|
306
|
|
|
# Filelock |
|
307
|
|
|
except filelock.Timeout as e: |
|
308
|
|
|
log = 'Mail Fetcher Lock Timeout {}' |
|
309
|
|
|
logger.warning(self, log.format(e.__str__())) |
|
310
|
|
|
|
|
311
|
|
|
# IMAP |
|
312
|
|
|
# TODO - G.M - 10-01-2017 - Support imapclient exceptions |
|
313
|
|
|
# when Imapclient stable will be 2.0+ |
|
314
|
|
|
|
|
315
|
|
|
except BadIMAPFetchResponse as e: |
|
316
|
|
|
log = 'Imap Fetch command return bad response.' \ |
|
317
|
|
|
'Is someone else connected to the mailbox ?: ' \ |
|
318
|
|
|
'{}' |
|
319
|
|
|
logger.error(self, log.format(e.__str__())) |
|
320
|
|
|
# Others |
|
321
|
|
|
except Exception as e: |
|
322
|
|
|
log = 'Mail Fetcher error {}' |
|
323
|
|
|
logger.error(self, log.format(e.__str__())) |
|
324
|
|
|
|
|
325
|
|
|
finally: |
|
326
|
|
|
# INFO - G.M - 2018-01-09 - Connection closing |
|
327
|
|
|
# Properly close connection according to |
|
328
|
|
|
# https://github.com/mjs/imapclient/pull/279/commits/043e4bd0c5c775c5a08cb5f1baa93876a46732ee |
|
329
|
|
|
# TODO : Use __exit__ method instead when imapclient stable will |
|
330
|
|
|
# be 2.0+ . |
|
331
|
|
|
if imapc: |
|
332
|
|
|
logger.debug(self, 'Try logout') |
|
333
|
|
|
try: |
|
334
|
|
|
imapc.logout() |
|
335
|
|
|
except Exception: |
|
336
|
|
|
try: |
|
337
|
|
|
imapc.shutdown() |
|
338
|
|
|
except Exception as e: |
|
339
|
|
|
log = "Can't logout, connection broken ? {}" |
|
340
|
|
|
logger.error(self, log.format(e.__str__())) |
|
341
|
|
|
|
|
342
|
|
|
if self.burst: |
|
343
|
|
|
self.stop() |
|
344
|
|
|
break |
|
345
|
|
|
|
|
346
|
|
|
if sleep_after_connection: |
|
347
|
|
|
logger.debug(self, 'sleep for {}'.format(self.heartbeat)) |
|
348
|
|
|
time.sleep(self.heartbeat) |
|
349
|
|
|
|
|
350
|
|
|
log = 'Mail Fetcher stopped' |
|
351
|
|
|
logger.debug(self, log) |
|
352
|
|
|
|
|
353
|
|
|
def _check_mail(self, imapc: imapclient.IMAPClient) -> None: |
|
354
|
|
|
with self.lock.acquire( |
|
355
|
|
|
timeout=MAIL_FETCHER_FILELOCK_TIMEOUT |
|
356
|
|
|
): |
|
357
|
|
|
messages = self._fetch(imapc) |
|
358
|
|
|
cleaned_mails = [DecodedMail(m.message, m.uid) |
|
359
|
|
|
for m in messages] |
|
360
|
|
|
self._notify_tracim(cleaned_mails, imapc) |
|
361
|
|
|
|
|
362
|
|
|
def stop(self) -> None: |
|
363
|
|
|
self._is_active = False |
|
364
|
|
|
|
|
365
|
|
|
def _fetch( |
|
366
|
|
|
self, |
|
367
|
|
|
imapc: imapclient.IMAPClient, |
|
368
|
|
|
) -> typing.List[MessageContainer]: |
|
369
|
|
|
""" |
|
370
|
|
|
Get news message from mailbox |
|
371
|
|
|
:return: list of new mails |
|
372
|
|
|
""" |
|
373
|
|
|
messages = [] |
|
374
|
|
|
|
|
375
|
|
|
logger.debug(self, 'Fetch unflagged messages') |
|
376
|
|
|
uids = imapc.search(['UNFLAGGED']) |
|
377
|
|
|
logger.debug(self, 'Found {} unflagged mails'.format( |
|
378
|
|
|
len(uids), |
|
379
|
|
|
)) |
|
380
|
|
|
for msgid, data in imapc.fetch(uids, ['BODY.PEEK[]']).items(): |
|
381
|
|
|
# INFO - G.M - 2017-12-08 - Fetch BODY.PEEK[] |
|
382
|
|
|
# Retrieve all mail(body and header) but don't set mail |
|
383
|
|
|
# as seen because of PEEK |
|
384
|
|
|
# see rfc3501 |
|
385
|
|
|
logger.debug(self, 'Fetch mail "{}"'.format( |
|
386
|
|
|
msgid, |
|
387
|
|
|
)) |
|
388
|
|
|
|
|
389
|
|
|
try: |
|
390
|
|
|
msg = message_from_bytes(data[b'BODY[]']) |
|
391
|
|
|
except KeyError as e: |
|
392
|
|
|
# INFO - G.M - 12-01-2018 - Fetch may return events response |
|
393
|
|
|
# In some specific case, fetch command may return events |
|
394
|
|
|
# response unrelated to fetch request. |
|
395
|
|
|
# This should happen only when someone-else use the mailbox |
|
396
|
|
|
# at the same time of the fetcher. |
|
397
|
|
|
# see https://github.com/mjs/imapclient/issues/334 |
|
398
|
|
|
except_msg = 'fetch response : {}'.format(str(data)) |
|
399
|
|
|
raise BadIMAPFetchResponse(except_msg) from e |
|
400
|
|
|
|
|
401
|
|
|
msg_container = MessageContainer(msg, msgid) |
|
402
|
|
|
messages.append(msg_container) |
|
403
|
|
|
|
|
404
|
|
|
return messages |
|
405
|
|
|
|
|
406
|
|
|
def _notify_tracim( |
|
407
|
|
|
self, |
|
408
|
|
|
mails: typing.List[DecodedMail], |
|
409
|
|
|
imapc: imapclient.IMAPClient |
|
410
|
|
|
) -> None: |
|
411
|
|
|
""" |
|
412
|
|
|
Send http request to tracim endpoint |
|
413
|
|
|
:param mails: list of mails to send |
|
414
|
|
|
:return: none |
|
415
|
|
|
""" |
|
416
|
|
|
logger.debug(self, 'Notify tracim about {} new responses'.format( |
|
417
|
|
|
len(mails), |
|
418
|
|
|
)) |
|
419
|
|
|
# TODO BS 20171124: Look around mail.get_from_address(), mail.get_key() |
|
420
|
|
|
# , mail.get_body() etc ... for raise InvalidEmailError if missing |
|
421
|
|
|
# required informations (actually get_from_address raise IndexError |
|
422
|
|
|
# if no from address for example) and catch it here |
|
423
|
|
|
while mails: |
|
424
|
|
|
mail = mails.pop() |
|
425
|
|
|
try: |
|
426
|
|
|
method, endpoint, json_body_dict = self._create_comment_request(mail) # nopep8 |
|
427
|
|
|
except NoSpecialKeyFound as exc: |
|
428
|
|
|
log = 'Failed to create comment request due to missing specialkey in mail {}' # nopep8 |
|
429
|
|
|
logger.error(self, log.format(exc.__str__())) |
|
430
|
|
|
continue |
|
431
|
|
|
except EmptyEmailBody as exc: |
|
432
|
|
|
log = 'Empty body, skip mail' |
|
433
|
|
|
logger.error(self, log) |
|
434
|
|
|
continue |
|
435
|
|
|
except Exception as exc: |
|
436
|
|
|
log = 'Failed to create comment request in mail fetcher error {}' # nopep8 |
|
437
|
|
|
logger.error(self, log.format(exc.__str__())) |
|
438
|
|
|
continue |
|
439
|
|
|
|
|
440
|
|
|
try: |
|
441
|
|
|
self._send_request( |
|
442
|
|
|
mail=mail, |
|
443
|
|
|
imapc=imapc, |
|
444
|
|
|
method=method, |
|
445
|
|
|
endpoint=endpoint, |
|
446
|
|
|
json_body_dict=json_body_dict, |
|
447
|
|
|
) |
|
448
|
|
|
except requests.exceptions.Timeout as e: |
|
449
|
|
|
log = 'Timeout error to transmit fetched mail to tracim : {}' |
|
450
|
|
|
logger.error(self, log.format(str(e))) |
|
451
|
|
|
except requests.exceptions.RequestException as e: |
|
452
|
|
|
log = 'Fail to transmit fetched mail to tracim : {}' |
|
453
|
|
|
logger.error(self, log.format(str(e))) |
|
454
|
|
|
|
|
455
|
|
|
def _get_auth_headers(self, user_email) -> dict: |
|
456
|
|
|
return { |
|
457
|
|
|
TRACIM_API_KEY_HEADER: self.api_key, |
|
458
|
|
|
TRACIM_API_USER_EMAIL_LOGIN_HEADER: user_email |
|
459
|
|
|
} |
|
460
|
|
|
|
|
461
|
|
|
def _get_content_info(self, content_id, user_email): |
|
462
|
|
|
endpoint = '{api_base_url}contents/{content_id}'.format( |
|
463
|
|
|
api_base_url=self.api_base_url, |
|
464
|
|
|
content_id=content_id, |
|
465
|
|
|
) |
|
466
|
|
|
result = requests.get( |
|
467
|
|
|
endpoint, |
|
468
|
|
|
headers=self._get_auth_headers(user_email) |
|
469
|
|
|
) |
|
470
|
|
|
if result.status_code not in [200, 204]: |
|
471
|
|
|
details = result.json().get('message') |
|
472
|
|
|
msg = 'bad status code {}(200 is valid) response when trying to get info about a content: {}' # nopep8 |
|
473
|
|
|
msg = msg.format(str(result.status_code), details) |
|
474
|
|
|
raise BadStatusCode(msg) |
|
475
|
|
|
return result.json() |
|
476
|
|
|
|
|
477
|
|
|
def _create_comment_request(self, mail: DecodedMail) -> typing.Tuple[str, str, dict]: # nopep8 |
|
478
|
|
|
content_id = mail.get_key() |
|
479
|
|
|
content_info = self._get_content_info(content_id, mail.get_from_address()) # nopep8 |
|
480
|
|
|
mail_body = mail.get_body( |
|
481
|
|
|
use_html_parsing=self.use_html_parsing, |
|
482
|
|
|
use_txt_parsing=self.use_txt_parsing, |
|
483
|
|
|
) |
|
484
|
|
|
endpoint = '{api_base_url}workspaces/{workspace_id}/contents/{content_id}/comments'.format( # nopep8 |
|
485
|
|
|
api_base_url=self.api_base_url, |
|
486
|
|
|
content_id=content_id, |
|
487
|
|
|
workspace_id=content_info['workspace_id'] |
|
488
|
|
|
) |
|
489
|
|
|
method = 'POST' |
|
490
|
|
|
body = { |
|
491
|
|
|
'raw_content': mail_body |
|
492
|
|
|
} |
|
493
|
|
|
return method, endpoint, body |
|
494
|
|
|
|
|
495
|
|
|
def _send_request( |
|
496
|
|
|
self, |
|
497
|
|
|
mail: DecodedMail, |
|
498
|
|
|
imapc: imapclient.IMAPClient, |
|
499
|
|
|
method: str, |
|
500
|
|
|
endpoint: str, |
|
501
|
|
|
json_body_dict: dict |
|
502
|
|
|
): |
|
503
|
|
|
logger.debug( |
|
504
|
|
|
self, |
|
505
|
|
|
'Contact API on {endpoint} with method {method} with body {body}'.format( # nopep8 |
|
506
|
|
|
endpoint=endpoint, |
|
507
|
|
|
method=method, |
|
508
|
|
|
body=str(json_body_dict), |
|
509
|
|
|
), |
|
510
|
|
|
) |
|
511
|
|
|
if method == 'POST': |
|
512
|
|
|
request_method = requests.post |
|
513
|
|
|
else: |
|
514
|
|
|
# TODO - G.M - 2018-08-24 - Better handling exception |
|
515
|
|
|
raise UnsupportedRequestMethod('Request method not supported') |
|
516
|
|
|
|
|
517
|
|
|
r = request_method( |
|
518
|
|
|
url=endpoint, |
|
519
|
|
|
json=json_body_dict, |
|
520
|
|
|
headers=self._get_auth_headers(mail.get_from_address()), |
|
521
|
|
|
) |
|
522
|
|
|
if r.status_code not in [200, 204]: |
|
523
|
|
|
details = r.json().get('message') |
|
524
|
|
|
msg = 'bad status code {} (200 and 204 are valid) response when sending mail to tracim: {}' # nopep8 |
|
525
|
|
|
msg = msg.format(str(r.status_code), details) |
|
526
|
|
|
raise BadStatusCode(msg) |
|
527
|
|
|
# Flag all correctly checked mail |
|
528
|
|
|
if r.status_code in [200, 204]: |
|
529
|
|
|
imapc.add_flags((mail.uid,), IMAP_CHECKED_FLAG) |
|
530
|
|
|
imapc.add_flags((mail.uid,), IMAP_SEEN_FLAG) |
|
531
|
|
|
|