(root)/
Python-3.12.0/
Lib/
test/
test_email/
test_message.py
       1  import unittest
       2  import textwrap
       3  from email import policy, message_from_string
       4  from email.message import EmailMessage, MIMEPart
       5  from test.test_email import TestEmailBase, parameterize
       6  
       7  
       8  # Helper.
       9  def first(iterable):
      10      return next(filter(lambda x: x is not None, iterable), None)
      11  
      12  
      13  class ESC[4;38;5;81mTest(ESC[4;38;5;149mTestEmailBase):
      14  
      15      policy = policy.default
      16  
      17      def test_error_on_setitem_if_max_count_exceeded(self):
      18          m = self._str_msg("")
      19          m['To'] = 'abc@xyz'
      20          with self.assertRaises(ValueError):
      21              m['To'] = 'xyz@abc'
      22  
      23      def test_rfc2043_auto_decoded_and_emailmessage_used(self):
      24          m = message_from_string(textwrap.dedent("""\
      25              Subject: Ayons asperges pour le =?utf-8?q?d=C3=A9jeuner?=
      26              From: =?utf-8?q?Pep=C3=A9?= Le Pew <pepe@example.com>
      27              To: "Penelope Pussycat" <"penelope@example.com">
      28              MIME-Version: 1.0
      29              Content-Type: text/plain; charset="utf-8"
      30  
      31              sample text
      32              """), policy=policy.default)
      33          self.assertEqual(m['subject'], "Ayons asperges pour le déjeuner")
      34          self.assertEqual(m['from'], "Pepé Le Pew <pepe@example.com>")
      35          self.assertIsInstance(m, EmailMessage)
      36  
      37  
      38  @parameterize
      39  class ESC[4;38;5;81mTestEmailMessageBase:
      40  
      41      policy = policy.default
      42  
      43      # The first argument is a triple (related, html, plain) of indices into the
      44      # list returned by 'walk' called on a Message constructed from the third.
      45      # The indices indicate which part should match the corresponding part-type
      46      # when passed to get_body (ie: the "first" part of that type in the
      47      # message).  The second argument is a list of indices into the 'walk' list
      48      # of the attachments that should be returned by a call to
      49      # 'iter_attachments'.  The third argument is a list of indices into 'walk'
      50      # that should be returned by a call to 'iter_parts'.  Note that the first
      51      # item returned by 'walk' is the Message itself.
      52  
      53      message_params = {
      54  
      55          'empty_message': (
      56              (None, None, 0),
      57              (),
      58              (),
      59              ""),
      60  
      61          'non_mime_plain': (
      62              (None, None, 0),
      63              (),
      64              (),
      65              textwrap.dedent("""\
      66                  To: foo@example.com
      67  
      68                  simple text body
      69                  """)),
      70  
      71          'mime_non_text': (
      72              (None, None, None),
      73              (),
      74              (),
      75              textwrap.dedent("""\
      76                  To: foo@example.com
      77                  MIME-Version: 1.0
      78                  Content-Type: image/jpg
      79  
      80                  bogus body.
      81                  """)),
      82  
      83          'plain_html_alternative': (
      84              (None, 2, 1),
      85              (),
      86              (1, 2),
      87              textwrap.dedent("""\
      88                  To: foo@example.com
      89                  MIME-Version: 1.0
      90                  Content-Type: multipart/alternative; boundary="==="
      91  
      92                  preamble
      93  
      94                  --===
      95                  Content-Type: text/plain
      96  
      97                  simple body
      98  
      99                  --===
     100                  Content-Type: text/html
     101  
     102                  <p>simple body</p>
     103                  --===--
     104                  """)),
     105  
     106          'plain_html_mixed': (
     107              (None, 2, 1),
     108              (),
     109              (1, 2),
     110              textwrap.dedent("""\
     111                  To: foo@example.com
     112                  MIME-Version: 1.0
     113                  Content-Type: multipart/mixed; boundary="==="
     114  
     115                  preamble
     116  
     117                  --===
     118                  Content-Type: text/plain
     119  
     120                  simple body
     121  
     122                  --===
     123                  Content-Type: text/html
     124  
     125                  <p>simple body</p>
     126  
     127                  --===--
     128                  """)),
     129  
     130          'plain_html_attachment_mixed': (
     131              (None, None, 1),
     132              (2,),
     133              (1, 2),
     134              textwrap.dedent("""\
     135                  To: foo@example.com
     136                  MIME-Version: 1.0
     137                  Content-Type: multipart/mixed; boundary="==="
     138  
     139                  --===
     140                  Content-Type: text/plain
     141  
     142                  simple body
     143  
     144                  --===
     145                  Content-Type: text/html
     146                  Content-Disposition: attachment
     147  
     148                  <p>simple body</p>
     149  
     150                  --===--
     151                  """)),
     152  
     153          'html_text_attachment_mixed': (
     154              (None, 2, None),
     155              (1,),
     156              (1, 2),
     157              textwrap.dedent("""\
     158                  To: foo@example.com
     159                  MIME-Version: 1.0
     160                  Content-Type: multipart/mixed; boundary="==="
     161  
     162                  --===
     163                  Content-Type: text/plain
     164                  Content-Disposition: AtTaChment
     165  
     166                  simple body
     167  
     168                  --===
     169                  Content-Type: text/html
     170  
     171                  <p>simple body</p>
     172  
     173                  --===--
     174                  """)),
     175  
     176          'html_text_attachment_inline_mixed': (
     177              (None, 2, 1),
     178              (),
     179              (1, 2),
     180              textwrap.dedent("""\
     181                  To: foo@example.com
     182                  MIME-Version: 1.0
     183                  Content-Type: multipart/mixed; boundary="==="
     184  
     185                  --===
     186                  Content-Type: text/plain
     187                  Content-Disposition: InLine
     188  
     189                  simple body
     190  
     191                  --===
     192                  Content-Type: text/html
     193                  Content-Disposition: inline
     194  
     195                  <p>simple body</p>
     196  
     197                  --===--
     198                  """)),
     199  
     200          # RFC 2387
     201          'related': (
     202              (0, 1, None),
     203              (2,),
     204              (1, 2),
     205              textwrap.dedent("""\
     206                  To: foo@example.com
     207                  MIME-Version: 1.0
     208                  Content-Type: multipart/related; boundary="==="; type=text/html
     209  
     210                  --===
     211                  Content-Type: text/html
     212  
     213                  <p>simple body</p>
     214  
     215                  --===
     216                  Content-Type: image/jpg
     217                  Content-ID: <image1>
     218  
     219                  bogus data
     220  
     221                  --===--
     222                  """)),
     223  
     224          # This message structure will probably never be seen in the wild, but
     225          # it proves we distinguish between text parts based on 'start'.  The
     226          # content would not, of course, actually work :)
     227          'related_with_start': (
     228              (0, 2, None),
     229              (1,),
     230              (1, 2),
     231              textwrap.dedent("""\
     232                  To: foo@example.com
     233                  MIME-Version: 1.0
     234                  Content-Type: multipart/related; boundary="==="; type=text/html;
     235                   start="<body>"
     236  
     237                  --===
     238                  Content-Type: text/html
     239                  Content-ID: <include>
     240  
     241                  useless text
     242  
     243                  --===
     244                  Content-Type: text/html
     245                  Content-ID: <body>
     246  
     247                  <p>simple body</p>
     248                  <!--#include file="<include>"-->
     249  
     250                  --===--
     251                  """)),
     252  
     253  
     254          'mixed_alternative_plain_related': (
     255              (3, 4, 2),
     256              (6, 7),
     257              (1, 6, 7),
     258              textwrap.dedent("""\
     259                  To: foo@example.com
     260                  MIME-Version: 1.0
     261                  Content-Type: multipart/mixed; boundary="==="
     262  
     263                  --===
     264                  Content-Type: multipart/alternative; boundary="+++"
     265  
     266                  --+++
     267                  Content-Type: text/plain
     268  
     269                  simple body
     270  
     271                  --+++
     272                  Content-Type: multipart/related; boundary="___"
     273  
     274                  --___
     275                  Content-Type: text/html
     276  
     277                  <p>simple body</p>
     278  
     279                  --___
     280                  Content-Type: image/jpg
     281                  Content-ID: <image1@cid>
     282  
     283                  bogus jpg body
     284  
     285                  --___--
     286  
     287                  --+++--
     288  
     289                  --===
     290                  Content-Type: image/jpg
     291                  Content-Disposition: attachment
     292  
     293                  bogus jpg body
     294  
     295                  --===
     296                  Content-Type: image/jpg
     297                  Content-Disposition: AttacHmenT
     298  
     299                  another bogus jpg body
     300  
     301                  --===--
     302                  """)),
     303  
     304          # This structure suggested by Stephen J. Turnbull...may not exist/be
     305          # supported in the wild, but we want to support it.
     306          'mixed_related_alternative_plain_html': (
     307              (1, 4, 3),
     308              (6, 7),
     309              (1, 6, 7),
     310              textwrap.dedent("""\
     311                  To: foo@example.com
     312                  MIME-Version: 1.0
     313                  Content-Type: multipart/mixed; boundary="==="
     314  
     315                  --===
     316                  Content-Type: multipart/related; boundary="+++"
     317  
     318                  --+++
     319                  Content-Type: multipart/alternative; boundary="___"
     320  
     321                  --___
     322                  Content-Type: text/plain
     323  
     324                  simple body
     325  
     326                  --___
     327                  Content-Type: text/html
     328  
     329                  <p>simple body</p>
     330  
     331                  --___--
     332  
     333                  --+++
     334                  Content-Type: image/jpg
     335                  Content-ID: <image1@cid>
     336  
     337                  bogus jpg body
     338  
     339                  --+++--
     340  
     341                  --===
     342                  Content-Type: image/jpg
     343                  Content-Disposition: attachment
     344  
     345                  bogus jpg body
     346  
     347                  --===
     348                  Content-Type: image/jpg
     349                  Content-Disposition: attachment
     350  
     351                  another bogus jpg body
     352  
     353                  --===--
     354                  """)),
     355  
     356          # Same thing, but proving we only look at the root part, which is the
     357          # first one if there isn't any start parameter.  That is, this is a
     358          # broken related.
     359          'mixed_related_alternative_plain_html_wrong_order': (
     360              (1, None, None),
     361              (6, 7),
     362              (1, 6, 7),
     363              textwrap.dedent("""\
     364                  To: foo@example.com
     365                  MIME-Version: 1.0
     366                  Content-Type: multipart/mixed; boundary="==="
     367  
     368                  --===
     369                  Content-Type: multipart/related; boundary="+++"
     370  
     371                  --+++
     372                  Content-Type: image/jpg
     373                  Content-ID: <image1@cid>
     374  
     375                  bogus jpg body
     376  
     377                  --+++
     378                  Content-Type: multipart/alternative; boundary="___"
     379  
     380                  --___
     381                  Content-Type: text/plain
     382  
     383                  simple body
     384  
     385                  --___
     386                  Content-Type: text/html
     387  
     388                  <p>simple body</p>
     389  
     390                  --___--
     391  
     392                  --+++--
     393  
     394                  --===
     395                  Content-Type: image/jpg
     396                  Content-Disposition: attachment
     397  
     398                  bogus jpg body
     399  
     400                  --===
     401                  Content-Type: image/jpg
     402                  Content-Disposition: attachment
     403  
     404                  another bogus jpg body
     405  
     406                  --===--
     407                  """)),
     408  
     409          'message_rfc822': (
     410              (None, None, None),
     411              (),
     412              (),
     413              textwrap.dedent("""\
     414                  To: foo@example.com
     415                  MIME-Version: 1.0
     416                  Content-Type: message/rfc822
     417  
     418                  To: bar@example.com
     419                  From: robot@examp.com
     420  
     421                  this is a message body.
     422                  """)),
     423  
     424          'mixed_text_message_rfc822': (
     425              (None, None, 1),
     426              (2,),
     427              (1, 2),
     428              textwrap.dedent("""\
     429                  To: foo@example.com
     430                  MIME-Version: 1.0
     431                  Content-Type: multipart/mixed; boundary="==="
     432  
     433                  --===
     434                  Content-Type: text/plain
     435  
     436                  Your message has bounced, sir.
     437  
     438                  --===
     439                  Content-Type: message/rfc822
     440  
     441                  To: bar@example.com
     442                  From: robot@examp.com
     443  
     444                  this is a message body.
     445  
     446                  --===--
     447                  """)),
     448  
     449           }
     450  
     451      def message_as_get_body(self, body_parts, attachments, parts, msg):
     452          m = self._str_msg(msg)
     453          allparts = list(m.walk())
     454          expected = [None if n is None else allparts[n] for n in body_parts]
     455          related = 0; html = 1; plain = 2
     456          self.assertEqual(m.get_body(), first(expected))
     457          self.assertEqual(m.get_body(preferencelist=(
     458                                          'related', 'html', 'plain')),
     459                           first(expected))
     460          self.assertEqual(m.get_body(preferencelist=('related', 'html')),
     461                           first(expected[related:html+1]))
     462          self.assertEqual(m.get_body(preferencelist=('related', 'plain')),
     463                           first([expected[related], expected[plain]]))
     464          self.assertEqual(m.get_body(preferencelist=('html', 'plain')),
     465                           first(expected[html:plain+1]))
     466          self.assertEqual(m.get_body(preferencelist=['related']),
     467                           expected[related])
     468          self.assertEqual(m.get_body(preferencelist=['html']), expected[html])
     469          self.assertEqual(m.get_body(preferencelist=['plain']), expected[plain])
     470          self.assertEqual(m.get_body(preferencelist=('plain', 'html')),
     471                           first(expected[plain:html-1:-1]))
     472          self.assertEqual(m.get_body(preferencelist=('plain', 'related')),
     473                           first([expected[plain], expected[related]]))
     474          self.assertEqual(m.get_body(preferencelist=('html', 'related')),
     475                           first(expected[html::-1]))
     476          self.assertEqual(m.get_body(preferencelist=('plain', 'html', 'related')),
     477                           first(expected[::-1]))
     478          self.assertEqual(m.get_body(preferencelist=('html', 'plain', 'related')),
     479                           first([expected[html],
     480                                  expected[plain],
     481                                  expected[related]]))
     482  
     483      def message_as_iter_attachment(self, body_parts, attachments, parts, msg):
     484          m = self._str_msg(msg)
     485          allparts = list(m.walk())
     486          attachments = [allparts[n] for n in attachments]
     487          self.assertEqual(list(m.iter_attachments()), attachments)
     488  
     489      def message_as_iter_parts(self, body_parts, attachments, parts, msg):
     490          def _is_multipart_msg(msg):
     491              return 'Content-Type: multipart' in msg
     492  
     493          m = self._str_msg(msg)
     494          allparts = list(m.walk())
     495          parts = [allparts[n] for n in parts]
     496          iter_parts = list(m.iter_parts()) if _is_multipart_msg(msg) else []
     497          self.assertEqual(iter_parts, parts)
     498  
     499      class ESC[4;38;5;81m_TestContentManager:
     500          def get_content(self, msg, *args, **kw):
     501              return msg, args, kw
     502          def set_content(self, msg, *args, **kw):
     503              self.msg = msg
     504              self.args = args
     505              self.kw = kw
     506  
     507      def test_get_content_with_cm(self):
     508          m = self._str_msg('')
     509          cm = self._TestContentManager()
     510          self.assertEqual(m.get_content(content_manager=cm), (m, (), {}))
     511          msg, args, kw = m.get_content('foo', content_manager=cm, bar=1, k=2)
     512          self.assertEqual(msg, m)
     513          self.assertEqual(args, ('foo',))
     514          self.assertEqual(kw, dict(bar=1, k=2))
     515  
     516      def test_get_content_default_cm_comes_from_policy(self):
     517          p = policy.default.clone(content_manager=self._TestContentManager())
     518          m = self._str_msg('', policy=p)
     519          self.assertEqual(m.get_content(), (m, (), {}))
     520          msg, args, kw = m.get_content('foo', bar=1, k=2)
     521          self.assertEqual(msg, m)
     522          self.assertEqual(args, ('foo',))
     523          self.assertEqual(kw, dict(bar=1, k=2))
     524  
     525      def test_set_content_with_cm(self):
     526          m = self._str_msg('')
     527          cm = self._TestContentManager()
     528          m.set_content(content_manager=cm)
     529          self.assertEqual(cm.msg, m)
     530          self.assertEqual(cm.args, ())
     531          self.assertEqual(cm.kw, {})
     532          m.set_content('foo', content_manager=cm, bar=1, k=2)
     533          self.assertEqual(cm.msg, m)
     534          self.assertEqual(cm.args, ('foo',))
     535          self.assertEqual(cm.kw, dict(bar=1, k=2))
     536  
     537      def test_set_content_default_cm_comes_from_policy(self):
     538          cm = self._TestContentManager()
     539          p = policy.default.clone(content_manager=cm)
     540          m = self._str_msg('', policy=p)
     541          m.set_content()
     542          self.assertEqual(cm.msg, m)
     543          self.assertEqual(cm.args, ())
     544          self.assertEqual(cm.kw, {})
     545          m.set_content('foo', bar=1, k=2)
     546          self.assertEqual(cm.msg, m)
     547          self.assertEqual(cm.args, ('foo',))
     548          self.assertEqual(cm.kw, dict(bar=1, k=2))
     549  
     550      # outcome is whether xxx_method should raise ValueError error when called
     551      # on multipart/subtype.  Blank outcome means it depends on xxx (add
     552      # succeeds, make raises).  Note: 'none' means there are content-type
     553      # headers but payload is None...this happening in practice would be very
     554      # unusual, so treating it as if there were content seems reasonable.
     555      #    method          subtype           outcome
     556      subtype_params = (
     557          ('related',      'no_content',     'succeeds'),
     558          ('related',      'none',           'succeeds'),
     559          ('related',      'plain',          'succeeds'),
     560          ('related',      'related',        ''),
     561          ('related',      'alternative',    'raises'),
     562          ('related',      'mixed',          'raises'),
     563          ('alternative',  'no_content',     'succeeds'),
     564          ('alternative',  'none',           'succeeds'),
     565          ('alternative',  'plain',          'succeeds'),
     566          ('alternative',  'related',        'succeeds'),
     567          ('alternative',  'alternative',    ''),
     568          ('alternative',  'mixed',          'raises'),
     569          ('mixed',        'no_content',     'succeeds'),
     570          ('mixed',        'none',           'succeeds'),
     571          ('mixed',        'plain',          'succeeds'),
     572          ('mixed',        'related',        'succeeds'),
     573          ('mixed',        'alternative',    'succeeds'),
     574          ('mixed',        'mixed',          ''),
     575          )
     576  
     577      def _make_subtype_test_message(self, subtype):
     578          m = self.message()
     579          payload = None
     580          msg_headers =  [
     581              ('To', 'foo@bar.com'),
     582              ('From', 'bar@foo.com'),
     583              ]
     584          if subtype != 'no_content':
     585              ('content-shadow', 'Logrus'),
     586          msg_headers.append(('X-Random-Header', 'Corwin'))
     587          if subtype == 'text':
     588              payload = ''
     589              msg_headers.append(('Content-Type', 'text/plain'))
     590              m.set_payload('')
     591          elif subtype != 'no_content':
     592              payload = []
     593              msg_headers.append(('Content-Type', 'multipart/' + subtype))
     594          msg_headers.append(('X-Trump', 'Random'))
     595          m.set_payload(payload)
     596          for name, value in msg_headers:
     597              m[name] = value
     598          return m, msg_headers, payload
     599  
     600      def _check_disallowed_subtype_raises(self, m, method_name, subtype, method):
     601          with self.assertRaises(ValueError) as ar:
     602              getattr(m, method)()
     603          exc_text = str(ar.exception)
     604          self.assertIn(subtype, exc_text)
     605          self.assertIn(method_name, exc_text)
     606  
     607      def _check_make_multipart(self, m, msg_headers, payload):
     608          count = 0
     609          for name, value in msg_headers:
     610              if not name.lower().startswith('content-'):
     611                  self.assertEqual(m[name], value)
     612                  count += 1
     613          self.assertEqual(len(m), count+1) # +1 for new Content-Type
     614          part = next(m.iter_parts())
     615          count = 0
     616          for name, value in msg_headers:
     617              if name.lower().startswith('content-'):
     618                  self.assertEqual(part[name], value)
     619                  count += 1
     620          self.assertEqual(len(part), count)
     621          self.assertEqual(part.get_payload(), payload)
     622  
     623      def subtype_as_make(self, method, subtype, outcome):
     624          m, msg_headers, payload = self._make_subtype_test_message(subtype)
     625          make_method = 'make_' + method
     626          if outcome in ('', 'raises'):
     627              self._check_disallowed_subtype_raises(m, method, subtype, make_method)
     628              return
     629          getattr(m, make_method)()
     630          self.assertEqual(m.get_content_maintype(), 'multipart')
     631          self.assertEqual(m.get_content_subtype(), method)
     632          if subtype == 'no_content':
     633              self.assertEqual(len(m.get_payload()), 0)
     634              self.assertEqual(m.items(),
     635                               msg_headers + [('Content-Type',
     636                                               'multipart/'+method)])
     637          else:
     638              self.assertEqual(len(m.get_payload()), 1)
     639              self._check_make_multipart(m, msg_headers, payload)
     640  
     641      def subtype_as_make_with_boundary(self, method, subtype, outcome):
     642          # Doing all variation is a bit of overkill...
     643          m = self.message()
     644          if outcome in ('', 'raises'):
     645              m['Content-Type'] = 'multipart/' + subtype
     646              with self.assertRaises(ValueError) as cm:
     647                  getattr(m, 'make_' + method)()
     648              return
     649          if subtype == 'plain':
     650              m['Content-Type'] = 'text/plain'
     651          elif subtype != 'no_content':
     652              m['Content-Type'] = 'multipart/' + subtype
     653          getattr(m, 'make_' + method)(boundary="abc")
     654          self.assertTrue(m.is_multipart())
     655          self.assertEqual(m.get_boundary(), 'abc')
     656  
     657      def test_policy_on_part_made_by_make_comes_from_message(self):
     658          for method in ('make_related', 'make_alternative', 'make_mixed'):
     659              m = self.message(policy=self.policy.clone(content_manager='foo'))
     660              m['Content-Type'] = 'text/plain'
     661              getattr(m, method)()
     662              self.assertEqual(m.get_payload(0).policy.content_manager, 'foo')
     663  
     664      class ESC[4;38;5;81m_TestSetContentManager:
     665          def set_content(self, msg, content, *args, **kw):
     666              msg['Content-Type'] = 'text/plain'
     667              msg.set_payload(content)
     668  
     669      def subtype_as_add(self, method, subtype, outcome):
     670          m, msg_headers, payload = self._make_subtype_test_message(subtype)
     671          cm = self._TestSetContentManager()
     672          add_method = 'add_attachment' if method=='mixed' else 'add_' + method
     673          if outcome == 'raises':
     674              self._check_disallowed_subtype_raises(m, method, subtype, add_method)
     675              return
     676          getattr(m, add_method)('test', content_manager=cm)
     677          self.assertEqual(m.get_content_maintype(), 'multipart')
     678          self.assertEqual(m.get_content_subtype(), method)
     679          if method == subtype or subtype == 'no_content':
     680              self.assertEqual(len(m.get_payload()), 1)
     681              for name, value in msg_headers:
     682                  self.assertEqual(m[name], value)
     683              part = m.get_payload()[0]
     684          else:
     685              self.assertEqual(len(m.get_payload()), 2)
     686              self._check_make_multipart(m, msg_headers, payload)
     687              part = m.get_payload()[1]
     688          self.assertEqual(part.get_content_type(), 'text/plain')
     689          self.assertEqual(part.get_payload(), 'test')
     690          if method=='mixed':
     691              self.assertEqual(part['Content-Disposition'], 'attachment')
     692          elif method=='related':
     693              self.assertEqual(part['Content-Disposition'], 'inline')
     694          else:
     695              # Otherwise we don't guess.
     696              self.assertIsNone(part['Content-Disposition'])
     697  
     698      class ESC[4;38;5;81m_TestSetRaisingContentManager:
     699          class ESC[4;38;5;81mCustomError(ESC[4;38;5;149mException):
     700              pass
     701          def set_content(self, msg, content, *args, **kw):
     702              raise self.CustomError('test')
     703  
     704      def test_default_content_manager_for_add_comes_from_policy(self):
     705          cm = self._TestSetRaisingContentManager()
     706          m = self.message(policy=self.policy.clone(content_manager=cm))
     707          for method in ('add_related', 'add_alternative', 'add_attachment'):
     708              with self.assertRaises(self._TestSetRaisingContentManager.CustomError) as ar:
     709                  getattr(m, method)('')
     710              self.assertEqual(str(ar.exception), 'test')
     711  
     712      def message_as_clear(self, body_parts, attachments, parts, msg):
     713          m = self._str_msg(msg)
     714          m.clear()
     715          self.assertEqual(len(m), 0)
     716          self.assertEqual(list(m.items()), [])
     717          self.assertIsNone(m.get_payload())
     718          self.assertEqual(list(m.iter_parts()), [])
     719  
     720      def message_as_clear_content(self, body_parts, attachments, parts, msg):
     721          m = self._str_msg(msg)
     722          expected_headers = [h for h in m.keys()
     723                              if not h.lower().startswith('content-')]
     724          m.clear_content()
     725          self.assertEqual(list(m.keys()), expected_headers)
     726          self.assertIsNone(m.get_payload())
     727          self.assertEqual(list(m.iter_parts()), [])
     728  
     729      def test_is_attachment(self):
     730          m = self._make_message()
     731          self.assertFalse(m.is_attachment())
     732          m['Content-Disposition'] = 'inline'
     733          self.assertFalse(m.is_attachment())
     734          m.replace_header('Content-Disposition', 'attachment')
     735          self.assertTrue(m.is_attachment())
     736          m.replace_header('Content-Disposition', 'AtTachMent')
     737          self.assertTrue(m.is_attachment())
     738          m.set_param('filename', 'abc.png', 'Content-Disposition')
     739          self.assertTrue(m.is_attachment())
     740  
     741      def test_iter_attachments_mutation(self):
     742          # We had a bug where iter_attachments was mutating the list.
     743          m = self._make_message()
     744          m.set_content('arbitrary text as main part')
     745          m.add_related('more text as a related part')
     746          m.add_related('yet more text as a second "attachment"')
     747          orig = m.get_payload().copy()
     748          self.assertEqual(len(list(m.iter_attachments())), 2)
     749          self.assertEqual(m.get_payload(), orig)
     750  
     751  
     752  class ESC[4;38;5;81mTestEmailMessage(ESC[4;38;5;149mTestEmailMessageBase, ESC[4;38;5;149mTestEmailBase):
     753      message = EmailMessage
     754  
     755      def test_set_content_adds_MIME_Version(self):
     756          m = self._str_msg('')
     757          cm = self._TestContentManager()
     758          self.assertNotIn('MIME-Version', m)
     759          m.set_content(content_manager=cm)
     760          self.assertEqual(m['MIME-Version'], '1.0')
     761  
     762      class ESC[4;38;5;81m_MIME_Version_adding_CM:
     763          def set_content(self, msg, *args, **kw):
     764              msg['MIME-Version'] = '1.0'
     765  
     766      def test_set_content_does_not_duplicate_MIME_Version(self):
     767          m = self._str_msg('')
     768          cm = self._MIME_Version_adding_CM()
     769          self.assertNotIn('MIME-Version', m)
     770          m.set_content(content_manager=cm)
     771          self.assertEqual(m['MIME-Version'], '1.0')
     772  
     773      def test_as_string_uses_max_header_length_by_default(self):
     774          m = self._str_msg('Subject: long line' + ' ab'*50 + '\n\n')
     775          self.assertEqual(len(m.as_string().strip().splitlines()), 3)
     776  
     777      def test_as_string_allows_maxheaderlen(self):
     778          m = self._str_msg('Subject: long line' + ' ab'*50 + '\n\n')
     779          self.assertEqual(len(m.as_string(maxheaderlen=0).strip().splitlines()),
     780                           1)
     781          self.assertEqual(len(m.as_string(maxheaderlen=34).strip().splitlines()),
     782                           6)
     783  
     784      def test_as_string_unixform(self):
     785          m = self._str_msg('test')
     786          m.set_unixfrom('From foo@bar Thu Jan  1 00:00:00 1970')
     787          self.assertEqual(m.as_string(unixfrom=True),
     788                          'From foo@bar Thu Jan  1 00:00:00 1970\n\ntest')
     789          self.assertEqual(m.as_string(unixfrom=False), '\ntest')
     790  
     791      def test_str_defaults_to_policy_max_line_length(self):
     792          m = self._str_msg('Subject: long line' + ' ab'*50 + '\n\n')
     793          self.assertEqual(len(str(m).strip().splitlines()), 3)
     794  
     795      def test_str_defaults_to_utf8(self):
     796          m = EmailMessage()
     797          m['Subject'] = 'unicöde'
     798          self.assertEqual(str(m), 'Subject: unicöde\n\n')
     799  
     800      def test_folding_with_utf8_encoding_1(self):
     801          # bpo-36520
     802          #
     803          # Fold a line that contains UTF-8 words before
     804          # and after the whitespace fold point, where the
     805          # line length limit is reached within an ASCII
     806          # word.
     807  
     808          m = EmailMessage()
     809          m['Subject'] = 'Hello Wörld! Hello Wörld! '            \
     810                         'Hello Wörld! Hello Wörld!Hello Wörld!'
     811          self.assertEqual(bytes(m),
     812                           b'Subject: Hello =?utf-8?q?W=C3=B6rld!_Hello_W'
     813                           b'=C3=B6rld!_Hello_W=C3=B6rld!?=\n'
     814                           b' Hello =?utf-8?q?W=C3=B6rld!Hello_W=C3=B6rld!?=\n\n')
     815  
     816  
     817      def test_folding_with_utf8_encoding_2(self):
     818          # bpo-36520
     819          #
     820          # Fold a line that contains UTF-8 words before
     821          # and after the whitespace fold point, where the
     822          # line length limit is reached at the end of an
     823          # encoded word.
     824  
     825          m = EmailMessage()
     826          m['Subject'] = 'Hello Wörld! Hello Wörld! '                \
     827                         'Hello Wörlds123! Hello Wörld!Hello Wörld!'
     828          self.assertEqual(bytes(m),
     829                           b'Subject: Hello =?utf-8?q?W=C3=B6rld!_Hello_W'
     830                           b'=C3=B6rld!_Hello_W=C3=B6rlds123!?=\n'
     831                           b' Hello =?utf-8?q?W=C3=B6rld!Hello_W=C3=B6rld!?=\n\n')
     832  
     833      def test_folding_with_utf8_encoding_3(self):
     834          # bpo-36520
     835          #
     836          # Fold a line that contains UTF-8 words before
     837          # and after the whitespace fold point, where the
     838          # line length limit is reached at the end of the
     839          # first word.
     840  
     841          m = EmailMessage()
     842          m['Subject'] = 'Hello-Wörld!-Hello-Wörld!-Hello-Wörlds123! ' \
     843                         'Hello Wörld!Hello Wörld!'
     844          self.assertEqual(bytes(m), \
     845                           b'Subject: =?utf-8?q?Hello-W=C3=B6rld!-Hello-W'
     846                           b'=C3=B6rld!-Hello-W=C3=B6rlds123!?=\n'
     847                           b' Hello =?utf-8?q?W=C3=B6rld!Hello_W=C3=B6rld!?=\n\n')
     848  
     849      def test_folding_with_utf8_encoding_4(self):
     850          # bpo-36520
     851          #
     852          # Fold a line that contains UTF-8 words before
     853          # and after the fold point, where the first
     854          # word is UTF-8 and the fold point is within
     855          # the word.
     856  
     857          m = EmailMessage()
     858          m['Subject'] = 'Hello-Wörld!-Hello-Wörld!-Hello-Wörlds123!-Hello' \
     859                         ' Wörld!Hello Wörld!'
     860          self.assertEqual(bytes(m),
     861                           b'Subject: =?utf-8?q?Hello-W=C3=B6rld!-Hello-W'
     862                           b'=C3=B6rld!-Hello-W=C3=B6rlds123!?=\n'
     863                           b' =?utf-8?q?-Hello_W=C3=B6rld!Hello_W=C3=B6rld!?=\n\n')
     864  
     865      def test_folding_with_utf8_encoding_5(self):
     866          # bpo-36520
     867          #
     868          # Fold a line that contains a UTF-8 word after
     869          # the fold point.
     870  
     871          m = EmailMessage()
     872          m['Subject'] = '123456789 123456789 123456789 123456789 123456789' \
     873                         ' 123456789 123456789 Hello Wörld!'
     874          self.assertEqual(bytes(m),
     875                           b'Subject: 123456789 123456789 123456789 123456789'
     876                           b' 123456789 123456789 123456789\n'
     877                           b' Hello =?utf-8?q?W=C3=B6rld!?=\n\n')
     878  
     879      def test_folding_with_utf8_encoding_6(self):
     880          # bpo-36520
     881          #
     882          # Fold a line that contains a UTF-8 word before
     883          # the fold point and ASCII words after
     884  
     885          m = EmailMessage()
     886          m['Subject'] = '123456789 123456789 123456789 123456789 Hello Wörld!' \
     887                         ' 123456789 123456789 123456789 123456789 123456789'   \
     888                         ' 123456789'
     889          self.assertEqual(bytes(m),
     890                           b'Subject: 123456789 123456789 123456789 123456789'
     891                           b' Hello =?utf-8?q?W=C3=B6rld!?=\n 123456789 '
     892                           b'123456789 123456789 123456789 123456789 '
     893                           b'123456789\n\n')
     894  
     895      def test_folding_with_utf8_encoding_7(self):
     896          # bpo-36520
     897          #
     898          # Fold a line twice that contains UTF-8 words before
     899          # and after the first fold point, and ASCII words
     900          # after the second fold point.
     901  
     902          m = EmailMessage()
     903          m['Subject'] = '123456789 123456789 Hello Wörld! Hello Wörld! '       \
     904                         '123456789-123456789 123456789 Hello Wörld! 123456789' \
     905                         ' 123456789'
     906          self.assertEqual(bytes(m),
     907                           b'Subject: 123456789 123456789 Hello =?utf-8?q?'
     908                           b'W=C3=B6rld!_Hello_W=C3=B6rld!?=\n'
     909                           b' 123456789-123456789 123456789 Hello '
     910                           b'=?utf-8?q?W=C3=B6rld!?= 123456789\n 123456789\n\n')
     911  
     912      def test_folding_with_utf8_encoding_8(self):
     913          # bpo-36520
     914          #
     915          # Fold a line twice that contains UTF-8 words before
     916          # the first fold point, and ASCII words after the
     917          # first fold point, and UTF-8 words after the second
     918          # fold point.
     919  
     920          m = EmailMessage()
     921          m['Subject'] = '123456789 123456789 Hello Wörld! Hello Wörld! '       \
     922                         '123456789 123456789 123456789 123456789 123456789 '   \
     923                         '123456789-123456789 123456789 Hello Wörld! 123456789' \
     924                         ' 123456789'
     925          self.assertEqual(bytes(m),
     926                           b'Subject: 123456789 123456789 Hello '
     927                           b'=?utf-8?q?W=C3=B6rld!_Hello_W=C3=B6rld!?=\n 123456789 '
     928                           b'123456789 123456789 123456789 123456789 '
     929                           b'123456789-123456789\n 123456789 Hello '
     930                           b'=?utf-8?q?W=C3=B6rld!?= 123456789 123456789\n\n')
     931  
     932      def test_get_body_malformed(self):
     933          """test for bpo-42892"""
     934          msg = textwrap.dedent("""\
     935              Message-ID: <674392CA.4347091@email.au>
     936              Date: Wed, 08 Nov 2017 08:50:22 +0700
     937              From: Foo Bar <email@email.au>
     938              MIME-Version: 1.0
     939              To: email@email.com <email@email.com>
     940              Subject: Python Email
     941              Content-Type: multipart/mixed;
     942              boundary="------------879045806563892972123996"
     943              X-Global-filter:Messagescannedforspamandviruses:passedalltests
     944  
     945              This is a multi-part message in MIME format.
     946              --------------879045806563892972123996
     947              Content-Type: text/plain; charset=ISO-8859-1; format=flowed
     948              Content-Transfer-Encoding: 7bit
     949  
     950              Your message is ready to be sent with the following file or link
     951              attachments:
     952              XU89 - 08.11.2017
     953              """)
     954          m = self._str_msg(msg)
     955          # In bpo-42892, this would raise
     956          # AttributeError: 'str' object has no attribute 'is_attachment'
     957          m.get_body()
     958  
     959  
     960  class ESC[4;38;5;81mTestMIMEPart(ESC[4;38;5;149mTestEmailMessageBase, ESC[4;38;5;149mTestEmailBase):
     961      # Doing the full test run here may seem a bit redundant, since the two
     962      # classes are almost identical.  But what if they drift apart?  So we do
     963      # the full tests so that any future drift doesn't introduce bugs.
     964      message = MIMEPart
     965  
     966      def test_set_content_does_not_add_MIME_Version(self):
     967          m = self._str_msg('')
     968          cm = self._TestContentManager()
     969          self.assertNotIn('MIME-Version', m)
     970          m.set_content(content_manager=cm)
     971          self.assertNotIn('MIME-Version', m)
     972  
     973      def test_string_payload_with_multipart_content_type(self):
     974          msg = message_from_string(textwrap.dedent("""\
     975          Content-Type: multipart/mixed; charset="utf-8"
     976  
     977          sample text
     978          """), policy=policy.default)
     979          attachments = msg.iter_attachments()
     980          self.assertEqual(list(attachments), [])
     981  
     982  
     983  if __name__ == '__main__':
     984      unittest.main()