diff -r 8c17e898e0e8 Doc/library/smtplib.rst --- a/Doc/library/smtplib.rst Tue Jun 28 10:25:04 2011 -0500 +++ b/Doc/library/smtplib.rst Tue Jun 28 22:37:14 2011 +0200 @@ -355,16 +355,21 @@ 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 - :class:`~email.generator.BytesGenerator` with ``\r\n`` as the *linesep*, and - calls :meth:`sendmail` to transmit the resulting message. + 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`. In a nutshell *from_addr* sets its value to the + :mailheader:`Sender` field if present or the :mailheader:`From` field, and + *to_adresses* 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`` won't transmit any + :mailheader:`Bcc` field from *msg*. If *msg* is a bounced message, some + :mailheader:`Resent-*` fields appear in the headers ; in such a case, those + fields are considered instead of the previous ones. It then serializes + *msg* using :class:`~email.generator.BytesGenerator` with ``\r\n`` as the + *linesep*, and calls :meth:`sendmail` to transmit the resulting message. .. versionadded:: 3.2 + .. versionchanged:: 3.3 Extraction of addresses has been made :rfc:`2822` compliant. .. method:: SMTP.quit() diff -r 8c17e898e0e8 Lib/smtplib.py --- a/Lib/smtplib.py Tue Jun 28 10:25:04 2011 -0500 +++ b/Lib/smtplib.py Tue Jun 28 22:37:14 2011 +0200 @@ -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 @@ -676,7 +677,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 @@ -759,24 +760,46 @@ """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. 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 transmited. 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 must consider the + # 'Resent-*' fields. + # If there is many resent block, we won't try to guess wich one is the + # correct one and return a ValueError + # TODO implement heuristics to guess the correct Resent-* block with an + # option for the user choosing the heuristic. + n_resent_block=msg.keys().count('Resent-Date') + if n_resent_block == 0: + is_resent = '' + elif n_resent_block == 1: + is_resent = 'Resent-' + else: + raise ValueError('send_message cannot handle message with more \ + than one Resent- header block') if from_addr is None: - from_addr = msg['From'] + from_addr = (msg[is_resent+'Sender'] if (is_resent+'Sender') in msg + else msg[is_resent+'From']) + # 'Sender' field has to be prefered to the 'From' field if + # present (RFC 2822 Section 3.6.2) 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[is_resent+'To'], + msg[is_resent+'Bcc'], + msg[is_resent+'Cc']) if f is not None] to_addrs = [a[1] for a in email.utils.getaddresses(addr_fields)] - del msg['Bcc'] + # Do not mutate msg, it should be returned unchanged to the user + msg_copy = copy.copy(msg) + del msg_copy[is_resent+'Bcc'] # (RFC 2822 Section 3.6.3) 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 -r 8c17e898e0e8 Lib/test/test_smtplib.py --- a/Lib/test/test_smtplib.py Tue Jun 28 10:25:04 2011 -0500 +++ b/Lib/test/test_smtplib.py Tue Jun 28 22:37:14 2011 +0200 @@ -320,13 +320,15 @@ # XXX (see comment in testSend) time.sleep(0.01) smtp.quit() + self.assertEqual(m['Bcc'], 'John Root , "Dinsdale" \ +') 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 appear to the receipient. del m['Bcc'] mexpect = '%s%s\n%s' % (MSG_BEGIN, m.as_string(), MSG_END) self.assertEqual(self.output.getvalue(), mexpect) @@ -365,6 +367,65 @@ re.MULTILINE) self.assertRegex(debugout, to_addr) + def testSendMessageWithMultipleFrom(self): + # Make sure nothing breaks if not all of the three 'to' headers exist + m = email.mime.text.MIMEText('A test message') + m['From'] = 'Bernard, Bianca' + m['Sender'] = 'the_rescuers@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@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@bar.com' + m['To'] = 'John' + m['CC'] = 'Sally, Fred' + m['Bcc'] = 'John Root , "Dinsdale" ' + m['Resent-Date'] = 'Thu, 1 Jan 1970 17:42:00 +0000' + m['Resent-From'] = 'holy@grail.net' + m['Resent-To'] = 'Martha , Jeff' + m['Resent-Bcc'] = 'doe@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 header is deleted before serialization. + 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@grail.net$", re.MULTILINE) + self.assertRegex(debugout, sender) + for addr in ('my_mom@great.cooker.com', 'Jeff', 'doe@losthope.net'): + to_addr = re.compile(r"^recips: .*'{}'.*$".format(addr), + re.MULTILINE) + self.assertRegex(debugout, to_addr) class NonConnectingTests(unittest.TestCase): diff -r 8c17e898e0e8 Misc/ACKS --- a/Misc/ACKS Tue Jun 28 10:25:04 2011 -0500 +++ b/Misc/ACKS Tue Jun 28 22:37:14 2011 +0200 @@ -278,6 +278,7 @@ Andy Eskilsson André Espaze Stefan Esser +Nicolas Estibals Stephen D Evans Carey Evans Tim Everett