Browse Source

filelock for unseen mail fetching

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

+ 1 - 0
.gitignore View File

70
 # Temporary files
70
 # Temporary files
71
 *~
71
 *~
72
 *.sqlite
72
 *.sqlite
73
+*.lock
73
 
74
 
74
 # npm packages
75
 # npm packages
75
 /node_modules/
76
 /node_modules/

+ 1 - 0
install/requirements.txt View File

67
 click==6.7
67
 click==6.7
68
 markdown==2.6.9
68
 markdown==2.6.9
69
 email_reply_parser==0.5.9
69
 email_reply_parser==0.5.9
70
+filelock==2.0.13

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

228
 email.reply.check.heartbeat = 60
228
 email.reply.check.heartbeat = 60
229
 email.reply.use_html_parsing = true
229
 email.reply.use_html_parsing = true
230
 email.reply.use_txt_parsing = true
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
 ## Radical (CalDav server) configuration
235
 ## Radical (CalDav server) configuration
233
 # radicale.server.host = 0.0.0.0
236
 # radicale.server.host = 0.0.0.0

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

392
             'email.reply.use_txt_parsing',
392
             'email.reply.use_txt_parsing',
393
             True,
393
             True,
394
         ))
394
         ))
395
+        self.EMAIL_REPLY_FILELOCK_PATH = tg.config.get(
396
+            'email.reply.filelock_path',
397
+        )
395
 
398
 
396
         self.TRACKER_JS_PATH = tg.config.get(
399
         self.TRACKER_JS_PATH = tg.config.get(
397
             'js_tracker_path',
400
             'js_tracker_path',

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

179
             token=cfg.EMAIL_REPLY_TOKEN,
179
             token=cfg.EMAIL_REPLY_TOKEN,
180
             use_html_parsing=cfg.EMAIL_REPLY_USE_HTML_PARSING,
180
             use_html_parsing=cfg.EMAIL_REPLY_USE_HTML_PARSING,
181
             use_txt_parsing=cfg.EMAIL_REPLY_USE_TXT_PARSING,
181
             use_txt_parsing=cfg.EMAIL_REPLY_USE_TXT_PARSING,
182
+            filelock_path=cfg.EMAIL_REPLY_FILELOCK_PATH,
182
         )
183
         )
183
         self._fetcher.run()
184
         self._fetcher.run()
184
 
185
 

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

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