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

Skip to content

Commit db4120b

Browse files
committed
merge #12147: make send_message correctly handle Sender and Resent- headers.
2 parents 020436b + ac4e5ab commit db4120b

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
@@ -348,21 +348,32 @@ An :class:`SMTP` instance has the following methods:
348348
.. versionchanged:: 3.2 *msg* may be a byte string.
349349

350350

351-
.. method:: SMTP.send_message(msg, from_addr=None, to_addrs=None, mail_options=[], rcpt_options=[])
351+
.. method:: SMTP.send_message(msg, from_addr=None, to_addrs=None, \
352+
mail_options=[], rcpt_options=[])
352353

353354
This is a convenience method for calling :meth:`sendmail` with the message
354355
represented by an :class:`email.message.Message` object. The arguments have
355356
the same meaning as for :meth:`sendmail`, except that *msg* is a ``Message``
356357
object.
357358

358-
If *from_addr* is ``None``, ``send_message`` sets its value to the value of
359-
the :mailheader:`From` header from *msg*. If *to_addrs* is ``None``,
360-
``send_message`` combines the values (if any) of the :mailheader:`To`,
361-
:mailheader:`CC`, and :mailheader:`Bcc` fields from *msg*. Regardless of
362-
the values of *from_addr* and *to_addrs*, ``send_message`` deletes any Bcc
363-
field from *msg*. It then serializes *msg* using
359+
If *from_addr* is ``None`` or *to_addrs* is ``None``, ``send_message`` fills
360+
those arguments with addresses extracted from the headers of *msg* as
361+
specified in :rfc:`2822`\: *from_addr* is set to the :mailheader:`Sender`
362+
field if it is present, and otherwise to the :mailheader:`From` field.
363+
*to_adresses* combines the values (if any) of the :mailheader:`To`,
364+
:mailheader:`Cc`, and :mailheader:`Bcc` fields from *msg*. If exactly one
365+
set of :mailheader:`Resent-*` headers appear in the message, the regular
366+
headers are ignored and the :mailheader:`Resent-*` headers are used instead.
367+
If the message contains more than one set of :mailheader:`Resent-*` headers,
368+
a :exc:`ValueError` is raised, since there is no way to unambiguously detect
369+
the most recent set of :mailheader:`Resent-` headers.
370+
371+
``send_message`` serializes *msg* using
364372
:class:`~email.generator.BytesGenerator` with ``\r\n`` as the *linesep*, and
365-
calls :meth:`sendmail` to transmit the resulting message.
373+
calls :meth:`sendmail` to transmit the resulting message. Regardless of the
374+
values of *from_addr* and *to_addrs*, ``send_message`` does not transmit any
375+
:mailheader:`Bcc` or :mailheader:`Resent-Bcc` headers that may appear
376+
in *msg*.
366377

367378
.. versionadded:: 3.2
368379

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

@@ -676,7 +677,7 @@ def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
676677
677678
msg may be a string containing characters in the ASCII range, or a byte
678679
string. A string is encoded to bytes using the ascii codec, and lone
679-
\r and \n characters are converted to \r\n characters.
680+
\\r and \\n characters are converted to \\r\\n characters.
680681
681682
If there has been no previous EHLO or HELO command this session, this
682683
method tries ESMTP EHLO first. If the server does ESMTP, message size
@@ -759,24 +760,49 @@ def send_message(self, msg, from_addr=None, to_addrs=None,
759760
"""Converts message to a bytestring and passes it to sendmail.
760761
761762
The arguments are as for sendmail, except that msg is an
762-
email.message.Message object. If from_addr is None, the from_addr is
763-
taken from the 'From' header of the Message. If to_addrs is None, its
764-
value is composed from the addresses listed in the 'To', 'CC', and
765-
'Bcc' fields. Regardless of the values of from_addr and to_addr, any
766-
Bcc field in the Message object is deleted. The Message object is then
767-
serialized using email.generator.BytesGenerator and sendmail is called
768-
to transmit the message.
763+
email.message.Message object. If from_addr is None or to_addrs is
764+
None, these arguments are taken from the headers of the Message as
765+
described in RFC 2822 (a ValueError is raised if there is more than
766+
one set of 'Resent-' headers). Regardless of the values of from_addr and
767+
to_addr, any Bcc field (or Resent-Bcc field, when the Message is a
768+
resent) of the Message object won't be transmitted. The Message
769+
object is then serialized using email.generator.BytesGenerator and
770+
sendmail is called to transmit the message.
771+
769772
"""
773+
# 'Resent-Date' is a mandatory field if the Message is resent (RFC 2822
774+
# Section 3.6.6). In such a case, we use the 'Resent-*' fields. However,
775+
# if there is more than one 'Resent-' block there's no way to
776+
# unambiguously determine which one is the most recent in all cases,
777+
# so rather than guess we raise a ValueError in that case.
778+
#
779+
# TODO implement heuristics to guess the correct Resent-* block with an
780+
# option allowing the user to enable the heuristics. (It should be
781+
# possible to guess correctly almost all of the time.)
782+
resent =msg.get_all('Resent-Date')
783+
if resent is None:
784+
header_prefix = ''
785+
elif len(resent) == 1:
786+
header_prefix = 'Resent-'
787+
else:
788+
raise ValueError("message has more than one 'Resent-' header block")
770789
if from_addr is None:
771-
from_addr = msg['From']
790+
# Prefer the sender field per RFC 2822:3.6.2.
791+
from_addr = (msg[header_prefix+'Sender']
792+
if (header_prefix+'Sender') in msg
793+
else msg[header_prefix+'From'])
772794
if to_addrs is None:
773-
addr_fields = [f for f in (msg['To'], msg['Bcc'], msg['CC'])
774-
if f is not None]
795+
addr_fields = [f for f in (msg[header_prefix+'To'],
796+
msg[header_prefix+'Bcc'],
797+
msg[header_prefix+'Cc']) if f is not None]
775798
to_addrs = [a[1] for a in email.utils.getaddresses(addr_fields)]
776-
del msg['Bcc']
799+
# Make a local copy so we can delete the bcc headers.
800+
msg_copy = copy.copy(msg)
801+
del msg_copy['Bcc']
802+
del msg_copy['Resent-Bcc']
777803
with io.BytesIO() as bytesmsg:
778804
g = email.generator.BytesGenerator(bytesmsg)
779-
g.flatten(msg, linesep='\r\n')
805+
g.flatten(msg_copy, linesep='\r\n')
780806
flatmsg = bytesmsg.getvalue()
781807
return self.sendmail(from_addr, to_addrs, flatmsg, mail_options,
782808
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
@@ -278,6 +278,7 @@ Ben Escoto
278278
Andy Eskilsson
279279
André Espaze
280280
Stefan Esser
281+
Nicolas Estibals
281282
Stephen D Evans
282283
Carey Evans
283284
Tim Everett

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,9 @@ Core and Builtins
203203
Library
204204
-------
205205

206+
- Issue #12147: Adjust the new-in-3.2 smtplib.send_message method for better
207+
conformance to the RFCs: correctly handle Sender and Resent- headers.
208+
206209
- Issue #12352: Fix a deadlock in multiprocessing.Heap when a block is freed by
207210
the garbage collector while the Heap lock is held.
208211

0 commit comments

Comments
 (0)