Browse Source

filelock for unseen mail fetching

Guénaël Muller 6 years ago
parent
commit
80ec9e01e1

+ 1 - 0
.gitignore View File

@@ -70,6 +70,7 @@ wsgidav.conf
70 70
 # Temporary files
71 71
 *~
72 72
 *.sqlite
73
+*.lock
73 74
 
74 75
 # npm packages
75 76
 /node_modules/

+ 1 - 0
install/requirements.txt View File

@@ -67,3 +67,4 @@ rq==0.7.1
67 67
 click==6.7
68 68
 markdown==2.6.9
69 69
 email_reply_parser==0.5.9
70
+filelock==2.0.13

+ 3 - 0
tracim/development.ini.base View File

@@ -228,6 +228,9 @@ email.reply.token = mysecuretoken
228 228
 email.reply.check.heartbeat = 60
229 229
 email.reply.use_html_parsing = true
230 230
 email.reply.use_txt_parsing = true
231
+# Lockfile path is required for email_reply feature,
232
+# it's just an empty file use to prevent concurrent access to imap unseen mail
233
+email.reply.filelock_path = %(here)s/email_fetcher.lock
231 234
 
232 235
 ## Radical (CalDav server) configuration
233 236
 # radicale.server.host = 0.0.0.0

+ 3 - 0
tracim/tracim/config/app_cfg.py View File

@@ -392,6 +392,9 @@ class CFG(object):
392 392
             'email.reply.use_txt_parsing',
393 393
             True,
394 394
         ))
395
+        self.EMAIL_REPLY_FILELOCK_PATH = tg.config.get(
396
+            'email.reply.filelock_path',
397
+        )
395 398
 
396 399
         self.TRACKER_JS_PATH = tg.config.get(
397 400
             'js_tracker_path',

+ 1 - 0
tracim/tracim/lib/daemons.py View File

@@ -179,6 +179,7 @@ class MailFetcherDaemon(Daemon):
179 179
             token=cfg.EMAIL_REPLY_TOKEN,
180 180
             use_html_parsing=cfg.EMAIL_REPLY_USE_HTML_PARSING,
181 181
             use_txt_parsing=cfg.EMAIL_REPLY_USE_TXT_PARSING,
182
+            filelock_path=cfg.EMAIL_REPLY_FILELOCK_PATH,
182 183
         )
183 184
         self._fetcher.run()
184 185
 

+ 55 - 10
tracim/tracim/lib/email_fetcher.py View File

@@ -10,6 +10,7 @@ from email.header import make_header
10 10
 from email.message import Message
11 11
 from email.utils import parseaddr
12 12
 
13
+import filelock
13 14
 import markdown
14 15
 import requests
15 16
 from email_reply_parser import EmailReplyParser
@@ -21,6 +22,8 @@ TRACIM_SPECIAL_KEY_HEADER = 'X-Tracim-Key'
21 22
 CONTENT_TYPE_TEXT_PLAIN = 'text/plain'
22 23
 CONTENT_TYPE_TEXT_HTML = 'text/html'
23 24
 
25
+IMAP_SEEN_FLAG = '\\Seen'
26
+IMAP_CHECKED_FLAG = '\\Flagged'
24 27
 
25 28
 class MessageContainer(object):
26 29
     def __init__(self, message: Message, uid: int) -> None:
@@ -153,6 +156,7 @@ class MailFetcher(object):
153 156
         token: str,
154 157
         use_html_parsing: bool,
155 158
         use_txt_parsing: bool,
159
+        filelock_path: str,
156 160
     ) -> None:
157 161
         """
158 162
         Fetch mail from a mailbox folder through IMAP and add their content to
@@ -182,7 +186,7 @@ class MailFetcher(object):
182 186
         self.token = token
183 187
         self.use_html_parsing = use_html_parsing
184 188
         self.use_txt_parsing = use_txt_parsing
185
-
189
+        self.lock = filelock.FileLock(filelock_path)
186 190
         self._is_active = True
187 191
 
188 192
     def run(self) -> None:
@@ -192,7 +196,8 @@ class MailFetcher(object):
192 196
             time.sleep(self.delay)
193 197
             try:
194 198
                 self._connect()
195
-                messages = self._fetch()
199
+                with self.lock.acquire(timeout=10):
200
+                    messages = self._fetch()
196 201
                 cleaned_mails = [DecodedMail(m.message, m.uid)
197 202
                                  for m in messages]
198 203
                 self._notify_tracim(cleaned_mails)
@@ -204,6 +209,7 @@ class MailFetcher(object):
204 209
 
205 210
     def stop(self) -> None:
206 211
         self._is_active = False
212
+        del self.lock
207 213
 
208 214
     def _connect(self) -> None:
209 215
         # TODO - G.M - 2017-11-15 Verify connection/disconnection
@@ -262,6 +268,7 @@ class MailFetcher(object):
262 268
             # Unseen file or All file from a directory (old one should be
263 269
             #  moved/ deleted from mailbox during this process) ?
264 270
             logger.debug(self, 'Fetch unseen messages')
271
+
265 272
             rv, data = self._connection.search(None, "(UNSEEN)")
266 273
             logger.debug(self, 'Response status {}'.format(
267 274
                 rv,
@@ -287,6 +294,7 @@ class MailFetcher(object):
287 294
                         msg = message_from_bytes(data[0][1])
288 295
                         msg_container = MessageContainer(msg, uid)
289 296
                         messages.append(msg_container)
297
+                        self._set_flag(uid, IMAP_SEEN_FLAG)
290 298
                     else:
291 299
                         log = 'IMAP : Unable to get mail : {}'
292 300
                         logger.error(self, log.format(str(rv)))
@@ -301,7 +309,7 @@ class MailFetcher(object):
301 309
     def _notify_tracim(
302 310
         self,
303 311
         mails: typing.List[DecodedMail],
304
-    ) -> typing.List[DecodedMail]:
312
+    ) -> None:
305 313
         """
306 314
         Send http request to tracim endpoint
307 315
         :param mails: list of mails to send
@@ -341,29 +349,66 @@ class MailFetcher(object):
341 349
                         str(r.status_code),
342 350
                         details,
343 351
                     ))
352
+                # Flag all correctly checked mail, unseen the others
353
+                if r.status_code in [200, 204, 400]:
354
+                    self._set_flag(mail.uid, IMAP_CHECKED_FLAG)
344 355
                 else:
345
-                    self._set_flag(mail.uid)
356
+                    self._unset_flag(mail.uid, IMAP_SEEN_FLAG)
346 357
             # TODO - G.M - Verify exception correctly works
347 358
             except requests.exceptions.Timeout as e:
348 359
                 log = 'Timeout error to transmit fetched mail to tracim : {}'
349 360
                 logger.error(self, log.format(str(e)))
350 361
                 unsended_mails.append(mail)
362
+                self._unset_flag(mail.uid, IMAP_SEEN_FLAG)
351 363
             except requests.exceptions.RequestException as e:
352 364
                 log = 'Fail to transmit fetched mail to tracim : {}'
353 365
                 logger.error(self, log.format(str(e)))
366
+                self._unset_flag(mail.uid, IMAP_SEEN_FLAG)
354 367
 
355
-        return unsended_mails
356
-
357
-    def _set_flag(self, uid):
368
+    def _set_flag(
369
+            self,
370
+            uid: int,
371
+            flag: str,
372
+            ) -> None:
358 373
         assert uid is not None
374
+
359 375
         rv, data = self._connection.store(
360 376
             uid,
361 377
             '+FLAGS',
362
-            '\\Seen'
378
+            flag,
379
+        )
380
+        if rv == 'OK':
381
+            log = 'Message {uid} set as {flag}.'.format(
382
+                uid=uid,
383
+                flag=flag)
384
+            logger.debug(self, log)
385
+        else:
386
+            log = 'Can not set Message {uid} as {flag} : {rv}'.format(
387
+                uid=uid,
388
+                flag=flag,
389
+                rv=rv)
390
+            logger.error(self, log)
391
+
392
+    def _unset_flag(
393
+            self,
394
+            uid: int,
395
+            flag: str,
396
+            ) -> None:
397
+        assert uid is not None
398
+
399
+        rv, data = self._connection.store(
400
+            uid,
401
+            '-FLAGS',
402
+            flag,
363 403
         )
364 404
         if rv == 'OK':
365
-            log = 'Message {} set as seen.'.format(uid)
405
+            log = 'Message {uid} unset as {flag}.'.format(
406
+                uid=uid,
407
+                flag=flag)
366 408
             logger.debug(self, log)
367 409
         else:
368
-            log = 'Can not set Message {} as seen : {}'.format(uid, rv)
410
+            log = 'Can not unset Message {uid} as {flag} : {rv}'.format(
411
+                uid=uid,
412
+                flag=flag,
413
+                rv=rv)
369 414
             logger.error(self, log)