Skip to content

Commit

Permalink
Merge esmtp-sendmail-7257-3: Sendmail should use ESMTP
Browse files Browse the repository at this point in the history
Author: hawkowl
Reviewers: exarkun, herrwolfe, txmanish
Fixes: twisted#7257

git-svn-id: svn://svn.twistedmatrix.com/svn/Twisted/trunk@44412 bbbe8e31-12d6-0310-92fd-ac37d47ddeeb
  • Loading branch information
hawkowl committed Apr 14, 2015
1 parent a90bbf6 commit 6d52ade
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 23 deletions.
3 changes: 3 additions & 0 deletions docs/mail/examples/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ SMTP servers
SMTP clients
------------

- :download:`sendmail_smtp.py` - sending email over plain SMTP with the high-level :api:`twisted.mail.smtp.sendmail <sendmail>` client.
- :download:`sendmail_gmail.py` - sending email encrypted ESMTP to GMail with the high-level :api:`twisted.mail.smtp.sendmail <sendmail>` client.
- :download:`sendmail_message.py` - sending a complex message with the high-level :api:`twisted.mail.smtp.sendmail <sendmail>` client.
- :download:`smtpclient_simple.py` - sending email using SMTP.
- :download:`smtpclient_tls.py` - send email using authentication and transport layer security.

Expand Down
17 changes: 17 additions & 0 deletions docs/mail/examples/sendmail_gmail.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from __future__ import print_function

from twisted.mail.smtp import sendmail
from twisted.internet.task import react

def main(reactor):

d = sendmail("smtp.gmail.com",
"alice@gmail.com",
["bob@gmail.com", "charlie@gmail.com"],
"This is my super awesome email, sent with Twisted!",
port=587, username="alice@gmail.com", password="*********")

d.addBoth(print)
return d

react(main)
25 changes: 25 additions & 0 deletions docs/mail/examples/sendmail_message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import print_function

from twisted.mail.smtp import sendmail
from twisted.internet.task import react

from email.mime.text import MIMEText

def main(reactor):
me = "alice@gmail.com"
to = ["bob@gmail.com", "charlie@gmail.com"]

message = MIMEText("This is my super awesome email, sent with Twisted!")
message["Subject"] = "Twisted is great!"
message["From"] = me
message["To"] = ", ".join(to)

d = sendmail("smtp.gmail.com", me, to, message,
port=587, username=me, password="*********",
requireAuthentication=True,
requireTransportSecurity=True)

d.addBoth(print)
return d

react(main)
15 changes: 15 additions & 0 deletions docs/mail/examples/sendmail_smtp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from __future__ import print_function

from twisted.mail.smtp import sendmail
from twisted.internet.task import react

def main(reactor):
d = sendmail("myinsecuremailserver.example.com",
"alice@example.com",
["bob@gmail.com", "charlie@gmail.com"],
"This is my super awesome email, sent with Twisted!")

d.addBoth(print)
return d

react(main)
10 changes: 10 additions & 0 deletions docs/mail/howto/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Developer Guides
================

.. toctree::
:hidden:

sending-mail


- :doc:`Sending Mail <sending-mail>`: Sending mail with Twisted
91 changes: 91 additions & 0 deletions docs/mail/howto/sending-mail.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
Sending Mail
============

Twisted contains many ways of sending email, but the simplest is :api:`twisted.mail.smtp.sendmail <sendmail>`.
Intended as a near drop-in replacement of :py:class:`smtplib.SMTP`\'s ``sendmail`` method, it provides the ability to send email over SMTP/ESMTP with minimal fuss or configuration.

Knowledge of Twisted's Deferreds is required for making full use of this document.


Sending an Email over SMTP
--------------------------

Although insecure, some email systems still use plain SMTP for sending email.
Plain SMTP has no authentication, no transport security (emails are transmitted in plain text), and should not be done over untrusted networks.

``sendmail``\'s positional arguments are, in order:

- The SMTP/ESMTP server you are sending the message to
- The email address you are sending from
- A ``list`` of email addresses you are sending to
- The message.

The following example shows these in action.

:download:`sendmail_smtp.py <../examples/sendmail_smtp.py>`

.. literalinclude:: ../examples/sendmail_smtp.py

Assuming that the values in it were replaced with real emails and a real SMTP server, it would send an email to the two addresses specified and print the return status.


Sending an Email over ESMTP
---------------------------

Extended SMTP (ESMTP) is an improved version of SMTP, and is used by most modern mail servers.
Unlike SMTP, ESMTP supports authentication and transport security (emails are encrypted in transit).
If you wish to send mail through services like GMail/Google Apps or Outlook.com/Office 365, you will have to use ESMTP.

Using ESMTP requires more options -- usually the default port of 25 is not open, so you must find out your email provider's TLS-enabled ESMTP port.
It also allows the use of authentication via a username and password.

The following example shows part of the ESMTP functionality of ``sendmail``.

:download:`sendmail_gmail.py <../examples/sendmail_gmail.py>`

.. literalinclude:: ../examples/sendmail_gmail.py

Assuming you own the account ``alice@gmail.com``, this would send an email to both ``bob@gmail.com`` and ``charlie@gmail.com``, and print out something like the following (formatted for clarity)::

(2, [('bob@gmail.com', 250, '2.1.5 OK hz13sm11691456pac.6 - gsmtp'),
('charlie@gmail.com', 250, '2.1.5 OK hz13sm11691456pac.6 - gsmtp')])

``sendmail`` returns a 2-tuple, containing the number of emails sent successfully (note that this is from you to the server you specified, not to the recepient -- emails may still be lost between that server and the recepient) and a list of statuses of the sent mail.
Each status is a 3-tuple containing the address it was sent to, the SMTP status code, and the server response.


Sending Complex Emails
----------------------

Sometimes you want to send more complicated emails -- ones with headers, or with attachments.
``sendmail`` supports using Python's :py:class:`email.Message`, which lets you make complex emails:

:download:`sendmail_message.py <../examples/sendmail_message.py>`

.. literalinclude:: ../examples/sendmail_message.py

For more information on how to use ``Message``, please see :ref:`the module's Python docs <py2:email-examples>`.


Enforcing Transport Security
----------------------------

To prevent downgrade attacks, you can pass ``requireTransportSecurity=True`` to ``sendmail``.
This means that your emails will not be transmitted in plain text.

For example::

sendmail("smtp.gmail.com", me, to, message,
port=587, username=me, password="*********",
requireTransportSecurity=True)


Conclusion
----------

In this document, you have seen how to:

#. Send an email over SMTP using :api:`twisted.mail.smtp.sendmail <sendmail>`.
#. Send an email over encrypted & authenticated ESMTP with :api:`twisted.mail.smtp.sendmail <sendmail>`.
#. Send a "complex" email containing a subject line using the stdlib's ``email.Message`` functionality.
#. Enforce transport security for emails sent using :api:`twisted.mail.smtp.sendmail <sendmail>`.
2 changes: 2 additions & 0 deletions docs/mail/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ Twisted Mail
:hidden:

examples/index
howto/index
tutorial/smtpclient/smtpclient


- :doc:`Examples <examples/index>`: short code examples using Twisted Mail
- :doc:`Developer Guides <howto/index>`: documentation on using Twisted Mail
- :doc:`Twisted Mail Tutorial <tutorial/smtpclient/smtpclient>`: Building an SMTP Client from Scratch
67 changes: 46 additions & 21 deletions twisted/mail/smtp.py
Original file line number Diff line number Diff line change
Expand Up @@ -2091,35 +2091,57 @@ def buildProtocol(self, addr):



def sendmail(smtphost, from_addr, to_addrs, msg,
senderDomainName=None, port=25, reactor=reactor):
"""Send an email
def sendmail(smtphost, from_addr, to_addrs, msg, senderDomainName=None, port=25,
reactor=reactor, username=None, password=None,
requireAuthentication=False, requireTransportSecurity=False):
"""
Send an email.
This interface is intended to be a replacement for L{smtplib.SMTP.sendmail}
and related methods. To maintain backwards compatibility, it will fall back
to plain SMTP, if ESMTP support is not available. If ESMTP support is
available, it will attempt to provide encryption via STARTTLS and
authentication if a secret is provided.
This interface is intended to be a direct replacement for
smtplib.SMTP.sendmail() (with the obvious change that
you specify the smtphost as well). Also, ESMTP options
are not accepted, as we don't do ESMTP yet. I reserve the
right to implement the ESMTP options differently.
@param smtphost: The host the message should be sent to.
@type smtphost: L{bytes}
@param smtphost: The host the message should be sent to
@param from_addr: The (envelope) address sending this mail.
@type from_addr: L{bytes}
@param to_addrs: A list of addresses to send this mail to. A string will
be treated as a list of one address
be treated as a list of one address.
@type to_addr: L{list} of L{bytes} or L{bytes}
@param msg: The message, including headers, either as a file or a string.
File-like objects need to support read() and close(). Lines must be
delimited by '\\n'. If you pass something that doesn't look like a
file, we try to convert it to a string (so you should be able to
pass an email.Message directly, but doing the conversion with
email.Generator manually will give you more control over the
process).
delimited by '\\n'. If you pass something that doesn't look like a file,
we try to convert it to a string (so you should be able to pass an
L{email.Message} directly, but doing the conversion with
L{email.Generator} manually will give you more control over the process).
@param senderDomainName: Name by which to identify. If None, try
to pick something sane (but this depends on external configuration
and may not succeed).
@param senderDomainName: Name by which to identify. If None, try to pick
something sane (but this depends on external configuration and may not
succeed).
@type senderDomainName: L{bytes}
@param port: Remote port to which to connect.
@type port: L{int}
@param username: The username to use, if wanting to authenticate.
@type username: L{bytes}
@param reactor: The reactor used to make TCP connection.
@param password: The secret to use, if wanting to authenticate. If you do
not specify this, SMTP authentication will not occur.
@type password: L{bytes}
@param requireTransportSecurity: Whether or not STARTTLS is required.
@type requireTransportSecurity: L{bool}
@param requireAuthentication: Whether or not authentication is required.
@type requireAuthentication: L{bool}
@param reactor: The L{reactor} used to make the TCP connection.
@rtype: L{Deferred}
@returns: A cancellable L{Deferred}, its callback will be called if a
Expand All @@ -2132,7 +2154,7 @@ def sendmail(smtphost, from_addr, to_addrs, msg,
of tuples (address, code, resp) giving the response to the RCPT command
for each address.
"""
if not hasattr(msg,'read'):
if not hasattr(msg, 'read'):
# It's not a file
msg = StringIO(str(msg))

Expand All @@ -2149,8 +2171,11 @@ def cancel(d):
else:
# Connection hasn't been made yet
connector.disconnect()

d = defer.Deferred(cancel)
factory = SMTPSenderFactory(from_addr, to_addrs, msg, d)
factory = ESMTPSenderFactory(username, password, from_addr, to_addrs, msg,
d, heloFallback=True, requireAuthentication=requireAuthentication,
requireTransportSecurity=requireTransportSecurity)

if senderDomainName is not None:
factory.domain = senderDomainName
Expand Down
60 changes: 58 additions & 2 deletions twisted/mail/test/test_smtp.py
Original file line number Diff line number Diff line change
Expand Up @@ -1828,8 +1828,64 @@ def test_defaultReactorIsGlobalReactor(self):
L{twisted.internet.reactor}.
"""
args, varArgs, keywords, defaults = inspect.getargspec(smtp.sendmail)
index = len(args) - args.index("reactor") + 1
self.assertEqual(reactor, defaults[index])
self.assertEqual(reactor, defaults[2])


def test_honorsESMTPArguments(self):
"""
L{twisted.mail.smtp.sendmail} creates the ESMTP factory with the ESMTP
arguments.
"""
reactor = MemoryReactor()
smtp.sendmail("localhost", "source@address", "recipient@address",
"message", reactor=reactor, username="foo",
password="bar", requireTransportSecurity=True,
requireAuthentication=True)
factory = reactor.tcpClients[0][2]
self.assertEqual(factory._requireTransportSecurity, True)
self.assertEqual(factory._requireAuthentication, True)
self.assertEqual(factory.username, "foo")
self.assertEqual(factory.password, "bar")


def test_messageFilePassthrough(self):
"""
L{twisted.mail.smtp.sendmail} will pass through the message untouched
if it is a file-like object.
"""
reactor = MemoryReactor()
messageFile = StringIO(b"File!")

smtp.sendmail("localhost", "source@address", "recipient@address",
messageFile, reactor=reactor)
factory = reactor.tcpClients[0][2]
self.assertIs(factory.file, messageFile)


def test_messageStringMadeFile(self):
"""
L{twisted.mail.smtp.sendmail} will turn non-file-like objects
(eg. strings) into file-like objects before sending.
"""
reactor = MemoryReactor()
smtp.sendmail("localhost", "source@address", "recipient@address",
"message", reactor=reactor)
factory = reactor.tcpClients[0][2]
messageFile = factory.file
messageFile.seek(0)
self.assertEqual(messageFile.read(), "message")


def test_senderDomainName(self):
"""
L{twisted.mail.smtp.sendmail} passes through the sender domain name, if
provided.
"""
reactor = MemoryReactor()
smtp.sendmail("localhost", "source@address", "recipient@address",
"message", reactor=reactor, senderDomainName="foo")
factory = reactor.tcpClients[0][2]
self.assertEqual(factory.domain, "foo")


def test_cancelBeforeConnectionMade(self):
Expand Down
1 change: 1 addition & 0 deletions twisted/mail/topfiles/7257.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
twisted.mail.smtp.sendmail now uses ESMTP. It will opportunistically enable encryption and allow the use of authentication.

0 comments on commit 6d52ade

Please sign in to comment.