Browse Source

Switch to fully oriented-object with DecodedMail object

Guénaël Muller 7 years ago
parent
commit
715adcff1e
2 changed files with 103 additions and 179 deletions
  1. 99 98
      tracim/tracim/lib/email_fetcher.py
  2. 4 81
      tracim/tracim/lib/test_email_fetcher.py

+ 99 - 98
tracim/tracim/lib/email_fetcher.py View File

@@ -19,101 +19,102 @@ from tracim.controllers.events import VALID_TOKEN_VALUE
19 19
 TRACIM_SPECIAL_KEY_HEADER = "X-Tracim-Key"
20 20
 
21 21
 
22
-def str_header(header: Header) -> str:
23
-    return str(make_header(decode_header(header)))
24
-
25
-
26
-def decode_mail(msg: Message)-> dict:
27
-    """
28
-    Get useful header and body content and decode from Message
29
-    :param msg:
30
-    :return:
31
-    """
32
-    mail_data = {}
33
-
34
-    try:
35
-        mail_data['subject'] = str_header(msg['subject'])
36
-        mail_data['from'] = parseaddr(msg['From'])[1]
37
-        # Reply key
38
-        mail_data['to'] = parseaddr(msg['To'])[1]
39
-        # INFO - G.M - 2017-11-15
40
-        #  We only need to save the first/oldest addr of references
41
-        mail_data['references'] = parseaddr(msg['References'])[1]
42
-        if TRACIM_SPECIAL_KEY_HEADER in msg:
43
-            mail_data[TRACIM_SPECIAL_KEY_HEADER] = str_header(msg[TRACIM_SPECIAL_KEY_HEADER])  # nopep8
44
-
45
-    except Exception:
46
-        # FIXME - G.M - 2017-11-15 - handle exceptions correctly
47
-        return {}
48
-
49
-    # TODO - G.M - 2017-11-15 - Parse properly HTML (and text ?) body
50
-    body = get_body_mime_part(msg)
51
-    if body:
52
-        charset = body.get_content_charset('iso-8859-1')
53
-        ctype = body.get_content_type()
54
-        if ctype == "text/plain":
55
-            mail_data['body'] = body.get_payload(decode=True).decode(charset)
56
-        elif ctype == "text/html":
57
-            mail_data['body'] = body.get_payload(decode=True).decode(charset)
22
+class DecodedMail(object):
23
+
24
+    def __init__(self, message: Message):
25
+        self._message = message
26
+
27
+    def _decode_header(self, header_title: str) -> typing.Optional[str]:
28
+        # FIXME : Handle exception
29
+        if header_title in self._message:
30
+            return str(make_header(decode_header(header)))
58 31
         else:
59
-            pass
60
-    else:
61
-        pass
62
-
63
-    return mail_data
64
-
65
-
66
-def get_body_mime_part(msg) -> Message:
67
-    # FIXME - G.M - 2017-11-16 - Use stdlib msg.get_body feature for py3.6+
68
-    # FIXME - G.M - 2017-11-16 - Check support for non-multipart mail
69
-    #assert msg.is_multipart()
70
-    part = None
71
-    # Check for html
72
-    for part in msg.walk():
73
-        ctype = part.get_content_type()
74
-        cdispo = str(part.get('Content-Disposition'))
75
-        if ctype == 'text/html' and 'attachment' not in cdispo:
76
-            return part
77
-    # checj fir plain text
78
-    for part in msg.walk():
79
-        ctype = part.get_content_type()
80
-        cdispo = str(part.get('Content-Disposition'))
81
-        if ctype == 'text/plain' and 'attachment' not in cdispo:
82
-            return part
83
-    return part
84
-
85
-
86
-def get_tracim_content_key(mail_data: dict) -> typing.Optional[str]:
87
-
88
-    """ Link mail_data dict to tracim content
89
-    First try checking special header, them check 'to' header
90
-    and finally check first(oldest) mail-id of 'references' header
91
-    """
92
-    key = None
93
-    if TRACIM_SPECIAL_KEY_HEADER in mail_data:
94
-        key = mail_data[TRACIM_SPECIAL_KEY_HEADER]
95
-    if key is None and 'to' in mail_data:
96
-        key = find_key_from_mail_adress(mail_data['to'])
97
-    if key is None and 'references' in mail_data:
98
-        mail_adress = mail_data['references']
99
-        key = find_key_from_mail_adress(mail_adress)
100
-    return key
101
-
102
-
103
-def find_key_from_mail_adress(mail_address: str) -> typing.Optional[str]:
104
-    """ Parse mail_adress-like string
105
-    to retrieve key.
106
-
107
-    :param mail_address: user+key@something like string
108
-    :return: key
109
-    """
110
-    username = mail_address.split('@')[0]
111
-    username_data = username.split('+')
112
-    if len(username_data) == 2:
113
-        key = username_data[1]
114
-    else:
32
+            return None
33
+
34
+    def get_subject(self) -> typing.Optional[str]:
35
+        return self._decode_header('subject')
36
+
37
+    def get_from_address(self) -> typing.Optional[str]:
38
+        return parseaddr(self._message['From'])[1]
39
+
40
+    def get_to_address(self)-> typing.Optional[str]:
41
+        return parseaddr(self._message['To'])[1]
42
+
43
+    def get_first_ref(self) -> typing.Optional[str]:
44
+        return parseaddr(self._message['References'])[1]
45
+
46
+    def get_special_key(self) -> typing.Optional[str]:
47
+        return self._decode_header(TRACIM_SPECIAL_KEY_HEADER)
48
+
49
+    def get_body(self) -> typing.Optional[str]:
50
+        body_part = self._get_mime_body_message()
51
+        body = None
52
+        if body_part:
53
+            charset = body_part.get_content_charset('iso-8859-1')
54
+            ctype = body_part.get_content_type()
55
+            if ctype == "text/plain":
56
+                body = body_part.get_payload(decode=True).decode(
57
+                    charset)
58
+            elif ctype == "text/html":
59
+                body = body_part.get_payload(decode=True).decode(
60
+                    charset)
61
+        return body
62
+
63
+    def _get_mime_body_message(self) -> typing.Optional[Message]:
64
+        # FIXME - G.M - 2017-11-16 - Use stdlib msg.get_body feature for py3.6+
65
+        # FIXME - G.M - 2017-11-16 - Check support for non-multipart mail
66
+        # assert msg.is_multipart()
67
+        part = None
68
+        # Check for html
69
+        for part in self._message.walk():
70
+            ctype = part.get_content_type()
71
+            cdispo = str(part.get('Content-Disposition'))
72
+            if ctype == 'text/html' and 'attachment' not in cdispo:
73
+                return part
74
+        # checj fir plain text
75
+        for part in self._message.walk():
76
+            ctype = part.get_content_type()
77
+            cdispo = str(part.get('Content-Disposition'))
78
+            if ctype == 'text/plain' and 'attachment' not in cdispo:
79
+                return part
80
+        return part
81
+
82
+    def get_key(self) -> typing.Optional[str]:
83
+
84
+        """
85
+        First try checking special header, them check 'to' header
86
+        and finally check first(oldest) mail-id of 'references' header
87
+        """
115 88
         key = None
116
-    return key
89
+        first_ref = self.get_first_ref()
90
+        to_address = self.get_to_address()
91
+        special_key = self.get_special_key()
92
+
93
+        if special_key:
94
+            key = special_key
95
+        if not key and to_address:
96
+            key = DecodedMail.find_key_from_mail_address(to_address)
97
+        if not key and first_ref:
98
+            key = DecodedMail.find_key_from_mail_address(first_ref)
99
+
100
+        return key
101
+
102
+    @staticmethod
103
+    def find_key_from_mail_address(mail_address: str) \
104
+            -> typing.Optional[str]:
105
+        """ Parse mail_adress-like string
106
+        to retrieve key.
107
+
108
+        :param mail_address: user+key@something like string
109
+        :return: key
110
+        """
111
+        username = mail_address.split('@')[0]
112
+        username_data = username.split('+')
113
+        if len(username_data) == 2:
114
+            key = username_data[1]
115
+        else:
116
+            key = None
117
+        return key
117 118
 
118 119
 
119 120
 class MailFetcher(object):
@@ -196,7 +197,8 @@ class MailFetcher(object):
196 197
                     rv, data = self._connection.fetch(num, '(RFC822)')
197 198
                     if rv == 'OK':
198 199
                         msg = message_from_bytes(data[0][1])
199
-                        self._mails.append(msg)
200
+                        decodedmsg = DecodedMail(msg)
201
+                        self._mails.append(decodedmsg)
200 202
                     else:
201 203
                         log = 'IMAP : Unable to get mail : {}'
202 204
                         logger.debug(self, log.format(str(rv)))
@@ -210,12 +212,11 @@ class MailFetcher(object):
210 212
     def _notify_tracim(self) -> None:
211 213
         while self._mails:
212 214
             mail = self._mails.pop()
213
-            decoded_mail = decode_mail(mail)
214 215
             msg = {"token": VALID_TOKEN_VALUE,
215
-                   "user_mail": decoded_mail['from'],
216
-                   "content_id": get_tracim_content_key(decoded_mail),
216
+                   "user_mail": mail.get_from_address(),
217
+                   "content_id": mail.get_key(),
217 218
                    "payload": {
218
-                       "content": decoded_mail['body'],
219
+                       "content": mail.get_body(),
219 220
                    }}
220 221
             # FIXME - G.M - 2017-11-15 - Catch exception from http request
221 222
             requests.post(self.endpoint, json=msg)

+ 4 - 81
tracim/tracim/lib/test_email_fetcher.py View File

@@ -1,88 +1,11 @@
1
-from email_fetcher import decode_mail, get_tracim_content_key,\
2
-    TRACIM_SPECIAL_KEY_HEADER,find_key_from_mail_adress
3
-from email.mime.multipart import MIMEMultipart
4
-from email.utils import parsedate_tz,mktime_tz
5
-import datetime
6
-
7
-# decode_mail
8
-
9
-def test_decode_mail_ok():
10
-    msg = MIMEMultipart()
11
-    msg['From'] = 'a@home'
12
-    msg['To'] = 'b@home'
13
-    msg['Subject'] = "test"
14
-    #msg.add_header('Reply-To', '<a+key@home>')
15
-    msg.add_header('References', '<reply+key@home>')
16
-    msg.add_header(TRACIM_SPECIAL_KEY_HEADER, 'key')
17
-    maildata=decode_mail(msg)
18
-
19
-    assert maildata == {
20
-        TRACIM_SPECIAL_KEY_HEADER: 'key',
21
-        'from': 'a@home',
22
-        'to': 'b@home',
23
-        'subject':'test',
24
-        'references':'reply+key@home',
25
-    }
26
-# get_tracim_content_key
27
-
28
-def test_get_tracim_content_key_empty():
29
-    mail_data={}
30
-    assert get_tracim_content_key(mail_data) == None
31
-
32
-def test_get_tracim_content_key_no_key():
33
-    mail_data={
34
-        'to':'a@b',
35
-        'references':'<a@b> <b@c>'
36
-    }
37
-    assert get_tracim_content_key(mail_data) == None
38
-
39
-def test_get_tracim_content_key_special_key():
40
-    mail_data={
41
-        'to':'a@b',
42
-        'references':'<a@b> <b@c>',
43
-        TRACIM_SPECIAL_KEY_HEADER : 'key'
44
-    }
45
-    assert get_tracim_content_key(mail_data) == 'key'
46
-
47
-
48
-def test_get_tracim_content_key_to_key():
49
-    mail_data={
50
-        'to':'a+key@b',
51
-        'references':'<a@b> <b@c>',
52
-    }
53
-    assert get_tracim_content_key(mail_data) == 'key'
54
-
55
-def test_get_tracim_content_key_references_key():
56
-    mail_data={
57
-        'to':'a@b',
58
-        'references':'<a+key@b> <b@c>',
59
-    }
60
-    assert get_tracim_content_key(mail_data) == 'key'
61
-
62
-def test_get_tracim_content_key_order():
63
-    mail_data={
64
-        'to':'a+2@b',
65
-        'references':'<a+3@b> <b@c>',
66
-        TRACIM_SPECIAL_KEY_HEADER: '1'
67
-    }
68
-    assert get_tracim_content_key(mail_data) == '1'
69
-    mail_data={
70
-        'to':'a+2@b',
71
-        'references':'<a+3@b> <b@c>',
72
-    }
73
-    assert get_tracim_content_key(mail_data) == '2'
74
-
75
-    mail_data={
76
-        'references':'<a+3@b> <b@c>',
77
-    }
78
-    assert get_tracim_content_key(mail_data) == '3'
1
+from email_fetcher import DecodedMail
79 2
 
80 3
 # find_key_from_mail_address
81 4
 
82 5
 def test_find_key_from_mail_address_no_key():
83
-    mail_adress="a@b"
84
-    assert find_key_from_mail_adress(mail_adress) == None
6
+    mail_address="a@b"
7
+    assert DecodedMail.find_key_from_mail_address(mail_address) == None
85 8
 
86 9
 def test_find_key_from_mail_adress_key():
87 10
     mail_address="a+key@b"
88
-    assert find_key_from_mail_adress(mail_address) == 'key'
11
+    assert DecodedMail.find_key_from_mail_address(mail_address) == 'key'