ÿØÿà JFIF    ÿÛ „ !.%+&8&+/1555$;@;4?.451 4,$,44444444444414444444444444444444444444444444444444ÿÀ  á á" ÿÄ     ÿÄ ?    !1AQaq"2‘¡±ÁðBRbrÑá#‚’¢²3S CñÿÄ   ÿÄ !    !1QAa‘2ÿÚ   ? 5˜Z¯V¦cø)›t/? z¨±>Õ5€¶‹Á¤·¼z¼Ü¬+ñ®v¤¨_ˆR­BFn©—˜ý®ç̝P8gýt·ÉSTŦˆìät?þé¼íìN/Þa)ì–í6ô… Ï¿øÃj´¿KÇü]ÿ ªô¹-eKànëÕHTx}ýSÜ›ÿ ”7Ø×&µ<¦  ¥ÑO¶[Ù¯ä¨ÞÃÿ PZ-¬;#õ|•oaÿ ©CìÞz3˜öː/¤­ñTûIØ}š^ mÓ%ªxˆ¥ÉŸu=Z+ISe¿45™¼u;ú&WØ÷€æßQ™®{|íx*TC“#ZŠìZ§²‹ 6pv…³¿¡äª*áZÐ%ÒOáˆo"x«OHk w±æ+¬V(kMúŸ5Vö«$ ÁrÏbàb57/luR ¸ÑÛj Òµì`Мq­û žICÀÊ•©4€Âcà¨Ï€O´<èÐ:›ù(Ë^L8þ‘ÍÌ#¸Ð_Ì©ÙK(Öz 4¬û+¸;ü’V’84‘¬ÃŽ:[â‡ÔÌáõp¢~§ªlæ£ö{®G>J¼"°‡7¯ÆÉèßû ‹É‹§ÁòÃýâßî ^ƾÙõ‹×óH#«LP½ïX=xÑÍ$|W?•~• îëÔ©ª‹ {ÝT…Kÿ ”hûâá)J*ö˜–ÔU;iÇ€/ ÆþjóZ\ýwØ=Ìm ºèËL9 ýèÆð/¨’¥öo=nË.%Îì ŽÕ¯È|{Oj²ƒE6e/ßdÄõ²Ìâ1O®ò×TsəԸhOMýíMˆ¿¼H˜l²,7Â¥#MF/Úf°Ö½± ¸–dr‹NýÊ íjqx{œÉ ä-È ¦ øÄër¨q°ð †nцýÑÄÆ’mä…n<0È™;ÁÝá¯ÁZƒ7FÀmì­ É&9ˆîéi¶ùN§Y• ÃZãAâ?•‡©‰ , ó¾IŸŠc1 4â&y­&pŠ­6;M À 0¹qç»p.á …ŸÅáK@%6·y6ƒ‰3?”úºŽ‰éX5ªPT §µ!=Mž«Ú½‹ÅgÂSâÉaþÓoö–¯ÁÔìR>5éÿ üs¶ÆUcÌ kÇR ]ÿ ù¬¼«VŽ;Â|‡~¢¦”ÏŰæ {L™Õ°Óv¹ò¸írޡעCÃ!íVÕ {¶»sŒNPg/ "uÕbkm²“$ďå¿é¹§°½æz¯6 †s¿!s–wÚÝ“™Œ °.ûj>·+™Òa…©Œ&rÝÎtÛë긪Ît’LAVp%c Úý[ÄzJ¾ÇàXXç@˜ó<êL]·T˜¾¥1Ó©V‡g´æ½¦Ý@¹óø!_@´ÞâSÁ —S3™•& ]@JHÚý©ZŽ €×æÔr»Áf!‡yÞ4Mv*èÓã_{‘åóUuљØ«Oïé*®EvÑ Œ÷‡U \"㪒ÍK+À 4“M¡ï:0¥5í!'<@î´”>Ç»&Z–ïCCV˜Ì5Šo&îhè.žû |ÓK©h$s6KìŒëã)¹hI¦GïOåóI;ììü#É$Š0…Ææ¥TØ.5­¾gn´ “ÂÖ\:hœ89G)J@„}œ:’Ò{/Š"¦_Æ×7Æ3VÇŠÊa]ÚŒÙ€Ä–=®uÁßâACZƒ§§£ Qnâ:«,×{tyø¬iÛcœÜÄ€H½ÄÍCk´÷šß .W'b¤Íåh]÷€=,Žv×cÚEÚHXJX¶îo¨FÒtèöŸ>ªª6[J®Fµ£sGÁeqõfe\íjÒÐïÄÐGˆe1Ø‹.Ø”‘Ëuø Y­ˆÜ ŽG|zùªüMpDnQWÄ”%JŠ™)â*p@Örš«ÕT2Ð%ˆG#ª„ ·¤!°ŸOTÂT¸aÚ%4&h™LµšØüÐ.F¿²ÐÞ_Ç‚¾ÅÃaÜ÷09Æ q€öy˜v‡85õN÷]¬äѼóS{°_MެúÔ#°Ç¸0åÞè2ëôPcvÆw9®ií1Ä8F™˜à‰´+‰Ik1òÝ7“Ñ×ÒsÝ\x‚h`ÞÑ`ó"|µEcý£n˜h`}GÞ !±ù²Ápü²ß6 0ïi󜵩SÈÇ7˜-ÕURO˜¦´f$ªž-Í6(œ}<„ éc øs]ŽŽ„*—¾ ìdŽ„)méª\¿êÎIg¾ØÞ~I#C/¼¼´EÁÈŽi8“©õådô·>euä ƒ'Ê×लR1ÉJE1ÐAát`t;ÇР%Ý<‡¥„ÍÆ`×Oyó)õiI€ñQaŸ4Ûù\áàaÃÔ¹HÃu¹*k€¦<„e S‡&õÏ B!ŽhüÞ`yj}mªf×\¿ Ç~æ­9‡û\՞Ǖg²1Žû5V7 !àöšm° c`ܬøÇìµÒ'P"?…´Ö,"§^•õލsÔ)6˜sæéÍR¼ ò|Sl”‹7 nPW Gòú÷½§O¯‡„l¡kSÞŒr½PÊ@æ¢pŽ-mÿ #Ÿ˜Àº¶Áä¦;ïÔæ$1££`“Õ>„—·ž)ßð³ñ#Ï Ô$¶œ‰ÊE‹À;÷º ¯«P:Ñ”8–IÊtpÞ3ª“>ê“þës4ò2OÏÕ­±zô†Õ§‰.÷ä¸;¿˜“'œ›žª}«Œ{ª±Ì 9ÔóÞÕ‡0 $íWV3Üì¬ —@kÝ4@¿r¼±½¬™›?øØæ´'Áé®CË3-g$˜ö‡×auÚi´Žp/êÛ æF›Ú2v‹ã¿¿,nB1̨ƃqÞa5͝@&Æû“él÷ \C²½UÍc ¯k×¢U ÖéQå™—-r wô ÞÏ<Ò=&=ÿ Ôê Òêˈt,i—;LîÜ á¸*ÚÃ1$êL•LÍ <É)ýÐà’ ;F™{ƒ™˜€&'}‚ãÄK`¡ÞT@I;®žZóè‚s’7®°›+§O­Åq©é»²9<Ô J ¼9O’HL»Ùïì¸rk¼Ž_ý‘TŸu[²ßÚŒ·ü÷B%¯E ŸÔX5êO´ Ç•€’I0 ÉJX` ñ¹õ%;µŸD‘«´€àwÒ™U ûئžÖö\×®×´8 ½‡ºÐÆÓ§?Àkmœ=;d5*@-ì0F Rªýš[Ü6âö̃ڸr*KA9· u*µæ£?U¸Âêí†8@¦X4 e-ò„0s{ HâUpU?¼mñRa°®a%Ð'tÉ×’\¾ÊÉ]t›h>·(Ë@R¼¡Ãt h}’O÷au<+nT…Ö…MӐ??Óe95 q>í/;&JSû °¯ÊéÞ øƒ*Ã2½Ài&:nôUl=¾¿5eˆ3”ñc|Ú2V”>„»&eE;«ÚäC p¢Û úy 9š[ŒÌx¼擼A&DåÒ¯ˆ¤ÀÌ;"˜ ÏQä¸åhÊ}Ûq«Û0WžÒ|»€ø®öCm5•\ÇÀ§Pe3£]0ÃàLDÉ‰1øªxjgwT‚÷¿LΨK‹›ùs—xˆÜ±µ kæ¸f‰‰ÜGk/LÛØ6d9ò¶ùA{ƒA3š/¬D¬khÓk‰`˜"㯒r¿±Óã jx‡°e}<Ñø\3y:'À•/h½Í€Ç4~g ?Û(¼]v‘ªlKÎâ~?O‚W%{Ì:“'©úNq¾›úo(X’¥¯ˆ nFê{Ç€ü?º'ë ø‹ì Þ09ŒÌç9Æ —ËC`j@ÓÄ(+a‹un¸#ÂꟋ{K`‘ÑÍÍ'à´»/Û,KW;Þ4²þð ï Nm|~fGÏ(…³Ã)«1ö­Õ ¥‡¨©ƒÃ™ü-s=à=U66Ï«Ýc蓦W¹íž®›nÔ%êÇìŒ<#Ü×84ån®Ð ÒåOC` ñânÑs‡¢ç 1õ%Îhì½Ã½® e:ݼUZo™`  ÅZŸŒÊ«ê1ÏÄo$q¹Þ€©ˆhÐÉä¯ñ[!…Ú˜àJ:x2$Íß&PåT£6ç— ‡Í*4Ýšçjÿ ‰É nófÐ ó(L5C•åÆ\rMÒ@ò }y-W}™üýVù—ú¢=Ù”c®‘< M ž ´Phr ¦©TD ‘ù.$´÷O‡‘V2Æò.=IUŒ=ž‡â¬i™aþÓåÙ?òUø'ØÖ•.~* šTŒ!•-×áºTâ®ä#õü'´ eýlYÅÓeÕKÂrT"CÚ@u!Óxƒ{š3€}1¿(r}%«nËamjÑ%ÑNEò v ˜à  σöK³,*º.àzù¨™Ó ÚçâU¦*¿ 9{%Ö¹ njûdaXöb) kÛÆ±ûÓ\°M7ˆÂ=û›ç¿Ã‚­V»Cg–8ÙêE- j)k$º`Ã-ùEýeBÆÇ]c¡°ñty&Òd0nõ'¡W+ƒ*|–øµFa\GQªEAÔp5\Ǽ·¼Ç8·õ -â§Ú[ ‡ uZeÖ 3}×d'+¹:ð+K†Û®s!Ï$úe€<Û”x)1»a­¡LC]¸µík…ÚàA»AYº{†ªS[¦5HÒ7ù --,ísòDØ€èk ÞÀîÜ ò@â( ËNˆë›4ô½•/¦o‡€Û7 ê•ÆêòðÜy'Án½µ á˜ݦ ndeo…[ì¶Ê,¥R³Ä=À±—–ß;£™´ñSâ*g§”ïaið‘Jå~™ÓÞ ß³Õ¢»8x埒²52>AÊb&-÷\7´éÄù€T˜,w;3{ï˜k…à¹ÄqÀ«œ{€\ ˆ¾[´¨јr &Úé„Ívˆ±8†¿]|¬ņ4I×pÞS1ÈÖz‰#Ìv‡G!YNògñ:màTz¢Ý1ô©^O=~ë|5Bã™ç•¼µõ•bÆ@úÕS¬ÈŒ#¬zünrŸ û” Z²•èðV"ÁHÚý©wÝ €7¼Ìu1hÑa3Éä û f$o¿É ™Ú›ÝçnpÒ3äÌ3†Í§,Äï]$‰/pê †«À¼¸e9­Æê_C]žƒ·ý·frÁN«, E=›Çq -‰öŒ:aÏ¿±í&£Í:-} 84‘ÿ eƒQÑeëSsuiA ³g㟥ú£?ÿ ʼn*”“÷aühe:ÊWa@ÒÞk±eØ] F Ô—r.åä˜ @ö¥ªZoÐýYL·¥S²G/‡ñ <~*ZÆ´è>JlòàÛÆ½ÿ 窘ìGN¢:I®KšJp/`íIÁÀõ#Ä-€ö­šµŒoF4|ÆQØÆ@Ì|£Ô…¢À{9˜è½Üó›€ôYÒÎYsið;ís¤€à²ˆ‚4qÉVŒI$ ‰"° æµ8cXGjœˏ¡Aâý•ËÜ¢ûï e·çLx']á"oÅÎê3¯Ç—¹”ó0nå‚âg{Œñ> S´˜îè°g238‚ãköÝfÚd´6Ò€;ò÷±¢™¼›º ¢Æ'¥Ðx'e¬ç ]bÈÆV¢ó‹kýBO ðÊâ$Ÿ!×T 3Mýמ žìٍàÌü‘8÷€àæØ8æ©6‰©L´«…oãpð„~Çk‰!ñ;‹”ÛžÍ àž±z Ÿôû øŸÝužÏ;ÿ #|u6™Þ¬ÚˆÐõA4¶â|ôl|Ê2ŽÇ¤ÝÅÇY.<#Aí.k§hóF‚”Y; M½Ö4hŸ4&›­¿tès´%FìL¥£Ãk‰ÇT¤haÁ¤ÚxfÉ`ÑìË›>i 3t‚:,–+^÷´–{Û–Nxi"x‘Ûg î¨>¥Õ܁ùZH,2Û“:8xÊ¢Çí9.É-Ìâã-=çjwµS˜dütžçwýGòú®®ûº_ˆýx$–¡ãøO EÚÛÏ÷R„×w+3£Á£öUMyR²¹âŒ°š›¸Ñãò9§Ó_Dl+Ùßc›úšGÅÌc†Ž!Ko=¶.‘Îÿ c²(2®V mª.ÿ ¹B›¹å ù„öŸSV>™ü¯$y:G¢Z×àøúdî¹û­·ýÇ´:•c LÍõi_‹ö+ÎæGÊè>OŠ•äž´§Þ{X}¨1ÚTc›»Qþ•êô°t¿OP?eæ~É{5]•ÙR£r5†nZ\ã@ &îJõ ¾àC°þV>fé¥/ü5ñÊIº_é5 ;e­h<@ Ä&æÃëE%;X,ÒãÆÞ`Oò¦kŸm#˜!ÀyÄ¢| óLšò¥Ä` ¶R=|ÈCâh5ò3DˆïF†ðÒ#ÅìÛœ?¸yhBãœí ZxßÎÄhºRK„`Þödvײ™ÀÈÑÒgŒuY w³%†ƒÓzõ ÖÏp‚dH®¦A´ù§»ÓÇMæ~)ˆð‡û:ù&Ä •vGD´À n ݇¼Ö8Fö óáà£~Ë¥x`oK|Ä?fxiØü%pìR>éò+Û±éÎ>núlFŤ'tq8LZÏvÃ?„¡ß±È⽆¯³íü@x|PöUäèØã¡ð‚ŒAìÏ"vÍwóŸÍ{ ý0.z È•Ö{,N¡£¡ŸKÕÙž>Ýœþ ÍÀ°<×EA!Å‚D™IúOÍ¡>ôG}Â` ÍßkÜL™Ž Þð™ {IøF²¹òQ3&!ÃÂÞz.d&Ï-sH¸,Ôõ˜ŽP€ 77ˆÝ¼ÊëÜw =cÕ Ú,ØÐ5ÎYÐ)ì´öœgŒ[¤ßv㙑8心>h]§µháYš£²ºÑ.{Ï7Sð•?´~×SÃKýJÛ˜ ™Íäiúu<µX¶1õ^kâçIÑ£sZ4h>j*ÔšD:4­¿_ ÷¸ Õxæÿ ¸?Mù _•­ÊÐ ä ÷ý ÑwL œ­ïnTkÛUÍN©ë:¦fV ¶ÜÔÜMªÅâA½–¿R×TXš-%iTÊT•‡Ù‚JôϐZxWÑè‰f‰òG º ×Õû2aZ7OU3[“×AT–ÞŒ…-‘¤”Ì ì&(ˆ¿­•ƒkï’:ðY¦W‘ Å)“†‘˜³Åtcø˜ñTÂwÚÇ4|üLÇªí–v- qˆèU qPE.†â‘˜µ Æ,ÐÅs]8¾„oúÑ i>ÜxxÈó)ƒ ´æÁâØ$À‰vžŸf$Ž |ãw;ÀÁIJ»b` {¦Ó¤Ú$©YÀ‘n@Óïž«9J¼êG m¤ ܯ¹ÌW4€ÐÒÅÛ‡#褕Ÿn-?í|с¥÷Ú¹¬'´ÞÜ9ÓK `hê£SÄSà?7—Wí_´…óB›»:=Ãïq`<8ñÓŒÑlú2d¬ê³£hÖ[l|$vÝro~'R®‰§°ñmY ͧäP |PUª¹·:3Œ[Û{Xÿ ºâ@‚W–Äé u‚ ¯´*=íή.pûÒdt @G‰¬ s¸ ëÉücr ÞæÑ¨Ê@>¤¢Ö±. Þ'¯°ÌME[YéïĵÂCå½ Ué©Áû'Ê9%eÔðNU”ë‘ÌsD3/®+UI˜9h.WC”빓$#:pz:YÓ ¿xž* ³$Í +$kñAŠ‹†¢ Uê>¸)_š¬÷©ßAÂÔb9ÇU ¯¾á•9¯ÏÏ÷O÷¼¼Fähal1‰3Ì[Ïr•´UCksNÐ] R‘¸¥H+§Šé†c©vÖÞ0iÓ76s†î!§=ß ¼~Ô'°Ãmäoäš³ªøi1úÉ)³yV8 CLÄØÁ‘WYïi€H6ÖÑiámø^ÈY´°Ñ7¥Û*—Ñ©L«Qƒï—Ùrÿ ›£Ð*š¸ˆL©ˆ$ˆ ÷¾D§9È®«qbqC)–ˆïv´çñsÑVT­Ø, <àïºÀO«Jý·õ àfPìð .wFšir´þ’2_Y *Æ€x\« ì€9š@ Ž|F⇥ˆkZ@hÖÄ0t¿-<“‹qµ¾*ZL¤Ú)&BJpÓF5=$„at*Zš$’ÑtdûÝRI1 2މ$€$I$#‰SÞ’Hë¬ï;Á$¡t$’`<(ñÇt)$‡Ð.Êf¢X’Kt=Éé$‚ˆªè¢oÝëòI%Rgcª÷ŠyI%¡‰ÿ !ñ)´õ $¤ Ô’IIGÿÙimport unittest import textwrap from email import policy, message_from_string from email.message import EmailMessage, MIMEPart from test.test_email import TestEmailBase, parameterize # Helper. 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("""\ Subject: Ayons asperges pour le =?utf-8?q?d=C3=A9jeuner?= From: =?utf-8?q?Pep=C3=A9?= Le Pew To: "Penelope Pussycat" <"penelope@example.com"> MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" sample text """), policy=policy.default) self.assertEqual(m['subject'], "Ayons asperges pour le déjeuner") self.assertEqual(m['from'], "Pepé Le Pew ") self.assertIsInstance(m, 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 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. message_params = { 'empty_message': ( (None, None, 0), (), (), ""), 'non_mime_plain': ( (None, None, 0), (), (), textwrap.dedent("""\ To: foo@example.com simple text body """)), 'mime_non_text': ( (None, None, None), (), (), textwrap.dedent("""\ To: foo@example.com MIME-Version: 1.0 Content-Type: image/jpg bogus body. """)), 'plain_html_alternative': ( (None, 2, 1), (), (1, 2), textwrap.dedent("""\ To: foo@example.com MIME-Version: 1.0 Content-Type: multipart/alternative; boundary="===" preamble --=== Content-Type: text/plain simple body --=== Content-Type: text/html

simple body

--===-- """)), 'plain_html_mixed': ( (None, 2, 1), (), (1, 2), textwrap.dedent("""\ To: foo@example.com MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="===" preamble --=== Content-Type: text/plain simple body --=== Content-Type: text/html

simple body

--===-- """)), 'plain_html_attachment_mixed': ( (None, None, 1), (2,), (1, 2), textwrap.dedent("""\ 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

simple body

--===-- """)), 'html_text_attachment_mixed': ( (None, 2, None), (1,), (1, 2), textwrap.dedent("""\ 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

simple body

--===-- """)), 'html_text_attachment_inline_mixed': ( (None, 2, 1), (), (1, 2), textwrap.dedent("""\ 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

simple body

--===-- """)), # RFC 2387 'related': ( (0, 1, None), (2,), (1, 2), textwrap.dedent("""\ To: foo@example.com MIME-Version: 1.0 Content-Type: multipart/related; boundary="==="; type=text/html --=== Content-Type: text/html

simple body

--=== Content-Type: image/jpg Content-ID: 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 :) 'related_with_start': ( (0, 2, None), (1,), (1, 2), textwrap.dedent("""\ To: foo@example.com MIME-Version: 1.0 Content-Type: multipart/related; boundary="==="; type=text/html; start="" --=== Content-Type: text/html Content-ID: useless text --=== Content-Type: text/html Content-ID:

simple body

--===-- """)), 'mixed_alternative_plain_related': ( (3, 4, 2), (6, 7), (1, 6, 7), textwrap.dedent("""\ 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

simple body

--___ Content-Type: image/jpg Content-ID: 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. 'mixed_related_alternative_plain_html': ( (1, 4, 3), (6, 7), (1, 6, 7), textwrap.dedent("""\ 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

simple body

--___-- --+++ Content-Type: image/jpg Content-ID: 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. 'mixed_related_alternative_plain_html_wrong_order': ( (1, None, None), (6, 7), (1, 6, 7), textwrap.dedent("""\ To: foo@example.com MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="===" --=== Content-Type: multipart/related; boundary="+++" --+++ Content-Type: image/jpg Content-ID: bogus jpg body --+++ Content-Type: multipart/alternative; boundary="___" --___ Content-Type: text/plain simple body --___ Content-Type: text/html

simple body

--___-- --+++-- --=== Content-Type: image/jpg Content-Disposition: attachment bogus jpg body --=== Content-Type: image/jpg Content-Disposition: attachment another bogus jpg body --===-- """)), 'message_rfc822': ( (None, None, None), (), (), textwrap.dedent("""\ 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. """)), 'mixed_text_message_rfc822': ( (None, None, 1), (2,), (1, 2), textwrap.dedent("""\ 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. --===-- """)), } 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)) # 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 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) # +1 for new Content-Type 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): # Doing all variation is a bit of overkill... 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: # Otherwise we don't guess. self.assertIsNone(part['Content-Disposition']) class _TestSetRaisingContentManager: def set_content(self, msg, content, *args, **kw): raise Exception('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(Exception) 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): # We had a bug where iter_attachments was mutating the list. 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): # 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. 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): # 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. 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): # 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. 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): # 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. 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): # bpo-36520 # # Fold a line that contains a UTF-8 word after # the fold point. 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): # bpo-36520 # # Fold a line that contains a UTF-8 word before # the fold point and ASCII words after 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): # 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. 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): # 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. 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): """test for bpo-42892""" msg = textwrap.dedent("""\ Message-ID: <674392CA.4347091@email.au> Date: Wed, 08 Nov 2017 08:50:22 +0700 From: Foo Bar MIME-Version: 1.0 To: 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 """) m = self._str_msg(msg) # In bpo-42892, this would raise # AttributeError: 'str' object has no attribute 'is_attachment' m.get_body() class TestMIMEPart(TestEmailMessageBase, 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 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("""\ Content-Type: multipart/mixed; charset="utf-8" sample text """), policy=policy.default) attachments = msg.iter_attachments() self.assertEqual(list(attachments), []) if __name__ == '__main__': unittest.main()