| 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)) |
| try: |
| addr = addr.encode('ascii') |
| except UnicodeEncodeError: # IDN |
| addr = str(Header(addr.encode(encoding), encoding)) |
| result.append(formataddr((nm, 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, cc=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 cc: |
| assert not isinstance(cc, basestring), '"cc" argument must be a list or tuple' |
| self.cc = list(cc) |
| else: |
| self.cc = [] |
| 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) |
| if self.cc: |
| msg['Cc'] = ', '.join(self.cc) |
| |
| # 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 Cc and Bcc entries). |
| """ |
| return self.to + self.cc + 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, |
| cc=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, cc) |
| 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 |