Ticket #10355: email_backends.3.diff
File email_backends.3.diff, 63.9 KB (added by , 15 years ago) |
---|
-
django/conf/global_settings.py
diff -r 4e009aed3ca6 django/conf/global_settings.py
a b 131 131 DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. 132 132 DATABASE_OPTIONS = {} # Set to empty dictionary for default. 133 133 134 # The email backend to use. For possible shortcuts see django.core.mail. 135 # The default is to use the 'smtp' backend for sending emails using a 136 # SMTP server. 137 # Third-party backends could be configured by a Python import path. 138 EMAIL_BACKEND = 'smtp' 139 134 140 # Host for sending e-mail. 135 141 EMAIL_HOST = 'localhost' 136 142 … … 142 148 EMAIL_HOST_PASSWORD = '' 143 149 EMAIL_USE_TLS = False 144 150 151 # Directory for saving messages when using the file backend. 152 EMAIL_FILE_PATH = None 153 145 154 # List of strings representing installed apps. 146 155 INSTALLED_APPS = () 147 156 -
deleted file django/core/mail.py
diff -r 4e009aed3ca6 django/core/mail.py
+ - 1 """2 Tools for sending email.3 """4 5 import mimetypes6 import os7 import smtplib8 import socket9 import time10 import random11 from email import Charset, Encoders12 from email.MIMEText import MIMEText13 from email.MIMEMultipart import MIMEMultipart14 from email.MIMEBase import MIMEBase15 from email.Header import Header16 from email.Utils import formatdate, parseaddr, formataddr17 18 from django.conf import settings19 from django.utils.encoding import smart_str, force_unicode20 21 # Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from22 # 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 given26 # 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 of30 # 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._fqdn39 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 the51 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 = 160 randint = random.randrange(100000)61 if idstring is None:62 idstring = ''63 else:64 idstring = '.' + idstring65 idhost = DNS_NAME66 msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost)67 return msgid68 69 class BadHeaderError(ValueError):70 pass71 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, val93 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_HOST112 self.port = port or settings.EMAIL_PORT113 self.username = username or settings.EMAIL_HOST_USER114 self.password = password or settings.EMAIL_HOST_PASSWORD115 self.use_tls = (use_tls is not None) and use_tls or settings.EMAIL_USE_TLS116 self.fail_silently = fail_silently117 self.connection = None118 119 def open(self):120 """121 Ensures we have a connection to the email server. Returns whether or122 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 False127 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 True139 except:140 if not self.fail_silently:141 raise142 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 connection150 # sometimes.151 self.connection.close()152 except:153 if self.fail_silently:154 return155 raise156 finally:157 self.connection = None158 159 def send_messages(self, email_messages):160 """161 Sends one or more EmailMessage objects and returns the number of email162 messages sent.163 """164 if not email_messages:165 return166 new_conn_created = self.open()167 if not self.connection:168 # We failed silently on open(). Trying to send would be pointless.169 return170 num_sent = 0171 for message in email_messages:172 sent = self._send(message)173 if sent:174 num_sent += 1175 if new_conn_created:176 self.close()177 return num_sent178 179 def _send(self, email_message):180 """A helper method that does the actual sending."""181 if not email_message.recipients():182 return False183 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 raise190 return False191 return True192 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 default200 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 multiple205 recipients).206 207 All strings used to create the message can be unicode strings (or UTF-8208 bytestrings). The SafeMIMEText class will handle any necessary encoding209 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_EMAIL222 self.subject = subject223 self.body = body224 self.attachments = attachments or []225 self.extra_headers = headers or {}226 self.connection = connection227 228 def get_connection(self, fail_silently=False):229 if not self.connection:230 self.connection = SMTPConnection(fail_silently=fail_silently)231 return self.connection232 233 def message(self):234 encoding = self.encoding or settings.DEFAULT_CHARSET235 msg = SafeMIMEText(smart_str(self.body, settings.DEFAULT_CHARSET),236 self.content_subtype, encoding)237 msg = self._create_message(msg)238 msg['Subject'] = self.subject239 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 to243 # 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] = value251 return msg252 253 def recipients(self):254 """255 Returns a list of all recipients of the email (includes direct256 addressees as well as Bcc entries).257 """258 return self.to + self.bcc259 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 to264 # send to.265 return 0266 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 can271 be omitted and the mimetype is guessed, if not provided.272 273 If the first parameter is a MIMEBase subclass it is inserted directly274 into the resulting message attachments.275 """276 if isinstance(filename, MIMEBase):277 assert content == mimetype == None278 self.attachments.append(filename)279 else:280 assert content is not None281 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 = msg295 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 msg304 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 attachment319 320 def _create_attachment(self, filename, content, mimetype=None):321 """322 Converts the filename, content, mimetype triple into a MIME attachment323 object.324 """325 if mimetype is None:326 mimetype, _ = mimetypes.guess_type(filename)327 if mimetype is None:328 mimetype = DEFAULT_ATTACHMENT_MIME_TYPE329 attachment = self._create_mime_attachment(content, mimetype)330 if filename:331 attachment.add_header('Content-Disposition', 'attachment',332 filename=filename)333 return attachment334 335 class EmailMultiAlternatives(EmailMessage):336 """337 A version of EmailMessage that makes it easy to send multipart/alternative338 messages. For example, including text and HTML versions of the text is339 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 multiple347 recipients).348 349 All strings used to create the message can be unicode strings (or UTF-8350 bytestrings). The SafeMIMEText class will handle any necessary encoding351 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 None359 assert mimetype is not None360 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 = msg368 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 msg374 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 members379 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 the385 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), sends396 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 the404 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 return416 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 return424 EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,425 settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS]426 ).send(fail_silently=fail_silently) -
new file django/core/mail/__init__.py
diff -r 4e009aed3ca6 django/core/mail/__init__.py
- + 1 """ 2 Tools for sending email. 3 """ 4 5 from django.conf import settings 6 from django.core.exceptions import ImproperlyConfigured 7 from django.core.mail.message import EmailMessage, EmailMultiAlternatives 8 from django.utils.importlib import import_module 9 10 # Imported for backwards compatibility 11 from django.core.mail.backends.smtp import EmailBackend as _SMTPConnection 12 13 14 # Shortcuts for builtin email backends. The dictionary maps backend names 15 # to module names. If settings.EMAIL_BACKEND isn't in this dictionary 16 # it's treated as a Python import path. 17 BACKENDS = { 18 'console': 'console', 19 'dummy': 'dummy', 20 'file': 'filebased', 21 'locmem': 'locmem', 22 'smtp': 'smtp', 23 } 24 25 26 def get_connection(backend=None, fail_silently=False, **kwds): 27 """Load an e-mail backend and return an instance of it. 28 29 If backend is None (default) settings.EMAIL_BACKEND is used. 30 31 Both fail_silently and other keyword arguments are used in the 32 constructor of the backend. 33 """ 34 path = backend or settings.EMAIL_BACKEND 35 if path in BACKENDS: 36 path = 'django.core.mail.backends.%s' % BACKENDS.get(path) 37 try: 38 mod = import_module(path) 39 except ImportError, e: 40 raise ImproperlyConfigured(('Error importing email backend %s: "%s"' 41 % (path, e))) 42 try: 43 cls = getattr(mod, 'EmailBackend') 44 except AttributeError: 45 raise ImproperlyConfigured(('Module "%s" does not define a ' 46 '"EmailBackend" class' % path)) 47 return cls(fail_silently=fail_silently, **kwds) 48 49 50 def send_mail(subject, message, from_email, recipient_list, 51 fail_silently=False, auth_user=None, auth_password=None): 52 """ 53 Easy wrapper for sending a single message to a recipient list. All members 54 of the recipient list will see the other recipients in the 'To' field. 55 56 If auth_user is None, the EMAIL_HOST_USER setting is used. 57 If auth_password is None, the EMAIL_HOST_PASSWORD setting is used. 58 59 Note: The API for this method is frozen. New code wanting to extend the 60 functionality should use the EmailMessage class directly. 61 """ 62 connection = get_connection(username=auth_user, password=auth_password, 63 fail_silently=fail_silently) 64 return EmailMessage(subject, message, from_email, recipient_list, 65 connection=connection).send() 66 67 68 def send_mass_mail(datatuple, fail_silently=False, auth_user=None, 69 auth_password=None): 70 """ 71 Given a datatuple of (subject, message, from_email, recipient_list), sends 72 each message to each recipient list. Returns the number of e-mails sent. 73 74 If from_email is None, the DEFAULT_FROM_EMAIL setting is used. 75 If auth_user and auth_password are set, they're used to log in. 76 If auth_user is None, the EMAIL_HOST_USER setting is used. 77 If auth_password is None, the EMAIL_HOST_PASSWORD setting is used. 78 79 Note: The API for this method is frozen. New code wanting to extend the 80 functionality should use the EmailMessage class directly. 81 """ 82 connection = get_connection(username=auth_user, password=auth_password, 83 fail_silently=fail_silently) 84 messages = [EmailMessage(subject, message, sender, recipient) 85 for subject, message, sender, recipient in datatuple] 86 return connection.send_messages(messages) 87 88 89 def mail_admins(subject, message, fail_silently=False): 90 """Sends a message to the admins, as defined by the ADMINS setting.""" 91 if not settings.ADMINS: 92 return 93 EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message, 94 settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS] 95 ).send(fail_silently=fail_silently) 96 97 98 def mail_managers(subject, message, fail_silently=False): 99 """Sends a message to the managers, as defined by the MANAGERS setting.""" 100 if not settings.MANAGERS: 101 return 102 EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message, 103 settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS] 104 ).send(fail_silently=fail_silently) 105 106 107 class SMTPConnection(_SMTPConnection): 108 109 def __init__(self, *args, **kwds): 110 import warnings 111 warnings.warn( 112 'mail.SMTPConnection is deprectated; use mail.get_connection() instead.', 113 DeprecationWarning 114 ) 115 super(SMTPConnection, self).__init__(*args, **kwds) -
new file django/core/mail/backends/__init__.py
diff -r 4e009aed3ca6 django/core/mail/backends/__init__.py
- + 1 # Mail backends shipped with Django. -
new file django/core/mail/backends/base.py
diff -r 4e009aed3ca6 django/core/mail/backends/base.py
- + 1 """Base email backend class.""" 2 3 4 class BaseEmailBackend(object): 5 """ 6 Base class for email backend implementations. 7 8 Subclasses must at least overwrite send_messages(). 9 """ 10 11 def __init__(self, fail_silently=False, **kwds): 12 self.fail_silently = fail_silently 13 14 def open(self): 15 """Open a network connection. 16 17 This method can be overwritten by backend implementations to 18 open a network connection. 19 20 It's up to the backend implementation to track the status of 21 a network connection if it's needed by the backend. 22 23 This method can be called by applications to force a single 24 network connection to be used when sending mails. See the 25 send_messages() method of the SMTP backend for a reference 26 implementation. 27 28 The default implementation does nothing. 29 """ 30 pass 31 32 def close(self): 33 """Close a network connection.""" 34 pass 35 36 def send_messages(self, email_messages): 37 """ 38 Sends one or more EmailMessage objects and returns the number of email 39 messages sent. 40 """ 41 raise NotImplementedError -
new file django/core/mail/backends/console.py
diff -r 4e009aed3ca6 django/core/mail/backends/console.py
- + 1 """ 2 Email backend that writes messages to console instead of sending them. 3 """ 4 5 import sys 6 7 from django.core.mail.backends.base import BaseEmailBackend 8 9 10 class EmailBackend(BaseEmailBackend): 11 12 def send_messages(self, email_messages): 13 for message in email_messages: 14 sys.stdout.write('%s\n' % message.message().as_string()) 15 return len(email_messages) -
new file django/core/mail/backends/dummy.py
diff -r 4e009aed3ca6 django/core/mail/backends/dummy.py
- + 1 """ 2 Dummy email backend that does nothing. 3 """ 4 5 6 from django.core.mail.backends.base import BaseEmailBackend 7 8 9 class EmailBackend(BaseEmailBackend): 10 11 def send_messages(self, email_messages): 12 return len(email_messages) -
new file django/core/mail/backends/filebased.py
diff -r 4e009aed3ca6 django/core/mail/backends/filebased.py
- + 1 """Email backend that writes messages to a file.""" 2 3 import datetime 4 import os 5 import threading 6 7 from django.conf import settings 8 from django.core.exceptions import ImproperlyConfigured 9 from django.core.mail.backends.base import BaseEmailBackend 10 11 12 class EmailBackend(BaseEmailBackend): 13 14 def __init__(self, *args, **kwds): 15 self.stream = None 16 self._lock = threading.RLock() 17 self._counter = 0 18 self._fname = None 19 if 'file_path' in kwds: 20 self.outdir = kwds.pop('file_path') 21 else: 22 self.outdir = settings.EMAIL_FILE_PATH 23 # Make sure self.outdir is a string. 24 if not isinstance(self.outdir, basestring): 25 raise ImproperlyConfigured('Path for saving emails is invalid: %r' % self.outdir) 26 self.outdir = os.path.abspath(self.outdir) 27 # Make sure that self.outdir is an directory if it exists. 28 if os.path.exists(self.outdir) and not os.path.isdir(self.outdir): 29 raise ImproperlyConfigured('Path for saving email messages exists, but is not a directory: %s' % self.outdir) 30 # Try to create it, if it not exists. 31 elif not os.path.exists(self.outdir): 32 try: 33 os.makedirs(self.outdir) 34 except OSError, err: 35 raise ImproperlyConfigured('Could not create directory for saving email messages: %s (%s)' % (self.outdir, err)) 36 # Make sure that self.outdir is writable. 37 if not os.access(self.outdir, os.W_OK): 38 raise ImproperlyConfigured('Could not write to directory: %s' % self.outdir) 39 # Finally, call super(). 40 super(EmailBackend, self).__init__(*args, **kwds) 41 42 def _get_filename(self): 43 """Return a unique file name.""" 44 if self._fname is None: 45 timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") 46 fname = "%s-%s.log" % (timestamp, abs(id(self))) 47 self._fname = os.path.join(self.outdir, fname) 48 return self._fname 49 50 def open(self): 51 if self.stream is None: 52 self.stream = open(self._get_filename(), 'a') 53 return True 54 return False 55 56 def close(self): 57 try: 58 if self.stream is not None: 59 self.stream.close() 60 finally: 61 self.stream = None 62 63 def send_messages(self, email_messages): 64 """Write all messages to the file in a thread-safe way.""" 65 if not email_messages: 66 return 67 self._lock.acquire() 68 try: 69 stream_created = self.open() 70 for message in email_messages: 71 self.stream.write('%s\n' % message.message().as_string()) 72 self.stream.write('-'*79) 73 self.stream.write('\n') 74 self.stream.flush() # flush after each message 75 if stream_created: 76 self.close() 77 except: 78 if not self.fail_silently: 79 raise 80 finally: 81 self._lock.release() 82 return len(email_messages) -
new file django/core/mail/backends/locmem.py
diff -r 4e009aed3ca6 django/core/mail/backends/locmem.py
- + 1 """ 2 Backend for test environment. 3 """ 4 5 from django.core import mail 6 from django.core.mail.backends.base import BaseEmailBackend 7 8 9 class EmailBackend(BaseEmailBackend): 10 """A email backend for use during test sessions. 11 12 The test connection stores email messages in a dummy outbox, 13 rather than sending them out on the wire. 14 15 The dummy outbox is accessible through the outbox instance attribute. 16 """ 17 18 def __init__(self, *args, **kwds): 19 super(EmailBackend, self).__init__(*args, **kwds) 20 if not hasattr(mail, 'outbox'): 21 mail.outbox = [] 22 23 def send_messages(self, messages): 24 """Redirect messages to the dummy outbox""" 25 mail.outbox.extend(messages) 26 return len(messages) -
new file django/core/mail/backends/smtp.py
diff -r 4e009aed3ca6 django/core/mail/backends/smtp.py
- + 1 """SMTP email backend class.""" 2 3 import smtplib 4 import socket 5 import threading 6 7 from django.conf import settings 8 from django.core.mail.backends.base import BaseEmailBackend 9 from django.core.mail.utils import DNS_NAME 10 11 12 class EmailBackend(BaseEmailBackend): 13 """ 14 A wrapper that manages the SMTP network connection. 15 """ 16 17 def __init__(self, host=None, port=None, username=None, password=None, 18 use_tls=None, fail_silently=False, **kwds): 19 super(EmailBackend, self).__init__(fail_silently=fail_silently) 20 self.host = host or settings.EMAIL_HOST 21 self.port = port or settings.EMAIL_PORT 22 self.username = username or settings.EMAIL_HOST_USER 23 self.password = password or settings.EMAIL_HOST_PASSWORD 24 self.use_tls = (use_tls is not None) and use_tls or settings.EMAIL_USE_TLS 25 self.connection = None 26 self._lock = threading.RLock() 27 28 def open(self): 29 """ 30 Ensures we have a connection to the email server. Returns whether or 31 not a new connection was required (True or False). 32 """ 33 if self.connection: 34 # Nothing to do if the connection is already open. 35 return False 36 try: 37 # If local_hostname is not specified, socket.getfqdn() gets used. 38 # For performance, we use the cached FQDN for local_hostname. 39 self.connection = smtplib.SMTP(self.host, self.port, 40 local_hostname=DNS_NAME.get_fqdn()) 41 if self.use_tls: 42 self.connection.ehlo() 43 self.connection.starttls() 44 self.connection.ehlo() 45 if self.username and self.password: 46 self.connection.login(self.username, self.password) 47 return True 48 except: 49 if not self.fail_silently: 50 raise 51 52 def close(self): 53 """Closes the connection to the email server.""" 54 try: 55 try: 56 self.connection.quit() 57 except socket.sslerror: 58 # This happens when calling quit() on a TLS connection 59 # sometimes. 60 self.connection.close() 61 except: 62 if self.fail_silently: 63 return 64 raise 65 finally: 66 self.connection = None 67 68 def send_messages(self, email_messages): 69 """ 70 Sends one or more EmailMessage objects and returns the number of email 71 messages sent. 72 """ 73 if not email_messages: 74 return 75 self._lock.acquire() 76 try: 77 new_conn_created = self.open() 78 if not self.connection: 79 # We failed silently on open(). 80 # Trying to send would be pointless. 81 return 82 num_sent = 0 83 for message in email_messages: 84 sent = self._send(message) 85 if sent: 86 num_sent += 1 87 if new_conn_created: 88 self.close() 89 finally: 90 self._lock.release() 91 return num_sent 92 93 def _send(self, email_message): 94 """A helper method that does the actual sending.""" 95 if not email_message.recipients(): 96 return False 97 try: 98 self.connection.sendmail(email_message.from_email, 99 email_message.recipients(), 100 email_message.message().as_string()) 101 except: 102 if not self.fail_silently: 103 raise 104 return False 105 return True -
new file django/core/mail/message.py
diff -r 4e009aed3ca6 django/core/mail/message.py
- + 1 import mimetypes 2 import os 3 import random 4 import time 5 from email import Charset, Encoders 6 from email.MIMEText import MIMEText 7 from email.MIMEMultipart import MIMEMultipart 8 from email.MIMEBase import MIMEBase 9 from email.Header import Header 10 from email.Utils import formatdate, parseaddr, formataddr 11 12 from django.conf import settings 13 from django.core.mail.utils import DNS_NAME 14 from django.utils.encoding import smart_str, force_unicode 15 16 # Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from 17 # some spam filters. 18 Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8') 19 20 # Default MIME type to use on attachments (if it is not explicitly given 21 # and cannot be guessed). 22 DEFAULT_ATTACHMENT_MIME_TYPE = 'application/octet-stream' 23 24 25 class BadHeaderError(ValueError): 26 pass 27 28 29 # Copied from Python standard library, with the following modifications: 30 # * Used cached hostname for performance. 31 # * Added try/except to support lack of getpid() in Jython (#5496). 32 def make_msgid(idstring=None): 33 """Returns a string suitable for RFC 2822 compliant Message-ID, e.g: 34 35 <20020201195627.33539.96671@nightshade.la.mastaler.com> 36 37 Optional idstring if given is a string used to strengthen the 38 uniqueness of the message id. 39 """ 40 timeval = time.time() 41 utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval)) 42 try: 43 pid = os.getpid() 44 except AttributeError: 45 # No getpid() in Jython, for example. 46 pid = 1 47 randint = random.randrange(100000) 48 if idstring is None: 49 idstring = '' 50 else: 51 idstring = '.' + idstring 52 idhost = DNS_NAME 53 msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost) 54 return msgid 55 56 57 def forbid_multi_line_headers(name, val): 58 """Forbids multi-line headers, to prevent header injection.""" 59 val = force_unicode(val) 60 if '\n' in val or '\r' in val: 61 raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (val, name)) 62 try: 63 val = val.encode('ascii') 64 except UnicodeEncodeError: 65 if name.lower() in ('to', 'from', 'cc'): 66 result = [] 67 for item in val.split(', '): 68 nm, addr = parseaddr(item) 69 nm = str(Header(nm, settings.DEFAULT_CHARSET)) 70 result.append(formataddr((nm, str(addr)))) 71 val = ', '.join(result) 72 else: 73 val = Header(val, settings.DEFAULT_CHARSET) 74 else: 75 if name.lower() == 'subject': 76 val = Header(val) 77 return name, val 78 79 80 class SafeMIMEText(MIMEText): 81 def __setitem__(self, name, val): 82 name, val = forbid_multi_line_headers(name, val) 83 MIMEText.__setitem__(self, name, val) 84 85 86 class SafeMIMEMultipart(MIMEMultipart): 87 def __setitem__(self, name, val): 88 name, val = forbid_multi_line_headers(name, val) 89 MIMEMultipart.__setitem__(self, name, val) 90 91 92 class EmailMessage(object): 93 """ 94 A container for email information. 95 """ 96 content_subtype = 'plain' 97 mixed_subtype = 'mixed' 98 encoding = None # None => use settings default 99 100 def __init__(self, subject='', body='', from_email=None, to=None, bcc=None, 101 connection=None, attachments=None, headers=None): 102 """ 103 Initialize a single email message (which can be sent to multiple 104 recipients). 105 106 All strings used to create the message can be unicode strings 107 (or UTF-8 bytestrings). The SafeMIMEText class will handle any 108 necessary encoding conversions. 109 """ 110 if to: 111 assert not isinstance(to, basestring), '"to" argument must be a list or tuple' 112 self.to = list(to) 113 else: 114 self.to = [] 115 if bcc: 116 assert not isinstance(bcc, basestring), '"bcc" argument must be a list or tuple' 117 self.bcc = list(bcc) 118 else: 119 self.bcc = [] 120 self.from_email = from_email or settings.DEFAULT_FROM_EMAIL 121 self.subject = subject 122 self.body = body 123 self.attachments = attachments or [] 124 self.extra_headers = headers or {} 125 self.connection = connection 126 127 def get_connection(self, fail_silently=False): 128 from django.core.mail import get_connection 129 if not self.connection: 130 self.connection = get_connection(fail_silently=fail_silently) 131 return self.connection 132 133 def message(self): 134 encoding = self.encoding or settings.DEFAULT_CHARSET 135 msg = SafeMIMEText(smart_str(self.body, settings.DEFAULT_CHARSET), 136 self.content_subtype, encoding) 137 msg = self._create_message(msg) 138 msg['Subject'] = self.subject 139 msg['From'] = self.extra_headers.pop('From', self.from_email) 140 msg['To'] = ', '.join(self.to) 141 142 # Email header names are case-insensitive (RFC 2045), so we have to 143 # accommodate that when doing comparisons. 144 header_names = [key.lower() for key in self.extra_headers] 145 if 'date' not in header_names: 146 msg['Date'] = formatdate() 147 if 'message-id' not in header_names: 148 msg['Message-ID'] = make_msgid() 149 for name, value in self.extra_headers.items(): 150 msg[name] = value 151 return msg 152 153 def recipients(self): 154 """ 155 Returns a list of all recipients of the email (includes direct 156 addressees as well as Bcc entries). 157 """ 158 return self.to + self.bcc 159 160 def send(self, fail_silently=False): 161 """Sends the email message.""" 162 if not self.recipients(): 163 # Don't bother creating the network connection if there's nobody to 164 # send to. 165 return 0 166 return self.get_connection(fail_silently).send_messages([self]) 167 168 def attach(self, filename=None, content=None, mimetype=None): 169 """ 170 Attaches a file with the given filename and content. The filename can 171 be omitted and the mimetype is guessed, if not provided. 172 173 If the first parameter is a MIMEBase subclass it is inserted directly 174 into the resulting message attachments. 175 """ 176 if isinstance(filename, MIMEBase): 177 assert content == mimetype == None 178 self.attachments.append(filename) 179 else: 180 assert content is not None 181 self.attachments.append((filename, content, mimetype)) 182 183 def attach_file(self, path, mimetype=None): 184 """Attaches a file from the filesystem.""" 185 filename = os.path.basename(path) 186 content = open(path, 'rb').read() 187 self.attach(filename, content, mimetype) 188 189 def _create_message(self, msg): 190 return self._create_attachments(msg) 191 192 def _create_attachments(self, msg): 193 if self.attachments: 194 body_msg = msg 195 msg = SafeMIMEMultipart(_subtype=self.mixed_subtype) 196 if self.body: 197 msg.attach(body_msg) 198 for attachment in self.attachments: 199 if isinstance(attachment, MIMEBase): 200 msg.attach(attachment) 201 else: 202 msg.attach(self._create_attachment(*attachment)) 203 return msg 204 205 def _create_mime_attachment(self, content, mimetype): 206 """ 207 Converts the content, mimetype pair into a MIME attachment object. 208 """ 209 basetype, subtype = mimetype.split('/', 1) 210 if basetype == 'text': 211 attachment = SafeMIMEText(smart_str(content, 212 settings.DEFAULT_CHARSET), subtype, settings.DEFAULT_CHARSET) 213 else: 214 # Encode non-text attachments with base64. 215 attachment = MIMEBase(basetype, subtype) 216 attachment.set_payload(content) 217 Encoders.encode_base64(attachment) 218 return attachment 219 220 def _create_attachment(self, filename, content, mimetype=None): 221 """ 222 Converts the filename, content, mimetype triple into a MIME attachment 223 object. 224 """ 225 if mimetype is None: 226 mimetype, _ = mimetypes.guess_type(filename) 227 if mimetype is None: 228 mimetype = DEFAULT_ATTACHMENT_MIME_TYPE 229 attachment = self._create_mime_attachment(content, mimetype) 230 if filename: 231 attachment.add_header('Content-Disposition', 'attachment', 232 filename=filename) 233 return attachment 234 235 236 class EmailMultiAlternatives(EmailMessage): 237 """ 238 A version of EmailMessage that makes it easy to send multipart/alternative 239 messages. For example, including text and HTML versions of the text is 240 made easier. 241 """ 242 alternative_subtype = 'alternative' 243 244 def __init__(self, subject='', body='', from_email=None, to=None, bcc=None, 245 connection=None, attachments=None, headers=None, alternatives=None): 246 """ 247 Initialize a single email message (which can be sent to multiple 248 recipients). 249 250 All strings used to create the message can be unicode strings (or UTF-8 251 bytestrings). The SafeMIMEText class will handle any necessary encoding 252 conversions. 253 """ 254 super(EmailMultiAlternatives, self).__init__(subject, body, from_email, to, bcc, connection, attachments, headers) 255 self.alternatives=alternatives or [] 256 257 def attach_alternative(self, content, mimetype): 258 """Attach an alternative content representation.""" 259 assert content is not None 260 assert mimetype is not None 261 self.alternatives.append((content, mimetype)) 262 263 def _create_message(self, msg): 264 return self._create_attachments(self._create_alternatives(msg)) 265 266 def _create_alternatives(self, msg): 267 if self.alternatives: 268 body_msg = msg 269 msg = SafeMIMEMultipart(_subtype=self.alternative_subtype) 270 if self.body: 271 msg.attach(body_msg) 272 for alternative in self.alternatives: 273 msg.attach(self._create_mime_attachment(*alternative)) 274 return msg -
new file django/core/mail/utils.py
diff -r 4e009aed3ca6 django/core/mail/utils.py
- + 1 """ 2 Email message and email sending related helper functions. 3 """ 4 5 import socket 6 7 8 # Cache the hostname, but do it lazily: socket.getfqdn() can take a couple of 9 # seconds, which slows down the restart of the server. 10 class CachedDnsName(object): 11 def __str__(self): 12 return self.get_fqdn() 13 14 def get_fqdn(self): 15 if not hasattr(self, '_fqdn'): 16 self._fqdn = socket.getfqdn() 17 return self._fqdn 18 19 DNS_NAME = CachedDnsName() -
django/test/utils.py
diff -r 4e009aed3ca6 django/test/utils.py
a b 28 28 signals.template_rendered.send(sender=self, template=self, context=context) 29 29 return self.nodelist.render(context) 30 30 31 class TestSMTPConnection(object):32 """A substitute SMTP connection for use during test sessions.33 The test connection stores email messages in a dummy outbox,34 rather than sending them out on the wire.35 36 """37 def __init__(*args, **kwargs):38 pass39 def open(self):40 "Mock the SMTPConnection open() interface"41 pass42 def close(self):43 "Mock the SMTPConnection close() interface"44 pass45 def send_messages(self, messages):46 "Redirect messages to the dummy outbox"47 mail.outbox.extend(messages)48 return len(messages)49 31 50 32 def setup_test_environment(): 51 33 """Perform any global pre-test setup. This involves: 52 34 53 35 - Installing the instrumented test renderer 54 - Diverting the email sending functions to a test buffer36 - Replacing the email backend with the email backend used for testing. 55 37 - Setting the active locale to match the LANGUAGE_CODE setting. 56 38 """ 39 from django.core.mail.backends.locmem import EmailBackend 57 40 Template.original_render = Template.render 58 41 Template.render = instrumented_test_render 59 42 60 mail.original_SMTPConnection = mail.SMTPConnection 61 mail.SMTPConnection = TestSMTPConnection 62 63 mail.outbox = [] 43 mail.original_email_backend = settings.EMAIL_BACKEND 44 settings.EMAIL_BACKEND = 'locmem' 45 mail.original_get_connection = mail.get_connection 46 mail.get_connection = lambda *a, **k: EmailBackend() 47 mail.original_smtp_connection = mail.SMTPConnection 48 mail.SMTPConnection = lambda *a, **k: EmailBackend() 49 if not hasattr(mail, 'outbox'): 50 mail.outbox = [] 64 51 65 52 deactivate() 66 53 … … 74 61 Template.render = Template.original_render 75 62 del Template.original_render 76 63 77 mail.SMTPConnection = mail.original_SMTPConnection 78 del mail.original_SMTPConnection 64 settings.EMAIL_BACKEND = mail.original_email_backend 65 del mail.original_email_backend 66 mail.get_connection = mail.original_get_connection 67 del mail.original_get_connection 68 mail.SMTPConnection = mail.original_smtp_connection 69 del mail.original_smtp_connection 79 70 80 del mail.outbox81 71 if hasattr(mail, 'outbox'): 72 mail.outbox = [] 82 73 83 74 def get_runner(settings): 84 75 test_path = settings.TEST_RUNNER.split('.') -
docs/ref/settings.txt
diff -r 4e009aed3ca6 docs/ref/settings.txt
a b 379 379 This is only used if ``CommonMiddleware`` is installed (see 380 380 :ref:`topics-http-middleware`). 381 381 382 .. setting:: EMAIL_BACKEND 383 384 EMAIL_BACKEND 385 ------------- 386 387 .. versionadded:: 1.2 388 389 Default: ``'smtp'`` 390 391 The backend to use for sending emails. For available backend see 392 :ref:`topics-email`. 393 394 .. setting:: EMAIL_FILE_PATH 395 396 EMAIL_FILE_PATH 397 --------------- 398 399 .. versionadded:: 1.2 400 401 Default: ``None`` 402 403 The directory to which the files are written by the 'file' email backend. 404 382 405 .. setting:: EMAIL_HOST 383 406 384 407 EMAIL_HOST -
docs/topics/email.txt
diff -r 4e009aed3ca6 docs/topics/email.txt
a b 178 178 179 179 .. _emailmessage-and-smtpconnection: 180 180 181 The EmailMessage and SMTPConnection classes182 ====================== =====================181 The EmailMessage class 182 ====================== 183 183 184 184 .. versionadded:: 1.0 185 185 186 186 Django's ``send_mail()`` and ``send_mass_mail()`` functions are actually thin 187 wrappers that make use of the ``EmailMessage`` and ``SMTPConnection`` classes 188 in ``django.core.mail``. If you ever need to customize the way Django sends 189 e-mail, you can subclass these two classes to suit your needs. 187 wrappers that make use of the ``EmailMessage`` class in ``django.core.mail``. 188 189 Not all features of the ``EmailMessage`` class are available through the 190 ``send_mail()`` and related wrapper functions. If you wish to use advanced 191 features, such as BCC'ed recipients, file attachments, or multi-part 192 e-mail, you'll need to create ``EmailMessage`` instances directly. 190 193 191 194 .. note:: 192 Not all features of the ``EmailMessage`` class are available through the193 ``send_mail()`` and related wrapper functions. If you wish to use advanced194 features, such as BCC'ed recipients, file attachments, or multi-part195 e-mail, you'll need to create ``EmailMessage`` instances directly.196 195 197 196 This is a design feature. ``send_mail()`` and related functions were 198 197 originally the only interface Django provided. However, the list of … … 201 200 original functions only for backwards compatibility. 202 201 203 202 In general, ``EmailMessage`` is responsible for creating the e-mail message 204 itself. ``SMTPConnection`` is responsible for the network connection side of 205 the operation. This means you can reuse the same connection (an 206 ``SMTPConnection`` instance) for multiple messages. 203 itself. The e-mail backend is responsible for sending the e-mail. By default 204 the SMTP backend is used to send e-mails using a SMTP server. 205 206 If you're sending lots of messages at once, the ``send_messages()`` method of 207 the e-mail backend is useful. It takes a list of ``EmailMessage`` 208 instances (or subclasses) and sends them over a single connection. For example, 209 if you have a function called ``get_notification_email()`` that returns a 210 list of ``EmailMessage`` objects representing some periodic e-mail you wish to 211 send out, you could send this with:: 212 213 connection = SMTPConnection() # Use default settings for connection 214 messages = get_notification_email() 215 connection.send_messages(messages) 216 217 If you want to resuse the same connection for multiple messages, but can't 218 use ``send_messages()``, you'll have to use the ``open()`` and ``close()`` 219 methods of the e-mail backend:: 220 221 from django.core import mail 222 223 connection = mail.get_connection() 224 connection.open() 225 email1 = mail.EmailMessage('Hello', 'Body goes here', 'from@example.com', 226 ['to1@example.com'], connection=connection) 227 email1.send() 228 email2 = mail.EmailMessage('Hello again', 'Body goes here', 229 'from@example.com', ['to1@example.com'], 230 connection=connection) 231 email2.send() 232 connection.close() 233 207 234 208 235 EmailMessage Objects 209 236 -------------------- … … 227 254 * ``bcc``: A list or tuple of addresses used in the "Bcc" header when 228 255 sending the e-mail. 229 256 230 * ``connection``: An ``SMTPConnection``instance. Use this parameter if257 * ``connection``: An e-mail backend instance. Use this parameter if 231 258 you want to use the same connection for multiple messages. If omitted, a 232 259 new connection is created when ``send()`` is called. 233 260 … … 331 358 msg.content_subtype = "html" # Main content is now text/html 332 359 msg.send() 333 360 334 SMTPConnection Objects 335 ---------------------- 361 362 E-Mail Backends 363 =============== 364 365 .. versionadded:: 1.2 366 367 The actual sending of an e-mail is handled by the e-mail backend. 368 By default the SMTP backend will be used. 369 370 The :meth:`get_connection` function in ``django.core.mail`` returns an 371 instance of the e-mail backend. 372 373 .. currentmodule:: django.core.mail 374 375 .. function:: get_connection(backend=None, fail_silently=False, *args, **kwargs) 376 377 If ``backend`` is ``None`` an instance of the e-mail backend configured 378 in your settings file is returned. Use this parameter to get a different 379 backend than configured in your ``EMAIL_BACKEND`` setting. 380 All other arguments are passed directly to the constructor of the 381 e-mail backend. 382 383 Your e-mail backend preferences goes in the ``EMAIL_BACKEND`` setting in 384 your settings file. Here's an explanation of all available values for 385 ``EMAIL_BACKEND``. 386 387 SMTP backend 388 ------------ 389 390 This is the default backend. E-mail will be sent through a SMTP server. 391 The server address and authentication credentials are set in the 392 :setting:`EMAIL_HOST`, :setting:`EMAIL_POST`, :setting:`EMAIL_HOST_USER`, 393 :setting:`EMAIL_HOST_PASSWORD` and :setting:`EMAIL_USE_TLS` settings in your 394 settings file. 395 396 The SMTP backend is configured in the default settings:: 397 398 EMAIL_BACKEND = 'smtp' 399 400 401 SMTPConnection objects 402 ~~~~~~~~~~~~~~~~~~~~~~ 403 404 .. deprecated:: 1.2 405 406 ``mail.SMTPConnection`` is deprecated, use 407 :meth:`~django.core.mail.get_connection` instead. 408 For backwards compatibility ``SMTPConnection`` is still available in 409 ``django.core.mail`` as an alias for the SMTP backend. 336 410 337 411 .. class:: SMTPConnection 338 412 … … 340 414 password for the SMTP server. If you don't specify one or more of those 341 415 options, they are read from your settings file. 342 416 343 If you're sending lots of messages at once, the ``send_messages()`` method of 344 the ``SMTPConnection`` class is useful. It takes a list of ``EmailMessage`` 345 instances (or subclasses) and sends them over a single connection. For example, 346 if you have a function called ``get_notification_email()`` that returns a 347 list of ``EmailMessage`` objects representing some periodic e-mail you wish to 348 send out, you could send this with:: 417 Console backend (for development) 418 --------------------------------- 349 419 350 connection = SMTPConnection() # Use default settings for connection 351 messages = get_notification_email() 352 connection.send_messages(messages) 420 Instead of sending out real e-mails the console backend just writes the e-mails 421 that would be send to the standard output. To use this backend, use 422 in your settings:: 353 423 354 Testing e-mail sending 355 ---------------------- 424 EMAIL_BACKEND = 'console' 425 426 File backend (for development) 427 ------------------------------ 428 429 The file backend writes e-mails to a file. A new file is created for each 430 new instance of this backend. The directory to which the files are written 431 is either taken from the :setting:`EMAIL_FILE_PATH` setting or from the 432 ``file_path`` keyword when creating a connection with ``get_connection()``. 433 To use this backend, use in your settings:: 434 435 EMAIL_BACKEND = 'file' 436 EMAIL_FILE_PATH = '/tmp/app-messages' # change this to a proper location 437 438 In-memory backend (for development and testing) 439 ----------------------------------------------- 440 441 The ``'locmem'`` backend stores messages in a special attribute of the 442 ``django.core.mail`` module. The ``outbox`` attribute is created when the 443 first message is send. It's a list with an ``EmailMessage`` instance for 444 each message that would be send. To use this backend, use in your settings:: 445 446 EMAIL_BACKEND = 'locmem' 447 448 This backend is used as a drop-in replacement for the original e-mail 449 backend configured in your settings file by the 450 :ref:`testing framework <topics-testing-email>`. 451 452 Dummy backend (for development) 453 ------------------------------- 454 455 As the name suggests the dummy backend does nothing with your messages. 456 To enable this backend, use in your settings file:: 457 458 EMAIL_BACKEND = 'dummy' 459 460 This backend can be useful during development, if you even want to suppress 461 the output produced by the console backend. 462 463 Using a custom e-mail backend 464 ----------------------------- 465 466 If you need to change how e-mails are send you can write your own e-mail 467 backend. The ``EMAIL_BACKEND`` setting in your settings file is then the 468 Python import path for your backend. 469 470 Custom e-mail backends should subclass ``BaseEmailBackend`` that is located 471 in the ``django.core.mail.backends.base`` module. A custom e-mail 472 backend must implement the ``send_messages(email_messages)`` method. This 473 method receives a list of ``EMailMessage`` instances and returns the number 474 of successfully delivered messages. If your backend should be capable of 475 network connection handling, you should implement both the ``open()`` 476 and ``close()`` methods too. Refer to ``SMTPEmailBackend`` for a reference 477 implementation. 478 479 Testing E-mail Sending 480 ====================== 356 481 357 482 The are times when you do not want Django to send e-mails at all. For example, 358 483 while developing a website, you probably don't want to send out thousands of … … 360 485 people under the right conditions, and that those e-mails will contain the 361 486 correct content. 362 487 363 The easiest way to test your project's use of e-mail is to use a "dumb" e-mail 364 server that receives the e-mails locally and displays them to the terminal, 365 but does not actually send anything. Python has a built-in way to accomplish 366 this with a single command:: 488 The easiest way to test your project's use of e-mail is to change the 489 ``EMAIL_BACKEND`` setting in your settings file to ``'console'`` to use 490 a backend that doesn't send any e-mail but writes them to the 491 terminal. 492 493 Alternatively use a "dumb" e-mail server that receives the e-mails locally 494 and displays them to the terminal, but does not actually send anything. 495 Python has a built-in way to accomplish this with a single command:: 367 496 368 497 python -m smtpd -n -c DebuggingServer localhost:1025 369 498 370 499 This command will start a simple SMTP server listening on port 1025 of 371 localhost. This server simply prints to standard output all e mail headers and372 the e mail body. You then only need to set the :setting:`EMAIL_HOST` and500 localhost. This server simply prints to standard output all e-mail headers and 501 the e-mail body. You then only need to set the :setting:`EMAIL_HOST` and 373 502 :setting:`EMAIL_PORT` accordingly, and you are set. 374 503 375 504 For more entailed testing and processing of e-mails locally, see the Python -
docs/topics/testing.txt
diff -r 4e009aed3ca6 docs/topics/testing.txt
a b 1104 1104 ``target_status_code`` will be the url and status code for the final 1105 1105 point of the redirect chain. 1106 1106 1107 .. _topics-testing-email: 1108 1107 1109 E-mail services 1108 1110 --------------- 1109 1111 … … 1117 1119 contents of each message -- without actually sending the messages. 1118 1120 1119 1121 The test runner accomplishes this by transparently replacing the normal 1120 :class:`~django.core.mail.SMTPConnection` class with a different version.1122 email backend with a testing backend. 1121 1123 (Don't worry -- this has no effect on any other e-mail senders outside of 1122 1124 Django, such as your machine's mail server, if you're running one.) 1123 1125 … … 1129 1131 ``django.core.mail.outbox``. This is a simple list of all 1130 1132 :class:`<~django.core.mail.EmailMessage>` instances that have been sent. 1131 1133 It does not exist under normal execution conditions, i.e., when you're not 1132 running unit tests. The outbox is created during test setup, along with the 1133 dummy :class:`<~django.core.mail.SMTPConnection>`. When the test framework is 1134 torn down, the standard :class:`<~django.core.mail.SMTPConnection>` class is 1134 running unit tests. The outbox is created during test setup. 1135 When the test framework is torn down, the standard mail backend 1135 1136 restored, and the test outbox is destroyed. 1136 1137 1137 1138 The ``outbox`` attribute is a special attribute that is created *only* when -
new file tests/regressiontests/mail/custombackend.py
diff -r 4e009aed3ca6 tests/regressiontests/mail/custombackend.py
- + 1 """A custom backend for testing.""" 2 3 from django.core.mail.backends.base import BaseEmailBackend 4 5 6 class EmailBackend(BaseEmailBackend): 7 8 def __init__(self, *args, **kwargs): 9 super(EmailBackend, self).__init__(*args, **kwargs) 10 self.test_outbox = [] 11 12 def send_messages(self, email_messages): 13 # Messages are stored in a instance variable for testing. 14 self.test_outbox.extend(email_messages) 15 return len(email_messages) -
tests/regressiontests/mail/tests.py
diff -r 4e009aed3ca6 tests/regressiontests/mail/tests.py
a b 1 1 # coding: utf-8 2 2 3 r""" 3 4 # Tests for the django.core.mail. 4 5 6 >>> import os 7 >>> import shutil 8 >>> import tempfile 5 9 >>> from django.conf import settings 6 10 >>> from django.core import mail 7 11 >>> from django.core.mail import EmailMessage, mail_admins, mail_managers, EmailMultiAlternatives 12 >>> from django.core.mail.backends.base import BaseEmailBackend 13 >>> from django.core.mail.backends import console, dummy, locmem, filebased, smtp 8 14 >>> from django.utils.translation import ugettext_lazy 9 15 10 16 # Test normal ascii character case: … … 138 144 JVBERi0xLjQuJS4uLg== 139 145 ... 140 146 147 # Make sure that the console backend writes to stdout 148 >>> connection = console.EmailBackend() 149 >>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) 150 >>> connection.send_messages([email]) 151 Content-Type: text/plain; charset="utf-8" 152 MIME-Version: 1.0 153 Content-Transfer-Encoding: quoted-printable 154 Subject: Subject 155 From: from@example.com 156 To: to@example.com 157 Date: ... 158 Message-ID: ... 159 160 Content 161 ... 162 1 163 164 # Make sure that dummy backends returns correct number of sent messages 165 >>> connection = dummy.EmailBackend() 166 >>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) 167 >>> connection.send_messages([email, email, email]) 168 3 169 170 # Make sure that locmen backend populates the outbox 171 >>> mail.outbox = [] 172 >>> connection = locmem.EmailBackend() 173 >>> email1 = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) 174 >>> email2 = EmailMessage('Subject 2', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) 175 >>> connection.send_messages([email1, email2]) 176 2 177 >>> len(mail.outbox) 178 2 179 >>> mail.outbox[0].subject 180 'Subject' 181 >>> mail.outbox[1].subject 182 'Subject 2' 183 184 # Make sure that multiple locmem connections share mail.outbox 185 >>> mail.outbox = [] 186 >>> connection1 = locmem.EmailBackend() 187 >>> connection2 = locmem.EmailBackend() 188 >>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) 189 >>> connection1.send_messages([email]) 190 1 191 >>> connection2.send_messages([email]) 192 1 193 >>> len(mail.outbox) 194 2 195 196 # Make sure that the file backend write to the right location 197 >>> tmp_dir = tempfile.mkdtemp() 198 >>> connection = filebased.EmailBackend(file_path=tmp_dir) 199 >>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) 200 >>> connection.send_messages([email]) 201 1 202 >>> len(os.listdir(tmp_dir)) 203 1 204 >>> print open(os.path.join(tmp_dir, os.listdir(tmp_dir)[0])).read() 205 Content-Type: text/plain; charset="utf-8" 206 MIME-Version: 1.0 207 Content-Transfer-Encoding: quoted-printable 208 Subject: Subject 209 From: from@example.com 210 To: to@example.com 211 Date: ... 212 Message-ID: ... 213 214 Content 215 ... 216 >>> connection2 = filebased.EmailBackend(file_path=tmp_dir) 217 >>> connection2.send_messages([email]) 218 1 219 >>> len(os.listdir(tmp_dir)) 220 2 221 >>> connection.send_messages([email]) 222 1 223 >>> len(os.listdir(tmp_dir)) 224 2 225 >>> email.connection = filebased.EmailBackend(file_path=tmp_dir) 226 >>> connection_created = connection.open() 227 >>> num_sent = email.send() 228 >>> len(os.listdir(tmp_dir)) 229 3 230 >>> num_sent = email.send() 231 >>> len(os.listdir(tmp_dir)) 232 3 233 >>> connection.close() 234 >>> shutil.rmtree(tmp_dir) 235 236 # Make sure that get_connection() accepts arbitrary keyword that might be 237 # used with custom backends. 238 # Note: mail.get_connection() is patched when the test environment is set 239 # up, we have to use mail.original_get_connection() instead. 240 >>> c = mail.original_get_connection(fail_silently=True, foo='bar') 241 >>> c.fail_silently 242 True 243 244 # Test custom backend defined in this suite. 245 >>> conn = mail.original_get_connection('regressiontests.mail.custombackend') 246 >>> hasattr(conn, 'test_outbox') 247 True 248 >>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) 249 >>> conn.send_messages([email]) 250 1 251 >>> len(conn.test_outbox) 252 1 253 254 # Test backend argument of mail.get_connection() 255 >>> isinstance(mail.original_get_connection('smtp'), smtp.EmailBackend) 256 True 257 >>> isinstance(mail.original_get_connection('locmem'), locmem.EmailBackend) 258 True 259 >>> isinstance(mail.original_get_connection('dummy'), dummy.EmailBackend) 260 True 261 >>> isinstance(mail.original_get_connection('console'), console.EmailBackend) 262 True 263 >>> tmp_dir = tempfile.mkdtemp() 264 >>> isinstance(mail.original_get_connection('file', file_path=tmp_dir), filebased.EmailBackend) 265 True 266 >>> shutil.rmtree(tmp_dir) 267 >>> settings.EMAIL_FILE_PATH = None 268 >>> isinstance(mail.get_connection(), locmem.EmailBackend) 269 True 270 141 271 """