[Python-checkins] cpython (3.2): #12147: make send_message correctly handle Sender and Resent- headers.

r.david.murray python-checkins at python.org
Sun Jul 3 03:14:31 CEST 2011


http://hg.python.org/cpython/rev/0f5ea42fb46c
changeset:   71138:0f5ea42fb46c
branch:      3.2
parent:      71134:14efcccc70e6
user:        R David Murray <rdmurray at bitdance.com>
date:        Sat Jul 02 21:03:19 2011 -0400
summary:
  #12147: make send_message correctly handle Sender and Resent- headers.

Original patch by Nicolas Estibals.  My tweaks to the patch were mostly
style/cosmetic, and adding more tests.

files:
  Doc/library/smtplib.rst  |   27 ++++-
  Lib/smtplib.py           |   52 +++++++++---
  Lib/test/test_smtplib.py |  111 ++++++++++++++++++++++++++-
  Misc/ACKS                |    1 +
  Misc/NEWS                |    3 +
  5 files changed, 172 insertions(+), 22 deletions(-)


diff --git a/Doc/library/smtplib.rst b/Doc/library/smtplib.rst
--- a/Doc/library/smtplib.rst
+++ b/Doc/library/smtplib.rst
@@ -323,21 +323,32 @@
    .. versionchanged:: 3.2 *msg* may be a byte string.
 
 
-.. method:: SMTP.send_message(msg, from_addr=None, to_addrs=None, mail_options=[], rcpt_options=[])
+.. method:: SMTP.send_message(msg, from_addr=None, to_addrs=None, \
+                              mail_options=[], rcpt_options=[])
 
    This is a convenience method for calling :meth:`sendmail` with the message
    represented by an :class:`email.message.Message` object.  The arguments have
    the same meaning as for :meth:`sendmail`, except that *msg* is a ``Message``
    object.
 
-   If *from_addr* is ``None``, ``send_message`` sets its value to the value of
-   the :mailheader:`From` header from *msg*.  If *to_addrs* is ``None``,
-   ``send_message`` combines the values (if any) of the :mailheader:`To`,
-   :mailheader:`CC`, and :mailheader:`Bcc` fields from *msg*.  Regardless of
-   the values of *from_addr* and *to_addrs*, ``send_message`` deletes any  Bcc
-   field from *msg*.  It then serializes *msg* using
+   If *from_addr* is ``None`` or *to_addrs* is ``None``, ``send_message`` fills
+   those arguments with addresses extracted from the headers of *msg* as
+   specified in :rfc:`2822`\: *from_addr* is set to the :mailheader:`Sender`
+   field if it is present, and otherwise to the :mailheader:`From` field.
+   *to_adresses* combines the values (if any) of the :mailheader:`To`,
+   :mailheader:`Cc`, and :mailheader:`Bcc` fields from *msg*.  If exactly one
+   set of :mailheader:`Resent-*` headers appear in the message, the regular
+   headers are ignored and the :mailheader:`Resent-*` headers are used instead.
+   If the message contains more than one set of :mailheader:`Resent-*` headers,
+   a :exc:`ValueError` is raised, since there is no way to unambiguously detect
+   the most recent set of :mailheader:`Resent-` headers.
+
+   ``send_message`` serializes *msg* using
    :class:`~email.generator.BytesGenerator` with ``\r\n`` as the *linesep*, and
-   calls :meth:`sendmail` to transmit the resulting message.
+   calls :meth:`sendmail` to transmit the resulting message.  Regardless of the
+   values of *from_addr* and *to_addrs*, ``send_message`` does not transmit any
+   :mailheader:`Bcc` or :mailheader:`Resent-Bcc` headers that may appear
+   in *msg*.
 
    .. versionadded:: 3.2
 
diff --git a/Lib/smtplib.py b/Lib/smtplib.py
old mode 100755
new mode 100644
--- a/Lib/smtplib.py
+++ b/Lib/smtplib.py
@@ -49,6 +49,7 @@
 import email.generator
 import base64
 import hmac
+import copy
 from email.base64mime import body_encode as encode_base64
 from sys import stderr
 
@@ -674,7 +675,7 @@
 
         msg may be a string containing characters in the ASCII range, or a byte
         string.  A string is encoded to bytes using the ascii codec, and lone
-        \r and \n characters are converted to \r\n characters.
+        \\r and \\n characters are converted to \\r\\n characters.
 
         If there has been no previous EHLO or HELO command this session, this
         method tries ESMTP EHLO first.  If the server does ESMTP, message size
@@ -757,24 +758,49 @@
         """Converts message to a bytestring and passes it to sendmail.
 
         The arguments are as for sendmail, except that msg is an
-        email.message.Message object.  If from_addr is None, the from_addr is
-        taken from the 'From' header of the Message.  If to_addrs is None, its
-        value is composed from the addresses listed in the 'To', 'CC', and
-        'Bcc' fields.  Regardless of the values of from_addr and to_addr, any
-        Bcc field in the Message object is deleted.  The Message object is then
-        serialized using email.generator.BytesGenerator and sendmail is called
-        to transmit the message.
+        email.message.Message object.  If from_addr is None or to_addrs is
+        None, these arguments are taken from the headers of the Message as
+        described in RFC 2822 (a ValueError is raised if there is more than
+        one set of 'Resent-' headers).  Regardless of the values of from_addr and
+        to_addr, any Bcc field (or Resent-Bcc field, when the Message is a
+        resent) of the Message object won't be transmitted.  The Message
+        object is then serialized using email.generator.BytesGenerator and
+        sendmail is called to transmit the message.
+
         """
+        # 'Resent-Date' is a mandatory field if the Message is resent (RFC 2822
+        # Section 3.6.6). In such a case, we use the 'Resent-*' fields.  However,
+        # if there is more than one 'Resent-' block there's no way to
+        # unambiguously determine which one is the most recent in all cases,
+        # so rather than guess we raise a ValueError in that case.
+        #
+        # TODO implement heuristics to guess the correct Resent-* block with an
+        # option allowing the user to enable the heuristics.  (It should be
+        # possible to guess correctly almost all of the time.)
+        resent =msg.get_all('Resent-Date')
+        if resent is None:
+            header_prefix = ''
+        elif len(resent) == 1:
+            header_prefix = 'Resent-'
+        else:
+            raise ValueError("message has more than one 'Resent-' header block")
         if from_addr is None:
-            from_addr = msg['From']
+            # Prefer the sender field per RFC 2822:3.6.2.
+            from_addr = (msg[header_prefix+'Sender']
+                           if (header_prefix+'Sender') in msg
+                           else msg[header_prefix+'From'])
         if to_addrs is None:
-            addr_fields = [f for f in (msg['To'], msg['Bcc'], msg['CC'])
-                            if f is not None]
+            addr_fields = [f for f in (msg[header_prefix+'To'],
+                                       msg[header_prefix+'Bcc'],
+                                       msg[header_prefix+'Cc']) if f is not None]
             to_addrs = [a[1] for a in email.utils.getaddresses(addr_fields)]
-        del msg['Bcc']
+        # Make a local copy so we can delete the bcc headers.
+        msg_copy = copy.copy(msg)
+        del msg_copy['Bcc']
+        del msg_copy['Resent-Bcc']
         with io.BytesIO() as bytesmsg:
             g = email.generator.BytesGenerator(bytesmsg)
-            g.flatten(msg, linesep='\r\n')
+            g.flatten(msg_copy, linesep='\r\n')
             flatmsg = bytesmsg.getvalue()
         return self.sendmail(from_addr, to_addrs, flatmsg, mail_options,
                              rcpt_options)
diff --git a/Lib/test/test_smtplib.py b/Lib/test/test_smtplib.py
--- a/Lib/test/test_smtplib.py
+++ b/Lib/test/test_smtplib.py
@@ -320,13 +320,16 @@
         # XXX (see comment in testSend)
         time.sleep(0.01)
         smtp.quit()
+        # make sure the Bcc header is still in the message.
+        self.assertEqual(m['Bcc'], 'John Root <root at localhost>, "Dinsdale" '
+                                    '<warped at silly.walks.com>')
 
         self.client_evt.set()
         self.serv_evt.wait()
         self.output.flush()
         # Add the X-Peer header that DebuggingServer adds
         m['X-Peer'] = socket.gethostbyname('localhost')
-        # The Bcc header is deleted before serialization.
+        # The Bcc header should not be transmitted.
         del m['Bcc']
         mexpect = '%s%s\n%s' % (MSG_BEGIN, m.as_string(), MSG_END)
         self.assertEqual(self.output.getvalue(), mexpect)
@@ -365,6 +368,112 @@
                                  re.MULTILINE)
             self.assertRegex(debugout, to_addr)
 
+    def testSendMessageWithSpecifiedAddresses(self):
+        # Make sure addresses specified in call override those in message.
+        m = email.mime.text.MIMEText('A test message')
+        m['From'] = 'foo at bar.com'
+        m['To'] = 'John, Dinsdale'
+        smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
+        smtp.send_message(m, from_addr='joe at example.com', to_addrs='foo at example.net')
+        # XXX (see comment in testSend)
+        time.sleep(0.01)
+        smtp.quit()
+
+        self.client_evt.set()
+        self.serv_evt.wait()
+        self.output.flush()
+        # Add the X-Peer header that DebuggingServer adds
+        m['X-Peer'] = socket.gethostbyname('localhost')
+        mexpect = '%s%s\n%s' % (MSG_BEGIN, m.as_string(), MSG_END)
+        self.assertEqual(self.output.getvalue(), mexpect)
+        debugout = smtpd.DEBUGSTREAM.getvalue()
+        sender = re.compile("^sender: joe at example.com$", re.MULTILINE)
+        self.assertRegex(debugout, sender)
+        for addr in ('John', 'Dinsdale'):
+            to_addr = re.compile(r"^recips: .*'{}'.*$".format(addr),
+                                 re.MULTILINE)
+            self.assertNotRegex(debugout, to_addr)
+        recip = re.compile(r"^recips: .*'foo at example.net'.*$", re.MULTILINE)
+        self.assertRegex(debugout, recip)
+
+    def testSendMessageWithMultipleFrom(self):
+        # Sender overrides To
+        m = email.mime.text.MIMEText('A test message')
+        m['From'] = 'Bernard, Bianca'
+        m['Sender'] = 'the_rescuers at Rescue-Aid-Society.com'
+        m['To'] = 'John, Dinsdale'
+        smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
+        smtp.send_message(m)
+        # XXX (see comment in testSend)
+        time.sleep(0.01)
+        smtp.quit()
+
+        self.client_evt.set()
+        self.serv_evt.wait()
+        self.output.flush()
+        # Add the X-Peer header that DebuggingServer adds
+        m['X-Peer'] = socket.gethostbyname('localhost')
+        mexpect = '%s%s\n%s' % (MSG_BEGIN, m.as_string(), MSG_END)
+        self.assertEqual(self.output.getvalue(), mexpect)
+        debugout = smtpd.DEBUGSTREAM.getvalue()
+        sender = re.compile("^sender: the_rescuers at Rescue-Aid-Society.com$", re.MULTILINE)
+        self.assertRegex(debugout, sender)
+        for addr in ('John', 'Dinsdale'):
+            to_addr = re.compile(r"^recips: .*'{}'.*$".format(addr),
+                                 re.MULTILINE)
+            self.assertRegex(debugout, to_addr)
+
+    def testSendMessageResent(self):
+        m = email.mime.text.MIMEText('A test message')
+        m['From'] = 'foo at bar.com'
+        m['To'] = 'John'
+        m['CC'] = 'Sally, Fred'
+        m['Bcc'] = 'John Root <root at localhost>, "Dinsdale" <warped at silly.walks.com>'
+        m['Resent-Date'] = 'Thu, 1 Jan 1970 17:42:00 +0000'
+        m['Resent-From'] = 'holy at grail.net'
+        m['Resent-To'] = 'Martha <my_mom at great.cooker.com>, Jeff'
+        m['Resent-Bcc'] = 'doe at losthope.net'
+        smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
+        smtp.send_message(m)
+        # XXX (see comment in testSend)
+        time.sleep(0.01)
+        smtp.quit()
+
+        self.client_evt.set()
+        self.serv_evt.wait()
+        self.output.flush()
+        # The Resent-Bcc headers are deleted before serialization.
+        del m['Bcc']
+        del m['Resent-Bcc']
+        # Add the X-Peer header that DebuggingServer adds
+        m['X-Peer'] = socket.gethostbyname('localhost')
+        mexpect = '%s%s\n%s' % (MSG_BEGIN, m.as_string(), MSG_END)
+        self.assertEqual(self.output.getvalue(), mexpect)
+        debugout = smtpd.DEBUGSTREAM.getvalue()
+        sender = re.compile("^sender: holy at grail.net$", re.MULTILINE)
+        self.assertRegex(debugout, sender)
+        for addr in ('my_mom at great.cooker.com', 'Jeff', 'doe at losthope.net'):
+            to_addr = re.compile(r"^recips: .*'{}'.*$".format(addr),
+                                 re.MULTILINE)
+            self.assertRegex(debugout, to_addr)
+
+    def testSendMessageMultipleResentRaises(self):
+        m = email.mime.text.MIMEText('A test message')
+        m['From'] = 'foo at bar.com'
+        m['To'] = 'John'
+        m['CC'] = 'Sally, Fred'
+        m['Bcc'] = 'John Root <root at localhost>, "Dinsdale" <warped at silly.walks.com>'
+        m['Resent-Date'] = 'Thu, 1 Jan 1970 17:42:00 +0000'
+        m['Resent-From'] = 'holy at grail.net'
+        m['Resent-To'] = 'Martha <my_mom at great.cooker.com>, Jeff'
+        m['Resent-Bcc'] = 'doe at losthope.net'
+        m['Resent-Date'] = 'Thu, 2 Jan 1970 17:42:00 +0000'
+        m['Resent-To'] = 'holy at grail.net'
+        m['Resent-From'] = 'Martha <my_mom at great.cooker.com>, Jeff'
+        smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
+        with self.assertRaises(ValueError):
+            smtp.send_message(m)
+        smtp.close()
 
 class NonConnectingTests(unittest.TestCase):
 
diff --git a/Misc/ACKS b/Misc/ACKS
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -265,6 +265,7 @@
 Ben Escoto
 Andy Eskilsson
 Stefan Esser
+Nicolas Estibals
 Stephen D Evans
 Carey Evans
 Tim Everett
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -28,6 +28,9 @@
 Library
 -------
 
+- Issue #12147: Adjust the new-in-3.2 smtplib.send_message method for better
+  conformance to the RFCs:  correctly handle Sender and Resent- headers.
+
 - Issue #12352: Fix a deadlock in multiprocessing.Heap when a block is freed by
   the garbage collector while the Heap lock is held.
 

-- 
Repository URL: http://hg.python.org/cpython


More information about the Python-checkins mailing list