summaryrefslogtreecommitdiff
path: root/src/mailman/pipeline/decorate.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/pipeline/decorate.py')
-rw-r--r--src/mailman/pipeline/decorate.py231
1 files changed, 231 insertions, 0 deletions
diff --git a/src/mailman/pipeline/decorate.py b/src/mailman/pipeline/decorate.py
new file mode 100644
index 000000000..e1fa0c155
--- /dev/null
+++ b/src/mailman/pipeline/decorate.py
@@ -0,0 +1,231 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+"""Decorate a message by sticking the header and footer around it."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'Decorate',
+ ]
+
+
+import re
+import logging
+
+from email.MIMEText import MIMEText
+from zope.interface import implements
+
+from mailman import Utils
+from mailman.Message import Message
+from mailman.config import config
+from mailman.i18n import _
+from mailman.interfaces.handler import IHandler
+from mailman.utilities.string import expand
+
+
+log = logging.getLogger('mailman.error')
+
+
+
+def process(mlist, msg, msgdata):
+ # Digests and Mailman-craft messages should not get additional headers
+ if msgdata.get('isdigest') or msgdata.get('nodecorate'):
+ return
+ d = {}
+ if msgdata.get('personalize'):
+ # Calculate the extra personalization dictionary. Note that the
+ # length of the recips list better be exactly 1.
+ recips = msgdata.get('recips', [])
+ assert len(recips) == 1, (
+ 'The number of intended recipients must be exactly 1')
+ recipient = recips[0].lower()
+ user = config.db.user_manager.get_user(recipient)
+ member = mlist.members.get_member(recipient)
+ d['user_address'] = recipient
+ if user is not None and member is not None:
+ d['user_delivered_to'] = member.address.original_address
+ # BAW: Hmm, should we allow this?
+ d['user_password'] = user.password
+ d['user_language'] = member.preferred_language
+ d['user_name'] = (user.real_name if user.real_name
+ else member.address.original_address)
+ d['user_optionsurl'] = member.options_url
+ # These strings are descriptive for the log file and shouldn't be i18n'd
+ d.update(msgdata.get('decoration-data', {}))
+ header = decorate(mlist, mlist.msg_header, d)
+ footer = decorate(mlist, mlist.msg_footer, d)
+ # Escape hatch if both the footer and header are empty
+ if not header and not footer:
+ return
+ # Be MIME smart here. We only attach the header and footer by
+ # concatenation when the message is a non-multipart of type text/plain.
+ # Otherwise, if it is not a multipart, we make it a multipart, and then we
+ # add the header and footer as text/plain parts.
+ #
+ # BJG: In addition, only add the footer if the message's character set
+ # matches the charset of the list's preferred language. This is a
+ # suboptimal solution, and should be solved by allowing a list to have
+ # multiple headers/footers, for each language the list supports.
+ #
+ # Also, if the list's preferred charset is us-ascii, we can always
+ # safely add the header/footer to a plain text message since all
+ # charsets Mailman supports are strict supersets of us-ascii --
+ # no, UTF-16 emails are not supported yet.
+ #
+ # TK: Message with 'charset=' cause trouble. So, instead of
+ # mgs.get_content_charset('us-ascii') ...
+ mcset = msg.get_content_charset() or 'us-ascii'
+ lcset = Utils.GetCharSet(mlist.preferred_language)
+ msgtype = msg.get_content_type()
+ # BAW: If the charsets don't match, should we add the header and footer by
+ # MIME multipart chroming the message?
+ wrap = True
+ if not msg.is_multipart() and msgtype == 'text/plain':
+ # Save the RFC-3676 format parameters.
+ format = msg.get_param('format')
+ delsp = msg.get_param('delsp')
+ # Save 'Content-Transfer-Encoding' header in case decoration fails.
+ cte = msg.get('content-transfer-encoding')
+ # header/footer is now in unicode (2.2)
+ try:
+ oldpayload = unicode(msg.get_payload(decode=True), mcset)
+ del msg['content-transfer-encoding']
+ frontsep = endsep = ''
+ if header and not header.endswith('\n'):
+ frontsep = '\n'
+ if footer and not oldpayload.endswith('\n'):
+ endsep = '\n'
+ payload = header + frontsep + oldpayload + endsep + footer
+ # When setting the payload for the message, try various charset
+ # encodings until one does not produce a UnicodeError. We'll try
+ # charsets in this order: the list's charset, the message's
+ # charset, then utf-8. It's okay if some of these are duplicates.
+ for cset in (lcset, mcset, 'utf-8'):
+ try:
+ msg.set_payload(payload.encode(cset), cset)
+ except UnicodeError:
+ pass
+ else:
+ if format:
+ msg.set_param('format', format)
+ if delsp:
+ msg.set_param('delsp', delsp)
+ wrap = False
+ break
+ except (LookupError, UnicodeError):
+ if cte:
+ # Restore the original c-t-e.
+ del msg['content-transfer-encoding']
+ msg['Content-Transfer-Encoding'] = cte
+ elif msg.get_content_type() == 'multipart/mixed':
+ # The next easiest thing to do is just prepend the header and append
+ # the footer as additional subparts
+ payload = msg.get_payload()
+ if not isinstance(payload, list):
+ payload = [payload]
+ if footer:
+ mimeftr = MIMEText(footer.encode(lcset), 'plain', lcset)
+ mimeftr['Content-Disposition'] = 'inline'
+ payload.append(mimeftr)
+ if header:
+ mimehdr = MIMEText(header.encode(lcset), 'plain', lcset)
+ mimehdr['Content-Disposition'] = 'inline'
+ payload.insert(0, mimehdr)
+ msg.set_payload(payload)
+ wrap = False
+ # If we couldn't add the header or footer in a less intrusive way, we can
+ # at least do it by MIME encapsulation. We want to keep as much of the
+ # outer chrome as possible.
+ if not wrap:
+ return
+ # Because of the way Message objects are passed around to process(), we
+ # need to play tricks with the outer message -- i.e. the outer one must
+ # remain the same instance. So we're going to create a clone of the outer
+ # message, with all the header chrome intact, then copy the payload to it.
+ # This will give us a clone of the original message, and it will form the
+ # basis of the interior, wrapped Message.
+ inner = Message()
+ # Which headers to copy? Let's just do the Content-* headers
+ for h, v in msg.items():
+ if h.lower().startswith('content-'):
+ inner[h] = v
+ inner.set_payload(msg.get_payload())
+ # For completeness
+ inner.set_unixfrom(msg.get_unixfrom())
+ inner.preamble = msg.preamble
+ inner.epilogue = msg.epilogue
+ # Don't copy get_charset, as this might be None, even if
+ # get_content_charset isn't. However, do make sure there is a default
+ # content-type, even if the original message was not MIME.
+ inner.set_default_type(msg.get_default_type())
+ # BAW: HACK ALERT.
+ if hasattr(msg, '__version__'):
+ inner.__version__ = msg.__version__
+ # Now, play games with the outer message to make it contain three
+ # subparts: the header (if any), the wrapped message, and the footer (if
+ # any).
+ payload = [inner]
+ if header:
+ mimehdr = MIMEText(header.encode(lcset), 'plain', lcset)
+ mimehdr['Content-Disposition'] = 'inline'
+ payload.insert(0, mimehdr)
+ if footer:
+ mimeftr = MIMEText(footer.encode(lcset), 'plain', lcset)
+ mimeftr['Content-Disposition'] = 'inline'
+ payload.append(mimeftr)
+ msg.set_payload(payload)
+ del msg['content-type']
+ del msg['content-transfer-encoding']
+ del msg['content-disposition']
+ msg['Content-Type'] = 'multipart/mixed'
+
+
+
+def decorate(mlist, template, extradict=None):
+ # Create a dictionary which includes the default set of interpolation
+ # variables allowed in headers and footers. These will be augmented by
+ # any key/value pairs in the extradict.
+ substitutions = dict(
+ real_name = mlist.real_name,
+ list_name = mlist.list_name,
+ fqdn_listname = mlist.fqdn_listname,
+ host_name = mlist.host_name,
+ listinfo_page = mlist.script_url('listinfo'),
+ description = mlist.description,
+ info = mlist.info,
+ )
+ if extradict is not None:
+ substitutions.update(extradict)
+ text = expand(template, substitutions)
+ # Turn any \r\n line endings into just \n
+ return re.sub(r' *\r?\n', r'\n', text)
+
+
+
+class Decorate:
+ """Decorate a message with headers and footers."""
+
+ implements(IHandler)
+
+ name = 'decorate'
+ description = _('Decorate a message with headers and footers.')
+
+ def process(self, mlist, msg, msgdata):
+ "See `IHandler`."""
+ process(mlist, msg, msgdata)