Browse Source

Add Outlook.com compat with email_body_parser

Guénaël Muller 6 years ago
parent
commit
7c6df37177

+ 47 - 42
tracim/tracim/lib/email_body_parser.py View File

@@ -26,12 +26,19 @@ class BodyMailParts(object):
26 26
     Data Structure to Distinct part of a Mail body into a "list" of BodyMailPart
27 27
     When 2 similar BodyMailPart (same part_type) are added one after the other,
28 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 31
     This object doesn't provide other set method than append() in order to
31 32
     preserve object coherence.
32 33
     """
33 34
     def __init__(self) -> None:
34 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 43
     def __len__(self) -> int:
37 44
         return len(self._list)
@@ -45,11 +52,12 @@ class BodyMailParts(object):
45 52
 
46 53
     def append(self, value) -> None:
47 54
         BodyMailParts._check_value(value)
48
-        self._check_sign_last_elem(value)
49 55
         self._append(value)
50 56
 
51 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 61
             self._list[-1].text += value.text
54 62
         else:
55 63
             self._list.append(value)
@@ -59,37 +67,6 @@ class BodyMailParts(object):
59 67
         if not isinstance(value, BodyMailPart):
60 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 70
     def drop_part_type(self, part_type: str) -> None:
94 71
         """
95 72
         Drop all elem of one part_type
@@ -151,6 +128,7 @@ class HtmlMailQuoteChecker(HtmlChecker):
151 128
         return cls._is_standard_quote(elem) \
152 129
                or cls._is_thunderbird_quote(elem) \
153 130
                or cls._is_gmail_quote(elem) \
131
+               or cls._is_outlook_com_quote(elem) \
154 132
                or cls._is_yahoo_quote(elem) \
155 133
                or cls._is_roundcube_quote(elem)
156 134
 
@@ -183,6 +161,15 @@ class HtmlMailQuoteChecker(HtmlChecker):
183 161
         return False
184 162
 
185 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 173
     def _is_yahoo_quote(
187 174
             cls,
188 175
             elem: typing.Union[Tag, NavigableString]
@@ -205,7 +192,8 @@ class HtmlMailSignatureChecker(HtmlChecker):
205 192
             elem: typing.Union[Tag, NavigableString]
206 193
     ) -> bool:
207 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 198
     @classmethod
211 199
     def _is_thunderbird_signature(
@@ -233,6 +221,14 @@ class HtmlMailSignatureChecker(HtmlChecker):
233 221
                     return True
234 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 233
 class ParsedHTMLMail(object):
238 234
     """
@@ -273,9 +269,18 @@ class ParsedHTMLMail(object):
273 269
                 tree.find().name.lower() in ['body', 'div']:
274 270
             tree.find().unwrap()
275 271
 
276
-        # drop some not useful html elem
272
+        # drop some html elem
277 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 284
                 tag.unwrap()
280 285
                 continue
281 286
             if tag.name.lower() in ['script', 'style']:
@@ -298,11 +303,11 @@ class ParsedHTMLMail(object):
298 303
             elif HtmlMailSignatureChecker.is_signature(tag):
299 304
                 part_type = BodyMailPartType.Signature
300 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 311
         return elements
307 312
 
308 313
     @classmethod

+ 198 - 57
tracim/tracim/tests/library/test_email_body_parser.py View File

@@ -53,6 +53,16 @@ class TestHtmlMailQuoteChecker(TestStandard):
53 53
         main_elem = soup.find()
54 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 66
     # TODO - G.M - 2017-11-24 - Check Yahoo and New roundcube html mail with
57 67
     # correct mail example
58 68
 
@@ -96,6 +106,18 @@ class TestHtmlMailSignatureChecker(TestStandard):
96 106
         main_elem = soup.find()
97 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 121
 class TestBodyMailsParts(TestStandard):
100 122
 
101 123
     def test_unit__std_list_methods(self):
@@ -138,60 +160,6 @@ class TestBodyMailsParts(TestStandard):
138 160
         a = BodyMailPart('a', BodyMailPartType.Main)
139 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 163
     def test_unit__drop_part_type(self):
196 164
         mail_parts = BodyMailParts()
197 165
         a = BodyMailPart('a', BodyMailPartType.Main)
@@ -251,13 +219,14 @@ class TestBodyMailsParts(TestStandard):
251 219
 
252 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 223
         text_only = '''<div dir="ltr">Voici le texte<br></div>'''
256 224
         mail = ParsedHTMLMail(text_only)
257 225
         elements = mail.get_elements()
258 226
         assert len(elements) == 1
259 227
         assert elements[0].part_type == BodyMailPartType.Main
260 228
 
229
+    def test_other__check_gmail_mail_text_signature(self):
261 230
         text_and_signature = '''
262 231
         <div dir="ltr">POF<br clear="all"><div><br>-- <br>
263 232
         <div class="gmail_signature" data-smartmail="gmail_signature">
@@ -273,6 +242,7 @@ class TestParsedMail(TestStandard):
273 242
         assert elements[0].part_type == BodyMailPartType.Main
274 243
         assert elements[1].part_type == BodyMailPartType.Signature
275 244
 
245
+    def test_other__check_gmail_mail_text_quote(self):
276 246
         text_and_quote = '''
277 247
         <div dir="ltr">Réponse<br>
278 248
         <div class="gmail_extra"><br>
@@ -301,6 +271,7 @@ class TestParsedMail(TestStandard):
301 271
         assert elements[0].part_type == BodyMailPartType.Main
302 272
         assert elements[1].part_type == BodyMailPartType.Quote
303 273
 
274
+    def test_other__check_gmail_mail_text_quote_text(self):
304 275
         text_quote_text = '''
305 276
               <div dir="ltr">Avant<br>
306 277
               <div class="gmail_extra"><br>
@@ -338,7 +309,7 @@ class TestParsedMail(TestStandard):
338 309
         assert elements[1].part_type == BodyMailPartType.Quote
339 310
         assert elements[2].part_type == BodyMailPartType.Main
340 311
 
341
-
312
+    def test_other__check_gmail_mail_text_quote_signature(self):
342 313
         text_quote_signature = '''
343 314
         <div dir="ltr">Hey !<br>
344 315
                  </div>
@@ -390,6 +361,7 @@ class TestParsedMail(TestStandard):
390 361
         assert elements[0].part_type == BodyMailPartType.Main
391 362
         assert elements[1].part_type == BodyMailPartType.Quote
392 363
 
364
+    def test_other__check_gmail_mail_text_quote_text_signature(self):
393 365
         text_quote_text_sign = '''
394 366
         <div dir="ltr">Test<br>
395 367
         <div class="gmail_extra"><br>
@@ -442,7 +414,7 @@ class TestParsedMail(TestStandard):
442 414
         assert elements[2].part_type == BodyMailPartType.Main
443 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 419
         text_only = '''Coucou<br><br><br>'''
448 420
         mail = ParsedHTMLMail(text_only)
@@ -450,6 +422,7 @@ class TestParsedMail(TestStandard):
450 422
         assert len(elements) == 1
451 423
         assert elements[0].part_type == BodyMailPartType.Main
452 424
 
425
+    def test_other__check_thunderbird_mail_text_signature(self):
453 426
         text_and_signature = '''
454 427
         <p>Test<br>
455 428
         </p>
@@ -462,6 +435,7 @@ class TestParsedMail(TestStandard):
462 435
         assert elements[0].part_type == BodyMailPartType.Main
463 436
         assert elements[1].part_type == BodyMailPartType.Signature
464 437
 
438
+    def test_other__check_thunderbird_mail_text_quote(self):
465 439
         text_and_quote = '''
466 440
             <p>Pof<br>
467 441
             </p>
@@ -486,6 +460,7 @@ class TestParsedMail(TestStandard):
486 460
         assert elements[0].part_type == BodyMailPartType.Main
487 461
         assert elements[1].part_type == BodyMailPartType.Quote
488 462
 
463
+    def test_other__check_thunderbird_mail_text_quote_text(self):
489 464
         text_quote_text = '''
490 465
         <p>Pof<br>
491 466
         </p>
@@ -523,6 +498,7 @@ class TestParsedMail(TestStandard):
523 498
         assert elements[1].part_type == BodyMailPartType.Quote
524 499
         assert elements[2].part_type == BodyMailPartType.Main
525 500
 
501
+    def test_other__check_thunderbird_mail_text_quote_signature(self):
526 502
         text_quote_signature = '''
527 503
         <p>Coucou<br>
528 504
         </p>
@@ -554,6 +530,7 @@ class TestParsedMail(TestStandard):
554 530
         assert elements[1].part_type == BodyMailPartType.Quote
555 531
         assert elements[2].part_type == BodyMailPartType.Signature
556 532
 
533
+    def test_other__check_thunderbird_mail_text_quote_text_signature(self):
557 534
         text_quote_text_sign = '''
558 535
         <p>Avant<br>
559 536
         </p>
@@ -579,4 +556,168 @@ class TestParsedMail(TestStandard):
579 556
         assert elements[2].part_type == BodyMailPartType.Main
580 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