helper policypolicy default self assertequalm subject ayons asperges pour le djeuner self assertequalm from pep le pew pepeexample com self assertisinstancem emailmessage parameterize class testemailmessagebase policy policy default the first argument is a triple related html plain of indices into the list returned by walk called on a message constructed from the third the indices indicate which part should match the corresponding parttype when passed to getbody ie the first part of that type in the message the second argument is a list of indices into the walk list of the attachments that should be returned by a call to iterattachments the third argument is a list of indices into walk that should be returned by a call to iterparts note that the first item returned by walk is the message itself messageparams emptymessage none none 0 nonmimeplain none none 0 textwrap dedent to fooexample com simple text body plainhtmlalternative none 2 1 1 2 textwrap dedent to fooexample com mimeversion 1 0 contenttype multipartalternative boundary preamble contenttype textplain simple body contenttype texthtml psimple bodyp plainhtmlattachmentmixed none none 1 2 1 2 textwrap dedent to fooexample com mimeversion 1 0 contenttype multipartmixed boundary contenttype textplain simple body contenttype texthtml contentdisposition attachment psimple bodyp htmltextattachmentinlinemixed none 2 1 1 2 textwrap dedent to fooexample com mimeversion 1 0 contenttype multipartmixed boundary contenttype textplain contentdisposition inline simple body contenttype texthtml contentdisposition inline psimple bodyp rfc 2387 this message structure will probably never be seen in the wild but it proves we distinguish between text parts based on start the content would not of course actually work relatedwithstart 0 2 none 1 1 2 textwrap dedent to fooexample com mimeversion 1 0 contenttype multipartrelated boundary typetexthtml startbody contenttype texthtml contentid include useless text contenttype texthtml contentid body psimple bodyp include fileinclude this structure suggested by stephen j turnbull may not existbe supported in the wild but we want to support it mixedrelatedalternativeplainhtml 1 4 3 6 7 1 6 7 textwrap dedent to fooexample com mimeversion 1 0 contenttype multipartmixed boundary contenttype multipartrelated boundary contenttype multipartalternative boundary contenttype textplain simple body contenttype texthtml psimple bodyp contenttype imagejpg contentid image1cid bogus jpg body contenttype imagejpg contentdisposition attachment bogus jpg body contenttype imagejpg contentdisposition attachment another bogus jpg body same thing but proving we only look at the root part which is the first one if there isn t any start parameter that is this is a broken related messagerfc822 none none none textwrap dedent to fooexample com mimeversion 1 0 contenttype messagerfc822 to barexample com from robotexamp com this is a message body def messageasgetbodyself bodyparts attachments parts msg m self strmsgmsg allparts listm walk expected none if n is none else allpartsn for n in bodyparts related 0 html 1 plain 2 self assertequalm getbody firstexpected self assertequalm getbodypreferencelist related html plain firstexpected self assertequalm getbodypreferencelist related html firstexpectedrelated html1 self assertequalm getbodypreferencelist related plain firstexpectedrelated expectedplain self assertequalm getbodypreferencelist html plain firstexpectedhtml plain1 self assertequalm getbodypreferencelist related expectedrelated self assertequalm getbodypreferencelist html expectedhtml self assertequalm getbodypreferencelist plain expectedplain self assertequalm getbodypreferencelist plain html firstexpectedplain html1 1 self assertequalm getbodypreferencelist plain related firstexpectedplain expectedrelated self assertequalm getbodypreferencelist html related firstexpectedhtml 1 self assertequalm getbodypreferencelist plain html related firstexpected 1 self assertequalm getbodypreferencelist html plain related firstexpectedhtml expectedplain expectedrelated def messageasiterattachmentself bodyparts attachments parts msg m self strmsgmsg allparts listm walk attachments allpartsn for n in attachments self assertequallistm iterattachments attachments def messageasiterpartsself bodyparts attachments parts msg def ismultipartmsgmsg return contenttype multipart in msg m self strmsgmsg allparts listm walk parts allpartsn for n in parts iterparts listm iterparts if ismultipartmsgmsg else self assertequaliterparts parts class testcontentmanager def getcontentself msg args kw return msg args kw def setcontentself msg args kw self msg msg self args args self kw kw def testgetcontentwithcmself m self strmsg cm self testcontentmanager self assertequalm getcontentcontentmanagercm m msg args kw m getcontent foo contentmanagercm bar1 k2 self assertequalmsg m self assertequalargs foo self assertequalkw dictbar1 k2 def testgetcontentdefaultcmcomesfrompolicyself p policy default clonecontentmanagerself testcontentmanager m self strmsg policyp self assertequalm getcontent m msg args kw m getcontent foo bar1 k2 self assertequalmsg m self assertequalargs foo self assertequalkw dictbar1 k2 def testsetcontentwithcmself m self strmsg cm self testcontentmanager m setcontentcontentmanagercm self assertequalcm msg m self assertequalcm args self assertequalcm kw m setcontent foo contentmanagercm bar1 k2 self assertequalcm msg m self assertequalcm args foo self assertequalcm kw dictbar1 k2 def testsetcontentdefaultcmcomesfrompolicyself cm self testcontentmanager p policy default clonecontentmanagercm m self strmsg policyp m setcontent self assertequalcm msg m self assertequalcm args self assertequalcm kw m setcontent foo bar1 k2 self assertequalcm msg m self assertequalcm args foo self assertequalcm kw dictbar1 k2 outcome is whether xxxmethod should raise valueerror error when called on multipartsubtype blank outcome means it depends on xxx add succeeds make raises note none means there are contenttype headers but payload is none this happening in practice would be very unusual so treating it as if there were content seems reasonable method subtype outcome subtypeparams related nocontent succeeds related none succeeds related plain succeeds related related related alternative raises related mixed raises alternative nocontent succeeds alternative none succeeds alternative plain succeeds alternative related succeeds alternative alternative alternative mixed raises mixed nocontent succeeds mixed none succeeds mixed plain succeeds mixed related succeeds mixed alternative succeeds mixed mixed def makesubtypetestmessageself subtype m self message payload none msgheaders to foobar com from barfoo com if subtype nocontent contentshadow logrus msgheaders append xrandomheader corwin if subtype text payload msgheaders append contenttype textplain m setpayload elif subtype nocontent payload msgheaders append contenttype multipart subtype msgheaders append xtrump random m setpayloadpayload for name value in msgheaders mname value return m msgheaders payload def checkdisallowedsubtyperaisesself m methodname subtype method with self assertraisesvalueerror as ar getattrm method exctext strar exception self assertinsubtype exctext self assertinmethodname exctext def checkmakemultipartself m msgheaders payload count 0 for name value in msgheaders if not name lower startswith content self assertequalmname value count 1 self assertequallenm count1 1 for new contenttype part nextm iterparts count 0 for name value in msgheaders if name lower startswith content self assertequalpartname value count 1 self assertequallenpart count self assertequalpart getpayload payload def subtypeasmakeself method subtype outcome m msgheaders payload self makesubtypetestmessagesubtype makemethod make method if outcome in raises self checkdisallowedsubtyperaisesm method subtype makemethod return getattrm makemethod self assertequalm getcontentmaintype multipart self assertequalm getcontentsubtype method if subtype nocontent self assertequallenm getpayload 0 self assertequalm items msgheaders contenttype multipart method else self assertequallenm getpayload 1 self checkmakemultipartm msgheaders payload def subtypeasmakewithboundaryself method subtype outcome doing all variation is a bit of overkill m self message if outcome in raises m contenttype multipart subtype with self assertraisesvalueerror as cm getattrm make method return if subtype plain m contenttype textplain elif subtype nocontent m contenttype multipart subtype getattrm make methodboundaryabc self asserttruem ismultipart self assertequalm getboundary abc def testpolicyonpartmadebymakecomesfrommessageself for method in makerelated makealternative makemixed m self messagepolicyself policy clonecontentmanager foo m contenttype textplain getattrm method self assertequalm getpayload0 policy contentmanager foo class testsetcontentmanager def setcontentself msg content args kw msg contenttype textplain msg setpayloadcontent def subtypeasaddself method subtype outcome m msgheaders payload self makesubtypetestmessagesubtype cm self testsetcontentmanager addmethod addattachment if method mixed else add method if outcome raises self checkdisallowedsubtyperaisesm method subtype addmethod return getattrm addmethod test contentmanagercm self assertequalm getcontentmaintype multipart self assertequalm getcontentsubtype method if method subtype or subtype nocontent self assertequallenm getpayload 1 for name value in msgheaders self assertequalmname value part m getpayload0 else self assertequallenm getpayload 2 self checkmakemultipartm msgheaders payload part m getpayload1 self assertequalpart getcontenttype textplain self assertequalpart getpayload test if method mixed self assertequalpart contentdisposition attachment elif method related self assertequalpart contentdisposition inline else otherwise we don t guess self assertisnonepart contentdisposition class testsetraisingcontentmanager class customerrorexception pass def setcontentself msg content args kw raise self customerror test def testdefaultcontentmanagerforaddcomesfrompolicyself cm self testsetraisingcontentmanager m self messagepolicyself policy clonecontentmanagercm for method in addrelated addalternative addattachment with self assertraisesself testsetraisingcontentmanager customerror as ar getattrm method self assertequalstrar exception test def messageasclearself bodyparts attachments parts msg m self strmsgmsg m clear self assertequallenm 0 self assertequallistm items self assertisnonem getpayload self assertequallistm iterparts def messageasclearcontentself bodyparts attachments parts msg m self strmsgmsg expectedheaders h for h in m keys if not h lower startswith content m clearcontent self assertequallistm keys expectedheaders self assertisnonem getpayload self assertequallistm iterparts def testisattachmentself m self makemessage self assertfalsem isattachment m contentdisposition inline self assertfalsem isattachment m replaceheader contentdisposition attachment self asserttruem isattachment m replaceheader contentdisposition attachment self asserttruem isattachment m setparam filename abc png contentdisposition self asserttruem isattachment def testiterattachmentsmutationself we had a bug where iterattachments was mutating the list m self makemessage m setcontent arbitrary text as main part m addrelated more text as a related part m addrelated yet more text as a second attachment orig m getpayload copy self assertequallenlistm iterattachments 2 self assertequalm getpayload orig class testemailmessagetestemailmessagebase testemailbase message emailmessage def testsetcontentaddsmimeversionself m self strmsg cm self testcontentmanager self assertnotin mimeversion m m setcontentcontentmanagercm self assertequalm mimeversion 1 0 class mimeversionaddingcm def setcontentself msg args kw msg mimeversion 1 0 def testsetcontentdoesnotduplicatemimeversionself m self strmsg cm self mimeversionaddingcm self assertnotin mimeversion m m setcontentcontentmanagercm self assertequalm mimeversion 1 0 def testasstringusesmaxheaderlengthbydefaultself m self strmsg subject long line ab 50 nn self assertequallenm asstring strip splitlines 3 def testasstringallowsmaxheaderlenself m self strmsg subject long line ab 50 nn self assertequallenm asstringmaxheaderlen0 strip splitlines 1 self assertequallenm asstringmaxheaderlen34 strip splitlines 6 def testasstringunixformself m self strmsg test m setunixfrom from foobar thu jan 1 00 00 00 1970 self assertequalm asstringunixfromtrue from foobar thu jan 1 00 00 00 1970nntest self assertequalm asstringunixfromfalse ntest def teststrdefaultstopolicymaxlinelengthself m self strmsg subject long line ab 50 nn self assertequallenstrm strip splitlines 3 def teststrdefaultstoutf8self m emailmessage m subject unicde self assertequalstrm subject unicdenn def testfoldingwithutf8encoding1self bpo36520 fold a line that contains utf8 words before and after the whitespace fold point where the line length limit is reached within an ascii word m emailmessage m subject hello wrld hello wrld hello wrld hello wrld hello wrld self assertequalbytesm b subject hello utf8 q wc3b6rld hellow b c3b6rld hellowc3b6rld n b hello utf8 q wc3b6rld hellowc3b6rld nn def testfoldingwithutf8encoding2self bpo36520 fold a line that contains utf8 words before and after the whitespace fold point where the line length limit is reached at the end of an encoded word m emailmessage m subject hello wrld hello wrld hello wrlds123 hello wrld hello wrld self assertequalbytesm b subject hello utf8 q wc3b6rld hellow b c3b6rld hellowc3b6rlds123 n b hello utf8 q wc3b6rld hellowc3b6rld nn def testfoldingwithutf8encoding3self bpo36520 fold a line that contains utf8 words before and after the whitespace fold point where the line length limit is reached at the end of the first word m emailmessage m subject hellowrld hellowrld hellowrlds123 hello wrld hello wrld self assertequalbytesm b subject utf8 q hellowc3b6rld hellow b c3b6rld hellowc3b6rlds123 n b hello utf8 q wc3b6rld hellowc3b6rld nn def testfoldingwithutf8encoding4self bpo36520 fold a line that contains utf8 words before and after the fold point where the first word is utf8 and the fold point is within the word m emailmessage m subject hellowrld hellowrld hellowrlds123 hello wrld hello wrld self assertequalbytesm b subject utf8 q hellowc3b6rld hellow b c3b6rld hellowc3b6rlds123 n b utf8 q hellowc3b6rld hellowc3b6rld nn def testfoldingwithutf8encoding5self bpo36520 fold a line that contains a utf8 word after the fold point m emailmessage m subject 123456789 123456789 123456789 123456789 123456789 123456789 123456789 hello wrld self assertequalbytesm b subject 123456789 123456789 123456789 123456789 b 123456789 123456789 123456789n b hello utf8 q wc3b6rld nn def testfoldingwithutf8encoding6self bpo36520 fold a line that contains a utf8 word before the fold point and ascii words after m emailmessage m subject 123456789 123456789 123456789 123456789 hello wrld 123456789 123456789 123456789 123456789 123456789 123456789 self assertequalbytesm b subject 123456789 123456789 123456789 123456789 b hello utf8 q wc3b6rld n 123456789 b 123456789 123456789 123456789 123456789 b 123456789nn def testfoldingwithutf8encoding7self bpo36520 fold a line twice that contains utf8 words before and after the first fold point and ascii words after the second fold point m emailmessage m subject 123456789 123456789 hello wrld hello wrld 123456789123456789 123456789 hello wrld 123456789 123456789 self assertequalbytesm b subject 123456789 123456789 hello utf8 q b wc3b6rld hellowc3b6rld n b 123456789123456789 123456789 hello b utf8 q wc3b6rld 123456789n 123456789nn def testfoldingwithutf8encoding8self bpo36520 fold a line twice that contains utf8 words before the first fold point and ascii words after the first fold point and utf8 words after the second fold point m emailmessage m subject 123456789 123456789 hello wrld hello wrld 123456789 123456789 123456789 123456789 123456789 123456789123456789 123456789 hello wrld 123456789 123456789 self assertequalbytesm b subject 123456789 123456789 hello b utf8 q wc3b6rld hellowc3b6rld n 123456789 b 123456789 123456789 123456789 123456789 b 123456789123456789n 123456789 hello b utf8 q wc3b6rld 123456789 123456789nn def testgetbodymalformedself m self strmsgmsg in bpo42892 this would raise attributeerror str object has no attribute isattachment m getbody class testmimeparttestemailmessagebase testemailbase doing the full test run here may seem a bit redundant since the two classes are almost identical but what if they drift apart so we do the full tests so that any future drift doesn t introduce bugs message mimepart def testsetcontentdoesnotaddmimeversionself m self strmsg cm self testcontentmanager self assertnotin mimeversion m m setcontentcontentmanagercm self assertnotin mimeversion m def teststringpayloadwithmultipartcontenttypeself msg messagefromstringtextwrap dedent contenttype multipartmixed charsetutf8 sample text helper subject ayons asperges pour le utf 8 q d c3 a9jeuner from utf 8 q pep c3 a9 le pew pepe example com to penelope pussycat penelope example com mime version 1 0 content type text plain charset utf 8 sample text the first argument is a triple related html plain of indices into the list returned by walk called on a message constructed from the third the indices indicate which part should match the corresponding part type when passed to get_body ie the first part of that type in the message the second argument is a list of indices into the walk list of the attachments that should be returned by a call to iter_attachments the third argument is a list of indices into walk that should be returned by a call to iter_parts note that the first item returned by walk is the message itself to foo example com simple text body to foo example com mime version 1 0 content type image jpg bogus body to foo example com mime version 1 0 content type multipart alternative boundary preamble content type text plain simple body content type text html p simple body p to foo example com mime version 1 0 content type multipart mixed boundary preamble content type text plain simple body content type text html p simple body p to foo example com mime version 1 0 content type multipart mixed boundary content type text plain simple body content type text html content disposition attachment p simple body p to foo example com mime version 1 0 content type multipart mixed boundary content type text plain content disposition attachment simple body content type text html p simple body p to foo example com mime version 1 0 content type multipart mixed boundary content type text plain content disposition inline simple body content type text html content disposition inline p simple body p rfc 2387 to foo example com mime version 1 0 content type multipart related boundary type text html content type text html p simple body p content type image jpg content id image1 bogus data this message structure will probably never be seen in the wild but it proves we distinguish between text parts based on start the content would not of course actually work to foo example com mime version 1 0 content type multipart related boundary type text html start body content type text html content id include useless text content type text html content id body p simple body p include file include to foo example com mime version 1 0 content type multipart mixed boundary content type multipart alternative boundary content type text plain simple body content type multipart related boundary ___ ___ content type text html p simple body p ___ content type image jpg content id image1 cid bogus jpg body ___ content type image jpg content disposition attachment bogus jpg body content type image jpg content disposition attachment another bogus jpg body this structure suggested by stephen j turnbull may not exist be supported in the wild but we want to support it to foo example com mime version 1 0 content type multipart mixed boundary content type multipart related boundary content type multipart alternative boundary ___ ___ content type text plain simple body ___ content type text html p simple body p ___ content type image jpg content id image1 cid bogus jpg body content type image jpg content disposition attachment bogus jpg body content type image jpg content disposition attachment another bogus jpg body same thing but proving we only look at the root part which is the first one if there isn t any start parameter that is this is a broken related to foo example com mime version 1 0 content type multipart mixed boundary content type multipart related boundary content type image jpg content id image1 cid bogus jpg body content type multipart alternative boundary ___ ___ content type text plain simple body ___ content type text html p simple body p ___ content type image jpg content disposition attachment bogus jpg body content type image jpg content disposition attachment another bogus jpg body to foo example com mime version 1 0 content type message rfc822 to bar example com from robot examp com this is a message body to foo example com mime version 1 0 content type multipart mixed boundary content type text plain your message has bounced sir content type message rfc822 to bar example com from robot examp com this is a message body outcome is whether xxx_method should raise valueerror error when called on multipart subtype blank outcome means it depends on xxx add succeeds make raises note none means there are content type headers but payload is none this happening in practice would be very unusual so treating it as if there were content seems reasonable method subtype outcome 1 for new content type doing all variation is a bit of overkill otherwise we don t guess we had a bug where iter_attachments was mutating the list bpo 36520 fold a line that contains utf 8 words before and after the whitespace fold point where the line length limit is reached within an ascii word bpo 36520 fold a line that contains utf 8 words before and after the whitespace fold point where the line length limit is reached at the end of an encoded word bpo 36520 fold a line that contains utf 8 words before and after the whitespace fold point where the line length limit is reached at the end of the first word bpo 36520 fold a line that contains utf 8 words before and after the fold point where the first word is utf 8 and the fold point is within the word bpo 36520 fold a line that contains a utf 8 word after the fold point bpo 36520 fold a line that contains a utf 8 word before the fold point and ascii words after bpo 36520 fold a line twice that contains utf 8 words before and after the first fold point and ascii words after the second fold point bpo 36520 fold a line twice that contains utf 8 words before the first fold point and ascii words after the first fold point and utf 8 words after the second fold point test for bpo 42892 message id 674392ca 4347091 email au date wed 08 nov 2017 08 50 22 0700 from foo bar email email au mime version 1 0 to email email com email email com subject python email content type multipart mixed boundary 879045806563892972123996 x global filter messagescannedforspamandviruses passedalltests this is a multi part message in mime format 879045806563892972123996 content type text plain charset iso 8859 1 format flowed content transfer encoding 7bit your message is ready to be sent with the following file or link attachments xu89 08 11 2017 in bpo 42892 this would raise attributeerror str object has no attribute is_attachment doing the full test run here may seem a bit redundant since the two classes are almost identical but what if they drift apart so we do the full tests so that any future drift doesn t introduce bugs content type multipart mixed charset utf 8 sample text
import unittest
import textwrap
from email import policy, message_from_string
from email.message import EmailMessage, MIMEPart
from test.test_email import TestEmailBase, parameterize



def first(iterable):
    return next(filter(lambda x: x is not None, iterable), None)


class Test(TestEmailBase):

    policy = policy.default

    def test_error_on_setitem_if_max_count_exceeded(self):
        m = self._str_msg("")
        m['To'] = 'abc@xyz'
        with self.assertRaises(ValueError):
            m['To'] = 'xyz@abc'

    def test_rfc2043_auto_decoded_and_emailmessage_used(self):
        m = message_from_string(textwrap.dedent(), policy=policy.default)
        self.assertEqual(m['subject'], "Ayons asperges pour le déjeuner")
        self.assertEqual(m['from'], "Pepé Le Pew <pepe@example.com>")
        self.assertIsInstance(m, EmailMessage)


@parameterize
class TestEmailMessageBase:

    policy = policy.default

    
    
    
    
    
    
    
    
    

    message_params = {

        'empty_message': (
            (None, None, 0),
            (),
            (),
            ""),

        'non_mime_plain': (
            (None, None, 0),
            (),
            (),
            textwrap.dedent()),

        'mime_non_text': (
            (None, None, None),
            (),
            (),
            textwrap.dedent()),

        'plain_html_alternative': (
            (None, 2, 1),
            (),
            (1, 2),
            textwrap.dedent()),

        'plain_html_mixed': (
            (None, 2, 1),
            (),
            (1, 2),
            textwrap.dedent()),

        'plain_html_attachment_mixed': (
            (None, None, 1),
            (2,),
            (1, 2),
            textwrap.dedent()),

        'html_text_attachment_mixed': (
            (None, 2, None),
            (1,),
            (1, 2),
            textwrap.dedent()),

        'html_text_attachment_inline_mixed': (
            (None, 2, 1),
            (),
            (1, 2),
            textwrap.dedent()),

        
        'related': (
            (0, 1, None),
            (2,),
            (1, 2),
            textwrap.dedent()),

        
        
        
        'related_with_start': (
            (0, 2, None),
            (1,),
            (1, 2),
            textwrap.dedent()),


        'mixed_alternative_plain_related': (
            (3, 4, 2),
            (6, 7),
            (1, 6, 7),
            textwrap.dedent()),

        
        
        'mixed_related_alternative_plain_html': (
            (1, 4, 3),
            (6, 7),
            (1, 6, 7),
            textwrap.dedent()),

        
        
        
        'mixed_related_alternative_plain_html_wrong_order': (
            (1, None, None),
            (6, 7),
            (1, 6, 7),
            textwrap.dedent()),

        'message_rfc822': (
            (None, None, None),
            (),
            (),
            textwrap.dedent()),

        'mixed_text_message_rfc822': (
            (None, None, 1),
            (2,),
            (1, 2),
            textwrap.dedent()),

         }

    def message_as_get_body(self, body_parts, attachments, parts, msg):
        m = self._str_msg(msg)
        allparts = list(m.walk())
        expected = [None if n is None else allparts[n] for n in body_parts]
        related = 0; html = 1; plain = 2
        self.assertEqual(m.get_body(), first(expected))
        self.assertEqual(m.get_body(preferencelist=(
                                        'related', 'html', 'plain')),
                         first(expected))
        self.assertEqual(m.get_body(preferencelist=('related', 'html')),
                         first(expected[related:html+1]))
        self.assertEqual(m.get_body(preferencelist=('related', 'plain')),
                         first([expected[related], expected[plain]]))
        self.assertEqual(m.get_body(preferencelist=('html', 'plain')),
                         first(expected[html:plain+1]))
        self.assertEqual(m.get_body(preferencelist=['related']),
                         expected[related])
        self.assertEqual(m.get_body(preferencelist=['html']), expected[html])
        self.assertEqual(m.get_body(preferencelist=['plain']), expected[plain])
        self.assertEqual(m.get_body(preferencelist=('plain', 'html')),
                         first(expected[plain:html-1:-1]))
        self.assertEqual(m.get_body(preferencelist=('plain', 'related')),
                         first([expected[plain], expected[related]]))
        self.assertEqual(m.get_body(preferencelist=('html', 'related')),
                         first(expected[html::-1]))
        self.assertEqual(m.get_body(preferencelist=('plain', 'html', 'related')),
                         first(expected[::-1]))
        self.assertEqual(m.get_body(preferencelist=('html', 'plain', 'related')),
                         first([expected[html],
                                expected[plain],
                                expected[related]]))

    def message_as_iter_attachment(self, body_parts, attachments, parts, msg):
        m = self._str_msg(msg)
        allparts = list(m.walk())
        attachments = [allparts[n] for n in attachments]
        self.assertEqual(list(m.iter_attachments()), attachments)

    def message_as_iter_parts(self, body_parts, attachments, parts, msg):
        def _is_multipart_msg(msg):
            return 'Content-Type: multipart' in msg

        m = self._str_msg(msg)
        allparts = list(m.walk())
        parts = [allparts[n] for n in parts]
        iter_parts = list(m.iter_parts()) if _is_multipart_msg(msg) else []
        self.assertEqual(iter_parts, parts)

    class _TestContentManager:
        def get_content(self, msg, *args, **kw):
            return msg, args, kw
        def set_content(self, msg, *args, **kw):
            self.msg = msg
            self.args = args
            self.kw = kw

    def test_get_content_with_cm(self):
        m = self._str_msg('')
        cm = self._TestContentManager()
        self.assertEqual(m.get_content(content_manager=cm), (m, (), {}))
        msg, args, kw = m.get_content('foo', content_manager=cm, bar=1, k=2)
        self.assertEqual(msg, m)
        self.assertEqual(args, ('foo',))
        self.assertEqual(kw, dict(bar=1, k=2))

    def test_get_content_default_cm_comes_from_policy(self):
        p = policy.default.clone(content_manager=self._TestContentManager())
        m = self._str_msg('', policy=p)
        self.assertEqual(m.get_content(), (m, (), {}))
        msg, args, kw = m.get_content('foo', bar=1, k=2)
        self.assertEqual(msg, m)
        self.assertEqual(args, ('foo',))
        self.assertEqual(kw, dict(bar=1, k=2))

    def test_set_content_with_cm(self):
        m = self._str_msg('')
        cm = self._TestContentManager()
        m.set_content(content_manager=cm)
        self.assertEqual(cm.msg, m)
        self.assertEqual(cm.args, ())
        self.assertEqual(cm.kw, {})
        m.set_content('foo', content_manager=cm, bar=1, k=2)
        self.assertEqual(cm.msg, m)
        self.assertEqual(cm.args, ('foo',))
        self.assertEqual(cm.kw, dict(bar=1, k=2))

    def test_set_content_default_cm_comes_from_policy(self):
        cm = self._TestContentManager()
        p = policy.default.clone(content_manager=cm)
        m = self._str_msg('', policy=p)
        m.set_content()
        self.assertEqual(cm.msg, m)
        self.assertEqual(cm.args, ())
        self.assertEqual(cm.kw, {})
        m.set_content('foo', bar=1, k=2)
        self.assertEqual(cm.msg, m)
        self.assertEqual(cm.args, ('foo',))
        self.assertEqual(cm.kw, dict(bar=1, k=2))

    
    
    
    
    
    
    subtype_params = (
        ('related',      'no_content',     'succeeds'),
        ('related',      'none',           'succeeds'),
        ('related',      'plain',          'succeeds'),
        ('related',      'related',        ''),
        ('related',      'alternative',    'raises'),
        ('related',      'mixed',          'raises'),
        ('alternative',  'no_content',     'succeeds'),
        ('alternative',  'none',           'succeeds'),
        ('alternative',  'plain',          'succeeds'),
        ('alternative',  'related',        'succeeds'),
        ('alternative',  'alternative',    ''),
        ('alternative',  'mixed',          'raises'),
        ('mixed',        'no_content',     'succeeds'),
        ('mixed',        'none',           'succeeds'),
        ('mixed',        'plain',          'succeeds'),
        ('mixed',        'related',        'succeeds'),
        ('mixed',        'alternative',    'succeeds'),
        ('mixed',        'mixed',          ''),
        )

    def _make_subtype_test_message(self, subtype):
        m = self.message()
        payload = None
        msg_headers =  [
            ('To', 'foo@bar.com'),
            ('From', 'bar@foo.com'),
            ]
        if subtype != 'no_content':
            ('content-shadow', 'Logrus'),
        msg_headers.append(('X-Random-Header', 'Corwin'))
        if subtype == 'text':
            payload = ''
            msg_headers.append(('Content-Type', 'text/plain'))
            m.set_payload('')
        elif subtype != 'no_content':
            payload = []
            msg_headers.append(('Content-Type', 'multipart/' + subtype))
        msg_headers.append(('X-Trump', 'Random'))
        m.set_payload(payload)
        for name, value in msg_headers:
            m[name] = value
        return m, msg_headers, payload

    def _check_disallowed_subtype_raises(self, m, method_name, subtype, method):
        with self.assertRaises(ValueError) as ar:
            getattr(m, method)()
        exc_text = str(ar.exception)
        self.assertIn(subtype, exc_text)
        self.assertIn(method_name, exc_text)

    def _check_make_multipart(self, m, msg_headers, payload):
        count = 0
        for name, value in msg_headers:
            if not name.lower().startswith('content-'):
                self.assertEqual(m[name], value)
                count += 1
        self.assertEqual(len(m), count+1) 
        part = next(m.iter_parts())
        count = 0
        for name, value in msg_headers:
            if name.lower().startswith('content-'):
                self.assertEqual(part[name], value)
                count += 1
        self.assertEqual(len(part), count)
        self.assertEqual(part.get_payload(), payload)

    def subtype_as_make(self, method, subtype, outcome):
        m, msg_headers, payload = self._make_subtype_test_message(subtype)
        make_method = 'make_' + method
        if outcome in ('', 'raises'):
            self._check_disallowed_subtype_raises(m, method, subtype, make_method)
            return
        getattr(m, make_method)()
        self.assertEqual(m.get_content_maintype(), 'multipart')
        self.assertEqual(m.get_content_subtype(), method)
        if subtype == 'no_content':
            self.assertEqual(len(m.get_payload()), 0)
            self.assertEqual(m.items(),
                             msg_headers + [('Content-Type',
                                             'multipart/'+method)])
        else:
            self.assertEqual(len(m.get_payload()), 1)
            self._check_make_multipart(m, msg_headers, payload)

    def subtype_as_make_with_boundary(self, method, subtype, outcome):
        
        m = self.message()
        if outcome in ('', 'raises'):
            m['Content-Type'] = 'multipart/' + subtype
            with self.assertRaises(ValueError) as cm:
                getattr(m, 'make_' + method)()
            return
        if subtype == 'plain':
            m['Content-Type'] = 'text/plain'
        elif subtype != 'no_content':
            m['Content-Type'] = 'multipart/' + subtype
        getattr(m, 'make_' + method)(boundary="abc")
        self.assertTrue(m.is_multipart())
        self.assertEqual(m.get_boundary(), 'abc')

    def test_policy_on_part_made_by_make_comes_from_message(self):
        for method in ('make_related', 'make_alternative', 'make_mixed'):
            m = self.message(policy=self.policy.clone(content_manager='foo'))
            m['Content-Type'] = 'text/plain'
            getattr(m, method)()
            self.assertEqual(m.get_payload(0).policy.content_manager, 'foo')

    class _TestSetContentManager:
        def set_content(self, msg, content, *args, **kw):
            msg['Content-Type'] = 'text/plain'
            msg.set_payload(content)

    def subtype_as_add(self, method, subtype, outcome):
        m, msg_headers, payload = self._make_subtype_test_message(subtype)
        cm = self._TestSetContentManager()
        add_method = 'add_attachment' if method=='mixed' else 'add_' + method
        if outcome == 'raises':
            self._check_disallowed_subtype_raises(m, method, subtype, add_method)
            return
        getattr(m, add_method)('test', content_manager=cm)
        self.assertEqual(m.get_content_maintype(), 'multipart')
        self.assertEqual(m.get_content_subtype(), method)
        if method == subtype or subtype == 'no_content':
            self.assertEqual(len(m.get_payload()), 1)
            for name, value in msg_headers:
                self.assertEqual(m[name], value)
            part = m.get_payload()[0]
        else:
            self.assertEqual(len(m.get_payload()), 2)
            self._check_make_multipart(m, msg_headers, payload)
            part = m.get_payload()[1]
        self.assertEqual(part.get_content_type(), 'text/plain')
        self.assertEqual(part.get_payload(), 'test')
        if method=='mixed':
            self.assertEqual(part['Content-Disposition'], 'attachment')
        elif method=='related':
            self.assertEqual(part['Content-Disposition'], 'inline')
        else:
            
            self.assertIsNone(part['Content-Disposition'])

    class _TestSetRaisingContentManager:
        class CustomError(Exception):
            pass
        def set_content(self, msg, content, *args, **kw):
            raise self.CustomError('test')

    def test_default_content_manager_for_add_comes_from_policy(self):
        cm = self._TestSetRaisingContentManager()
        m = self.message(policy=self.policy.clone(content_manager=cm))
        for method in ('add_related', 'add_alternative', 'add_attachment'):
            with self.assertRaises(self._TestSetRaisingContentManager.CustomError) as ar:
                getattr(m, method)('')
            self.assertEqual(str(ar.exception), 'test')

    def message_as_clear(self, body_parts, attachments, parts, msg):
        m = self._str_msg(msg)
        m.clear()
        self.assertEqual(len(m), 0)
        self.assertEqual(list(m.items()), [])
        self.assertIsNone(m.get_payload())
        self.assertEqual(list(m.iter_parts()), [])

    def message_as_clear_content(self, body_parts, attachments, parts, msg):
        m = self._str_msg(msg)
        expected_headers = [h for h in m.keys()
                            if not h.lower().startswith('content-')]
        m.clear_content()
        self.assertEqual(list(m.keys()), expected_headers)
        self.assertIsNone(m.get_payload())
        self.assertEqual(list(m.iter_parts()), [])

    def test_is_attachment(self):
        m = self._make_message()
        self.assertFalse(m.is_attachment())
        m['Content-Disposition'] = 'inline'
        self.assertFalse(m.is_attachment())
        m.replace_header('Content-Disposition', 'attachment')
        self.assertTrue(m.is_attachment())
        m.replace_header('Content-Disposition', 'AtTachMent')
        self.assertTrue(m.is_attachment())
        m.set_param('filename', 'abc.png', 'Content-Disposition')
        self.assertTrue(m.is_attachment())

    def test_iter_attachments_mutation(self):
        
        m = self._make_message()
        m.set_content('arbitrary text as main part')
        m.add_related('more text as a related part')
        m.add_related('yet more text as a second "attachment"')
        orig = m.get_payload().copy()
        self.assertEqual(len(list(m.iter_attachments())), 2)
        self.assertEqual(m.get_payload(), orig)


class TestEmailMessage(TestEmailMessageBase, TestEmailBase):
    message = EmailMessage

    def test_set_content_adds_MIME_Version(self):
        m = self._str_msg('')
        cm = self._TestContentManager()
        self.assertNotIn('MIME-Version', m)
        m.set_content(content_manager=cm)
        self.assertEqual(m['MIME-Version'], '1.0')

    class _MIME_Version_adding_CM:
        def set_content(self, msg, *args, **kw):
            msg['MIME-Version'] = '1.0'

    def test_set_content_does_not_duplicate_MIME_Version(self):
        m = self._str_msg('')
        cm = self._MIME_Version_adding_CM()
        self.assertNotIn('MIME-Version', m)
        m.set_content(content_manager=cm)
        self.assertEqual(m['MIME-Version'], '1.0')

    def test_as_string_uses_max_header_length_by_default(self):
        m = self._str_msg('Subject: long line' + ' ab'*50 + '\n\n')
        self.assertEqual(len(m.as_string().strip().splitlines()), 3)

    def test_as_string_allows_maxheaderlen(self):
        m = self._str_msg('Subject: long line' + ' ab'*50 + '\n\n')
        self.assertEqual(len(m.as_string(maxheaderlen=0).strip().splitlines()),
                         1)
        self.assertEqual(len(m.as_string(maxheaderlen=34).strip().splitlines()),
                         6)

    def test_as_string_unixform(self):
        m = self._str_msg('test')
        m.set_unixfrom('From foo@bar Thu Jan  1 00:00:00 1970')
        self.assertEqual(m.as_string(unixfrom=True),
                        'From foo@bar Thu Jan  1 00:00:00 1970\n\ntest')
        self.assertEqual(m.as_string(unixfrom=False), '\ntest')

    def test_str_defaults_to_policy_max_line_length(self):
        m = self._str_msg('Subject: long line' + ' ab'*50 + '\n\n')
        self.assertEqual(len(str(m).strip().splitlines()), 3)

    def test_str_defaults_to_utf8(self):
        m = EmailMessage()
        m['Subject'] = 'unicöde'
        self.assertEqual(str(m), 'Subject: unicöde\n\n')

    def test_folding_with_utf8_encoding_1(self):
        
        
        
        
        
        

        m = EmailMessage()
        m['Subject'] = 'Hello Wörld! Hello Wörld! '            \
                       'Hello Wörld! Hello Wörld!Hello Wörld!'
        self.assertEqual(bytes(m),
                         b'Subject: Hello =?utf-8?q?W=C3=B6rld!_Hello_W'
                         b'=C3=B6rld!_Hello_W=C3=B6rld!?=\n'
                         b' Hello =?utf-8?q?W=C3=B6rld!Hello_W=C3=B6rld!?=\n\n')


    def test_folding_with_utf8_encoding_2(self):
        
        
        
        
        
        

        m = EmailMessage()
        m['Subject'] = 'Hello Wörld! Hello Wörld! '                \
                       'Hello Wörlds123! Hello Wörld!Hello Wörld!'
        self.assertEqual(bytes(m),
                         b'Subject: Hello =?utf-8?q?W=C3=B6rld!_Hello_W'
                         b'=C3=B6rld!_Hello_W=C3=B6rlds123!?=\n'
                         b' Hello =?utf-8?q?W=C3=B6rld!Hello_W=C3=B6rld!?=\n\n')

    def test_folding_with_utf8_encoding_3(self):
        
        
        
        
        
        

        m = EmailMessage()
        m['Subject'] = 'Hello-Wörld!-Hello-Wörld!-Hello-Wörlds123! ' \
                       'Hello Wörld!Hello Wörld!'
        self.assertEqual(bytes(m), \
                         b'Subject: =?utf-8?q?Hello-W=C3=B6rld!-Hello-W'
                         b'=C3=B6rld!-Hello-W=C3=B6rlds123!?=\n'
                         b' Hello =?utf-8?q?W=C3=B6rld!Hello_W=C3=B6rld!?=\n\n')

    def test_folding_with_utf8_encoding_4(self):
        
        
        
        
        
        

        m = EmailMessage()
        m['Subject'] = 'Hello-Wörld!-Hello-Wörld!-Hello-Wörlds123!-Hello' \
                       ' Wörld!Hello Wörld!'
        self.assertEqual(bytes(m),
                         b'Subject: =?utf-8?q?Hello-W=C3=B6rld!-Hello-W'
                         b'=C3=B6rld!-Hello-W=C3=B6rlds123!?=\n'
                         b' =?utf-8?q?-Hello_W=C3=B6rld!Hello_W=C3=B6rld!?=\n\n')

    def test_folding_with_utf8_encoding_5(self):
        
        
        
        

        m = EmailMessage()
        m['Subject'] = '123456789 123456789 123456789 123456789 123456789' \
                       ' 123456789 123456789 Hello Wörld!'
        self.assertEqual(bytes(m),
                         b'Subject: 123456789 123456789 123456789 123456789'
                         b' 123456789 123456789 123456789\n'
                         b' Hello =?utf-8?q?W=C3=B6rld!?=\n\n')

    def test_folding_with_utf8_encoding_6(self):
        
        
        
        

        m = EmailMessage()
        m['Subject'] = '123456789 123456789 123456789 123456789 Hello Wörld!' \
                       ' 123456789 123456789 123456789 123456789 123456789'   \
                       ' 123456789'
        self.assertEqual(bytes(m),
                         b'Subject: 123456789 123456789 123456789 123456789'
                         b' Hello =?utf-8?q?W=C3=B6rld!?=\n 123456789 '
                         b'123456789 123456789 123456789 123456789 '
                         b'123456789\n\n')

    def test_folding_with_utf8_encoding_7(self):
        
        
        
        
        

        m = EmailMessage()
        m['Subject'] = '123456789 123456789 Hello Wörld! Hello Wörld! '       \
                       '123456789-123456789 123456789 Hello Wörld! 123456789' \
                       ' 123456789'
        self.assertEqual(bytes(m),
                         b'Subject: 123456789 123456789 Hello =?utf-8?q?'
                         b'W=C3=B6rld!_Hello_W=C3=B6rld!?=\n'
                         b' 123456789-123456789 123456789 Hello '
                         b'=?utf-8?q?W=C3=B6rld!?= 123456789\n 123456789\n\n')

    def test_folding_with_utf8_encoding_8(self):
        
        
        
        
        
        

        m = EmailMessage()
        m['Subject'] = '123456789 123456789 Hello Wörld! Hello Wörld! '       \
                       '123456789 123456789 123456789 123456789 123456789 '   \
                       '123456789-123456789 123456789 Hello Wörld! 123456789' \
                       ' 123456789'
        self.assertEqual(bytes(m),
                         b'Subject: 123456789 123456789 Hello '
                         b'=?utf-8?q?W=C3=B6rld!_Hello_W=C3=B6rld!?=\n 123456789 '
                         b'123456789 123456789 123456789 123456789 '
                         b'123456789-123456789\n 123456789 Hello '
                         b'=?utf-8?q?W=C3=B6rld!?= 123456789 123456789\n\n')

    def test_get_body_malformed(self):
        
        msg = textwrap.dedent()
        m = self._str_msg(msg)
        
        
        m.get_body()


class TestMIMEPart(TestEmailMessageBase, TestEmailBase):
    
    
    
    message = MIMEPart

    def test_set_content_does_not_add_MIME_Version(self):
        m = self._str_msg('')
        cm = self._TestContentManager()
        self.assertNotIn('MIME-Version', m)
        m.set_content(content_manager=cm)
        self.assertNotIn('MIME-Version', m)

    def test_string_payload_with_multipart_content_type(self):
        msg = message_from_string(textwrap.dedent(), policy=policy.default)
        attachments = msg.iter_attachments()
        self.assertEqual(list(attachments), [])


if __name__ == '__main__':
    unittest.main()