#10321: Add support for sending binary DATA and Message objects to smtplib
diff --git a/Doc/includes/email-mime.py b/Doc/includes/email-mime.py
index f64df83..8ddd486 100644
--- a/Doc/includes/email-mime.py
+++ b/Doc/includes/email-mime.py
@@ -27,5 +27,5 @@
 
 # Send the email via our own SMTP server.
 s = smtplib.SMTP()
-s.sendmail(me, family, msg.as_string())
+s.sendmail(msg)
 s.quit()
diff --git a/Doc/includes/email-simple.py b/Doc/includes/email-simple.py
index 689511e..b069ab0 100644
--- a/Doc/includes/email-simple.py
+++ b/Doc/includes/email-simple.py
@@ -17,8 +17,7 @@
 msg['From'] = me
 msg['To'] = you
 
-# Send the message via our own SMTP server, but don't include the
-# envelope header.
+# Send the message via our own SMTP server.
 s = smtplib.SMTP()
-s.sendmail(me, [you], msg.as_string())
+s.sendmail(msg)
 s.quit()
diff --git a/Doc/library/smtplib.rst b/Doc/library/smtplib.rst
index 0c65290..858301d 100644
--- a/Doc/library/smtplib.rst
+++ b/Doc/library/smtplib.rst
@@ -274,9 +274,14 @@
    .. note::
 
       The *from_addr* and *to_addrs* parameters are used to construct the message
-      envelope used by the transport agents. The :class:`SMTP` does not modify the
+      envelope used by the transport agents.  ``sendmail`` does not modify the
       message headers in any way.
 
+   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.  A byte string
+   is not modified.
+
    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 and
    each of the specified options will be passed to it (if the option is in the
@@ -311,6 +316,27 @@
    Unless otherwise noted, the connection will be open even after an exception is
    raised.
 
+   .. versionchanged:: 3.2 *msg* may be a byte string.
+
+
+.. 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
+   :class:`~email.generator.BytesGenerator` with ``\r\n`` as the *linesep*, and
+   calls :meth:`sendmail` to transmit the resulting message.
+
+   .. versionadded:: 3.2
+
 
 .. method:: SMTP.quit()
 
@@ -366,5 +392,5 @@
 .. note::
 
    In general, you will want to use the :mod:`email` package's features to
-   construct an email message, which you can then convert to a string and send
-   via :meth:`sendmail`; see :ref:`email-examples`.
+   construct an email message, which you can then send
+   via :meth:`~smtplib.SMTP.send_message`; see :ref:`email-examples`.
diff --git a/Doc/whatsnew/3.2.rst b/Doc/whatsnew/3.2.rst
index 87f9e94..613331f 100644
--- a/Doc/whatsnew/3.2.rst
+++ b/Doc/whatsnew/3.2.rst
@@ -540,6 +540,14 @@
 
   (Contributed by Neil Schemenauer and Nick Coghlan; :issue:`5178`.)
 
+* The :mod:`smtplib` :class:`~smtplib.SMTP` class now accepts a byte string
+  for the *msg* argument to the :meth:`~smtplib.SMTP.sendmail` method,
+  and a new method, :meth:`~smtplib.SMTP.send_message` accepts a
+  :class:`~email.message.Message` object and can optionally obtain the
+  *from_addr* and *to_addrs* addresses directly from the object.
+
+  (Contributed by R. David Murray, :issue:`10321`.)
+
 
 Multi-threading
 ===============
diff --git a/Lib/smtplib.py b/Lib/smtplib.py
index 1c1a9d1..ccb2236 100755
--- a/Lib/smtplib.py
+++ b/Lib/smtplib.py
@@ -42,8 +42,11 @@
 # This was modified from the Python 1.5 library HTTP lib.
 
 import socket
+import io
 import re
 import email.utils
+import email.message
+import email.generator
 import base64
 import hmac
 from email.base64mime import body_encode as encode_base64
@@ -57,6 +60,7 @@
 SMTP_PORT = 25
 SMTP_SSL_PORT = 465
 CRLF="\r\n"
+bCRLF=b"\r\n"
 
 OLDSTYLE_AUTH = re.compile(r"auth=(.*)", re.I)
 
@@ -147,6 +151,7 @@
     else:
         return "<%s>" % m
 
+# Legacy method kept for backward compatibility.
 def quotedata(data):
     """Quote data for email.
 
@@ -156,6 +161,12 @@
     return re.sub(r'(?m)^\.', '..',
         re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data))
 
+def _quote_periods(bindata):
+    return re.sub(br'(?m)^\.', '..', bindata)
+
+def _fix_eols(data):
+    return  re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data)
+
 try:
     import ssl
 except ImportError:
@@ -469,7 +480,9 @@
         Automatically quotes lines beginning with a period per rfc821.
         Raises SMTPDataError if there is an unexpected reply to the
         DATA command; the return value from this method is the final
-        response code received when the all data is sent.
+        response code received when the all data is sent.  If msg
+        is a string, lone '\r' and '\n' characters are converted to
+        '\r\n' characters.  If msg is bytes, it is transmitted as is.
         """
         self.putcmd("data")
         (code,repl)=self.getreply()
@@ -477,10 +490,12 @@
         if code != 354:
             raise SMTPDataError(code,repl)
         else:
-            q = quotedata(msg)
-            if q[-2:] != CRLF:
-                q = q + CRLF
-            q = q + "." + CRLF
+            if isinstance(msg, str):
+                msg = _fix_eols(msg).encode('ascii')
+            q = _quote_periods(msg)
+            if q[-2:] != bCRLF:
+                q = q + bCRLF
+            q = q + b"." + bCRLF
             self.send(q)
             (code,msg)=self.getreply()
             if self.debuglevel >0 : print("data:", (code,msg), file=stderr)
@@ -648,6 +663,10 @@
             - rcpt_options : List of ESMTP options (such as DSN commands) for
                              all the rcpt commands.
 
+        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.
+
         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
         and each of the specified options will be passed to it.  If EHLO
@@ -693,6 +712,8 @@
         """
         self.ehlo_or_helo_if_needed()
         esmtp_opts = []
+        if isinstance(msg, str):
+            msg = _fix_eols(msg).encode('ascii')
         if self.does_esmtp:
             # Hmmm? what's this? -ddm
             # self.esmtp_features['7bit']=""
@@ -700,7 +721,6 @@
                 esmtp_opts.append("size=%d" % len(msg))
             for option in mail_options:
                 esmtp_opts.append(option)
-
         (code,resp) = self.mail(from_addr, esmtp_opts)
         if code != 250:
             self.rset()
@@ -723,6 +743,33 @@
         #if we got here then somebody got our mail
         return senderrs
 
+    def send_message(self, msg, from_addr=None, to_addrs=None,
+                mail_options=[], rcpt_options={}):
+        """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.
+        """
+        if from_addr is None:
+            from_addr = msg['From']
+        if to_addrs is None:
+            addr_fields = [f for f in (msg['To'], msg['Bcc'], msg['CC'])
+                            if f is not None]
+            to_addrs = [a[1] for a in email.utils.getaddresses(addr_fields)]
+        del msg['Bcc']
+        with io.BytesIO() as bytesmsg:
+            g = email.generator.BytesGenerator(bytesmsg)
+            g.flatten(msg, linesep='\r\n')
+            flatmsg = bytesmsg.getvalue()
+        return self.sendmail(from_addr, to_addrs, flatmsg, mail_options,
+                             rcpt_options)
+
 
     def close(self):
         """Close the connection to the SMTP server."""
diff --git a/Lib/test/test_smtplib.py b/Lib/test/test_smtplib.py
index 6390a86d..795586a 100644
--- a/Lib/test/test_smtplib.py
+++ b/Lib/test/test_smtplib.py
@@ -1,9 +1,11 @@
 import asyncore
+import email.mime.text
 import email.utils
 import socket
 import smtpd
 import smtplib
 import io
+import re
 import sys
 import time
 import select
@@ -57,6 +59,13 @@
     def tearDown(self):
         smtplib.socket = socket
 
+    # This method is no longer used but is retained for backward compatibility,
+    # so test to make sure it still works.
+    def testQuoteData(self):
+        teststr  = "abc\n.jkl\rfoo\r\n..blue"
+        expected = "abc\r\n..jkl\r\nfoo\r\n...blue"
+        self.assertEqual(expected, smtplib.quotedata(teststr))
+
     def testBasic1(self):
         mock_socket.reply_with(b"220 Hola mundo")
         # connects
@@ -150,6 +159,8 @@
 @unittest.skipUnless(threading, 'Threading required for this test.')
 class DebuggingServerTests(unittest.TestCase):
 
+    maxDiff = None
+
     def setUp(self):
         self.real_getfqdn = socket.getfqdn
         socket.getfqdn = mock_socket.getfqdn
@@ -161,6 +172,9 @@
         self._threads = support.threading_setup()
         self.serv_evt = threading.Event()
         self.client_evt = threading.Event()
+        # Capture SMTPChannel debug output
+        self.old_DEBUGSTREAM = smtpd.DEBUGSTREAM
+        smtpd.DEBUGSTREAM = io.StringIO()
         # Pick a random unused port by passing 0 for the port number
         self.serv = smtpd.DebuggingServer((HOST, 0), ('nowhere', -1))
         # Keep a note of what port was assigned
@@ -183,6 +197,9 @@
         support.threading_cleanup(*self._threads)
         # restore sys.stdout
         sys.stdout = self.old_stdout
+        # restore DEBUGSTREAM
+        smtpd.DEBUGSTREAM.close()
+        smtpd.DEBUGSTREAM = self.old_DEBUGSTREAM
 
     def testBasic(self):
         # connect
@@ -247,6 +264,95 @@
         mexpect = '%s%s\n%s' % (MSG_BEGIN, m, MSG_END)
         self.assertEqual(self.output.getvalue(), mexpect)
 
+    def testSendBinary(self):
+        m = b'A test message'
+        smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
+        smtp.sendmail('John', 'Sally', m)
+        # XXX (see comment in testSend)
+        time.sleep(0.01)
+        smtp.quit()
+
+        self.client_evt.set()
+        self.serv_evt.wait()
+        self.output.flush()
+        mexpect = '%s%s\n%s' % (MSG_BEGIN, m.decode('ascii'), MSG_END)
+        self.assertEqual(self.output.getvalue(), mexpect)
+
+    def testSendMessage(self):
+        m = email.mime.text.MIMEText('A test message')
+        smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
+        smtp.send_message(m, from_addr='John', to_addrs='Sally')
+        # 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
+        # XXX: I'm not sure hardcoding this IP will work on linux-vserver.
+        m['X-Peer'] = '127.0.0.1'
+        mexpect = '%s%s\n%s' % (MSG_BEGIN, m.as_string(), MSG_END)
+        self.assertEqual(self.output.getvalue(), mexpect)
+
+    def testSendMessageWithAddresses(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>'
+        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
+        # XXX: I'm not sure hardcoding this IP will work on linux-vserver.
+        m['X-Peer'] = '127.0.0.1'
+        # The Bcc header is deleted before serialization.
+        del m['Bcc']
+        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: foo@bar.com$", re.MULTILINE)
+        self.assertRegexpMatches(debugout, sender)
+        for addr in ('John', 'Sally', 'Fred', 'root@localhost',
+                     'warped@silly.walks.com'):
+            to_addr = re.compile(r"^recips: .*'{}'.*$".format(addr),
+                                 re.MULTILINE)
+            self.assertRegexpMatches(debugout, to_addr)
+
+    def testSendMessageWithSomeAddresses(self):
+        # Make sure nothing breaks if not all of the three 'to' headers exist
+        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)
+        # 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
+        # XXX: I'm not sure hardcoding this IP will work on linux-vserver.
+        m['X-Peer'] = '127.0.0.1'
+        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: foo@bar.com$", re.MULTILINE)
+        self.assertRegexpMatches(debugout, sender)
+        for addr in ('John', 'Dinsdale'):
+            to_addr = re.compile(r"^recips: .*'{}'.*$".format(addr),
+                                 re.MULTILINE)
+            self.assertRegexpMatches(debugout, to_addr)
+
 
 class NonConnectingTests(unittest.TestCase):
 
diff --git a/Misc/NEWS b/Misc/NEWS
index b9db5c5..f5eb3a9 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -60,6 +60,9 @@
 Library
 -------
 
+- Issue #10321: Added support for binary data to smtplib.SMTP.sendmail,
+  and a new method send_message to send an email.message.Message object.
+
 - Issue #6011: sysconfig and distutils.sysconfig use the surrogateescape error
   handler to parse the Makefile file.  Avoid a UnicodeDecodeError if the source
   code directory name contains a non-ASCII character and the locale encoding is