summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorR David Murray <rdmurray@bitdance.com>2011-07-03 01:10:44 (GMT)
committerR David Murray <rdmurray@bitdance.com>2011-07-03 01:10:44 (GMT)
commitdb4120bf9d7ea434b27ad531cd33ed4987b6dcf3 (patch)
tree01db653f2214b6d37029481a36c667de9317ec58 /Lib
parent020436b0d4ae271638ed5d0881c1fa7f7c0a1b09 (diff)
parentac4e5abc788dfd10474fe3b0a6c5c802d3159763 (diff)
downloadcpython-db4120bf9d7ea434b27ad531cd33ed4987b6dcf3.zip
cpython-db4120bf9d7ea434b27ad531cd33ed4987b6dcf3.tar.gz
cpython-db4120bf9d7ea434b27ad531cd33ed4987b6dcf3.tar.bz2
merge #12147: make send_message correctly handle Sender and Resent- headers.
Diffstat (limited to 'Lib')
-rw-r--r--[-rwxr-xr-x]Lib/smtplib.py52
-rw-r--r--Lib/test/test_smtplib.py111
2 files changed, 149 insertions, 14 deletions
diff --git a/Lib/smtplib.py b/Lib/smtplib.py
index f9d1c99..b89b5a5 100755..100644
--- a/Lib/smtplib.py
+++ b/Lib/smtplib.py
@@ -49,6 +49,7 @@ import email.message
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 @@ class SMTP:
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,49 @@ class SMTP:
"""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
index 1204707..95c1bfc 100644
--- a/Lib/test/test_smtplib.py
+++ b/Lib/test/test_smtplib.py
@@ -320,13 +320,16 @@ class DebuggingServerTests(unittest.TestCase):
# 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@localhost>, "Dinsdale" '
+ '<warped@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 @@ class DebuggingServerTests(unittest.TestCase):
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@bar.com'
+ m['To'] = 'John, Dinsdale'
+ smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
+ smtp.send_message(m, from_addr='joe@example.com', to_addrs='foo@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@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@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@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 <root@localhost>, "Dinsdale" <warped@silly.walks.com>'
+ m['Resent-Date'] = 'Thu, 1 Jan 1970 17:42:00 +0000'
+ m['Resent-From'] = 'holy@grail.net'
+ m['Resent-To'] = 'Martha <my_mom@great.cooker.com>, 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 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@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)
+
+ def testSendMessageMultipleResentRaises(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 <root@localhost>, "Dinsdale" <warped@silly.walks.com>'
+ m['Resent-Date'] = 'Thu, 1 Jan 1970 17:42:00 +0000'
+ m['Resent-From'] = 'holy@grail.net'
+ m['Resent-To'] = 'Martha <my_mom@great.cooker.com>, Jeff'
+ m['Resent-Bcc'] = 'doe@losthope.net'
+ m['Resent-Date'] = 'Thu, 2 Jan 1970 17:42:00 +0000'
+ m['Resent-To'] = 'holy@grail.net'
+ m['Resent-From'] = 'Martha <my_mom@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):