web/lib/django/core/mail/message.py
changeset 29 cc9b7e14412b
equal deleted inserted replaced
28:b758351d191f 29:cc9b7e14412b
       
     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, getaddresses, 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, encoding):
       
    58     """Forbids multi-line headers, to prevent header injection."""
       
    59     encoding = encoding or settings.DEFAULT_CHARSET
       
    60     val = force_unicode(val)
       
    61     if '\n' in val or '\r' in val:
       
    62         raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (val, name))
       
    63     try:
       
    64         val = val.encode('ascii')
       
    65     except UnicodeEncodeError:
       
    66         if name.lower() in ('to', 'from', 'cc'):
       
    67             result = []
       
    68             for nm, addr in getaddresses((val,)):
       
    69                 nm = str(Header(nm.encode(encoding), encoding))
       
    70                 result.append(formataddr((nm, str(addr))))
       
    71             val = ', '.join(result)
       
    72         else:
       
    73             val = Header(val.encode(encoding), encoding)
       
    74     else:
       
    75         if name.lower() == 'subject':
       
    76             val = Header(val)
       
    77     return name, val
       
    78 
       
    79 class SafeMIMEText(MIMEText):
       
    80     
       
    81     def __init__(self, text, subtype, charset):
       
    82         self.encoding = charset
       
    83         MIMEText.__init__(self, text, subtype, charset)
       
    84     
       
    85     def __setitem__(self, name, val):    
       
    86         name, val = forbid_multi_line_headers(name, val, self.encoding)
       
    87         MIMEText.__setitem__(self, name, val)
       
    88 
       
    89 class SafeMIMEMultipart(MIMEMultipart):
       
    90     
       
    91     def __init__(self, _subtype='mixed', boundary=None, _subparts=None, encoding=None, **_params):
       
    92         self.encoding = encoding
       
    93         MIMEMultipart.__init__(self, _subtype, boundary, _subparts, **_params)
       
    94         
       
    95     def __setitem__(self, name, val):
       
    96         name, val = forbid_multi_line_headers(name, val, self.encoding)
       
    97         MIMEMultipart.__setitem__(self, name, val)
       
    98 
       
    99 class EmailMessage(object):
       
   100     """
       
   101     A container for email information.
       
   102     """
       
   103     content_subtype = 'plain'
       
   104     mixed_subtype = 'mixed'
       
   105     encoding = None     # None => use settings default
       
   106 
       
   107     def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
       
   108                  connection=None, attachments=None, headers=None):
       
   109         """
       
   110         Initialize a single email message (which can be sent to multiple
       
   111         recipients).
       
   112 
       
   113         All strings used to create the message can be unicode strings
       
   114         (or UTF-8 bytestrings). The SafeMIMEText class will handle any
       
   115         necessary encoding conversions.
       
   116         """
       
   117         if to:
       
   118             assert not isinstance(to, basestring), '"to" argument must be a list or tuple'
       
   119             self.to = list(to)
       
   120         else:
       
   121             self.to = []
       
   122         if bcc:
       
   123             assert not isinstance(bcc, basestring), '"bcc" argument must be a list or tuple'
       
   124             self.bcc = list(bcc)
       
   125         else:
       
   126             self.bcc = []
       
   127         self.from_email = from_email or settings.DEFAULT_FROM_EMAIL
       
   128         self.subject = subject
       
   129         self.body = body
       
   130         self.attachments = attachments or []
       
   131         self.extra_headers = headers or {}
       
   132         self.connection = connection
       
   133 
       
   134     def get_connection(self, fail_silently=False):
       
   135         from django.core.mail import get_connection
       
   136         if not self.connection:
       
   137             self.connection = get_connection(fail_silently=fail_silently)
       
   138         return self.connection
       
   139 
       
   140     def message(self):
       
   141         encoding = self.encoding or settings.DEFAULT_CHARSET
       
   142         msg = SafeMIMEText(smart_str(self.body, encoding),
       
   143                            self.content_subtype, encoding)
       
   144         msg = self._create_message(msg)
       
   145         msg['Subject'] = self.subject
       
   146         msg['From'] = self.extra_headers.get('From', self.from_email)
       
   147         msg['To'] = ', '.join(self.to)
       
   148 
       
   149         # Email header names are case-insensitive (RFC 2045), so we have to
       
   150         # accommodate that when doing comparisons.
       
   151         header_names = [key.lower() for key in self.extra_headers]
       
   152         if 'date' not in header_names:
       
   153             msg['Date'] = formatdate()
       
   154         if 'message-id' not in header_names:
       
   155             msg['Message-ID'] = make_msgid()
       
   156         for name, value in self.extra_headers.items():
       
   157             if name.lower() == 'from':  # From is already handled
       
   158                 continue
       
   159             msg[name] = value
       
   160         return msg
       
   161 
       
   162     def recipients(self):
       
   163         """
       
   164         Returns a list of all recipients of the email (includes direct
       
   165         addressees as well as Bcc entries).
       
   166         """
       
   167         return self.to + self.bcc
       
   168 
       
   169     def send(self, fail_silently=False):
       
   170         """Sends the email message."""
       
   171         if not self.recipients():
       
   172             # Don't bother creating the network connection if there's nobody to
       
   173             # send to.
       
   174             return 0
       
   175         return self.get_connection(fail_silently).send_messages([self])
       
   176 
       
   177     def attach(self, filename=None, content=None, mimetype=None):
       
   178         """
       
   179         Attaches a file with the given filename and content. The filename can
       
   180         be omitted and the mimetype is guessed, if not provided.
       
   181 
       
   182         If the first parameter is a MIMEBase subclass it is inserted directly
       
   183         into the resulting message attachments.
       
   184         """
       
   185         if isinstance(filename, MIMEBase):
       
   186             assert content == mimetype == None
       
   187             self.attachments.append(filename)
       
   188         else:
       
   189             assert content is not None
       
   190             self.attachments.append((filename, content, mimetype))
       
   191 
       
   192     def attach_file(self, path, mimetype=None):
       
   193         """Attaches a file from the filesystem."""
       
   194         filename = os.path.basename(path)
       
   195         content = open(path, 'rb').read()
       
   196         self.attach(filename, content, mimetype)
       
   197 
       
   198     def _create_message(self, msg):
       
   199         return self._create_attachments(msg)
       
   200 
       
   201     def _create_attachments(self, msg):
       
   202         if self.attachments:
       
   203             encoding = self.encoding or settings.DEFAULT_CHARSET
       
   204             body_msg = msg
       
   205             msg = SafeMIMEMultipart(_subtype=self.mixed_subtype, encoding=encoding)
       
   206             if self.body:
       
   207                 msg.attach(body_msg)
       
   208             for attachment in self.attachments:
       
   209                 if isinstance(attachment, MIMEBase):
       
   210                     msg.attach(attachment)
       
   211                 else:
       
   212                     msg.attach(self._create_attachment(*attachment))
       
   213         return msg
       
   214 
       
   215     def _create_mime_attachment(self, content, mimetype):
       
   216         """
       
   217         Converts the content, mimetype pair into a MIME attachment object.
       
   218         """
       
   219         basetype, subtype = mimetype.split('/', 1)
       
   220         if basetype == 'text':
       
   221             encoding = self.encoding or settings.DEFAULT_CHARSET
       
   222             attachment = SafeMIMEText(smart_str(content, encoding), subtype, encoding)
       
   223         else:
       
   224             # Encode non-text attachments with base64.
       
   225             attachment = MIMEBase(basetype, subtype)
       
   226             attachment.set_payload(content)
       
   227             Encoders.encode_base64(attachment)
       
   228         return attachment
       
   229 
       
   230     def _create_attachment(self, filename, content, mimetype=None):
       
   231         """
       
   232         Converts the filename, content, mimetype triple into a MIME attachment
       
   233         object.
       
   234         """
       
   235         if mimetype is None:
       
   236             mimetype, _ = mimetypes.guess_type(filename)
       
   237             if mimetype is None:
       
   238                 mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
       
   239         attachment = self._create_mime_attachment(content, mimetype)
       
   240         if filename:
       
   241             attachment.add_header('Content-Disposition', 'attachment',
       
   242                                   filename=filename)
       
   243         return attachment
       
   244 
       
   245 
       
   246 class EmailMultiAlternatives(EmailMessage):
       
   247     """
       
   248     A version of EmailMessage that makes it easy to send multipart/alternative
       
   249     messages. For example, including text and HTML versions of the text is
       
   250     made easier.
       
   251     """
       
   252     alternative_subtype = 'alternative'
       
   253 
       
   254     def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
       
   255             connection=None, attachments=None, headers=None, alternatives=None):
       
   256         """
       
   257         Initialize a single email message (which can be sent to multiple
       
   258         recipients).
       
   259 
       
   260         All strings used to create the message can be unicode strings (or UTF-8
       
   261         bytestrings). The SafeMIMEText class will handle any necessary encoding
       
   262         conversions.
       
   263         """
       
   264         super(EmailMultiAlternatives, self).__init__(subject, body, from_email, to, bcc, connection, attachments, headers)
       
   265         self.alternatives=alternatives or []
       
   266 
       
   267     def attach_alternative(self, content, mimetype):
       
   268         """Attach an alternative content representation."""
       
   269         assert content is not None
       
   270         assert mimetype is not None
       
   271         self.alternatives.append((content, mimetype))
       
   272 
       
   273     def _create_message(self, msg):
       
   274         return self._create_attachments(self._create_alternatives(msg))
       
   275 
       
   276     def _create_alternatives(self, msg):
       
   277         encoding = self.encoding or settings.DEFAULT_CHARSET
       
   278         if self.alternatives:
       
   279             body_msg = msg
       
   280             msg = SafeMIMEMultipart(_subtype=self.alternative_subtype, encoding=encoding)
       
   281             if self.body:
       
   282                 msg.attach(body_msg)
       
   283             for alternative in self.alternatives:
       
   284                 msg.attach(self._create_mime_attachment(*alternative))
       
   285         return msg