Browse Source

Mail Fetcher: Better exceptions handlers

Guénaël Muller 7 years ago
parent
commit
d3bd2088eb
1 changed files with 82 additions and 37 deletions
  1. 82 37
      tracim/tracim/lib/email_fetcher.py

+ 82 - 37
tracim/tracim/lib/email_fetcher.py View File

3
 import time
3
 import time
4
 import json
4
 import json
5
 import typing
5
 import typing
6
+import socket
7
+import ssl
8
+
6
 from email import message_from_bytes
9
 from email import message_from_bytes
7
 from email.header import decode_header
10
 from email.header import decode_header
8
 from email.header import make_header
11
 from email.header import make_header
12
 import filelock
15
 import filelock
13
 import markdown
16
 import markdown
14
 import requests
17
 import requests
15
-from imapclient import IMAPClient, FLAGGED, SEEN
18
+import imapclient
16
 
19
 
17
 from email_reply_parser import EmailReplyParser
20
 from email_reply_parser import EmailReplyParser
18
 from tracim.lib.base import logger
21
 from tracim.lib.base import logger
23
 CONTENT_TYPE_TEXT_PLAIN = 'text/plain'
26
 CONTENT_TYPE_TEXT_PLAIN = 'text/plain'
24
 CONTENT_TYPE_TEXT_HTML = 'text/html'
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
 MAIL_FETCHER_FILELOCK_TIMEOUT = 10
32
 MAIL_FETCHER_FILELOCK_TIMEOUT = 10
30
 MAIL_FETCHER_CONNECTION_TIMEOUT = 60*3
33
 MAIL_FETCHER_CONNECTION_TIMEOUT = 60*3
32
 # that 29 minutes according to rfc2177.(server wait 30min by default)
35
 # that 29 minutes according to rfc2177.(server wait 30min by default)
33
 
36
 
34
 
37
 
35
-
36
 class MessageContainer(object):
38
 class MessageContainer(object):
37
     def __init__(self, message: Message, uid: int) -> None:
39
     def __init__(self, message: Message, uid: int) -> None:
38
         self.message = message
40
         self.message = message
180
         :param folder: mail folder where new mail are fetched
182
         :param folder: mail folder where new mail are fetched
181
         :param use_idle: use IMAP IDLE(server notification) when available
183
         :param use_idle: use IMAP IDLE(server notification) when available
182
         :param heartbeat: seconds to wait before fetching new mail again
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
         :param endpoint: tracim http endpoint where decoded mail are send.
188
         :param endpoint: tracim http endpoint where decoded mail are send.
186
         :param token: token to authenticate http connexion
189
         :param token: token to authenticate http connexion
187
         :param use_html_parsing: parse html mail
190
         :param use_html_parsing: parse html mail
206
     def run(self) -> None:
209
     def run(self) -> None:
207
         logger.info(self, 'Starting MailFetcher')
210
         logger.info(self, 'Starting MailFetcher')
208
         while self._is_active:
211
         while self._is_active:
209
-
210
-            # login/connection
212
+            imapc = None
213
+            sleep_after_connection = True
211
             try:
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
                 imapc.login(self.user, self.password)
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
                 logger.debug(self, 'Select folder {}'.format(
223
                 logger.debug(self, 'Select folder {}'.format(
228
                     self.folder,
224
                     self.folder,
229
                 ))
225
                 ))
231
 
227
 
232
                 # force renew connection when deadline is reached
228
                 # force renew connection when deadline is reached
233
                 deadline = time.time() + self.connection_max_lifetime
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
                     # check for new mails
244
                     # check for new mails
236
                     self._check_mail(imapc)
245
                     self._check_mail(imapc)
237
 
246
 
238
-
239
                     if self.use_idle and imapc.has_capability('IDLE'):
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
                         logger.debug(self, 'wail for event(IDLE)')
249
                         logger.debug(self, 'wail for event(IDLE)')
242
                         imapc.idle()
250
                         imapc.idle()
243
                         imapc.idle_check(
251
                         imapc.idle_check(
250
                                   'support it, use polling instead.'
258
                                   'support it, use polling instead.'
251
                             logger.warning(self, log)
259
                             logger.warning(self, log)
252
                         # normal polling mode : sleep a define duration
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
                         time.sleep(self.heartbeat)
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
             except filelock.Timeout as e:
286
             except filelock.Timeout as e:
258
                 log = 'Mail Fetcher Lock Timeout {}'
287
                 log = 'Mail Fetcher Lock Timeout {}'
259
                 logger.warning(self, log.format(e.__str__()))
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
             except Exception as e:
295
             except Exception as e:
261
                 log = 'Mail Fetcher error {}'
296
                 log = 'Mail Fetcher error {}'
262
                 logger.error(self, log.format(e.__str__()))
297
                 logger.error(self, log.format(e.__str__()))
298
+
263
             finally:
299
             finally:
264
                 # INFO - G.M - 2018-01-09 - Connection closing
300
                 # INFO - G.M - 2018-01-09 - Connection closing
265
                 # Properly close connection according to
301
                 # Properly close connection according to
266
                 # https://github.com/mjs/imapclient/pull/279/commits/043e4bd0c5c775c5a08cb5f1baa93876a46732ee
302
                 # https://github.com/mjs/imapclient/pull/279/commits/043e4bd0c5c775c5a08cb5f1baa93876a46732ee
267
                 # TODO : Use __exit__ method instead when imapclient stable will
303
                 # TODO : Use __exit__ method instead when imapclient stable will
268
                 # be 2.0+ .
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
                     try:
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
         with self.lock.acquire(
324
         with self.lock.acquire(
281
                 timeout=MAIL_FETCHER_FILELOCK_TIMEOUT
325
                 timeout=MAIL_FETCHER_FILELOCK_TIMEOUT
282
         ):
326
         ):
288
     def stop(self) -> None:
332
     def stop(self) -> None:
289
         self._is_active = False
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
         Get news message from mailbox
338
         Get news message from mailbox
294
         :return: list of new mails
339
         :return: list of new mails
320
     def _notify_tracim(
365
     def _notify_tracim(
321
         self,
366
         self,
322
         mails: typing.List[DecodedMail],
367
         mails: typing.List[DecodedMail],
323
-        imapc: IMAPClient
368
+        imapc: imapclient.IMAPClient
324
     ) -> None:
369
     ) -> None:
325
         """
370
         """
326
         Send http request to tracim endpoint
371
         Send http request to tracim endpoint