summaryrefslogtreecommitdiff
path: root/mailman/Message.py
diff options
context:
space:
mode:
authorBarry Warsaw2008-02-27 01:26:18 -0500
committerBarry Warsaw2008-02-27 01:26:18 -0500
commita1c73f6c305c7f74987d99855ba59d8fa823c253 (patch)
tree65696889450862357c9e05c8e9a589f1bdc074ac /mailman/Message.py
parent3f31f8cce369529d177cfb5a7c66346ec1e12130 (diff)
downloadmailman-a1c73f6c305c7f74987d99855ba59d8fa823c253.tar.gz
mailman-a1c73f6c305c7f74987d99855ba59d8fa823c253.tar.zst
mailman-a1c73f6c305c7f74987d99855ba59d8fa823c253.zip
Diffstat (limited to 'mailman/Message.py')
-rw-r--r--mailman/Message.py310
1 files changed, 310 insertions, 0 deletions
diff --git a/mailman/Message.py b/mailman/Message.py
new file mode 100644
index 000000000..5d20fb897
--- /dev/null
+++ b/mailman/Message.py
@@ -0,0 +1,310 @@
+# Copyright (C) 1998-2008 by the Free Software Foundation, Inc.
+#
+# This program 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 2
+# of the License, or (at your option) any later version.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""Standard Mailman message object.
+
+This is a subclass of mimeo.Message but provides a slightly extended interface
+which is more convenient for use inside Mailman.
+"""
+
+import re
+import email
+import email.message
+import email.utils
+
+from email.charset import Charset
+from email.header import Header
+
+from mailman import Utils
+from mailman.configuration import config
+
+COMMASPACE = ', '
+
+mo = re.match(r'([\d.]+)', email.__version__)
+VERSION = tuple([int(s) for s in mo.group().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):
+ value = email.message.Message.__getitem__(self, key)
+ if isinstance(value, str):
+ return unicode(value, 'ascii')
+ return value
+
+ def get(self, name, failobj=None):
+ 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):
+ 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, d):
+ # The base class attributes have changed over time. Which could
+ # affect Mailman if messages are sitting in the queue at the time of
+ # upgrading the email package. We shouldn't burden email with this,
+ # so we handle schema updates here.
+ self.__dict__ = d
+ # We know that email 2.4.3 is up-to-date
+ version = d.get('__version__', (0, 0, 0))
+ d['__version__'] = VERSION
+ if version >= VERSION:
+ return
+ # Messages grew a _charset attribute between email version 0.97 and 1.1
+ if not d.has_key('_charset'):
+ self._charset = None
+ # Messages grew a _default_type attribute between v2.1 and v2.2
+ if not d.has_key('_default_type'):
+ # We really have no idea whether this message object is contained
+ # inside a multipart/digest or not, so I think this is the best we
+ # can do.
+ self._default_type = 'text/plain'
+ # Header instances used to allow both strings and Charsets in their
+ # _chunks, but by email 2.4.3 now it's just Charsets.
+ headers = []
+ hchanged = 0
+ for k, v in self._headers:
+ if isinstance(v, Header):
+ chunks = []
+ cchanged = 0
+ for s, charset in v._chunks:
+ if isinstance(charset, str):
+ charset = Charset(charset)
+ cchanged = 1
+ chunks.append((s, charset))
+ if cchanged:
+ v._chunks = chunks
+ hchanged = 1
+ headers.append((k, v))
+ if hchanged:
+ self._headers = headers
+
+ # I think this method ought to eventually be deprecated
+ def get_sender(self, use_envelope=None, preserve_case=0):
+ """Return the address considered to be the author of the email.
+
+ This can return either the From: header, the Sender: header or the
+ envelope header (a.k.a. the unixfrom header). The first non-empty
+ header value found is returned. However the search order is
+ determined by the following:
+
+ - If config.USE_ENVELOPE_SENDER is true, then the search order is
+ Sender:, From:, unixfrom
+
+ - Otherwise, the search order is From:, Sender:, unixfrom
+
+ The optional argument use_envelope, if given overrides the
+ config.USE_ENVELOPE_SENDER setting. It should be set to either 0 or 1
+ (don't use None since that indicates no-override).
+
+ unixfrom should never be empty. The return address is always
+ lowercased, unless preserve_case is true.
+
+ This method differs from get_senders() in that it returns one and only
+ one address, and uses a different search order.
+ """
+ senderfirst = config.USE_ENVELOPE_SENDER
+ if use_envelope is not None:
+ senderfirst = use_envelope
+ if senderfirst:
+ headers = ('sender', 'from')
+ else:
+ headers = ('from', 'sender')
+ for h in headers:
+ # Use only the first occurrance of Sender: or From:, although it's
+ # not likely there will be more than one.
+ fieldval = self[h]
+ if not fieldval:
+ continue
+ addrs = email.utils.getaddresses([fieldval])
+ try:
+ realname, address = addrs[0]
+ except IndexError:
+ continue
+ if address:
+ break
+ else:
+ # We didn't find a non-empty header, so let's fall back to the
+ # unixfrom address. This should never be empty, but if it ever
+ # is, it's probably a Really Bad Thing. Further, we just assume
+ # that if the unixfrom exists, the second field is the address.
+ unixfrom = self.get_unixfrom()
+ if unixfrom:
+ address = unixfrom.split()[1]
+ else:
+ # TBD: now what?!
+ address = ''
+ if not preserve_case:
+ return address.lower()
+ return address
+
+ def get_senders(self, preserve_case=0, headers=None):
+ """Return a list of addresses representing the author of the email.
+
+ The list will contain the following addresses (in order)
+ depending on availability:
+
+ 1. From:
+ 2. unixfrom
+ 3. Reply-To:
+ 4. Sender:
+
+ The return addresses are always lower cased, unless `preserve_case' is
+ true. Optional `headers' gives an alternative search order, with None
+ meaning, search the unixfrom header. Items in `headers' are field
+ names without the trailing colon.
+ """
+ if headers is None:
+ headers = config.SENDER_HEADERS
+ pairs = []
+ for h in headers:
+ if h is None:
+ # get_unixfrom() returns None if there's no envelope
+ fieldval = self.get_unixfrom() or ''
+ try:
+ pairs.append(('', fieldval.split()[1]))
+ except IndexError:
+ # Ignore badly formatted unixfroms
+ pass
+ else:
+ fieldvals = self.get_all(h)
+ if fieldvals:
+ pairs.extend(email.utils.getaddresses(fieldvals))
+ authors = []
+ for pair in pairs:
+ address = pair[1]
+ if address is not None and not preserve_case:
+ address = address.lower()
+ authors.append(address)
+ return authors
+
+ 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 = Utils.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. Yes, the MTA would give us one, but
+ # this is useful for logging to logs/smtp.
+ if 'message-id' not in self:
+ self['Message-ID'] = email.utils.make_msgid()
+ # Ditto for Date: which is 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
+ from mailman.queue import Switchboard
+ virginq = Switchboard(config.VIRGINQUEUE_DIR)
+ # 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)
+ virginq.enqueue(self, **enqueue_kws)
+
+
+
+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.SITE_OWNER_ADDRESS
+ 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
+ from mailman.queue import Switchboard
+ virginq = Switchboard(config.VIRGINQUEUE_DIR)
+ # 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)