|
@@ -3,6 +3,9 @@
|
3
|
3
|
import time
|
4
|
4
|
import json
|
5
|
5
|
import typing
|
|
6
|
+import socket
|
|
7
|
+import ssl
|
|
8
|
+
|
6
|
9
|
from email import message_from_bytes
|
7
|
10
|
from email.header import decode_header
|
8
|
11
|
from email.header import make_header
|
|
@@ -12,7 +15,7 @@ from email.utils import parseaddr
|
12
|
15
|
import filelock
|
13
|
16
|
import markdown
|
14
|
17
|
import requests
|
15
|
|
-from imapclient import IMAPClient, FLAGGED, SEEN
|
|
18
|
+import imapclient
|
16
|
19
|
|
17
|
20
|
from email_reply_parser import EmailReplyParser
|
18
|
21
|
from tracim.lib.base import logger
|
|
@@ -23,8 +26,8 @@ TRACIM_SPECIAL_KEY_HEADER = 'X-Tracim-Key'
|
23
|
26
|
CONTENT_TYPE_TEXT_PLAIN = 'text/plain'
|
24
|
27
|
CONTENT_TYPE_TEXT_HTML = 'text/html'
|
25
|
28
|
|
26
|
|
-IMAP_SEEN_FLAG = SEEN
|
27
|
|
-IMAP_CHECKED_FLAG = FLAGGED
|
|
29
|
+IMAP_SEEN_FLAG = imapclient.SEEN
|
|
30
|
+IMAP_CHECKED_FLAG = imapclient.FLAGGED
|
28
|
31
|
|
29
|
32
|
MAIL_FETCHER_FILELOCK_TIMEOUT = 10
|
30
|
33
|
MAIL_FETCHER_CONNECTION_TIMEOUT = 60*3
|
|
@@ -32,7 +35,6 @@ MAIL_FETCHER_IDLE_RESPONSE_TIMEOUT = 60*9 # this should be not more
|
32
|
35
|
# that 29 minutes according to rfc2177.(server wait 30min by default)
|
33
|
36
|
|
34
|
37
|
|
35
|
|
-
|
36
|
38
|
class MessageContainer(object):
|
37
|
39
|
def __init__(self, message: Message, uid: int) -> None:
|
38
|
40
|
self.message = message
|
|
@@ -180,8 +182,9 @@ class MailFetcher(object):
|
180
|
182
|
:param folder: mail folder where new mail are fetched
|
181
|
183
|
:param use_idle: use IMAP IDLE(server notification) when available
|
182
|
184
|
:param heartbeat: seconds to wait before fetching new mail again
|
183
|
|
- :param connection_max_lifetime: maximum duration allowed for a connection.
|
184
|
|
- connection is automatically renew when his lifetime excess this value
|
|
185
|
+ :param connection_max_lifetime: maximum duration allowed for a
|
|
186
|
+ connection . connection are automatically renew when their
|
|
187
|
+ lifetime excess this duration.
|
185
|
188
|
:param endpoint: tracim http endpoint where decoded mail are send.
|
186
|
189
|
:param token: token to authenticate http connexion
|
187
|
190
|
:param use_html_parsing: parse html mail
|
|
@@ -206,24 +209,17 @@ class MailFetcher(object):
|
206
|
209
|
def run(self) -> None:
|
207
|
210
|
logger.info(self, 'Starting MailFetcher')
|
208
|
211
|
while self._is_active:
|
209
|
|
-
|
210
|
|
- # login/connection
|
|
212
|
+ imapc = None
|
|
213
|
+ sleep_after_connection = True
|
211
|
214
|
try:
|
212
|
|
- imapc = IMAPClient(self.host,
|
213
|
|
- self.port,
|
214
|
|
- ssl=self.use_ssl,
|
215
|
|
- timeout=MAIL_FETCHER_CONNECTION_TIMEOUT)
|
|
215
|
+ imapc = imapclient.IMAPClient(
|
|
216
|
+ self.host,
|
|
217
|
+ self.port,
|
|
218
|
+ ssl=self.use_ssl,
|
|
219
|
+ timeout=MAIL_FETCHER_CONNECTION_TIMEOUT
|
|
220
|
+ )
|
216
|
221
|
imapc.login(self.user, self.password)
|
217
|
|
- except Exception as e:
|
218
|
|
- log = 'Fail to connect to IMAP {}'
|
219
|
|
- logger.error(self, log.format(e.__str__()))
|
220
|
|
- logger.debug(self, 'sleep for {}'.format(self.heartbeat))
|
221
|
|
- time.sleep(self.heartbeat)
|
222
|
|
- continue
|
223
|
222
|
|
224
|
|
- # fetching
|
225
|
|
- try:
|
226
|
|
- # select folder
|
227
|
223
|
logger.debug(self, 'Select folder {}'.format(
|
228
|
224
|
self.folder,
|
229
|
225
|
))
|
|
@@ -231,13 +227,25 @@ class MailFetcher(object):
|
231
|
227
|
|
232
|
228
|
# force renew connection when deadline is reached
|
233
|
229
|
deadline = time.time() + self.connection_max_lifetime
|
234
|
|
- while time.time() < deadline:
|
|
230
|
+ while True:
|
|
231
|
+ if not self._is_active:
|
|
232
|
+ logger.warning(self, 'Mail Fetcher process aborted')
|
|
233
|
+ sleep_after_connection = False
|
|
234
|
+ break
|
|
235
|
+
|
|
236
|
+ if time.time() > deadline:
|
|
237
|
+ logger.debug(
|
|
238
|
+ self,
|
|
239
|
+ "MailFetcher Connection Lifetime limit excess"
|
|
240
|
+ ", Try Re-new connection")
|
|
241
|
+ sleep_after_connection = False
|
|
242
|
+ break
|
|
243
|
+
|
235
|
244
|
# check for new mails
|
236
|
245
|
self._check_mail(imapc)
|
237
|
246
|
|
238
|
|
-
|
239
|
247
|
if self.use_idle and imapc.has_capability('IDLE'):
|
240
|
|
- # IDLE_mode: wait until event from server
|
|
248
|
+ # IDLE_mode wait until event from server
|
241
|
249
|
logger.debug(self, 'wail for event(IDLE)')
|
242
|
250
|
imapc.idle()
|
243
|
251
|
imapc.idle_check(
|
|
@@ -250,33 +258,69 @@ class MailFetcher(object):
|
250
|
258
|
'support it, use polling instead.'
|
251
|
259
|
logger.warning(self, log)
|
252
|
260
|
# normal polling mode : sleep a define duration
|
253
|
|
- logger.debug(self, 'sleep for {}'.format(self.heartbeat))
|
|
261
|
+ logger.debug(self,
|
|
262
|
+ 'sleep for {}'.format(self.heartbeat))
|
254
|
263
|
time.sleep(self.heartbeat)
|
255
|
264
|
|
256
|
|
- logger.debug(self,"Lifetime limit excess, Renew connection")
|
|
265
|
+ # Socket
|
|
266
|
+ except (socket.error,
|
|
267
|
+ socket.gaierror,
|
|
268
|
+ socket.herror) as e:
|
|
269
|
+ log = 'Socket fail with IMAP connection {}'
|
|
270
|
+ logger.error(self, log.format(e.__str__()))
|
|
271
|
+
|
|
272
|
+ except socket.timeout as e:
|
|
273
|
+ log = 'Socket timeout on IMAP connection {}'
|
|
274
|
+ logger.error(self, log.format(e.__str__()))
|
|
275
|
+
|
|
276
|
+ # SSL
|
|
277
|
+ except ssl.SSLError as e:
|
|
278
|
+ log = 'SSL error on IMAP connection'
|
|
279
|
+ logger.error(self, log.format(e.__str__()))
|
|
280
|
+
|
|
281
|
+ except ssl.CertificateError as e:
|
|
282
|
+ log = 'SSL Certificate verification failed on IMAP connection'
|
|
283
|
+ logger.error(self, log.format(e.__str__()))
|
|
284
|
+
|
|
285
|
+ # Filelock
|
257
|
286
|
except filelock.Timeout as e:
|
258
|
287
|
log = 'Mail Fetcher Lock Timeout {}'
|
259
|
288
|
logger.warning(self, log.format(e.__str__()))
|
|
289
|
+
|
|
290
|
+ # IMAP
|
|
291
|
+ # TODO - G.M - 10-01-2017 - Support imapclient exceptions
|
|
292
|
+ # when Imapclient stable will be 2.0+
|
|
293
|
+
|
|
294
|
+ # Others
|
260
|
295
|
except Exception as e:
|
261
|
296
|
log = 'Mail Fetcher error {}'
|
262
|
297
|
logger.error(self, log.format(e.__str__()))
|
|
298
|
+
|
263
|
299
|
finally:
|
264
|
300
|
# INFO - G.M - 2018-01-09 - Connection closing
|
265
|
301
|
# Properly close connection according to
|
266
|
302
|
# https://github.com/mjs/imapclient/pull/279/commits/043e4bd0c5c775c5a08cb5f1baa93876a46732ee
|
267
|
303
|
# TODO : Use __exit__ method instead when imapclient stable will
|
268
|
304
|
# be 2.0+ .
|
269
|
|
- logger.debug(self, 'Try logout')
|
270
|
|
- try:
|
271
|
|
- imapc.logout()
|
272
|
|
- except Exception:
|
|
305
|
+ if imapc:
|
|
306
|
+ logger.debug(self, 'Try logout')
|
273
|
307
|
try:
|
274
|
|
- imapc.shutdown()
|
275
|
|
- except Exception as e:
|
276
|
|
- log = "Can't logout, connection broken ? {}"
|
277
|
|
- logger.error(self, log.format(e.__str__()))
|
|
308
|
+ imapc.logout()
|
|
309
|
+ except Exception:
|
|
310
|
+ try:
|
|
311
|
+ imapc.shutdown()
|
|
312
|
+ except Exception as e:
|
|
313
|
+ log = "Can't logout, connection broken ? {}"
|
|
314
|
+ logger.error(self, log.format(e.__str__()))
|
|
315
|
+
|
|
316
|
+ if sleep_after_connection:
|
|
317
|
+ logger.debug(self, 'sleep for {}'.format(self.heartbeat))
|
|
318
|
+ time.sleep(self.heartbeat)
|
|
319
|
+
|
|
320
|
+ log = 'Mail Fetcher stopped'
|
|
321
|
+ logger.debug(self, log)
|
278
|
322
|
|
279
|
|
- def _check_mail(self, imapc: IMAPClient) -> None:
|
|
323
|
+ def _check_mail(self, imapc: imapclient.IMAPClient) -> None:
|
280
|
324
|
with self.lock.acquire(
|
281
|
325
|
timeout=MAIL_FETCHER_FILELOCK_TIMEOUT
|
282
|
326
|
):
|
|
@@ -288,7 +332,8 @@ class MailFetcher(object):
|
288
|
332
|
def stop(self) -> None:
|
289
|
333
|
self._is_active = False
|
290
|
334
|
|
291
|
|
- def _fetch(self, imapc: IMAPClient) -> typing.List[MessageContainer]:
|
|
335
|
+ def _fetch(self, imapc: imapclient.IMAPClient) \
|
|
336
|
+ -> typing.List[MessageContainer]:
|
292
|
337
|
"""
|
293
|
338
|
Get news message from mailbox
|
294
|
339
|
:return: list of new mails
|
|
@@ -320,7 +365,7 @@ class MailFetcher(object):
|
320
|
365
|
def _notify_tracim(
|
321
|
366
|
self,
|
322
|
367
|
mails: typing.List[DecodedMail],
|
323
|
|
- imapc: IMAPClient
|
|
368
|
+ imapc: imapclient.IMAPClient
|
324
|
369
|
) -> None:
|
325
|
370
|
"""
|
326
|
371
|
Send http request to tracim endpoint
|