~ubuntu-branches/ubuntu/quantal/python-django/quantal-security

« back to all changes in this revision

Viewing changes to django/core/mail.py

  • Committer: Bazaar Package Importer
  • Author(s): Chris Lamb
  • Date: 2010-05-21 07:52:55 UTC
  • mfrom: (1.3.6 upstream)
  • mto: This revision was merged to the branch mainline in revision 28.
  • Revision ID: james.westby@ubuntu.com-20100521075255-ii78v1dyfmyu3uzx
Tags: upstream-1.2
ImportĀ upstreamĀ versionĀ 1.2

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
"""
2
 
Tools for sending email.
3
 
"""
4
 
 
5
 
import mimetypes
6
 
import os
7
 
import smtplib
8
 
import socket
9
 
import time
10
 
import random
11
 
from email import Charset, Encoders
12
 
from email.MIMEText import MIMEText
13
 
from email.MIMEMultipart import MIMEMultipart
14
 
from email.MIMEBase import MIMEBase
15
 
from email.Header import Header
16
 
from email.Utils import formatdate, parseaddr, formataddr
17
 
 
18
 
from django.conf import settings
19
 
from django.utils.encoding import smart_str, force_unicode
20
 
 
21
 
# Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from
22
 
# some spam filters.
23
 
Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8')
24
 
 
25
 
# Default MIME type to use on attachments (if it is not explicitly given
26
 
# and cannot be guessed).
27
 
DEFAULT_ATTACHMENT_MIME_TYPE = 'application/octet-stream'
28
 
 
29
 
# Cache the hostname, but do it lazily: socket.getfqdn() can take a couple of
30
 
# seconds, which slows down the restart of the server.
31
 
class CachedDnsName(object):
32
 
    def __str__(self):
33
 
        return self.get_fqdn()
34
 
 
35
 
    def get_fqdn(self):
36
 
        if not hasattr(self, '_fqdn'):
37
 
            self._fqdn = socket.getfqdn()
38
 
        return self._fqdn
39
 
 
40
 
DNS_NAME = CachedDnsName()
41
 
 
42
 
# Copied from Python standard library, with the following modifications:
43
 
# * Used cached hostname for performance.
44
 
# * Added try/except to support lack of getpid() in Jython (#5496).
45
 
def make_msgid(idstring=None):
46
 
    """Returns a string suitable for RFC 2822 compliant Message-ID, e.g:
47
 
 
48
 
    <20020201195627.33539.96671@nightshade.la.mastaler.com>
49
 
 
50
 
    Optional idstring if given is a string used to strengthen the
51
 
    uniqueness of the message id.
52
 
    """
53
 
    timeval = time.time()
54
 
    utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval))
55
 
    try:
56
 
        pid = os.getpid()
57
 
    except AttributeError:
58
 
        # No getpid() in Jython, for example.
59
 
        pid = 1
60
 
    randint = random.randrange(100000)
61
 
    if idstring is None:
62
 
        idstring = ''
63
 
    else:
64
 
        idstring = '.' + idstring
65
 
    idhost = DNS_NAME
66
 
    msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost)
67
 
    return msgid
68
 
 
69
 
class BadHeaderError(ValueError):
70
 
    pass
71
 
 
72
 
def forbid_multi_line_headers(name, val):
73
 
    """Forbids multi-line headers, to prevent header injection."""
74
 
    val = force_unicode(val)
75
 
    if '\n' in val or '\r' in val:
76
 
        raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (val, name))
77
 
    try:
78
 
        val = val.encode('ascii')
79
 
    except UnicodeEncodeError:
80
 
        if name.lower() in ('to', 'from', 'cc'):
81
 
            result = []
82
 
            for item in val.split(', '):
83
 
                nm, addr = parseaddr(item)
84
 
                nm = str(Header(nm, settings.DEFAULT_CHARSET))
85
 
                result.append(formataddr((nm, str(addr))))
86
 
            val = ', '.join(result)
87
 
        else:
88
 
            val = Header(val, settings.DEFAULT_CHARSET)
89
 
    else:
90
 
        if name.lower() == 'subject':
91
 
            val = Header(val)
92
 
    return name, val
93
 
 
94
 
class SafeMIMEText(MIMEText):
95
 
    def __setitem__(self, name, val):
96
 
        name, val = forbid_multi_line_headers(name, val)
97
 
        MIMEText.__setitem__(self, name, val)
98
 
 
99
 
class SafeMIMEMultipart(MIMEMultipart):
100
 
    def __setitem__(self, name, val):
101
 
        name, val = forbid_multi_line_headers(name, val)
102
 
        MIMEMultipart.__setitem__(self, name, val)
103
 
 
104
 
class SMTPConnection(object):
105
 
    """
106
 
    A wrapper that manages the SMTP network connection.
107
 
    """
108
 
 
109
 
    def __init__(self, host=None, port=None, username=None, password=None,
110
 
                 use_tls=None, fail_silently=False):
111
 
        self.host = host or settings.EMAIL_HOST
112
 
        self.port = port or settings.EMAIL_PORT
113
 
        self.username = username or settings.EMAIL_HOST_USER
114
 
        self.password = password or settings.EMAIL_HOST_PASSWORD
115
 
        self.use_tls = (use_tls is not None) and use_tls or settings.EMAIL_USE_TLS
116
 
        self.fail_silently = fail_silently
117
 
        self.connection = None
118
 
 
119
 
    def open(self):
120
 
        """
121
 
        Ensures we have a connection to the email server. Returns whether or
122
 
        not a new connection was required (True or False).
123
 
        """
124
 
        if self.connection:
125
 
            # Nothing to do if the connection is already open.
126
 
            return False
127
 
        try:
128
 
            # If local_hostname is not specified, socket.getfqdn() gets used.
129
 
            # For performance, we use the cached FQDN for local_hostname.
130
 
            self.connection = smtplib.SMTP(self.host, self.port,
131
 
                                           local_hostname=DNS_NAME.get_fqdn())
132
 
            if self.use_tls:
133
 
                self.connection.ehlo()
134
 
                self.connection.starttls()
135
 
                self.connection.ehlo()
136
 
            if self.username and self.password:
137
 
                self.connection.login(self.username, self.password)
138
 
            return True
139
 
        except:
140
 
            if not self.fail_silently:
141
 
                raise
142
 
 
143
 
    def close(self):
144
 
        """Closes the connection to the email server."""
145
 
        try:
146
 
            try:
147
 
                self.connection.quit()
148
 
            except socket.sslerror:
149
 
                # This happens when calling quit() on a TLS connection
150
 
                # sometimes.
151
 
                self.connection.close()
152
 
            except:
153
 
                if self.fail_silently:
154
 
                    return
155
 
                raise
156
 
        finally:
157
 
            self.connection = None
158
 
 
159
 
    def send_messages(self, email_messages):
160
 
        """
161
 
        Sends one or more EmailMessage objects and returns the number of email
162
 
        messages sent.
163
 
        """
164
 
        if not email_messages:
165
 
            return
166
 
        new_conn_created = self.open()
167
 
        if not self.connection:
168
 
            # We failed silently on open(). Trying to send would be pointless.
169
 
            return
170
 
        num_sent = 0
171
 
        for message in email_messages:
172
 
            sent = self._send(message)
173
 
            if sent:
174
 
                num_sent += 1
175
 
        if new_conn_created:
176
 
            self.close()
177
 
        return num_sent
178
 
 
179
 
    def _send(self, email_message):
180
 
        """A helper method that does the actual sending."""
181
 
        if not email_message.recipients():
182
 
            return False
183
 
        try:
184
 
            self.connection.sendmail(email_message.from_email,
185
 
                    email_message.recipients(),
186
 
                    email_message.message().as_string())
187
 
        except:
188
 
            if not self.fail_silently:
189
 
                raise
190
 
            return False
191
 
        return True
192
 
 
193
 
class EmailMessage(object):
194
 
    """
195
 
    A container for email information.
196
 
    """
197
 
    content_subtype = 'plain'
198
 
    mixed_subtype = 'mixed'
199
 
    encoding = None     # None => use settings default
200
 
 
201
 
    def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
202
 
            connection=None, attachments=None, headers=None):
203
 
        """
204
 
        Initialize a single email message (which can be sent to multiple
205
 
        recipients).
206
 
 
207
 
        All strings used to create the message can be unicode strings (or UTF-8
208
 
        bytestrings). The SafeMIMEText class will handle any necessary encoding
209
 
        conversions.
210
 
        """
211
 
        if to:
212
 
            assert not isinstance(to, basestring), '"to" argument must be a list or tuple'
213
 
            self.to = list(to)
214
 
        else:
215
 
            self.to = []
216
 
        if bcc:
217
 
            assert not isinstance(bcc, basestring), '"bcc" argument must be a list or tuple'
218
 
            self.bcc = list(bcc)
219
 
        else:
220
 
            self.bcc = []
221
 
        self.from_email = from_email or settings.DEFAULT_FROM_EMAIL
222
 
        self.subject = subject
223
 
        self.body = body
224
 
        self.attachments = attachments or []
225
 
        self.extra_headers = headers or {}
226
 
        self.connection = connection
227
 
 
228
 
    def get_connection(self, fail_silently=False):
229
 
        if not self.connection:
230
 
            self.connection = SMTPConnection(fail_silently=fail_silently)
231
 
        return self.connection
232
 
 
233
 
    def message(self):
234
 
        encoding = self.encoding or settings.DEFAULT_CHARSET
235
 
        msg = SafeMIMEText(smart_str(self.body, settings.DEFAULT_CHARSET),
236
 
                           self.content_subtype, encoding)
237
 
        msg = self._create_message(msg)
238
 
        msg['Subject'] = self.subject
239
 
        msg['From'] = self.extra_headers.pop('From', self.from_email)
240
 
        msg['To'] = ', '.join(self.to)
241
 
 
242
 
        # Email header names are case-insensitive (RFC 2045), so we have to
243
 
        # accommodate that when doing comparisons.
244
 
        header_names = [key.lower() for key in self.extra_headers]
245
 
        if 'date' not in header_names:
246
 
            msg['Date'] = formatdate()
247
 
        if 'message-id' not in header_names:
248
 
            msg['Message-ID'] = make_msgid()
249
 
        for name, value in self.extra_headers.items():
250
 
            msg[name] = value
251
 
        return msg
252
 
 
253
 
    def recipients(self):
254
 
        """
255
 
        Returns a list of all recipients of the email (includes direct
256
 
        addressees as well as Bcc entries).
257
 
        """
258
 
        return self.to + self.bcc
259
 
 
260
 
    def send(self, fail_silently=False):
261
 
        """Sends the email message."""
262
 
        if not self.recipients():
263
 
            # Don't bother creating the network connection if there's nobody to
264
 
            # send to.
265
 
            return 0
266
 
        return self.get_connection(fail_silently).send_messages([self])
267
 
 
268
 
    def attach(self, filename=None, content=None, mimetype=None):
269
 
        """
270
 
        Attaches a file with the given filename and content. The filename can
271
 
        be omitted and the mimetype is guessed, if not provided.
272
 
 
273
 
        If the first parameter is a MIMEBase subclass it is inserted directly
274
 
        into the resulting message attachments.
275
 
        """
276
 
        if isinstance(filename, MIMEBase):
277
 
            assert content == mimetype == None
278
 
            self.attachments.append(filename)
279
 
        else:
280
 
            assert content is not None
281
 
            self.attachments.append((filename, content, mimetype))
282
 
 
283
 
    def attach_file(self, path, mimetype=None):
284
 
        """Attaches a file from the filesystem."""
285
 
        filename = os.path.basename(path)
286
 
        content = open(path, 'rb').read()
287
 
        self.attach(filename, content, mimetype)
288
 
 
289
 
    def _create_message(self, msg):
290
 
        return self._create_attachments(msg)
291
 
 
292
 
    def _create_attachments(self, msg):
293
 
        if self.attachments:
294
 
            body_msg = msg
295
 
            msg = SafeMIMEMultipart(_subtype=self.mixed_subtype)
296
 
            if self.body:
297
 
                msg.attach(body_msg)
298
 
            for attachment in self.attachments:
299
 
                if isinstance(attachment, MIMEBase):
300
 
                    msg.attach(attachment)
301
 
                else:
302
 
                    msg.attach(self._create_attachment(*attachment))
303
 
        return msg
304
 
 
305
 
    def _create_mime_attachment(self, content, mimetype):
306
 
        """
307
 
        Converts the content, mimetype pair into a MIME attachment object.
308
 
        """
309
 
        basetype, subtype = mimetype.split('/', 1)
310
 
        if basetype == 'text':
311
 
            attachment = SafeMIMEText(smart_str(content,
312
 
                settings.DEFAULT_CHARSET), subtype, settings.DEFAULT_CHARSET)
313
 
        else:
314
 
            # Encode non-text attachments with base64.
315
 
            attachment = MIMEBase(basetype, subtype)
316
 
            attachment.set_payload(content)
317
 
            Encoders.encode_base64(attachment)
318
 
        return attachment
319
 
 
320
 
    def _create_attachment(self, filename, content, mimetype=None):
321
 
        """
322
 
        Converts the filename, content, mimetype triple into a MIME attachment
323
 
        object.
324
 
        """
325
 
        if mimetype is None:
326
 
            mimetype, _ = mimetypes.guess_type(filename)
327
 
            if mimetype is None:
328
 
                mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
329
 
        attachment = self._create_mime_attachment(content, mimetype)
330
 
        if filename:
331
 
            attachment.add_header('Content-Disposition', 'attachment',
332
 
                                  filename=filename)
333
 
        return attachment
334
 
 
335
 
class EmailMultiAlternatives(EmailMessage):
336
 
    """
337
 
    A version of EmailMessage that makes it easy to send multipart/alternative
338
 
    messages. For example, including text and HTML versions of the text is
339
 
    made easier.
340
 
    """
341
 
    alternative_subtype = 'alternative'
342
 
 
343
 
    def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
344
 
            connection=None, attachments=None, headers=None, alternatives=None):
345
 
        """
346
 
        Initialize a single email message (which can be sent to multiple
347
 
        recipients).
348
 
 
349
 
        All strings used to create the message can be unicode strings (or UTF-8
350
 
        bytestrings). The SafeMIMEText class will handle any necessary encoding
351
 
        conversions.
352
 
        """
353
 
        super(EmailMultiAlternatives, self).__init__(subject, body, from_email, to, bcc, connection, attachments, headers)
354
 
        self.alternatives=alternatives or []
355
 
 
356
 
    def attach_alternative(self, content, mimetype):
357
 
        """Attach an alternative content representation."""
358
 
        assert content is not None
359
 
        assert mimetype is not None
360
 
        self.alternatives.append((content, mimetype))
361
 
 
362
 
    def _create_message(self, msg):
363
 
        return self._create_attachments(self._create_alternatives(msg))
364
 
 
365
 
    def _create_alternatives(self, msg):
366
 
        if self.alternatives:
367
 
            body_msg = msg
368
 
            msg = SafeMIMEMultipart(_subtype=self.alternative_subtype)
369
 
            if self.body:
370
 
                msg.attach(body_msg)
371
 
            for alternative in self.alternatives:
372
 
                msg.attach(self._create_mime_attachment(*alternative))
373
 
        return msg
374
 
 
375
 
def send_mail(subject, message, from_email, recipient_list,
376
 
              fail_silently=False, auth_user=None, auth_password=None):
377
 
    """
378
 
    Easy wrapper for sending a single message to a recipient list. All members
379
 
    of the recipient list will see the other recipients in the 'To' field.
380
 
 
381
 
    If auth_user is None, the EMAIL_HOST_USER setting is used.
382
 
    If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
383
 
 
384
 
    Note: The API for this method is frozen. New code wanting to extend the
385
 
    functionality should use the EmailMessage class directly.
386
 
    """
387
 
    connection = SMTPConnection(username=auth_user, password=auth_password,
388
 
                                fail_silently=fail_silently)
389
 
    return EmailMessage(subject, message, from_email, recipient_list,
390
 
                        connection=connection).send()
391
 
 
392
 
def send_mass_mail(datatuple, fail_silently=False, auth_user=None,
393
 
                   auth_password=None):
394
 
    """
395
 
    Given a datatuple of (subject, message, from_email, recipient_list), sends
396
 
    each message to each recipient list. Returns the number of e-mails sent.
397
 
 
398
 
    If from_email is None, the DEFAULT_FROM_EMAIL setting is used.
399
 
    If auth_user and auth_password are set, they're used to log in.
400
 
    If auth_user is None, the EMAIL_HOST_USER setting is used.
401
 
    If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
402
 
 
403
 
    Note: The API for this method is frozen. New code wanting to extend the
404
 
    functionality should use the EmailMessage class directly.
405
 
    """
406
 
    connection = SMTPConnection(username=auth_user, password=auth_password,
407
 
                                fail_silently=fail_silently)
408
 
    messages = [EmailMessage(subject, message, sender, recipient)
409
 
                for subject, message, sender, recipient in datatuple]
410
 
    return connection.send_messages(messages)
411
 
 
412
 
def mail_admins(subject, message, fail_silently=False):
413
 
    """Sends a message to the admins, as defined by the ADMINS setting."""
414
 
    if not settings.ADMINS:
415
 
        return
416
 
    EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
417
 
                 settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS]
418
 
                 ).send(fail_silently=fail_silently)
419
 
 
420
 
def mail_managers(subject, message, fail_silently=False):
421
 
    """Sends a message to the managers, as defined by the MANAGERS setting."""
422
 
    if not settings.MANAGERS:
423
 
        return
424
 
    EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
425
 
                 settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS]
426
 
                 ).send(fail_silently=fail_silently)