浏览代码

Add Outlook.com compat with email_body_parser

Guénaël Muller 7 年前
父节点
当前提交
7c6df37177
共有 2 个文件被更改,包括 245 次插入99 次删除
  1. 47 42
      tracim/tracim/lib/email_body_parser.py
  2. 198 57
      tracim/tracim/tests/library/test_email_body_parser.py

+ 47 - 42
tracim/tracim/lib/email_body_parser.py 查看文件

26
     Data Structure to Distinct part of a Mail body into a "list" of BodyMailPart
26
     Data Structure to Distinct part of a Mail body into a "list" of BodyMailPart
27
     When 2 similar BodyMailPart (same part_type) are added one after the other,
27
     When 2 similar BodyMailPart (same part_type) are added one after the other,
28
     it doesn't create a new Part, it just merge those elements into one.
28
     it doesn't create a new Part, it just merge those elements into one.
29
-    It should always have only one Signature type part, at the end of the body.
29
+    It should always have only one Signature type part, normally
30
+    at the end of the body.
30
     This object doesn't provide other set method than append() in order to
31
     This object doesn't provide other set method than append() in order to
31
     preserve object coherence.
32
     preserve object coherence.
32
     """
33
     """
33
     def __init__(self) -> None:
34
     def __init__(self) -> None:
34
         self._list = []  # type; List[BodyMailPart]
35
         self._list = []  # type; List[BodyMailPart]
36
+        # INFO - G.M -
37
+        # automatically merge new value with last item if true, without any
38
+        # part_type check, same type as the older one, useful when some tag
39
+        # say "all elem after me is Signature"
40
+        self.follow = False
41
+
35
 
42
 
36
     def __len__(self) -> int:
43
     def __len__(self) -> int:
37
         return len(self._list)
44
         return len(self._list)
45
 
52
 
46
     def append(self, value) -> None:
53
     def append(self, value) -> None:
47
         BodyMailParts._check_value(value)
54
         BodyMailParts._check_value(value)
48
-        self._check_sign_last_elem(value)
49
         self._append(value)
55
         self._append(value)
50
 
56
 
51
     def _append(self, value):
57
     def _append(self, value):
52
-        if len(self._list) > 0 and self._list[-1].part_type == value.part_type:
58
+        same_type_as_last =len(self._list) > 0 and \
59
+                           self._list[-1].part_type == value.part_type
60
+        if same_type_as_last or self.follow :
53
             self._list[-1].text += value.text
61
             self._list[-1].text += value.text
54
         else:
62
         else:
55
             self._list.append(value)
63
             self._list.append(value)
59
         if not isinstance(value, BodyMailPart):
67
         if not isinstance(value, BodyMailPart):
60
             raise TypeError()
68
             raise TypeError()
61
 
69
 
62
-    def _check_sign_last_elem(self, value: BodyMailPart) -> None:
63
-        """
64
-        Check if last elem is a signature, if true, refuse to add a
65
-        non-signature item.
66
-        :param value: BodyMailPart to check
67
-        :return: None
68
-        """
69
-        if len(self._list) > 0:
70
-            if self._list[-1].part_type == BodyMailPartType.Signature and \
71
-                            value.part_type != BodyMailPartType.Signature:
72
-                raise SignatureIndexError(
73
-                    "Can't add element after signature element.")
74
-
75
-    def disable_signature(self) -> None:
76
-        """
77
-        Consider the chosen signature to a normal main content element
78
-        :return: None
79
-        """
80
-        if (
81
-            len(self._list) > 0 and
82
-            self._list[-1].part_type == BodyMailPartType.Signature
83
-        ):
84
-            self._list[-1].part_type = BodyMailPartType.Main
85
-            # If possible, concatenate with previous elem
86
-            if (
87
-                len(self._list) > 1 and
88
-                self._list[-2].part_type == BodyMailPartType.Main
89
-            ):
90
-                self._list[-2].text += self._list[-1].text
91
-                del self._list[-1]
92
-
93
     def drop_part_type(self, part_type: str) -> None:
70
     def drop_part_type(self, part_type: str) -> None:
94
         """
71
         """
95
         Drop all elem of one part_type
72
         Drop all elem of one part_type
151
         return cls._is_standard_quote(elem) \
128
         return cls._is_standard_quote(elem) \
152
                or cls._is_thunderbird_quote(elem) \
129
                or cls._is_thunderbird_quote(elem) \
153
                or cls._is_gmail_quote(elem) \
130
                or cls._is_gmail_quote(elem) \
131
+               or cls._is_outlook_com_quote(elem) \
154
                or cls._is_yahoo_quote(elem) \
132
                or cls._is_yahoo_quote(elem) \
155
                or cls._is_roundcube_quote(elem)
133
                or cls._is_roundcube_quote(elem)
156
 
134
 
183
         return False
161
         return False
184
 
162
 
185
     @classmethod
163
     @classmethod
164
+    def _is_outlook_com_quote(
165
+        cls,
166
+        elem: typing.Union[Tag, NavigableString]
167
+    ) -> bool:
168
+        if cls._has_attr_value(elem, 'id', 'divRplyFwdMsg'):
169
+            return True
170
+        return False
171
+
172
+    @classmethod
186
     def _is_yahoo_quote(
173
     def _is_yahoo_quote(
187
             cls,
174
             cls,
188
             elem: typing.Union[Tag, NavigableString]
175
             elem: typing.Union[Tag, NavigableString]
205
             elem: typing.Union[Tag, NavigableString]
192
             elem: typing.Union[Tag, NavigableString]
206
     ) -> bool:
193
     ) -> bool:
207
         return cls._is_thunderbird_signature(elem) \
194
         return cls._is_thunderbird_signature(elem) \
208
-               or cls._is_gmail_signature(elem)
195
+               or cls._is_gmail_signature(elem) \
196
+               or cls._is_outlook_com_signature(elem)
209
 
197
 
210
     @classmethod
198
     @classmethod
211
     def _is_thunderbird_signature(
199
     def _is_thunderbird_signature(
233
                     return True
221
                     return True
234
         return False
222
         return False
235
 
223
 
224
+    @classmethod
225
+    def _is_outlook_com_signature(
226
+            cls,
227
+            elem: typing.Union[Tag, NavigableString]
228
+    ) -> bool:
229
+        if cls._has_attr_value(elem, 'id', 'Signature'):
230
+            return True
231
+        return False
236
 
232
 
237
 class ParsedHTMLMail(object):
233
 class ParsedHTMLMail(object):
238
     """
234
     """
273
                 tree.find().name.lower() in ['body', 'div']:
269
                 tree.find().name.lower() in ['body', 'div']:
274
             tree.find().unwrap()
270
             tree.find().unwrap()
275
 
271
 
276
-        # drop some not useful html elem
272
+        # drop some html elem
277
         for tag in tree.findAll():
273
         for tag in tree.findAll():
278
-            if tag.name.lower() in ['br']:
274
+            # HACK - G.M - 2017-11-28 - Unwrap outlook.com mail
275
+            # if Text -> Signature -> Quote Mail
276
+            # Text and signature are wrapped into divtagdefaultwrapper
277
+            if tag.attrs.get('id'):
278
+                if 'divtagdefaultwrapper' in tag.attrs['id']:
279
+                    tag.unwrap()
280
+            # Hack - G.M - 2017-11-28 : remove tag with no enclosure
281
+            # <br> and <hr> tag alone broke html.parser tree,
282
+            # Using another parser may be a solution.
283
+            if tag.name.lower() in ['br','hr']:
279
                 tag.unwrap()
284
                 tag.unwrap()
280
                 continue
285
                 continue
281
             if tag.name.lower() in ['script', 'style']:
286
             if tag.name.lower() in ['script', 'style']:
298
             elif HtmlMailSignatureChecker.is_signature(tag):
303
             elif HtmlMailSignatureChecker.is_signature(tag):
299
                 part_type = BodyMailPartType.Signature
304
                 part_type = BodyMailPartType.Signature
300
             element = BodyMailPart(txt, part_type)
305
             element = BodyMailPart(txt, part_type)
301
-            try:
302
-                elements.append(element)
303
-            except SignatureIndexError:
304
-                elements.disable_signature()
305
-                elements.append(element)
306
+            elements.append(element)
307
+            # INFO - G.M - 2017-11-28 - Outlook.com special case
308
+            # all after quote tag is quote
309
+            if HtmlMailQuoteChecker._is_outlook_com_quote(tag):
310
+                elements.follow = True
306
         return elements
311
         return elements
307
 
312
 
308
     @classmethod
313
     @classmethod

+ 198 - 57
tracim/tracim/tests/library/test_email_body_parser.py 查看文件

53
         main_elem = soup.find()
53
         main_elem = soup.find()
54
         assert HtmlMailQuoteChecker._is_gmail_quote(main_elem) is False
54
         assert HtmlMailQuoteChecker._is_gmail_quote(main_elem) is False
55
 
55
 
56
+    def test_unit__is_outlook_com_quote_ok(self):
57
+        soup = BeautifulSoup('<div id="divRplyFwdMsg"></div>', 'html.parser')
58
+        main_elem = soup.find()
59
+        assert HtmlMailQuoteChecker._is_outlook_com_quote(main_elem) is True
60
+
61
+    def test_unit__is_outlook_com_quote_no(self):
62
+        soup = BeautifulSoup('<div id="Signature"></div>', 'html.parser')
63
+        main_elem = soup.find()
64
+        assert HtmlMailQuoteChecker._is_outlook_com_quote(main_elem) is False
65
+
56
     # TODO - G.M - 2017-11-24 - Check Yahoo and New roundcube html mail with
66
     # TODO - G.M - 2017-11-24 - Check Yahoo and New roundcube html mail with
57
     # correct mail example
67
     # correct mail example
58
 
68
 
96
         main_elem = soup.find()
106
         main_elem = soup.find()
97
         assert HtmlMailSignatureChecker._is_gmail_signature(main_elem) is True
107
         assert HtmlMailSignatureChecker._is_gmail_signature(main_elem) is True
98
 
108
 
109
+    def test_unit__is_outlook_com_signature_no(self):
110
+        soup = BeautifulSoup('<div id="divRplyFwdMsg"></div>', 'html.parser')
111
+        main_elem = soup.find()
112
+        assert HtmlMailSignatureChecker._is_outlook_com_signature(main_elem) \
113
+               is False
114
+
115
+    def test_unit__is_outlook_com_signature_ok(self):
116
+        soup = BeautifulSoup('<div id="Signature"></div>', 'html.parser')
117
+        main_elem = soup.find()
118
+        assert HtmlMailSignatureChecker._is_outlook_com_signature(main_elem) \
119
+               is True
120
+
99
 class TestBodyMailsParts(TestStandard):
121
 class TestBodyMailsParts(TestStandard):
100
 
122
 
101
     def test_unit__std_list_methods(self):
123
     def test_unit__std_list_methods(self):
138
         a = BodyMailPart('a', BodyMailPartType.Main)
160
         a = BodyMailPart('a', BodyMailPartType.Main)
139
         mail_parts._check_value(a)
161
         mail_parts._check_value(a)
140
 
162
 
141
-    @raises(SignatureIndexError)
142
-    def test_unit__check_sign_last_elem_check_main_after_sign(self):
143
-        mail_parts = BodyMailParts()
144
-        a = BodyMailPart('a', BodyMailPartType.Main)
145
-        mail_parts._list.append(a)
146
-        b = BodyMailPart('b', BodyMailPartType.Signature)
147
-        mail_parts._list.append(b)
148
-        c = BodyMailPart('c', BodyMailPartType.Main)
149
-        mail_parts._check_sign_last_elem(c)
150
-
151
-    def test_unit__check_sign_last_elem_check_sign_after_sign(self):
152
-        mail_parts = BodyMailParts()
153
-        a = BodyMailPart('a', BodyMailPartType.Main)
154
-        mail_parts._list.append(a)
155
-        b = BodyMailPart('b', BodyMailPartType.Signature)
156
-        mail_parts._list.append(b)
157
-        c = BodyMailPart('c', BodyMailPartType.Signature)
158
-        mail_parts._check_sign_last_elem(c)
159
-
160
-    def test_unit__disable_signature_no_sign(self):
161
-        mail_parts = BodyMailParts()
162
-        a = BodyMailPart('a', BodyMailPartType.Main)
163
-        mail_parts._list.append(a)
164
-        b = BodyMailPart('b', BodyMailPartType.Quote)
165
-        mail_parts._list.append(b)
166
-        mail_parts.disable_signature()
167
-        assert mail_parts[1] == b
168
-
169
-    def test_unit__disable_signature_sign_quote_as_previous_elem(self):
170
-        mail_parts = BodyMailParts()
171
-        a = BodyMailPart('a', BodyMailPartType.Main)
172
-        mail_parts._list.append(a)
173
-        b = BodyMailPart('b', BodyMailPartType.Quote)
174
-        mail_parts._list.append(b)
175
-        c = BodyMailPart('c', BodyMailPartType.Signature)
176
-        mail_parts._list.append(c)
177
-        mail_parts.disable_signature()
178
-        assert len(mail_parts) == 3
179
-        assert mail_parts[2].text == 'c'
180
-        assert mail_parts[2].part_type == BodyMailPartType.Main
181
-
182
-    def test_unit__disable_signature_sign_main_as_previous_elem(self):
183
-        mail_parts = BodyMailParts()
184
-        a = BodyMailPart('a', BodyMailPartType.Quote)
185
-        mail_parts._list.append(a)
186
-        b = BodyMailPart('b', BodyMailPartType.Main)
187
-        mail_parts._list.append(b)
188
-        c = BodyMailPart('c', BodyMailPartType.Signature)
189
-        mail_parts._list.append(c)
190
-        mail_parts.disable_signature()
191
-        assert len(mail_parts) == 2
192
-        assert mail_parts[1].text == 'bc'
193
-        assert mail_parts[1].part_type == BodyMailPartType.Main
194
-
195
     def test_unit__drop_part_type(self):
163
     def test_unit__drop_part_type(self):
196
         mail_parts = BodyMailParts()
164
         mail_parts = BodyMailParts()
197
         a = BodyMailPart('a', BodyMailPartType.Main)
165
         a = BodyMailPart('a', BodyMailPartType.Main)
251
 
219
 
252
 class TestParsedMail(TestStandard):
220
 class TestParsedMail(TestStandard):
253
 
221
 
254
-    def test_other__check_gmail_mail(self):
222
+    def test_other__check_gmail_mail_text_only(self):
255
         text_only = '''<div dir="ltr">Voici le texte<br></div>'''
223
         text_only = '''<div dir="ltr">Voici le texte<br></div>'''
256
         mail = ParsedHTMLMail(text_only)
224
         mail = ParsedHTMLMail(text_only)
257
         elements = mail.get_elements()
225
         elements = mail.get_elements()
258
         assert len(elements) == 1
226
         assert len(elements) == 1
259
         assert elements[0].part_type == BodyMailPartType.Main
227
         assert elements[0].part_type == BodyMailPartType.Main
260
 
228
 
229
+    def test_other__check_gmail_mail_text_signature(self):
261
         text_and_signature = '''
230
         text_and_signature = '''
262
         <div dir="ltr">POF<br clear="all"><div><br>-- <br>
231
         <div dir="ltr">POF<br clear="all"><div><br>-- <br>
263
         <div class="gmail_signature" data-smartmail="gmail_signature">
232
         <div class="gmail_signature" data-smartmail="gmail_signature">
273
         assert elements[0].part_type == BodyMailPartType.Main
242
         assert elements[0].part_type == BodyMailPartType.Main
274
         assert elements[1].part_type == BodyMailPartType.Signature
243
         assert elements[1].part_type == BodyMailPartType.Signature
275
 
244
 
245
+    def test_other__check_gmail_mail_text_quote(self):
276
         text_and_quote = '''
246
         text_and_quote = '''
277
         <div dir="ltr">Réponse<br>
247
         <div dir="ltr">Réponse<br>
278
         <div class="gmail_extra"><br>
248
         <div class="gmail_extra"><br>
301
         assert elements[0].part_type == BodyMailPartType.Main
271
         assert elements[0].part_type == BodyMailPartType.Main
302
         assert elements[1].part_type == BodyMailPartType.Quote
272
         assert elements[1].part_type == BodyMailPartType.Quote
303
 
273
 
274
+    def test_other__check_gmail_mail_text_quote_text(self):
304
         text_quote_text = '''
275
         text_quote_text = '''
305
               <div dir="ltr">Avant<br>
276
               <div dir="ltr">Avant<br>
306
               <div class="gmail_extra"><br>
277
               <div class="gmail_extra"><br>
338
         assert elements[1].part_type == BodyMailPartType.Quote
309
         assert elements[1].part_type == BodyMailPartType.Quote
339
         assert elements[2].part_type == BodyMailPartType.Main
310
         assert elements[2].part_type == BodyMailPartType.Main
340
 
311
 
341
-
312
+    def test_other__check_gmail_mail_text_quote_signature(self):
342
         text_quote_signature = '''
313
         text_quote_signature = '''
343
         <div dir="ltr">Hey !<br>
314
         <div dir="ltr">Hey !<br>
344
                  </div>
315
                  </div>
390
         assert elements[0].part_type == BodyMailPartType.Main
361
         assert elements[0].part_type == BodyMailPartType.Main
391
         assert elements[1].part_type == BodyMailPartType.Quote
362
         assert elements[1].part_type == BodyMailPartType.Quote
392
 
363
 
364
+    def test_other__check_gmail_mail_text_quote_text_signature(self):
393
         text_quote_text_sign = '''
365
         text_quote_text_sign = '''
394
         <div dir="ltr">Test<br>
366
         <div dir="ltr">Test<br>
395
         <div class="gmail_extra"><br>
367
         <div class="gmail_extra"><br>
442
         assert elements[2].part_type == BodyMailPartType.Main
414
         assert elements[2].part_type == BodyMailPartType.Main
443
         assert elements[3].part_type == BodyMailPartType.Signature
415
         assert elements[3].part_type == BodyMailPartType.Signature
444
 
416
 
445
-    def test_other__check_thunderbird_mail(self):
417
+    def test_other__check_thunderbird_mail_text_only(self):
446
 
418
 
447
         text_only = '''Coucou<br><br><br>'''
419
         text_only = '''Coucou<br><br><br>'''
448
         mail = ParsedHTMLMail(text_only)
420
         mail = ParsedHTMLMail(text_only)
450
         assert len(elements) == 1
422
         assert len(elements) == 1
451
         assert elements[0].part_type == BodyMailPartType.Main
423
         assert elements[0].part_type == BodyMailPartType.Main
452
 
424
 
425
+    def test_other__check_thunderbird_mail_text_signature(self):
453
         text_and_signature = '''
426
         text_and_signature = '''
454
         <p>Test<br>
427
         <p>Test<br>
455
         </p>
428
         </p>
462
         assert elements[0].part_type == BodyMailPartType.Main
435
         assert elements[0].part_type == BodyMailPartType.Main
463
         assert elements[1].part_type == BodyMailPartType.Signature
436
         assert elements[1].part_type == BodyMailPartType.Signature
464
 
437
 
438
+    def test_other__check_thunderbird_mail_text_quote(self):
465
         text_and_quote = '''
439
         text_and_quote = '''
466
             <p>Pof<br>
440
             <p>Pof<br>
467
             </p>
441
             </p>
486
         assert elements[0].part_type == BodyMailPartType.Main
460
         assert elements[0].part_type == BodyMailPartType.Main
487
         assert elements[1].part_type == BodyMailPartType.Quote
461
         assert elements[1].part_type == BodyMailPartType.Quote
488
 
462
 
463
+    def test_other__check_thunderbird_mail_text_quote_text(self):
489
         text_quote_text = '''
464
         text_quote_text = '''
490
         <p>Pof<br>
465
         <p>Pof<br>
491
         </p>
466
         </p>
523
         assert elements[1].part_type == BodyMailPartType.Quote
498
         assert elements[1].part_type == BodyMailPartType.Quote
524
         assert elements[2].part_type == BodyMailPartType.Main
499
         assert elements[2].part_type == BodyMailPartType.Main
525
 
500
 
501
+    def test_other__check_thunderbird_mail_text_quote_signature(self):
526
         text_quote_signature = '''
502
         text_quote_signature = '''
527
         <p>Coucou<br>
503
         <p>Coucou<br>
528
         </p>
504
         </p>
554
         assert elements[1].part_type == BodyMailPartType.Quote
530
         assert elements[1].part_type == BodyMailPartType.Quote
555
         assert elements[2].part_type == BodyMailPartType.Signature
531
         assert elements[2].part_type == BodyMailPartType.Signature
556
 
532
 
533
+    def test_other__check_thunderbird_mail_text_quote_text_signature(self):
557
         text_quote_text_sign = '''
534
         text_quote_text_sign = '''
558
         <p>Avant<br>
535
         <p>Avant<br>
559
         </p>
536
         </p>
579
         assert elements[2].part_type == BodyMailPartType.Main
556
         assert elements[2].part_type == BodyMailPartType.Main
580
         assert elements[3].part_type == BodyMailPartType.Signature
557
         assert elements[3].part_type == BodyMailPartType.Signature
581
 
558
 
559
+    # INFO - G.M - 2017-11-28 - Test for outlook.com webapp html mail
560
+    # outlook.com ui doesn't seems to allow complex reply, new message
561
+    # and signature are always before quoted one.
562
+
563
+    def test_other__check_outlook_com_mail_text_only(self):
564
+
565
+        text_only = '''
566
+        <div id="divtagdefaultwrapper"
567
+        style="font-size:12pt;color:#000000;
568
+        font-family:Calibri,Helvetica,sans-serif;"
569
+        dir="ltr">
570
+        <p style="margin-top:0;margin-bottom:0">message<br>
571
+        </p>
572
+        </div>
573
+        '''
574
+        mail = ParsedHTMLMail(text_only)
575
+        elements = mail.get_elements()
576
+        assert len(elements) == 1
577
+        assert elements[0].part_type == BodyMailPartType.Main
578
+
579
+    def test_other__check_outlook_com_mail_text_signature(self):
580
+        text_and_signature = '''
581
+        <div id="divtagdefaultwrapper"
582
+        style="font-size:12pt;color:#000000;
583
+        font-family:Calibri,Helvetica,sans-serif;"
584
+          dir="ltr">
585
+          <p style="margin-top:0;margin-bottom:0">Test<br>
586
+          </p>
587
+          <p style="margin-top:0;margin-bottom:0"><br>
588
+          </p>
589
+          <div id="Signature">
590
+            <div id="divtagdefaultwrapper" style="font-size: 12pt; color:
591
+              rgb(0, 0, 0); background-color: rgb(255, 255, 255);
592
+              font-family:
593
+              Calibri,Arial,Helvetica,sans-serif,&quot;EmojiFont&quot;,&quot;Apple
594
+              Color Emoji&quot;,&quot;Segoe UI
595
+              Emoji&quot;,NotoColorEmoji,&quot;Segoe UI
596
+              Symbol&quot;,&quot;Android Emoji&quot;,EmojiSymbols;">
597
+              Envoyé à partir de <a href="http://aka.ms/weboutlook"
598
+                id="LPNoLP">Outlook</a></div>
599
+          </div>
600
+        </div>
601
+        '''
602
+        mail = ParsedHTMLMail(text_and_signature)
603
+        elements = mail.get_elements()
604
+        assert len(elements) == 2
605
+        assert elements[0].part_type == BodyMailPartType.Main
606
+        assert elements[1].part_type == BodyMailPartType.Signature
607
+
608
+    def test_other__check_outlook_com_mail_text_quote(self):
609
+        text_and_quote = '''
610
+        <div id="divtagdefaultwrapper"
611
+        style="font-size:12pt;color:#000000;font-family:Calibri,Helvetica,sans-serif;"
612
+        dir="ltr">
613
+        <p style="margin-top:0;margin-bottom:0">Salut !<br>
614
+        </p>
615
+        </div>
616
+        <hr style="display:inline-block;width:98%" tabindex="-1">
617
+        <div id="divRplyFwdMsg" dir="ltr"><font style="font-size:11pt"
618
+        color="#000000" face="Calibri, sans-serif"><b>De :</b> John Doe<br>
619
+        <b>Envoyé :</b> mardi 28 novembre 2017 12:44:59<br>
620
+        <b>À :</b> dev.bidule@localhost.fr<br>
621
+        <b>Objet :</b> voila</font>
622
+        <div>&nbsp;</div>
623
+        </div>
624
+        <style type="text/css" style="display:none">
625
+        <!--
626
+        p
627
+        &#x09;{margin-top:0;
628
+        &#x09;margin-bottom:0}
629
+        -->
630
+        </style>
631
+        <div dir="ltr">
632
+        <div id="x_divtagdefaultwrapper" dir="ltr" style="font-size:12pt;
633
+        color:#000000; font-family:Calibri,Helvetica,sans-serif">
634
+        Contenu
635
+        <p style="margin-top:0; margin-bottom:0"><br>
636
+        </p>
637
+        <div id="x_Signature">
638
+          <div id="x_divtagdefaultwrapper" dir="ltr"
639
+            style="font-size:12pt; color:rgb(0,0,0);
640
+            background-color:rgb(255,255,255);
641
+        font-family:Calibri,Arial,Helvetica,sans-serif,&quot;EmojiFont&quot;,&quot;Apple
642
+            Color Emoji&quot;,&quot;Segoe UI
643
+            Emoji&quot;,NotoColorEmoji,&quot;Segoe UI
644
+            Symbol&quot;,&quot;Android Emoji&quot;,EmojiSymbols">
645
+            DLMQDNLQNDMLQS<br>
646
+            qs<br>
647
+            dqsd<br>
648
+            d<br>
649
+            qsd<br>
650
+          </div>
651
+        </div>
652
+        </div>
653
+        </div>
654
+        '''
655
+        mail = ParsedHTMLMail(text_and_quote)
656
+        elements = mail.get_elements()
657
+        assert len(elements) == 2
658
+        assert elements[0].part_type == BodyMailPartType.Main
659
+        assert elements[1].part_type == BodyMailPartType.Quote
660
+
661
+    def test_other__check_outlook_com_mail_text_signature_quote(self):
662
+        text_signature_quote = '''
663
+        <div id="divtagdefaultwrapper"
664
+        style="font-size:12pt;color:#000000;font-family:Calibri,Helvetica,sans-serif;"
665
+        dir="ltr">
666
+        <p style="margin-top:0;margin-bottom:0">Salut !<br>
667
+        </p>
668
+        <p style="margin-top:0;margin-bottom:0"><br>
669
+        </p>
670
+        <div id="Signature">
671
+        <div id="divtagdefaultwrapper" dir="ltr" style="font-size: 12pt;
672
+        color: rgb(0, 0, 0); background-color: rgb(255, 255, 255);
673
+        font-family:
674
+        Calibri,Arial,Helvetica,sans-serif,&quot;EmojiFont&quot;,&quot;Apple
675
+        Color Emoji&quot;,&quot;Segoe UI
676
+        Emoji&quot;,NotoColorEmoji,&quot;Segoe UI
677
+        Symbol&quot;,&quot;Android Emoji&quot;,EmojiSymbols;">
678
+        Envoyée depuis Outlook<br>
679
+        </div>
680
+        </div>
681
+        </div>
682
+        <hr style="display:inline-block;width:98%" tabindex="-1">
683
+        <div id="divRplyFwdMsg" dir="ltr"><font style="font-size:11pt"
684
+        color="#000000" face="Calibri, sans-serif"><b>De :</b> John Doe
685
+        &lt;dev.bidule@localhost.fr&gt;<br>
686
+        <b>Envoyé :</b> mardi 28 novembre 2017 12:51:42<br>
687
+        <b>À :</b> John Doe<br>
688
+        <b>Objet :</b> Re: Test</font>
689
+        <div>&nbsp;</div>
690
+        </div>
691
+        <div style="background-color:#FFFFFF">
692
+        <p>Coucou<br>
693
+        </p>
694
+        <br>
695
+        <div class="x_moz-cite-prefix">Le 28/11/2017 à 12:39, John Doe a
696
+        écrit&nbsp;:<br>
697
+        </div>
698
+        <blockquote type="cite">
699
+        <div id="x_divtagdefaultwrapper" dir="ltr">
700
+        <p>Test<br>
701
+        </p>
702
+        <p><br>
703
+        </p>
704
+        <div id="x_Signature">
705
+        <div id="x_divtagdefaultwrapper">Envoyé à partir de <a
706
+        href="http://aka.ms/weboutlook" id="LPNoLP">
707
+        Outlook</a></div>
708
+        </div>
709
+        </div>
710
+        </blockquote>
711
+        <br>
712
+        <div class="x_moz-signature">-- <br>
713
+        TEST DE signature</div>
714
+        </div>
715
+        '''
716
+
717
+        mail = ParsedHTMLMail(text_signature_quote)
718
+        elements = mail.get_elements()
719
+        assert len(elements) == 3
720
+        assert elements[0].part_type == BodyMailPartType.Main
721
+        assert elements[1].part_type == BodyMailPartType.Signature
722
+        assert elements[2].part_type == BodyMailPartType.Quote
582
 
723