|
@@ -13,7 +13,7 @@ from email import message_from_bytes
|
13
|
13
|
|
14
|
14
|
import markdown
|
15
|
15
|
import requests
|
16
|
|
-from bs4 import BeautifulSoup
|
|
16
|
+from bs4 import BeautifulSoup, Tag
|
17
|
17
|
from email_reply_parser import EmailReplyParser
|
18
|
18
|
|
19
|
19
|
from tracim.lib.base import logger
|
|
@@ -35,27 +35,26 @@ CONTENT_TYPE_TEXT_HTML = 'text/html'
|
35
|
35
|
|
36
|
36
|
|
37
|
37
|
class DecodedMail(object):
|
38
|
|
-
|
39
|
|
- def __init__(self, message: Message):
|
|
38
|
+ def __init__(self, message: Message) -> None:
|
40
|
39
|
self._message = message
|
41
|
40
|
|
42
|
41
|
def _decode_header(self, header_title: str) -> typing.Optional[str]:
|
43
|
42
|
# FIXME : Handle exception
|
44
|
43
|
if header_title in self._message:
|
45
|
|
- return str(make_header(decode_header(header)))
|
|
44
|
+ return str(make_header(decode_header(self._message[header_title])))
|
46
|
45
|
else:
|
47
|
46
|
return None
|
48
|
47
|
|
49
|
48
|
def get_subject(self) -> typing.Optional[str]:
|
50
|
49
|
return self._decode_header('subject')
|
51
|
50
|
|
52
|
|
- def get_from_address(self) -> typing.Optional[str]:
|
|
51
|
+ def get_from_address(self) -> str:
|
53
|
52
|
return parseaddr(self._message['From'])[1]
|
54
|
53
|
|
55
|
|
- def get_to_address(self)-> typing.Optional[str]:
|
|
54
|
+ def get_to_address(self) -> str:
|
56
|
55
|
return parseaddr(self._message['To'])[1]
|
57
|
56
|
|
58
|
|
- def get_first_ref(self) -> typing.Optional[str]:
|
|
57
|
+ def get_first_ref(self) -> str:
|
59
|
58
|
return parseaddr(self._message['References'])[1]
|
60
|
59
|
|
61
|
60
|
def get_special_key(self) -> typing.Optional[str]:
|
|
@@ -80,14 +79,14 @@ class DecodedMail(object):
|
80
|
79
|
return body
|
81
|
80
|
|
82
|
81
|
@classmethod
|
83
|
|
- def _parse_txt_body(cls, txt_body: str):
|
|
82
|
+ def _parse_txt_body(cls, txt_body: str) -> str:
|
84
|
83
|
txt_body = EmailReplyParser.parse_reply(txt_body)
|
85
|
84
|
html_body = markdown.markdown(txt_body)
|
86
|
85
|
body = DecodedMail._parse_html_body(html_body)
|
87
|
86
|
return body
|
88
|
87
|
|
89
|
88
|
@classmethod
|
90
|
|
- def _parse_html_body(cls, html_body: str):
|
|
89
|
+ def _parse_html_body(cls, html_body: str) -> str:
|
91
|
90
|
soup = BeautifulSoup(html_body, 'html.parser')
|
92
|
91
|
config = BEAUTIFULSOUP_HTML_BODY_PARSE_CONFIG
|
93
|
92
|
for tag in soup.findAll():
|
|
@@ -103,7 +102,7 @@ class DecodedMail(object):
|
103
|
102
|
return str(soup)
|
104
|
103
|
|
105
|
104
|
@classmethod
|
106
|
|
- def _tag_to_extract(cls, tag) -> bool:
|
|
105
|
+ def _tag_to_extract(cls, tag: Tag) -> bool:
|
107
|
106
|
config = BEAUTIFULSOUP_HTML_BODY_PARSE_CONFIG
|
108
|
107
|
if tag.name.lower() in config['tag_blacklist']:
|
109
|
108
|
return True
|
|
@@ -154,9 +153,13 @@ class DecodedMail(object):
|
154
|
153
|
if first_ref:
|
155
|
154
|
return DecodedMail.find_key_from_mail_address(first_ref)
|
156
|
155
|
|
|
156
|
+ return None
|
|
157
|
+
|
157
|
158
|
@classmethod
|
158
|
|
- def find_key_from_mail_address(cls, mail_address: str) \
|
159
|
|
- -> typing.Optional[str]:
|
|
159
|
+ def find_key_from_mail_address(
|
|
160
|
+ cls,
|
|
161
|
+ mail_address: str,
|
|
162
|
+ ) -> typing.Optional[str]:
|
160
|
163
|
""" Parse mail_adress-like string
|
161
|
164
|
to retrieve key.
|
162
|
165
|
|
|
@@ -171,18 +174,18 @@ class DecodedMail(object):
|
171
|
174
|
|
172
|
175
|
|
173
|
176
|
class MailFetcher(object):
|
174
|
|
-
|
175
|
|
- def __init__(self,
|
176
|
|
- host: str,
|
177
|
|
- port: str,
|
178
|
|
- user: str,
|
179
|
|
- password: str,
|
180
|
|
- use_ssl: bool,
|
181
|
|
- folder: str,
|
182
|
|
- delay: int,
|
183
|
|
- endpoint: str,
|
184
|
|
- token: str) \
|
185
|
|
- -> None:
|
|
177
|
+ def __init__(
|
|
178
|
+ self,
|
|
179
|
+ host: str,
|
|
180
|
+ port: str,
|
|
181
|
+ user: str,
|
|
182
|
+ password: str,
|
|
183
|
+ use_ssl: bool,
|
|
184
|
+ folder: str,
|
|
185
|
+ delay: int,
|
|
186
|
+ endpoint: str,
|
|
187
|
+ token: str,
|
|
188
|
+ ) -> None:
|
186
|
189
|
"""
|
187
|
190
|
Fetch mail from a mailbox folder through IMAP and add their content to
|
188
|
191
|
Tracim through http according to mail Headers.
|
|
@@ -255,7 +258,7 @@ class MailFetcher(object):
|
255
|
258
|
self._connection.logout()
|
256
|
259
|
self._connection = None
|
257
|
260
|
|
258
|
|
- def _fetch(self) -> list:
|
|
261
|
+ def _fetch(self) -> typing.List[Message]:
|
259
|
262
|
"""
|
260
|
263
|
Get news message from mailbox
|
261
|
264
|
:return: list of new mails
|
|
@@ -266,16 +269,16 @@ class MailFetcher(object):
|
266
|
269
|
if rv == 'OK':
|
267
|
270
|
# get mails
|
268
|
271
|
# TODO - G.M - 2017-11-15 Which files to select as new file ?
|
269
|
|
- # Unseen file or All file from a directory (old one should be moved/
|
270
|
|
- # deleted from mailbox during this process) ?
|
|
272
|
+ # Unseen file or All file from a directory (old one should be
|
|
273
|
+ # moved/ deleted from mailbox during this process) ?
|
271
|
274
|
rv, data = self._connection.search(None, "(UNSEEN)")
|
272
|
275
|
if rv == 'OK':
|
273
|
276
|
# get mail content
|
274
|
277
|
for num in data[0].split():
|
275
|
|
- # INFO - G.M - 2017-11-23 - Fetch (RFC288) to retrieve all complete mails
|
276
|
|
- # see example : https://docs.python.org/fr/3.5/library/imaplib.html#imap4-example .
|
277
|
|
- # Be careful, This method remove also mails from Unseen mails
|
278
|
|
-
|
|
278
|
+ # INFO - G.M - 2017-11-23 - Fetch (RFC288) to retrieve all
|
|
279
|
+ # complete mails see example : https://docs.python.org/fr/3.5/library/imaplib.html#imap4-example . # nopep8
|
|
280
|
+ # Be careful, This method remove also mails from Unseen
|
|
281
|
+ # mails
|
279
|
282
|
rv, data = self._connection.fetch(num, '(RFC822)')
|
280
|
283
|
if rv == 'OK':
|
281
|
284
|
msg = message_from_bytes(data[0][1])
|
|
@@ -291,13 +294,20 @@ class MailFetcher(object):
|
291
|
294
|
logger.debug(self, log.format(str(rv)))
|
292
|
295
|
return messages
|
293
|
296
|
|
294
|
|
- def _notify_tracim(self, mails: list) -> list:
|
|
297
|
+ def _notify_tracim(
|
|
298
|
+ self,
|
|
299
|
+ mails: typing.List[DecodedMail],
|
|
300
|
+ ) -> typing.List[DecodedMail]:
|
295
|
301
|
"""
|
296
|
302
|
Send http request to tracim endpoint
|
297
|
303
|
:param mails: list of mails to send
|
298
|
304
|
:return: unsended mails
|
299
|
305
|
"""
|
300
|
306
|
unsended_mails = []
|
|
307
|
+ # TODO BS 20171124: Look around mail.get_from_address(), mail.get_key()
|
|
308
|
+ # , mail.get_body() etc ... for raise InvalidEmailError if missing
|
|
309
|
+ # required informations (actually get_from_address raise IndexError
|
|
310
|
+ # if no from address for example) and catch it here
|
301
|
311
|
while mails:
|
302
|
312
|
mail = mails.pop()
|
303
|
313
|
msg = {'token': self.token,
|
|
@@ -312,13 +322,14 @@ class MailFetcher(object):
|
312
|
322
|
log = 'bad status code response when sending mail to tracim: {}' # nopep8
|
313
|
323
|
logger.error(self, log.format(str(r.status_code)))
|
314
|
324
|
# TODO - G.M - Verify exception correctly works
|
315
|
|
- except requests.exceptions.Timeout:
|
|
325
|
+ except requests.exceptions.Timeout as e:
|
316
|
326
|
log = 'Timeout error to transmit fetched mail to tracim : {}'
|
317
|
327
|
logger.error(self, log.format(str(e)))
|
318
|
|
- unsended_mail.append(mail)
|
|
328
|
+ unsended_mails.append(mail)
|
319
|
329
|
break
|
320
|
330
|
except requests.exceptions.RequestException as e:
|
321
|
331
|
log = 'Fail to transmit fetched mail to tracim : {}'
|
322
|
332
|
logger.error(self, log.format(str(e)))
|
323
|
333
|
break
|
|
334
|
+
|
324
|
335
|
return unsended_mails
|