From 98c52ea14883f0261fd7a2f2fe8db42d96331ddb Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 9 Feb 2009 22:19:18 -0500 Subject: Move mailman.Message to mailman.email.Message. Rename Message.get_sender() to Message.sender (property) and Message.get_senders() to Message.senders (another property). The semantics of .sender is slightly different too; it no longer consults config.mailman.use_envelope_sender. Add absolute_import and unicode_literals to Utils.py, and clean up a few imports. --- src/mailman/email/message.py | 251 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 src/mailman/email/message.py (limited to 'src/mailman/email/message.py') diff --git a/src/mailman/email/message.py b/src/mailman/email/message.py new file mode 100644 index 000000000..ca36e7ae5 --- /dev/null +++ b/src/mailman/email/message.py @@ -0,0 +1,251 @@ +# Copyright (C) 1998-2009 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see . + +"""Standard Mailman message object. + +This is a subclass of email.message.Message but provides a slightly extended +interface which is more convenient for use inside Mailman. It also supports +safe pickle deserialization, even if the email package adds additional Message +attributes. +""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'Message', + ] + + +import re +import email +import email.message +import email.utils + +from email.charset import Charset +from email.header import Header +from lazr.config import as_boolean + +from mailman.Utils import GetCharSet +from mailman.config import config + + +COMMASPACE = ', ' +VERSION = tuple(int(v) for v in email.__version__.split('.')) + + + +class Message(email.message.Message): + def __init__(self): + # We need a version number so that we can optimize __setstate__(). + self.__version__ = VERSION + email.message.Message.__init__(self) + + def __getitem__(self, key): + # Ensure that header values are unicodes. + value = email.message.Message.__getitem__(self, key) + if isinstance(value, str): + return unicode(value, 'ascii') + return value + + def get(self, name, failobj=None): + # Ensure that header values are unicodes. + value = email.message.Message.get(self, name, failobj) + if isinstance(value, str): + return unicode(value, 'ascii') + return value + + def get_all(self, name, failobj=None): + # Ensure all header values are unicodes. + missing = object() + all_values = email.message.Message.get_all(self, name, missing) + if all_values is missing: + return failobj + return [(unicode(value, 'ascii') if isinstance(value, str) else value) + for value in all_values] + + # BAW: For debugging w/ bin/dumpdb. Apparently pprint uses repr. + def __repr__(self): + return self.__str__() + + def __setstate__(self, values): + # The base class has grown and changed attributes over time. This can + # break messages sitting in Mailman's queues at the time of upgrading + # the email package. We can't (yet) change the email package to be + # safer for pickling, so we handle such changes here. Note that we're + # using Python 2.6's email package version 4.0.1 as a base line here. + self.__dict__ = values + # The pickled instance should have an __version__ string, but it may + # not if it's an email package message. + version = values.get('__version__', (0, 0, 0)) + values['__version__'] = VERSION + # There's really nothing to check; there's nothing newer than email + # 4.0.1 at the moment. + + @property + def sender(self): + """The address considered to be the author of the email. + + This is the first non-None value in the list of senders. + + :return: The email address of the first found sender, or the empty + string if no sender address was found. + :rtype: email address + """ + for address in self.senders: + # This could be None or the empty string. + if address: + return address + return '' + + @property + def senders(self): + """Return a list of addresses representing the author of the email. + + The list will contain email addresses in the order determined by the + configuration variable `sender_headers` in the `[mailman]` section. + By default it uses this list of headers in order: + + 1. From: + 2. envelope sender (i.e. From_, unixfrom, or RFC 2821 MAIL FROM) + 3. Reply-To: + 4. Sender: + + The return addresses are guaranteed to be lower case or None. There + may be more than four values in the returned list, since some of the + originator headers above can appear multiple times in the message, or + contain multiple values. + + :return: The list of email addresses that can be considered the sender + of the message. + :rtype: A list of email addresses or Nones + """ + envelope_sender = self.get_unixfrom() + senders = [] + for header in config.mailman.sender_headers.split(): + header = header.lower() + if header == 'from_': + senders.append(envelope_sender.lower() + if envelope_sender is not None + else '') + else: + field_values = self.get_all(header, []) + senders.extend(address.lower() for (real_name, address) + in email.utils.getaddresses(field_values)) + return senders + + def get_filename(self, failobj=None): + """Some MUA have bugs in RFC2231 filename encoding and cause + Mailman to stop delivery in Scrubber.py (called from ToDigest.py). + """ + try: + filename = email.message.Message.get_filename(self, failobj) + return filename + except (UnicodeError, LookupError, ValueError): + return failobj + + + +class UserNotification(Message): + """Class for internally crafted messages.""" + + def __init__(self, recip, sender, subject=None, text=None, lang=None): + Message.__init__(self) + charset = 'us-ascii' + if lang is not None: + charset = GetCharSet(lang) + if text is not None: + self.set_payload(text.encode(charset), charset) + if subject is None: + subject = '(no subject)' + self['Subject'] = Header(subject.encode(charset), charset, + header_name='Subject', errors='replace') + self['From'] = sender + if isinstance(recip, list): + self['To'] = COMMASPACE.join(recip) + self.recips = recip + else: + self['To'] = recip + self.recips = [recip] + + def send(self, mlist, **_kws): + """Sends the message by enqueuing it to the 'virgin' queue. + + This is used for all internally crafted messages. + """ + # Since we're crafting the message from whole cloth, let's make sure + # this message has a Message-ID. + if 'message-id' not in self: + self['Message-ID'] = email.utils.make_msgid() + # Ditto for Date: as required by RFC 2822. + if 'date' not in self: + self['Date'] = email.utils.formatdate(localtime=True) + # UserNotifications are typically for admin messages, and for messages + # other than list explosions. Send these out as Precedence: bulk, but + # don't override an existing Precedence: header. + if 'precedence' not in self: + self['Precedence'] = 'bulk' + self._enqueue(mlist, **_kws) + + def _enqueue(self, mlist, **_kws): + # Not imported at module scope to avoid import loop + virginq = config.switchboards['virgin'] + # The message metadata better have a 'recip' attribute. + enqueue_kws = dict( + recips=self.recips, + nodecorate=True, + reduced_list_headers=True, + ) + if mlist is not None: + enqueue_kws['listname'] = mlist.fqdn_listname + enqueue_kws.update(_kws) + # Keywords must be strings in Python 2.6. + str_keywords = dict() + for key, val in enqueue_kws.items(): + str_keywords[str(key)] = val + virginq.enqueue(self, **str_keywords) + + + +class OwnerNotification(UserNotification): + """Like user notifications, but this message goes to the list owners.""" + + def __init__(self, mlist, subject=None, text=None, tomoderators=True): + if tomoderators: + roster = mlist.moderators + else: + roster = mlist.owners + recips = [address.address for address in roster.addresses] + sender = config.mailman.site_owner + lang = mlist.preferred_language + UserNotification.__init__(self, recips, sender, subject, text, lang) + # Hack the To header to look like it's going to the -owner address + del self['to'] + self['To'] = mlist.owner_address + self._sender = sender + + def _enqueue(self, mlist, **_kws): + # Not imported at module scope to avoid import loop + virginq = config.switchboards['virgin'] + # The message metadata better have a `recip' attribute + virginq.enqueue(self, + listname=mlist.fqdn_listname, + recips=self.recips, + nodecorate=True, + reduced_list_headers=True, + envsender=self._sender, + **_kws) -- cgit v1.2.3-70-g09d2