Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ac4e5ab

Browse files
committed
#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.
1 parent 623e8b8 commit ac4e5ab

5 files changed

Lines changed: 172 additions & 22 deletions

File tree

Doc/library/smtplib.rst

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -323,21 +323,32 @@ An :class:`SMTP` instance has the following methods:
323323
.. versionchanged:: 3.2 *msg* may be a byte string.
324324

325325

326-
.. method:: SMTP.send_message(msg, from_addr=None, to_addrs=None, mail_options=[], rcpt_options=[])
326+
.. method:: SMTP.send_message(msg, from_addr=None, to_addrs=None, \
327+
mail_options=[], rcpt_options=[])
327328

328329
This is a convenience method for calling :meth:`sendmail` with the message
329330
represented by an :class:`email.message.Message` object. The arguments have
330331
the same meaning as for :meth:`sendmail`, except that *msg* is a ``Message``
331332
object.
332333

333-
If *from_addr* is ``None``, ``send_message`` sets its value to the value of
334-
the :mailheader:`From` header from *msg*. If *to_addrs* is ``None``,
335-
``send_message`` combines the values (if any) of the :mailheader:`To`,
336-
:mailheader:`CC`, and :mailheader:`Bcc` fields from *msg*. Regardless of
337-
the values of *from_addr* and *to_addrs*, ``send_message`` deletes any Bcc
338-
field from *msg*. It then serializes *msg* using
334+
If *from_addr* is ``None`` or *to_addrs* is ``None``, ``send_message`` fills
335+
those arguments with addresses extracted from the headers of *msg* as
336+
specified in :rfc:`2822`\: *from_addr* is set to the :mailheader:`Sender`
337+
field if it is present, and otherwise to the :mailheader:`From` field.
338+
*to_adresses* combines the values (if any) of the :mailheader:`To`,
339+
:mailheader:`Cc`, and :mailheader:`Bcc` fields from *msg*. If exactly one
340+
set of :mailheader:`Resent-*` headers appear in the message, the regular
341+
headers are ignored and the :mailheader:`Resent-*` headers are used instead.
342+
If the message contains more than one set of :mailheader:`Resent-*` headers,
343+
a :exc:`ValueError` is raised, since there is no way to unambiguously detect
344+
the most recent set of :mailheader:`Resent-` headers.
345+
346+
``send_message`` serializes *msg* using
339347
:class:`~email.generator.BytesGenerator` with ``\r\n`` as the *linesep*, and
340-
calls :meth:`sendmail` to transmit the resulting message.
348+
calls :meth:`sendmail` to transmit the resulting message. Regardless of the
349+
values of *from_addr* and *to_addrs*, ``send_message`` does not transmit any
350+
:mailheader:`Bcc` or :mailheader:`Resent-Bcc` headers that may appear
351+
in *msg*.
341352

342353
.. versionadded:: 3.2
343354

Lib/smtplib.py

100755100644
Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import email.generator
5050
import base64
5151
import hmac
52+
import copy
5253
from email.base64mime import body_encode as encode_base64
5354
from sys import stderr
5455

@@ -674,7 +675,7 @@ def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
674675
675676
msg may be a string containing characters in the ASCII range, or a byte
676677
string. A string is encoded to bytes using the ascii codec, and lone
677-
\r and \n characters are converted to \r\n characters.
678+
\\r and \\n characters are converted to \\r\\n characters.
678679
679680
If there has been no previous EHLO or HELO command this session, this
680681
method tries ESMTP EHLO first. If the server does ESMTP, message size
@@ -757,24 +758,49 @@ def send_message(self, msg, from_addr=None, to_addrs=None,
757758
"""Converts message to a bytestring and passes it to sendmail.
758759
759760
The arguments are as for sendmail, except that msg is an
760-
email.message.Message object. If from_addr is None, the from_addr is
761-
taken from the 'From' header of the Message. If to_addrs is None, its
762-
value is composed from the addresses listed in the 'To', 'CC', and
763-
'Bcc' fields. Regardless of the values of from_addr and to_addr, any
764-
Bcc field in the Message object is deleted. The Message object is then
765-
serialized using email.generator.BytesGenerator and sendmail is called
766-
to transmit the message.
761+
email.message.Message object. If from_addr is None or to_addrs is
762+
None, these arguments are taken from the headers of the Message as
763+
described in RFC 2822 (a ValueError is raised if there is more than
764+
one set of 'Resent-' headers). Regardless of the values of from_addr and
765+
to_addr, any Bcc field (or Resent-Bcc field, when the Message is a
766+
resent) of the Message object won't be transmitted. The Message
767+
object is then serialized using email.generator.BytesGenerator and
768+
sendmail is called to transmit the message.
769+
767770
"""
771+
# 'Resent-Date' is a mandatory field if the Message is resent (RFC 2822
772+
# Section 3.6.6). In such a case, we use the 'Resent-*' fields. However,
773+
# if there is more than one 'Resent-' block there's no way to
774+
# unambiguously determine which one is the most recent in all cases,
775+
# so rather than guess we raise a ValueError in that case.
776+
#
777+
# TODO implement heuristics to guess the correct Resent-* block with an
778+
# option allowing the user to enable the heuristics. (It should be
779+
# possible to guess correctly almost all of the time.)
780+
resent =msg.get_all('Resent-Date')
781+
if resent is None:
782+
header_prefix = ''
783+
elif len(resent) == 1:
784+
header_prefix = 'Resent-'
785+
else:
786+
raise ValueError("message has more than one 'Resent-' header block")
768787
if from_addr is None:
769-
from_addr = msg['From']
788+
# Prefer the sender field per RFC 2822:3.6.2.
789+
from_addr = (msg[header_prefix+'Sender']
790+
if (header_prefix+'Sender') in msg
791+
else msg[header_prefix+'From'])
770792
if to_addrs is None:
771-
addr_fields = [f for f in (msg['To'], msg['Bcc'], msg['CC'])
772-
if f is not None]
793+
addr_fields = [f for f in (msg[header_prefix+'To'],
794+
msg[header_prefix+'Bcc'],
795+
msg[header_prefix+'Cc']) if f is not None]
773796
to_addrs = [a[1] for a in email.utils.getaddresses(addr_fields)]
774-
del msg['Bcc']
797+
# Make a local copy so we can delete the bcc headers.
798+
msg_copy = copy.copy(msg)
799+
del msg_copy['Bcc']
800+
del msg_copy['Resent-Bcc']
775801
with io.BytesIO() as bytesmsg:
776802
g = email.generator.BytesGenerator(bytesmsg)
777-
g.flatten(msg, linesep='\r\n')
803+
g.flatten(msg_copy, linesep='\r\n')
778804
flatmsg = bytesmsg.getvalue()
779805
return self.sendmail(from_addr, to_addrs, flatmsg, mail_options,
780806
rcpt_options)

Lib/test/test_smtplib.py

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,13 +320,16 @@ def testSendMessageWithAddresses(self):
320320
# XXX (see comment in testSend)
321321
time.sleep(0.01)
322322
smtp.quit()
323+
# make sure the Bcc header is still in the message.
324+
self.assertEqual(m['Bcc'], 'John Root <root@localhost>, "Dinsdale" '
325+
323326

324327
self.client_evt.set()
325328
self.serv_evt.wait()
326329
self.output.flush()
327330
# Add the X-Peer header that DebuggingServer adds
328331
m['X-Peer'] = socket.gethostbyname('localhost')
329-
# The Bcc header is deleted before serialization.
332+
# The Bcc header should not be transmitted.
330333
del m['Bcc']
331334
mexpect = '%s%s\n%s' % (MSG_BEGIN, m.as_string(), MSG_END)
332335
self.assertEqual(self.output.getvalue(), mexpect)
@@ -365,6 +368,112 @@ def testSendMessageWithSomeAddresses(self):
365368
re.MULTILINE)
366369
self.assertRegex(debugout, to_addr)
367370

371+
def testSendMessageWithSpecifiedAddresses(self):
372+
# Make sure addresses specified in call override those in message.
373+
m = email.mime.text.MIMEText('A test message')
374+
m['From'] = '[email protected]'
375+
m['To'] = 'John, Dinsdale'
376+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
377+
smtp.send_message(m, from_addr='[email protected]', to_addrs='[email protected]')
378+
# XXX (see comment in testSend)
379+
time.sleep(0.01)
380+
smtp.quit()
381+
382+
self.client_evt.set()
383+
self.serv_evt.wait()
384+
self.output.flush()
385+
# Add the X-Peer header that DebuggingServer adds
386+
m['X-Peer'] = socket.gethostbyname('localhost')
387+
mexpect = '%s%s\n%s' % (MSG_BEGIN, m.as_string(), MSG_END)
388+
self.assertEqual(self.output.getvalue(), mexpect)
389+
debugout = smtpd.DEBUGSTREAM.getvalue()
390+
sender = re.compile("^sender: [email protected]$", re.MULTILINE)
391+
self.assertRegex(debugout, sender)
392+
for addr in ('John', 'Dinsdale'):
393+
to_addr = re.compile(r"^recips: .*'{}'.*$".format(addr),
394+
re.MULTILINE)
395+
self.assertNotRegex(debugout, to_addr)
396+
recip = re.compile(r"^recips: .*'[email protected]'.*$", re.MULTILINE)
397+
self.assertRegex(debugout, recip)
398+
399+
def testSendMessageWithMultipleFrom(self):
400+
# Sender overrides To
401+
m = email.mime.text.MIMEText('A test message')
402+
m['From'] = 'Bernard, Bianca'
403+
m['Sender'] = '[email protected]'
404+
m['To'] = 'John, Dinsdale'
405+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
406+
smtp.send_message(m)
407+
# XXX (see comment in testSend)
408+
time.sleep(0.01)
409+
smtp.quit()
410+
411+
self.client_evt.set()
412+
self.serv_evt.wait()
413+
self.output.flush()
414+
# Add the X-Peer header that DebuggingServer adds
415+
m['X-Peer'] = socket.gethostbyname('localhost')
416+
mexpect = '%s%s\n%s' % (MSG_BEGIN, m.as_string(), MSG_END)
417+
self.assertEqual(self.output.getvalue(), mexpect)
418+
debugout = smtpd.DEBUGSTREAM.getvalue()
419+
sender = re.compile("^sender: [email protected]$", re.MULTILINE)
420+
self.assertRegex(debugout, sender)
421+
for addr in ('John', 'Dinsdale'):
422+
to_addr = re.compile(r"^recips: .*'{}'.*$".format(addr),
423+
re.MULTILINE)
424+
self.assertRegex(debugout, to_addr)
425+
426+
def testSendMessageResent(self):
427+
m = email.mime.text.MIMEText('A test message')
428+
m['From'] = '[email protected]'
429+
m['To'] = 'John'
430+
m['CC'] = 'Sally, Fred'
431+
m['Bcc'] = 'John Root <root@localhost>, "Dinsdale" <[email protected]>'
432+
m['Resent-Date'] = 'Thu, 1 Jan 1970 17:42:00 +0000'
433+
m['Resent-From'] = '[email protected]'
434+
m['Resent-To'] = 'Martha <[email protected]>, Jeff'
435+
m['Resent-Bcc'] = '[email protected]'
436+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
437+
smtp.send_message(m)
438+
# XXX (see comment in testSend)
439+
time.sleep(0.01)
440+
smtp.quit()
441+
442+
self.client_evt.set()
443+
self.serv_evt.wait()
444+
self.output.flush()
445+
# The Resent-Bcc headers are deleted before serialization.
446+
del m['Bcc']
447+
del m['Resent-Bcc']
448+
# Add the X-Peer header that DebuggingServer adds
449+
m['X-Peer'] = socket.gethostbyname('localhost')
450+
mexpect = '%s%s\n%s' % (MSG_BEGIN, m.as_string(), MSG_END)
451+
self.assertEqual(self.output.getvalue(), mexpect)
452+
debugout = smtpd.DEBUGSTREAM.getvalue()
453+
sender = re.compile("^sender: [email protected]$", re.MULTILINE)
454+
self.assertRegex(debugout, sender)
455+
for addr in ('[email protected]', 'Jeff', '[email protected]'):
456+
to_addr = re.compile(r"^recips: .*'{}'.*$".format(addr),
457+
re.MULTILINE)
458+
self.assertRegex(debugout, to_addr)
459+
460+
def testSendMessageMultipleResentRaises(self):
461+
m = email.mime.text.MIMEText('A test message')
462+
m['From'] = '[email protected]'
463+
m['To'] = 'John'
464+
m['CC'] = 'Sally, Fred'
465+
m['Bcc'] = 'John Root <root@localhost>, "Dinsdale" <[email protected]>'
466+
m['Resent-Date'] = 'Thu, 1 Jan 1970 17:42:00 +0000'
467+
m['Resent-From'] = '[email protected]'
468+
m['Resent-To'] = 'Martha <[email protected]>, Jeff'
469+
m['Resent-Bcc'] = '[email protected]'
470+
m['Resent-Date'] = 'Thu, 2 Jan 1970 17:42:00 +0000'
471+
m['Resent-To'] = '[email protected]'
472+
m['Resent-From'] = 'Martha <[email protected]>, Jeff'
473+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
474+
with self.assertRaises(ValueError):
475+
smtp.send_message(m)
476+
smtp.close()
368477

369478
class NonConnectingTests(unittest.TestCase):
370479

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ Michael Ernst
265265
Ben Escoto
266266
Andy Eskilsson
267267
Stefan Esser
268+
Nicolas Estibals
268269
Stephen D Evans
269270
Carey Evans
270271
Tim Everett

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ Core and Builtins
2828
Library
2929
-------
3030

31+
- Issue #12147: Adjust the new-in-3.2 smtplib.send_message method for better
32+
conformance to the RFCs: correctly handle Sender and Resent- headers.
33+
3134
- Issue #12352: Fix a deadlock in multiprocessing.Heap when a block is freed by
3235
the garbage collector while the Heap lock is held.
3336

0 commit comments

Comments
 (0)