diff -r b758351d191f -r cc9b7e14412b web/lib/django/core/mail/message.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/lib/django/core/mail/message.py Tue May 25 02:43:45 2010 +0200 @@ -0,0 +1,285 @@ +import mimetypes +import os +import random +import time +from email import Charset, Encoders +from email.MIMEText import MIMEText +from email.MIMEMultipart import MIMEMultipart +from email.MIMEBase import MIMEBase +from email.Header import Header +from email.Utils import formatdate, getaddresses, formataddr + +from django.conf import settings +from django.core.mail.utils import DNS_NAME +from django.utils.encoding import smart_str, force_unicode + +# Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from +# some spam filters. +Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8') + +# Default MIME type to use on attachments (if it is not explicitly given +# and cannot be guessed). +DEFAULT_ATTACHMENT_MIME_TYPE = 'application/octet-stream' + + +class BadHeaderError(ValueError): + pass + + +# Copied from Python standard library, with the following modifications: +# * Used cached hostname for performance. +# * Added try/except to support lack of getpid() in Jython (#5496). +def make_msgid(idstring=None): + """Returns a string suitable for RFC 2822 compliant Message-ID, e.g: + + <20020201195627.33539.96671@nightshade.la.mastaler.com> + + Optional idstring if given is a string used to strengthen the + uniqueness of the message id. + """ + timeval = time.time() + utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval)) + try: + pid = os.getpid() + except AttributeError: + # No getpid() in Jython, for example. + pid = 1 + randint = random.randrange(100000) + if idstring is None: + idstring = '' + else: + idstring = '.' + idstring + idhost = DNS_NAME + msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost) + return msgid + + +def forbid_multi_line_headers(name, val, encoding): + """Forbids multi-line headers, to prevent header injection.""" + encoding = encoding or settings.DEFAULT_CHARSET + val = force_unicode(val) + if '\n' in val or '\r' in val: + raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (val, name)) + try: + val = val.encode('ascii') + except UnicodeEncodeError: + if name.lower() in ('to', 'from', 'cc'): + result = [] + for nm, addr in getaddresses((val,)): + nm = str(Header(nm.encode(encoding), encoding)) + result.append(formataddr((nm, str(addr)))) + val = ', '.join(result) + else: + val = Header(val.encode(encoding), encoding) + else: + if name.lower() == 'subject': + val = Header(val) + return name, val + +class SafeMIMEText(MIMEText): + + def __init__(self, text, subtype, charset): + self.encoding = charset + MIMEText.__init__(self, text, subtype, charset) + + def __setitem__(self, name, val): + name, val = forbid_multi_line_headers(name, val, self.encoding) + MIMEText.__setitem__(self, name, val) + +class SafeMIMEMultipart(MIMEMultipart): + + def __init__(self, _subtype='mixed', boundary=None, _subparts=None, encoding=None, **_params): + self.encoding = encoding + MIMEMultipart.__init__(self, _subtype, boundary, _subparts, **_params) + + def __setitem__(self, name, val): + name, val = forbid_multi_line_headers(name, val, self.encoding) + MIMEMultipart.__setitem__(self, name, val) + +class EmailMessage(object): + """ + A container for email information. + """ + content_subtype = 'plain' + mixed_subtype = 'mixed' + encoding = None # None => use settings default + + def __init__(self, subject='', body='', from_email=None, to=None, bcc=None, + connection=None, attachments=None, headers=None): + """ + Initialize a single email message (which can be sent to multiple + recipients). + + All strings used to create the message can be unicode strings + (or UTF-8 bytestrings). The SafeMIMEText class will handle any + necessary encoding conversions. + """ + if to: + assert not isinstance(to, basestring), '"to" argument must be a list or tuple' + self.to = list(to) + else: + self.to = [] + if bcc: + assert not isinstance(bcc, basestring), '"bcc" argument must be a list or tuple' + self.bcc = list(bcc) + else: + self.bcc = [] + self.from_email = from_email or settings.DEFAULT_FROM_EMAIL + self.subject = subject + self.body = body + self.attachments = attachments or [] + self.extra_headers = headers or {} + self.connection = connection + + def get_connection(self, fail_silently=False): + from django.core.mail import get_connection + if not self.connection: + self.connection = get_connection(fail_silently=fail_silently) + return self.connection + + def message(self): + encoding = self.encoding or settings.DEFAULT_CHARSET + msg = SafeMIMEText(smart_str(self.body, encoding), + self.content_subtype, encoding) + msg = self._create_message(msg) + msg['Subject'] = self.subject + msg['From'] = self.extra_headers.get('From', self.from_email) + msg['To'] = ', '.join(self.to) + + # Email header names are case-insensitive (RFC 2045), so we have to + # accommodate that when doing comparisons. + header_names = [key.lower() for key in self.extra_headers] + if 'date' not in header_names: + msg['Date'] = formatdate() + if 'message-id' not in header_names: + msg['Message-ID'] = make_msgid() + for name, value in self.extra_headers.items(): + if name.lower() == 'from': # From is already handled + continue + msg[name] = value + return msg + + def recipients(self): + """ + Returns a list of all recipients of the email (includes direct + addressees as well as Bcc entries). + """ + return self.to + self.bcc + + def send(self, fail_silently=False): + """Sends the email message.""" + if not self.recipients(): + # Don't bother creating the network connection if there's nobody to + # send to. + return 0 + return self.get_connection(fail_silently).send_messages([self]) + + def attach(self, filename=None, content=None, mimetype=None): + """ + Attaches a file with the given filename and content. The filename can + be omitted and the mimetype is guessed, if not provided. + + If the first parameter is a MIMEBase subclass it is inserted directly + into the resulting message attachments. + """ + if isinstance(filename, MIMEBase): + assert content == mimetype == None + self.attachments.append(filename) + else: + assert content is not None + self.attachments.append((filename, content, mimetype)) + + def attach_file(self, path, mimetype=None): + """Attaches a file from the filesystem.""" + filename = os.path.basename(path) + content = open(path, 'rb').read() + self.attach(filename, content, mimetype) + + def _create_message(self, msg): + return self._create_attachments(msg) + + def _create_attachments(self, msg): + if self.attachments: + encoding = self.encoding or settings.DEFAULT_CHARSET + body_msg = msg + msg = SafeMIMEMultipart(_subtype=self.mixed_subtype, encoding=encoding) + if self.body: + msg.attach(body_msg) + for attachment in self.attachments: + if isinstance(attachment, MIMEBase): + msg.attach(attachment) + else: + msg.attach(self._create_attachment(*attachment)) + return msg + + def _create_mime_attachment(self, content, mimetype): + """ + Converts the content, mimetype pair into a MIME attachment object. + """ + basetype, subtype = mimetype.split('/', 1) + if basetype == 'text': + encoding = self.encoding or settings.DEFAULT_CHARSET + attachment = SafeMIMEText(smart_str(content, encoding), subtype, encoding) + else: + # Encode non-text attachments with base64. + attachment = MIMEBase(basetype, subtype) + attachment.set_payload(content) + Encoders.encode_base64(attachment) + return attachment + + def _create_attachment(self, filename, content, mimetype=None): + """ + Converts the filename, content, mimetype triple into a MIME attachment + object. + """ + if mimetype is None: + mimetype, _ = mimetypes.guess_type(filename) + if mimetype is None: + mimetype = DEFAULT_ATTACHMENT_MIME_TYPE + attachment = self._create_mime_attachment(content, mimetype) + if filename: + attachment.add_header('Content-Disposition', 'attachment', + filename=filename) + return attachment + + +class EmailMultiAlternatives(EmailMessage): + """ + A version of EmailMessage that makes it easy to send multipart/alternative + messages. For example, including text and HTML versions of the text is + made easier. + """ + alternative_subtype = 'alternative' + + def __init__(self, subject='', body='', from_email=None, to=None, bcc=None, + connection=None, attachments=None, headers=None, alternatives=None): + """ + Initialize a single email message (which can be sent to multiple + recipients). + + All strings used to create the message can be unicode strings (or UTF-8 + bytestrings). The SafeMIMEText class will handle any necessary encoding + conversions. + """ + super(EmailMultiAlternatives, self).__init__(subject, body, from_email, to, bcc, connection, attachments, headers) + self.alternatives=alternatives or [] + + def attach_alternative(self, content, mimetype): + """Attach an alternative content representation.""" + assert content is not None + assert mimetype is not None + self.alternatives.append((content, mimetype)) + + def _create_message(self, msg): + return self._create_attachments(self._create_alternatives(msg)) + + def _create_alternatives(self, msg): + encoding = self.encoding or settings.DEFAULT_CHARSET + if self.alternatives: + body_msg = msg + msg = SafeMIMEMultipart(_subtype=self.alternative_subtype, encoding=encoding) + if self.body: + msg.attach(body_msg) + for alternative in self.alternatives: + msg.attach(self._create_mime_attachment(*alternative)) + return msg