Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #3366 -- Part 1 of the email code refactoring and feature exten…

…sion. This

part refactors email sending into a more object-oriented interface in order to
make adding new features possible without making the API unusable. Thanks to
Gary Wilson for doing the design thinking and initial coding on this.

Includes documentation addition, but it probably needs a rewrite/edit, since
I'm not very happy with it at the moment.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@5141 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 95d7cb27d04bca14f98bbfc9b990488cb913df2c 1 parent 1f88c7f
Malcolm Tredinnick authored May 03, 2007
201  django/core/mail.py
... ...
@@ -1,9 +1,12 @@
1  
-# Use this module for e-mailing.
  1
+"""
  2
+Tools for sending email.
  3
+"""
2 4
 
3 5
 from django.conf import settings
4 6
 from email.MIMEText import MIMEText
5 7
 from email.Header import Header
6 8
 from email.Utils import formatdate
  9
+import os
7 10
 import smtplib
8 11
 import socket
9 12
 import time
@@ -22,6 +25,28 @@ def get_fqdn(self):
22 25
 
23 26
 DNS_NAME = CachedDnsName()
24 27
 
  28
+# Copied from Python standard library and modified to used the cached hostname
  29
+# for performance.
  30
+def make_msgid(idstring=None):
  31
+    """Returns a string suitable for RFC 2822 compliant Message-ID, e.g:
  32
+
  33
+    <20020201195627.33539.96671@nightshade.la.mastaler.com>
  34
+
  35
+    Optional idstring if given is a string used to strengthen the
  36
+    uniqueness of the message id.
  37
+    """
  38
+    timeval = time.time()
  39
+    utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval))
  40
+    pid = os.getpid()
  41
+    randint = random.randrange(100000)
  42
+    if idstring is None:
  43
+        idstring = ''
  44
+    else:
  45
+        idstring = '.' + idstring
  46
+    idhost = DNS_NAME
  47
+    msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost)
  48
+    return msgid
  49
+
25 50
 class BadHeaderError(ValueError):
26 51
     pass
27 52
 
@@ -34,6 +59,117 @@ def __setitem__(self, name, val):
34 59
             val = Header(val, settings.DEFAULT_CHARSET)
35 60
         MIMEText.__setitem__(self, name, val)
36 61
 
  62
+class SMTPConnection(object):
  63
+    """
  64
+    A wrapper that manages the SMTP network connection.
  65
+    """
  66
+
  67
+    def __init__(self, host=None, port=None, username=None, password=None,
  68
+                 fail_silently=False):
  69
+        if host is None:
  70
+            self.host = settings.EMAIL_HOST
  71
+        if port is None:
  72
+            self.port = settings.EMAIL_PORT
  73
+        if username is None:
  74
+        	self.username = settings.EMAIL_HOST_USER
  75
+        if password is None:
  76
+	        self.password = settings.EMAIL_HOST_PASSWORD
  77
+        self.fail_silently = fail_silently
  78
+        self.connection = None
  79
+
  80
+    def open(self):
  81
+        """
  82
+        Ensure we have a connection to the email server. Returns whether or not
  83
+        a new connection was required.
  84
+        """
  85
+        if self.connection:
  86
+            # Nothing to do if the connection is already open.
  87
+            return False
  88
+        try:
  89
+            self.connection = smtplib.SMTP(self.host, self.port)
  90
+            if self.username and self.password:
  91
+                self.connection.login(self.username, self.password)
  92
+            return True
  93
+        except:
  94
+            if not self.fail_silently:
  95
+                raise
  96
+
  97
+    def close(self):
  98
+        """Close the connection to the email server."""
  99
+        try:
  100
+            try:
  101
+                self.connection.quit()
  102
+            except:
  103
+                if self.fail_silently:
  104
+                    return
  105
+                raise
  106
+        finally:
  107
+            self.connection = None
  108
+
  109
+    def send_messages(self, email_messages):
  110
+        """
  111
+        Send one or more EmailMessage objects and return the number of email
  112
+        messages sent.
  113
+        """
  114
+        if not email_messages:
  115
+            return
  116
+        new_conn_created = self.open()
  117
+        if not self.connection:
  118
+            # We failed silently on open(). Trying to send would be pointless.
  119
+            return
  120
+        num_sent = 0
  121
+        for message in email_messages:
  122
+            sent = self._send(message)
  123
+            if sent:
  124
+                num_sent += 1
  125
+        if new_conn_created:
  126
+            self.close()
  127
+        return num_sent
  128
+
  129
+    def _send(self, email_message):
  130
+        """A helper method that does the actual sending."""
  131
+        if not email_message.to:
  132
+            return False
  133
+        try:
  134
+            self.connection.sendmail(email_message.from_email,
  135
+                    email_message.to, email_message.message.as_string()())
  136
+        except:
  137
+            if not self.fail_silently:
  138
+                raise
  139
+            return False
  140
+        return True
  141
+
  142
+class EmailMessage(object):
  143
+    """
  144
+    A container for email information.
  145
+    """
  146
+    def __init__(self, subject='', body='', from_email=None, to=None, connection=None):
  147
+        self.to = to or []
  148
+        if from_email is None:
  149
+            self.from_email = settings.DEFAULT_FROM_EMAIL
  150
+        else:
  151
+            self.from_email = from_email
  152
+        self.subject = subject
  153
+        self.body = body
  154
+        self.connection = connection
  155
+
  156
+    def get_connection(self, fail_silently=False):
  157
+        if not self.connection:
  158
+            self.connection = SMTPConnection(fail_silently=fail_silently)
  159
+        return self.connection
  160
+
  161
+    def message(self):
  162
+        msg = SafeMIMEText(self.body, 'plain', settings.DEFAULT_CHARSET)
  163
+        msg['Subject'] = self.subject
  164
+        msg['From'] = self.from_email
  165
+        msg['To'] = ', '.join(self.to)
  166
+        msg['Date'] = formatdate()
  167
+        msg['Message-ID'] = make_msgid()
  168
+
  169
+    def send(self, fail_silently=False):
  170
+        """Send the email message."""
  171
+        return self.get_connection(fail_silently).send_messages([self])
  172
+
37 173
 def send_mail(subject, message, from_email, recipient_list, fail_silently=False, auth_user=None, auth_password=None):
38 174
     """
39 175
     Easy wrapper for sending a single message to a recipient list. All members
@@ -41,8 +177,13 @@ def send_mail(subject, message, from_email, recipient_list, fail_silently=False,
41 177
 
42 178
     If auth_user is None, the EMAIL_HOST_USER setting is used.
43 179
     If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
  180
+
  181
+    NOTE: This method is deprecated. It exists for backwards compatibility.
  182
+    New code should use the EmailMessage class directly.
44 183
     """
45  
-    return send_mass_mail([[subject, message, from_email, recipient_list]], fail_silently, auth_user, auth_password)
  184
+    connection = SMTPConnection(username=auth_user, password=auth_password,
  185
+                                 fail_silently=fail_silently)
  186
+    return EmailMessage(subject, message, from_email, recipient_list, connection=connection).send()
46 187
 
47 188
 def send_mass_mail(datatuple, fail_silently=False, auth_user=None, auth_password=None):
48 189
     """
@@ -53,52 +194,24 @@ def send_mass_mail(datatuple, fail_silently=False, auth_user=None, auth_password
53 194
     If auth_user and auth_password are set, they're used to log in.
54 195
     If auth_user is None, the EMAIL_HOST_USER setting is used.
55 196
     If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
  197
+
  198
+    NOTE: This method is deprecated. It exists for backwards compatibility.
  199
+    New code should use the EmailMessage class directly.
56 200
     """
57  
-    if auth_user is None:
58  
-        auth_user = settings.EMAIL_HOST_USER
59  
-    if auth_password is None:
60  
-        auth_password = settings.EMAIL_HOST_PASSWORD
61  
-    try:
62  
-        server = smtplib.SMTP(settings.EMAIL_HOST, settings.EMAIL_PORT)
63  
-        if auth_user and auth_password:
64  
-            server.login(auth_user, auth_password)
65  
-    except:
66  
-        if fail_silently:
67  
-            return
68  
-        raise
69  
-    num_sent = 0
70  
-    for subject, message, from_email, recipient_list in datatuple:
71  
-        if not recipient_list:
72  
-            continue
73  
-        from_email = from_email or settings.DEFAULT_FROM_EMAIL
74  
-        msg = SafeMIMEText(message, 'plain', settings.DEFAULT_CHARSET)
75  
-        msg['Subject'] = subject
76  
-        msg['From'] = from_email
77  
-        msg['To'] = ', '.join(recipient_list)
78  
-        msg['Date'] = formatdate()
79  
-        try:
80  
-            random_bits = str(random.getrandbits(64))
81  
-        except AttributeError: # Python 2.3 doesn't have random.getrandbits().
82  
-            random_bits = ''.join([random.choice('1234567890') for i in range(19)])
83  
-        msg['Message-ID'] = "<%d.%s@%s>" % (time.time(), random_bits, DNS_NAME)
84  
-        try:
85  
-            server.sendmail(from_email, recipient_list, msg.as_string())
86  
-            num_sent += 1
87  
-        except:
88  
-            if not fail_silently:
89  
-                raise
90  
-    try:
91  
-        server.quit()
92  
-    except:
93  
-        if fail_silently:
94  
-            return
95  
-        raise
96  
-    return num_sent
  201
+    connection = SMTPConnection(username=auth_user, password=auth_password,
  202
+                                 fail_silently=fail_silently)
  203
+    messages = [EmailMessage(subject, message, sender, recipient) for subject, message, sender, recipient in datatuple]
  204
+    return connection.send_messages(messages)
97 205
 
98 206
 def mail_admins(subject, message, fail_silently=False):
99 207
     "Sends a message to the admins, as defined by the ADMINS setting."
100  
-    send_mail(settings.EMAIL_SUBJECT_PREFIX + subject, message, settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS], fail_silently)
  208
+    EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
  209
+            settings.SERVER_EMAIL, [a[1] for a in
  210
+                settings.ADMINS]).send(fail_silently=fail_silently)
101 211
 
102 212
 def mail_managers(subject, message, fail_silently=False):
103 213
     "Sends a message to the managers, as defined by the MANAGERS setting."
104  
-    send_mail(settings.EMAIL_SUBJECT_PREFIX + subject, message, settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS], fail_silently)
  214
+    EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
  215
+            settings.SERVER_EMAIL, [a[1] for a in
  216
+                settings.MANAGERS]).send(fail_silently=fail_silently)
  217
+
46  docs/email.txt
@@ -183,3 +183,49 @@ from the request's POST data, sends that to admin@example.com and redirects to
183 183
             return HttpResponse('Make sure all fields are entered and valid.')
184 184
 
185 185
 .. _Header injection: http://securephp.damonkohler.com/index.php/Email_Injection
  186
+
  187
+The EmailMessage and SMTPConnection classes
  188
+===========================================
  189
+
  190
+Django's `send_mail()` and `send_mass_mail()` functions are actually thin
  191
+wrappers that make use of the `EmailMessage` and `SMTPConnection` classes in
  192
+`django.mail`.  If you ever need to customize the way Django sends email, you
  193
+can subclass these two classes to suit your needs.
  194
+
  195
+.. note::
  196
+    Not all features of the `EmailMessage` class are available through the
  197
+    `send_mail()` and related wrapper functions. If you wish to use advanced
  198
+    features such as including BCC recipients or multi-part email, you will
  199
+    need to create `EmailMessage` instances directly.
  200
+
  201
+In general, `EmailMessage` is responsible for creating the email message
  202
+itself. `SMTPConnection` is responsible for the network connection side of the
  203
+operation. This means you can reuse the same connection (an `SMTPConnection`
  204
+instance) for multiple messages.
  205
+
  206
+The `EmailMessage` class has the following methods that you can use:
  207
+
  208
+ * `send()` sends the message, using either the connection that is specified
  209
+   in the `connection` attribute, or creating a new connection if none already
  210
+   exists.
  211
+ * `message()` constructs a `django.core.mail.SafeMIMEText` object (a
  212
+   sub-class of Python's `email.MIMEText.MIMEText` class) holding the message
  213
+   to be sent. If you ever need to extend the `EmailMessage` class, you will
  214
+   probably want to override this method to put the content you wish into the
  215
+   MIME object.
  216
+
  217
+The `SMTPConnection` class is initialized with the host, port, username and
  218
+password for the SMTP server. If you don't specify one or more of those
  219
+options, they are read from your settings file.
  220
+
  221
+If you are sending lots of messages at once, the `send_messages()` method of
  222
+the `SMTPConnection` class will be useful. It takes a list of `EmailMessage`
  223
+instances (or sub-classes) and sends them over a single connection. For
  224
+example, if you have a function called `get_notification_email()` that returns a
  225
+list of `EmailMessage` objects representing some periodic email you wish to
  226
+send out, you could send this with::
  227
+
  228
+    connection = SMTPConnection()   # Use default settings for connection
  229
+    messages = get_notification_email()
  230
+    connection.send_messages(messages)
  231
+

0 notes on commit 95d7cb2

Please sign in to comment.
Something went wrong with that request. Please try again.