diff --git a/marrow/mailer/message.py b/marrow/mailer/message.py index 37b4e4e..5b81ec6 100644 --- a/marrow/mailer/message.py +++ b/marrow/mailer/message.py @@ -1,6 +1,6 @@ # encoding: utf-8 -"""MIME-encoded electronic mail message classes.""" +"""MIME-encoded electronic mail message class.""" import imghdr import os @@ -23,7 +23,7 @@ class Message(object): """Represents an e-mail message.""" - + sender = AutoConverter('_sender', Address, False) author = AutoConverter('_author', AddressList) authors = author @@ -32,25 +32,23 @@ class Message(object): bcc = AutoConverter('_bcc', AddressList) reply = AutoConverter('_reply', AddressList) notify = AutoConverter('_notify', AddressList) - + def __init__(self, author=None, to=None, subject=None, **kw): """Instantiate a new Message object. - + No arguments are required, as everything can be set using class properties. Alternatively, __everything__ can be set using the constructor, using named arguments. The first three positional arguments can be used to quickly prepare a simple message. """ - - # Internally used attributes. - + + # Internally used attributes self._id = None self._processed = False self._dirty = False self.mailer = None - - # Default values. - + + # Default values self.subject = None self.date = datetime.now() self.encoding = 'utf-8' @@ -63,7 +61,7 @@ def __init__(self, author=None, to=None, subject=None, **kw): self.headers = [] self.retries = 3 self.brand = True - + self._sender = None self._author = AddressList() self._to = AddressList() @@ -71,80 +69,79 @@ def __init__(self, author=None, to=None, subject=None, **kw): self._bcc = AddressList() self._reply = AddressList() self._notify = AddressList() - - # Overrides at initialization time. - + + # Overrides at initialization time if author is not None: self.author = author - + if to is not None: self.to = to - + if subject is not None: self.subject = subject - + for k in kw: if not hasattr(self, k): raise TypeError("Unexpected keyword argument: %s" % k) - + setattr(self, k, kw[k]) - + def __setattr__(self, name, value): """Set the dirty flag as properties are updated.""" object.__setattr__(self, name, value) if name not in ('bcc', '_id', '_dirty', '_processed'): object.__setattr__(self, '_dirty', True) - + def __str__(self): return self.mime.as_string() - + @property def id(self): if not self._id or (self._processed and self._dirty): self._id = make_msgid() self._processed = False return self._id - + @property def envelope(self): """Returns the address of the envelope sender address (SMTP from, if not set the sender, if this one isn't set too, the author).""" if not self.sender and not self.author: raise ValueError("Unable to determine message sender; no author or sender defined.") - + return self.sender or self.author[0] - + @property def recipients(self): return AddressList(self.to + self.cc + self.bcc) - + def _mime_document(self, plain, rich=None): if not rich: message = plain - + else: message = MIMEMultipart('alternative') message.attach(plain) - + if not self.embedded: message.attach(rich) - + else: embedded = MIMEMultipart('related') embedded.attach(rich) for attachment in self.embedded: embedded.attach(attachment) message.attach(embedded) - + if self.attachments: attachments = MIMEMultipart() attachments.attach(message) for attachment in self.attachments: attachments.attach(attachment) message = attachments - + return message - + def _build_date_header_string(self, date_value): """Gets the date_value (may be None, basestring, float or datetime.datetime instance) and returns a valid date string as per @@ -153,8 +150,10 @@ def _build_date_header_string(self, date_value): date_value = time.mktime(date_value.timetuple()) if not isinstance(date_value, basestring): date_value = formatdate(date_value, localtime=True) - return date_value - + # Encode it here to avoid this: + # Date: =?utf-8?q?Sat=2C_01_Sep_2012_13=3A08=3A29_-0300?= + return date_value.encode('ascii') + def _build_header_list(self, author, sender): date_value = self._build_date_header_string(self.date) headers = [ @@ -169,89 +168,90 @@ def _build_header_list(self, author, sender): ('Organization', self.organization), ('X-Priority', self.priority), ] - + if self.brand: headers.extend([ ('X-Mailer', "marrow.mailer {0}".format(release.version)) ]) - + if isinstance(self.headers, dict): for key in self.headers: headers.append((key, self.headers[key])) - + else: headers.extend(self.headers) - + return headers - + def _add_headers_to_message(self, message, headers): for header in headers: if header[1] is None or (isinstance(header[1], list) and not header[1]): continue - + name, value = header - + if isinstance(value, Address): value = value.encode(self.encoding) elif isinstance(value, AddressList): value = value.encode(self.encoding) - + if isinstance(value, unicode): value = Header(value, self.encoding) else: value = Header(value) - + message[name] = value - + @property def mime(self): """Produce the final MIME message.""" author = self.author sender = self.sender - + if not author: raise ValueError("You must specify an author.") - + if not self.subject: raise ValueError("You must specify a subject.") - + if len(self.recipients) == 0: raise ValueError("You must specify at least one recipient.") - + if not self.plain: raise ValueError("You must provide plain text content.") - + # DISCUSS: Take the first author, or raise this error? # if len(author) > 1 and len(sender) == 0: # raise ValueError('If there are multiple authors of message, you must specify a sender!') - + # if len(sender) > 1: # raise ValueError('You must not specify more than one sender!') - + if not self._dirty and self._processed: return self._mime - + self._processed = False - + plain = MIMEText(self._callable(self.plain), 'plain', self.encoding) - + rich = None if self.rich: rich = MIMEText(self._callable(self.rich), 'html', self.encoding) - + message = self._mime_document(plain, rich) headers = self._build_header_list(author, sender) self._add_headers_to_message(message, headers) - + self._mime = message self._processed = True self._dirty = False - + return message - - def attach(self, name, data=None, maintype=None, subtype=None, inline=False): + + def attach(self, name, data=None, maintype=None, subtype=None, + inline=False): """Attach a file to this message. - + :param name: Path to the file to attach if data is None, or the name of the file if the ``data`` argument is given :param data: Contents of the file to attach, or None if the data is to @@ -265,7 +265,7 @@ def attach(self, name, data=None, maintype=None, subtype=None, inline=False): "inline" (True) or "attachment" (False) """ self._dirty = True - + if not maintype: maintype, _ = guess_type(name) if not maintype: @@ -285,7 +285,7 @@ def attach(self, name, data=None, maintype=None, subtype=None, inline=False): part.set_payload(data.read()) else: raise TypeError("Unable to read attachment contents") - + if inline: part.add_header('Content-Disposition', 'inline', filename=name) part.add_header('Content-ID', '<%s>' % name) @@ -293,12 +293,12 @@ def attach(self, name, data=None, maintype=None, subtype=None, inline=False): else: part.add_header('Content-Disposition', 'attachment', filename=name) self.attachments.append(part) - + def embed(self, name, data=None): """Attach an image file and prepare for HTML embedding. - + This method should only be used to embed images. - + :param name: Path to the image to embed if data is None, or the name of the file if the ``data`` argument is given :param data: Contents of the image to embed, or None if the data is to @@ -314,19 +314,19 @@ def embed(self, name, data=None): data = data.read() else: raise TypeError("Unable to read image contents") - + subtype = imghdr.what(None, data) self.attach(name, data, 'image', subtype, True) - + @staticmethod def _callable(var): if hasattr(var, '__call__'): return var() - + return var - + def send(self): if not self.mailer: - raise NotImplementedError("Message instance is not bound to a Mailer. Use mailer.send() instead.") - + raise NotImplementedError("Message instance is not bound to " \ + "a Mailer. Use mailer.send() instead.") return self.mailer.send(self)