# HG changeset patch # User Paul Boddie # Date 1368747912 -7200 # Node ID 3d7cc925d29eff91c503e1d0c1b50b008ecce4ce # Parent 94922f130a8ec255a977ace280dbf2588f45662c Added support for sending HTML-formatted messages with optional attachments in a multipart/related bundle, employing a special formatter to detect and rewrite attachment references, and fixing the PostMessage action to handle incoming messages of this kind. Added a separate action for reading message attachments/components which will be used to support the presentation of received messages. Added a preview function to the SendMessage action. diff -r 94922f130a8e -r 3d7cc925d29e actions/PostMessage.py --- a/actions/PostMessage.py Fri Apr 19 15:15:40 2013 +0200 +++ b/actions/PostMessage.py Fri May 17 01:45:12 2013 +0200 @@ -12,7 +12,7 @@ from MoinMoin.user import User from MoinMoin import wikiutil from MoinSupport import ItemStore, getHeader, getMetadata, getWikiDict, writeHeaders -from MoinMessage import GPG, Message, MoinMessageError +from MoinMessage import GPG, Message, MoinMessageError, is_collection from email.parser import Parser import time @@ -241,7 +241,7 @@ # Handle a single part. - if not update.is_multipart(): + if not is_collection(update): self.handle_message_parts([update], update) # Or a collection of alternative representations for a single diff -r 94922f130a8e -r 3d7cc925d29e actions/ReadMessage.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/actions/ReadMessage.py Fri May 17 01:45:12 2013 +0200 @@ -0,0 +1,144 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin - ReadMessage Action + + @copyright: 2012, 2013 by Paul Boddie + @license: GNU GPL (v2 or later), see COPYING.txt for details. +""" + +from MoinMoin.action import ActionBase +from MoinSupport import * +from email.parser import Parser + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +Dependencies = [] + +class ReadMessage(ActionBase, ActionSupport): + + "An action that can read a stored message component." + + def __init__(self, pagename, request): + + """ + On the page with the given 'pagename', use the given 'request' to access + message components. + """ + + ActionBase.__init__(self, pagename, request) + self.store = ItemStore(self.page, "messages", "message-locks") + + def get_form_html(self, buttons_html): + + "Present an interface for accessing a message component." + + _ = self._ + request = self.request + form = self.get_form() + + message = form.get("message", [""])[0] + part = form.get("part", [""])[0] + + # Fill in the fields and labels. + + d = { + "buttons_html" : buttons_html, + "message_label" : _("Message number"), + "message_default" : escattr(message), + "part_label" : _("Part identifier"), + "part_default" : escattr(part), + } + + # Prepare the output HTML. + + html = ''' + + + + + + + + + + + + + +
+ +
+ +
+ %(buttons_html)s +
''' % d + + return html + + def do_action(self): + + "Attempt to send the message." + + _ = self._ + request = self.request + form = self.get_form() + + message_number = form.get("message", [None])[0] + part_identifier = form.get("part", [None])[0] + + if not message_number: + return 0, _("A message number must be given.") + + if not part_identifier: + return 0, _("A part identifier must be given.") + + # Obtain the message. + + try: + message_text = self.store[int(message_number)] + except (IndexError, ValueError): + return 0, _("No such message is stored on this page.") + + # Visit the message parts, looking for the indicated component. + + message = Parser().parse(StringIO(message_text)) + + if message.is_multipart(): + for part in message.get_payload(): + + # For the selected component, return the content as a response + # to the current request. + + if part.get("Content-ID") == part_identifier: + charset = part.get_content_charset() + headers = [ + "Content-Type: %s%s" % ( + part.get_content_type(), + charset and ("; charset=%s" % charset) or "" + ) + ] + get_send_headers(request)(headers) + request.write(part.get_payload(decode=True)) + return 1, None + + return 0, _("No such component in the indicated message.") + + def render_success(self, msg, msgtype=None): + + """ + Render neither 'msg' nor 'msgtype' since a resource has already been + produced. + NOTE: msgtype is optional because MoinMoin 1.5.x does not support it. + """ + + pass + +# Action function. + +def execute(pagename, request): + ReadMessage(pagename, request).render() + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 94922f130a8e -r 3d7cc925d29e actions/SendMessage.py --- a/actions/SendMessage.py Fri Apr 19 15:15:40 2013 +0200 +++ b/actions/SendMessage.py Fri May 17 01:45:12 2013 +0200 @@ -6,12 +6,18 @@ @license: GNU GPL (v2 or later), see COPYING.txt for details. """ -from MoinMoin.action import ActionBase +from MoinMoin.action import ActionBase, AttachFile +from MoinMoin.formatter import text_html from MoinMoin.log import getLogger +from MoinMoin import config from MoinMessage import GPG, MoinMessageError, Message, sendMessage from MoinSupport import * -from MoinMoin.wikiutil import escape +from MoinMoin.wikiutil import escape, MimeType, parseQueryString, taintfilename + +from email.mime.image import MIMEImage +from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText +import urllib Dependencies = [] @@ -29,6 +35,7 @@ message = form.get("message", [""])[0] recipient = form.get("recipient", [""])[0] + preview = form.get("preview", [""])[0] # Get a list of potential recipients. @@ -43,6 +50,11 @@ recipients_list.sort() + # Prepare any preview. + + request.formatter.setPage(self.page) + preview_output = preview and formatText(message, request, request.formatter) or "" + # Fill in the fields and labels. d = { @@ -50,7 +62,9 @@ "recipient_label" : _("Recipient"), "recipients_list" : "\n".join(recipients_list), "message_label" : _("Message text"), - "message_default" : escattr(message), + "message_default" : escape(message), + "preview_label" : _("Preview message"), + "preview_output" : preview_output, } # Prepare the output HTML. @@ -67,13 +81,25 @@ - - + + - + + + + + + + +%(preview_output)s + + + + + %(buttons_html)s @@ -107,13 +133,58 @@ # Construct a message from the request. message = Message() - message.add_update(MIMEText(text, "moin")) + + container = MIMEMultipart("related") + container["Update-Action"] = "store" + + # Add the message body and any attachments. + + fmt = OutgoingHTMLFormatter(request) + fmt.setPage(request.page) + body = formatText(text, request, fmt) + + container.attach(MIMEText(body, "html")) + + for pos, (pagename, filename) in enumerate(fmt.attachments): + + # Obtain the attachment path. + + filename = taintfilename(filename) + path = AttachFile.getFilename(request, pagename, filename) + + # Obtain the attachment content. + + f = open(path, "rb") + try: + body = f.read() + finally: + f.close() + + # Determine the attachment type. + + mimetype = MimeType(filename=filename) + + # NOTE: Support a limited set of explicit part types for now. + + if mimetype.major == "image": + part = MIMEImage(body, mimetype.minor, **mimetype.params) + elif mimetype.major == "text": + part = MIMEText(body, mimetype.minor, mimetype.charset, **mimetype.params) + else: + part = MIMEApplication(body, mimetype.minor, **mimetype.params) + + # Label the attachment and include it in the message. + + part["Content-ID"] = "attachment%d" % pos + container.attach(part) + + message.add_update(container) # Get the sender details for signing messages. # This is not the same as the details for authenticating users in the # PostMessage action since the fingerprints refer to public keys. - signing_users = getWikiDict(getattr(request.cfg, "moinmessage_gpg_signing_users_page", "MoinMessageSigningUserDict"), request) + signing_users = self.get_signing_users() signer = signing_users and signing_users.get(request.user.name) # Get the recipient details. @@ -153,7 +224,89 @@ return getattr(self.request.cfg, "moinmessage_gpg_homedir") def get_recipients(self): - return getWikiDict(getattr(self.request.cfg, "moinmessage_gpg_recipients_page", "MoinMessageRecipientsDict"), self.request) + return getWikiDict( + getattr(self.request.cfg, "moinmessage_gpg_recipients_page", "MoinMessageRecipientsDict"), + self.request) + + def get_signing_users(self): + return getWikiDict( + getattr(self.request.cfg, "moinmessage_gpg_signing_users_page", "MoinMessageSigningUserDict"), + self.request) + +# Special message formatters. + +def unquoteWikinameURL(url, charset=config.charset): + + """ + The inverse of wikiutil.quoteWikinameURL, returning the page name referenced + by the given 'url', with the page name assumed to be encoded using the given + 'charset' (or default charset if omitted). + """ + + return unicode(urllib.unquote(url), encoding=charset) + +def getAttachmentFromURL(url, request): + + """ + Return a (page name, attachment filename) tuple for the attachment + referenced by the given 'url', using the 'request' to interpret the + structure of 'url'. + + If 'url' does not refer to an attachment on this wiki, None is returned. + """ + + script = request.getScriptname() + if not url.startswith(script): + return None + + path = url[len(script):].lstrip("/") + try: + qpagename, qs = path.split("?", 1) + except ValueError: + qpagename = path + qs = None + + pagename = unquoteWikinameURL(qpagename) + qs = qs and parseQueryString(qs) or {} + return pagename, qs.get("target") or qs.get("drawing") + +class OutgoingHTMLFormatter(text_html.Formatter): + + """ + Handle outgoing HTML content by identifying attachments and rewriting their + locations. References to bundled attachments are done using RFC 2111: + + https://tools.ietf.org/html/rfc2111 + + Messages employing references between parts are meant to comply with RFC + 2387: + + https://tools.ietf.org/html/rfc2387 + """ + + def __init__(self, request, **kw): + text_html.Formatter.__init__(self, request, **kw) + self.attachments = [] + + def add_attachment(self, location): + details = getAttachmentFromURL(location, self.request) + if details: + pos = len(self.attachments) + self.attachments.append(details) + return "cid:attachment%d" % pos + else: + return None + + def image(self, src=None, **kw): + src = src or kw.get("src") + ref = src and self.add_attachment(src) + return text_html.Formatter.image(self, ref or src, **kw) + + def transclusion(self, on, **kw): + if on: + data = kw.get("data") + kw["data"] = data and self.add_attachment(data) + return text_html.Formatter.transclusion(self, on, **kw) # Action function.