Browse Source

Switch to imapclient lib

Guénaël Muller 6 years ago
parent
commit
3505a46d13
2 changed files with 44 additions and 152 deletions
  1. 1 0
      install/requirements.txt
  2. 43 152
      tracim/tracim/lib/email_fetcher.py

+ 1 - 0
install/requirements.txt View File

@@ -68,3 +68,4 @@ click==6.7
68 68
 markdown==2.6.9
69 69
 email_reply_parser==0.5.9
70 70
 filelock==2.0.13
71
+imapclient==1.1.0

+ 43 - 152
tracim/tracim/lib/email_fetcher.py View File

@@ -1,7 +1,6 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 
3 3
 import time
4
-import imaplib
5 4
 import json
6 5
 import typing
7 6
 from email import message_from_bytes
@@ -13,6 +12,8 @@ from email.utils import parseaddr
13 12
 import filelock
14 13
 import markdown
15 14
 import requests
15
+from imapclient import IMAPClient, FLAGGED, SEEN
16
+
16 17
 from email_reply_parser import EmailReplyParser
17 18
 from tracim.lib.base import logger
18 19
 from tracim.lib.email_processing.parser import ParsedHTMLMail
@@ -22,8 +23,8 @@ TRACIM_SPECIAL_KEY_HEADER = 'X-Tracim-Key'
22 23
 CONTENT_TYPE_TEXT_PLAIN = 'text/plain'
23 24
 CONTENT_TYPE_TEXT_HTML = 'text/html'
24 25
 
25
-IMAP_SEEN_FLAG = '\\Seen'
26
-IMAP_CHECKED_FLAG = '\\Flagged'
26
+IMAP_SEEN_FLAG = SEEN
27
+IMAP_CHECKED_FLAG = FLAGGED
27 28
 MAIL_FETCHER_FILELOCK_TIMEOUT = 10
28 29
 
29 30
 
@@ -176,7 +177,6 @@ class MailFetcher(object):
176 177
         :param use_html_parsing: parse html mail
177 178
         :param use_txt_parsing: parse txt mail
178 179
         """
179
-        self._connection = None
180 180
         self.host = host
181 181
         self.port = port
182 182
         self.user = user
@@ -194,67 +194,31 @@ class MailFetcher(object):
194 194
     def run(self) -> None:
195 195
         logger.info(self, 'Starting MailFetcher')
196 196
         while self._is_active:
197
-            logger.debug(self, 'sleep for {}'.format(self.delay))
198
-            time.sleep(self.delay)
199 197
             try:
200
-                self._connect()
198
+                imapc = IMAPClient(self.host, ssl=self.use_ssl)
199
+                logger.debug(self, 'sleep for {}'.format(self.delay))
200
+                time.sleep(self.delay)
201
+                imapc.login(self.user, self.password)
201 202
                 with self.lock.acquire(
202 203
                         timeout=MAIL_FETCHER_FILELOCK_TIMEOUT
203 204
                 ):
204
-                    messages = self._fetch()
205
-                cleaned_mails = [DecodedMail(m.message, m.uid)
206
-                                 for m in messages]
207
-                self._notify_tracim(cleaned_mails)
208
-                self._disconnect()
205
+                    messages = self._fetch(imapc)
206
+                    cleaned_mails = [DecodedMail(m.message, m.uid)
207
+                                     for m in messages]
208
+                    self._notify_tracim(cleaned_mails, imapc)
209 209
             except filelock.Timeout as e:
210 210
                 log = 'Mail Fetcher Lock Timeout {}'
211 211
                 logger.warning(self, log.format(e.__str__()))
212 212
             except Exception as e:
213
-                # TODO - G.M - 2017-11-23 - Identify possible exceptions
214
-                log = 'IMAP error: {}'
215
-                logger.warning(self, log.format(e.__str__()))
213
+                log = 'Mail Fetcher error {}'
214
+                logger.error(self, log.format(e.__str__()))
215
+            finally:
216
+                imapc.logout()
216 217
 
217 218
     def stop(self) -> None:
218 219
         self._is_active = False
219 220
 
220
-    def _connect(self) -> None:
221
-        # TODO - G.M - 2017-11-15 Verify connection/disconnection
222
-        # Are old connexion properly close this way ?
223
-        if self._connection:
224
-            logger.debug(self, 'Disconnect from IMAP')
225
-            self._disconnect()
226
-        # TODO - G.M - 2017-11-23 Support for predefined SSLContext ?
227
-        # without ssl_context param, tracim use default security configuration
228
-        # which is great in most case.
229
-        if self.use_ssl:
230
-            logger.debug(self, 'Connect IMAP {}:{} using SSL'.format(
231
-                self.host,
232
-                self.port,
233
-            ))
234
-            self._connection = imaplib.IMAP4_SSL(self.host, self.port)
235
-        else:
236
-            logger.debug(self, 'Connect IMAP {}:{}'.format(
237
-                self.host,
238
-                self.port,
239
-            ))
240
-            self._connection = imaplib.IMAP4(self.host, self.port)
241
-
242
-        try:
243
-            logger.debug(self, 'Login IMAP with login {}'.format(
244
-                self.user,
245
-            ))
246
-            self._connection.login(self.user, self.password)
247
-        except Exception as e:
248
-            log = 'Error during execution: {}'
249
-            logger.error(self, log.format(e.__str__()), exc_info=1)
250
-
251
-    def _disconnect(self) -> None:
252
-        if self._connection:
253
-            self._connection.close()
254
-            self._connection.logout()
255
-            self._connection = None
256
-
257
-    def _fetch(self) -> typing.List[MessageContainer]:
221
+    def _fetch(self, imapclient: IMAPClient) -> typing.List[MessageContainer]:
258 222
         """
259 223
         Get news message from mailbox
260 224
         :return: list of new mails
@@ -264,67 +228,43 @@ class MailFetcher(object):
264 228
         logger.debug(self, 'Fetch messages from folder {}'.format(
265 229
             self.folder,
266 230
         ))
267
-        rv, data = self._connection.select(self.folder)
268
-        logger.debug(self, 'Response status {}'.format(
269
-            rv,
231
+
232
+        imapclient.select_folder(self.folder)
233
+        logger.debug(self, 'Fetch unseen messages')
234
+        uids = imapclient.search(['UNSEEN'])
235
+        logger.debug(self, 'Found {} unseen mails'.format(
236
+            len(uids),
237
+        ))
238
+        imapclient.add_flags(uids, IMAP_SEEN_FLAG)
239
+        logger.debug(self, 'Temporary Flag {} mails as seen'.format(
240
+            len(uids),
270 241
         ))
271
-        if rv == 'OK':
272
-            # get mails
273
-            # TODO - G.M -  2017-11-15 Which files to select as new file ?
274
-            # Unseen file or All file from a directory (old one should be
275
-            #  moved/ deleted from mailbox during this process) ?
276
-            logger.debug(self, 'Fetch unseen messages')
277
-
278
-            rv, data = self._connection.search(None, "(UNSEEN)")
279
-            logger.debug(self, 'Response status {}'.format(
280
-                rv,
242
+        for msgid, data in imapclient.fetch(uids, ['BODY.PEEK[]']).items():
243
+            # INFO - G.M - 2017-12-08 - Fetch BODY.PEEK[]
244
+            # Retrieve all mail(body and header) but don't set mail
245
+            # as seen because of PEEK
246
+            # see rfc3501
247
+            logger.debug(self, 'Fetch mail "{}"'.format(
248
+                msgid,
281 249
             ))
282
-            if rv == 'OK':
283
-                # get mail content
284
-                logger.debug(self, 'Found {} unseen mails'.format(
285
-                    len(data[0].split()),
286
-                ))
287
-                for uid in data[0].split():
288
-                    # INFO - G.M - 2017-12-08 - Fetch BODY.PEEK[]
289
-                    # Retrieve all mail(body and header) but don't set mail
290
-                    # as seen because of PEEK
291
-                    # see rfc3501
292
-                    logger.debug(self, 'Fetch mail "{}"'.format(
293
-                        uid,
294
-                    ))
295
-                    rv, data = self._connection.fetch(uid, 'BODY.PEEK[]')
296
-                    logger.debug(self, 'Response status {}'.format(
297
-                        rv,
298
-                    ))
299
-                    if rv == 'OK':
300
-                        msg = message_from_bytes(data[0][1])
301
-                        msg_container = MessageContainer(msg, uid)
302
-                        messages.append(msg_container)
303
-                        self._set_flag(uid, IMAP_SEEN_FLAG)
304
-                    else:
305
-                        log = 'IMAP : Unable to get mail : {}'
306
-                        logger.error(self, log.format(str(rv)))
307
-            else:
308
-                log = 'IMAP : Unable to get unseen mail : {}'
309
-                logger.error(self, log.format(str(rv)))
310
-        else:
311
-            log = 'IMAP : Unable to open mailbox : {}'
312
-            logger.error(self, log.format(str(rv)))
250
+            msg = message_from_bytes(data[b'BODY[]'])
251
+            msg_container = MessageContainer(msg, msgid)
252
+            messages.append(msg_container)
313 253
         return messages
314 254
 
315 255
     def _notify_tracim(
316 256
         self,
317 257
         mails: typing.List[DecodedMail],
258
+        imapc: IMAPClient
318 259
     ) -> None:
319 260
         """
320 261
         Send http request to tracim endpoint
321 262
         :param mails: list of mails to send
322
-        :return: unsended mails
263
+        :return: none
323 264
         """
324 265
         logger.debug(self, 'Notify tracim about {} new responses'.format(
325 266
             len(mails),
326 267
         ))
327
-        unsended_mails = []
328 268
         # TODO BS 20171124: Look around mail.get_from_address(), mail.get_key()
329 269
         # , mail.get_body() etc ... for raise InvalidEmailError if missing
330 270
         #  required informations (actually get_from_address raise IndexError
@@ -357,64 +297,15 @@ class MailFetcher(object):
357 297
                     ))
358 298
                 # Flag all correctly checked mail, unseen the others
359 299
                 if r.status_code in [200, 204, 400]:
360
-                    self._set_flag(mail.uid, IMAP_CHECKED_FLAG)
300
+                    imapc.add_flags((mail.uid,), IMAP_CHECKED_FLAG)
361 301
                 else:
362
-                    self._unset_flag(mail.uid, IMAP_SEEN_FLAG)
302
+                    imapc.remove_flags((mail.uid,), IMAP_SEEN_FLAG)
363 303
             # TODO - G.M - Verify exception correctly works
364 304
             except requests.exceptions.Timeout as e:
365 305
                 log = 'Timeout error to transmit fetched mail to tracim : {}'
366 306
                 logger.error(self, log.format(str(e)))
367
-                unsended_mails.append(mail)
368
-                self._unset_flag(mail.uid, IMAP_SEEN_FLAG)
307
+                imapc.remove_flags((mail.uid,), IMAP_SEEN_FLAG)
369 308
             except requests.exceptions.RequestException as e:
370 309
                 log = 'Fail to transmit fetched mail to tracim : {}'
371 310
                 logger.error(self, log.format(str(e)))
372
-                self._unset_flag(mail.uid, IMAP_SEEN_FLAG)
373
-
374
-    def _set_flag(
375
-            self,
376
-            uid: int,
377
-            flag: str,
378
-            ) -> None:
379
-        assert uid is not None
380
-
381
-        rv, data = self._connection.store(
382
-            uid,
383
-            '+FLAGS',
384
-            flag,
385
-        )
386
-        if rv == 'OK':
387
-            log = 'Message {uid} set as {flag}.'.format(
388
-                uid=uid,
389
-                flag=flag)
390
-            logger.debug(self, log)
391
-        else:
392
-            log = 'Can not set Message {uid} as {flag} : {rv}'.format(
393
-                uid=uid,
394
-                flag=flag,
395
-                rv=rv)
396
-            logger.error(self, log)
397
-
398
-    def _unset_flag(
399
-            self,
400
-            uid: int,
401
-            flag: str,
402
-            ) -> None:
403
-        assert uid is not None
404
-
405
-        rv, data = self._connection.store(
406
-            uid,
407
-            '-FLAGS',
408
-            flag,
409
-        )
410
-        if rv == 'OK':
411
-            log = 'Message {uid} unset as {flag}.'.format(
412
-                uid=uid,
413
-                flag=flag)
414
-            logger.debug(self, log)
415
-        else:
416
-            log = 'Can not unset Message {uid} as {flag} : {rv}'.format(
417
-                uid=uid,
418
-                flag=flag,
419
-                rv=rv)
420
-            logger.error(self, log)
311
+                imapc.remove_flags((mail.uid,), IMAP_SEEN_FLAG)