Bläddra i källkod

Mail Fetcher: Better exceptions handlers

Guénaël Muller 6 år sedan
förälder
incheckning
d3bd2088eb
1 ändrade filer med 82 tillägg och 37 borttagningar
  1. 82 37
      tracim/tracim/lib/email_fetcher.py

+ 82 - 37
tracim/tracim/lib/email_fetcher.py Visa fil

@@ -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