# HG changeset patch # User Paul Boddie # Date 1390856278 -3600 # Node ID 2a3660a2dc5d4ff763c5cfe5361107d2b1d47ff2 # Parent 46c6c4b00a25b303291b4b5b24abe49125890e16 Moved outgoing message formatting code to the support library. Made incoming message recipient routing optional. Added an extra exception type for transfer-related errors. diff -r 46c6c4b00a25 -r 2a3660a2dc5d MoinMessage.py --- a/MoinMessage.py Mon Jan 27 19:20:33 2014 +0100 +++ b/MoinMessage.py Mon Jan 27 21:57:58 2014 +0100 @@ -151,6 +151,12 @@ class MoinMessageBadContent(MoinMessageDecodingError): pass +class MoinMessageTransferError(MoinMessageError): + def __init__(self, code, message, body): + MoinMessageError.__init__(self, message) + self.code = code + self.body = body + class GPG: "A wrapper around the gpg command using a particular configuration." @@ -521,7 +527,7 @@ resp = req.getresponse() if resp.status >= 400: - raise MoinMessageError, "Message sending failed (%s): %s" % (resp.status, resp.read()) + raise MoinMessageTransferError(resp.status, "Message sending failed (%s)" % resp.status, resp.read()) return resp diff -r 46c6c4b00a25 -r 2a3660a2dc5d MoinMessageSupport.py --- a/MoinMessageSupport.py Mon Jan 27 19:20:33 2014 +0100 +++ b/MoinMessageSupport.py Mon Jan 27 21:57:58 2014 +0100 @@ -6,19 +6,27 @@ @license: GNU GPL (v2 or later), see COPYING.txt for details. """ +from MoinMoin import config from MoinMoin.Page import Page +from MoinMoin.action import AttachFile +from MoinMoin.formatter import text_html from MoinMoin.log import getLogger from MoinMoin.user import User -from MoinMoin import wikiutil -from MoinSupport import getHeader, getMetadata, getWikiDict, writeHeaders, \ - parseDictEntry -from ItemSupport import ItemStore -from TokenSupport import getIdentifiers +from MoinMoin.wikiutil import parseQueryString, taintfilename, \ + version2timestamp, getInterwikiHomePage + from MoinMessage import GPG, Message, MoinMessageError, \ MoinMessageMissingPart, MoinMessageBadContent, \ is_signed, is_encrypted, getContentAndSignature +from MoinSupport import getHeader, getMetadata, getWikiDict, writeHeaders, \ + parseDictEntry, getStaticContentDirectory +from ItemSupport import ItemStore +from TokenSupport import getIdentifiers + from email.parser import Parser +from os.path import abspath, exists, join import time +import urllib RECIPIENT_PARAMETERS = ("type", "location", "fingerprint") @@ -63,16 +71,21 @@ try: parameters = get_recipient_details(request, message["To"], main=True) except MoinMessageRecipientError, exc: - writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") - request.write("The recipient indicated in the message is not known to this site. " - "Details: %s" % exc.message) - return + + # Reject missing recipients if being strict and not relying only + # on signatures and user actions. + + if getattr(request, "moinmessage_reject_missing_global_recipients", False): + writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") + request.write("The recipient indicated in the message is not known to this site. " + "Details: %s" % exc.message) + return else: if parameters["type"] == "page": self.page = Page(request, parameters["location"]) self.init_store() - # NOTE: Support "url". + # NOTE: Support "url" for message forwarding. # Handle the parsed message. @@ -137,7 +150,7 @@ request.write("Encrypted data must be provided as application/octet-stream.") return - # Reject any unencryptable message. + # Reject any undecryptable message. except MoinMessageError: writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") @@ -190,7 +203,7 @@ if message.date: store_date = time.gmtime(self.store.mtime()) - page_date = time.gmtime(wikiutil.version2timestamp(self.page.mtime_usecs())) + page_date = time.gmtime(version2timestamp(self.page.mtime_usecs())) last_date = max(store_date, page_date) # Reject messages older than the page date. @@ -308,7 +321,7 @@ subpage = getattr(request.cfg, "moinmessage_gpg_recipients_page", "MoinMessageRecipientsDict") if not main: - homedetails = wikiutil.getInterwikiHomePage(request) + homedetails = getInterwikiHomePage(request) if homedetails: homewiki, homepage = homedetails @@ -424,4 +437,137 @@ return result +# Access to static Moin content. + +htdocs = None + +def get_htdocs(request): + + "Use the 'request' to find the htdocs directory." + + global htdocs + htdocs = getStaticContentDirectory(request) + + if not htdocs: + htdocs_in_cfg = getattr(request.cfg, "moinmessage_static_files", None) + if htdocs_in_cfg and exists(htdocs_in_cfg): + htdocs = htdocs_in_cfg + return htdocs + + return htdocs + +# 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 (full path, 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. + """ + + # Detect static resources. + + htdocs_dir = get_htdocs(request) + + if htdocs_dir: + prefix = request.cfg.url_prefix_static + + # Normalise the + + if not prefix.endswith("/"): + prefix += "/" + + if url.startswith(prefix): + filename = url[len(prefix):] + + # Obtain the resource path. + + path = abspath(join(htdocs_dir, filename)) + + if exists(path): + return path, taintfilename(filename) + + # Detect attachments and other resources. + + script = request.getScriptname() + + # Normalise the URL. + + if not script.endswith("/"): + script += "/" + + # Reject URLs outside the wiki. + + 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 {} + + filename = qs.get("target") or qs.get("drawing") + filename = taintfilename(filename) + + # Obtain the attachment path. + + path = AttachFile.getFilename(request, pagename, filename) + return path, filename + +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) + # vim: tabstop=4 expandtab shiftwidth=4 diff -r 46c6c4b00a25 -r 2a3660a2dc5d README.txt --- a/README.txt Mon Jan 27 19:20:33 2014 +0100 +++ b/README.txt Mon Jan 27 21:57:58 2014 +0100 @@ -127,6 +127,13 @@ This causes messages sent to a wiki using the PostMessage action to be rejected if date information is missing. + moinmessage_reject_missing_global_recipients (optional, default is False) + This causes messages sent to a wiki using the PostMessage action to be + rejected if the global recipients mapping does not contain the recipient. + This is potentially useful as an extra measure to reject unsolicited + messages in addition to defining user actions and requiring messages to be + signed by a known identity. + moinmessage_static_files (optional, may refer to the built-in htdocs directory) This explicitly defines the path to static resources used by Moin, enabling such resources to be attached to messages. When set, the path must refer to @@ -303,6 +310,14 @@ MoinMessageRecipientsDict unless overridden by the configuration, as a subpage of their own home page. +The Recipients Mapping and Incoming Messages +-------------------------------------------- + +The recipients mapping can also be used to route incoming messages, and if the +moinmessage_reject_missing_global_recipients setting is enabled, any message +recipient specified in the "To" header of a message that is not present in the +recipients mapping will cause the message to be rejected. + The Relays Mapping ------------------ diff -r 46c6c4b00a25 -r 2a3660a2dc5d actions/SendMessage.py --- a/actions/SendMessage.py Mon Jan 27 19:20:33 2014 +0100 +++ b/actions/SendMessage.py Mon Jan 27 21:57:58 2014 +0100 @@ -6,52 +6,22 @@ @license: GNU GPL (v2 or later), see COPYING.txt for details. """ -from MoinMoin.action import ActionBase, AttachFile -from MoinMoin.formatter import text_html -from MoinMoin.log import getLogger +from MoinMoin.action import ActionBase from MoinMoin.Page import Page -from MoinMoin import config +from MoinMoin.wikiutil import escape, MimeType + from MoinMessage import GPG, MoinMessageError, Message, sendMessage, timestamp, \ as_string from MoinMessageSupport import get_signing_users, get_recipients, get_relays, \ - get_recipient_details, MoinMessageRecipientError + get_recipient_details, \ + MoinMessageRecipientError, OutgoingHTMLFormatter from MoinSupport import * from ItemSupport import ItemStore -from MoinMoin.wikiutil import escape, MimeType, parseQueryString, \ - taintfilename from email.mime.base import MIMEBase from email.mime.image import MIMEImage from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -from os.path import abspath, exists, join -import urllib - -try: - from MoinMoin.web import static - htdocs = abspath(join(static.__file__, "htdocs")) -except ImportError: - htdocs = None - -Dependencies = [] - -def get_htdocs(request): - - "Use the 'request' to find the htdocs directory." - - global htdocs - - if not htdocs: - htdocs_in_cfg = getattr(request.cfg, "moinmessage_static_files", None) - if htdocs_in_cfg and exists(htdocs_in_cfg): - htdocs = htdocs_in_cfg - return htdocs - htdocs_in_data = abspath(join(request.cfg.data_dir, "../htdocs")) - if exists(htdocs_in_data): - htdocs = htdocs_in_data - return htdocs - - return htdocs class SendMessage(ActionBase, ActionSupport): @@ -347,120 +317,6 @@ return getattr(self.request.cfg, "moinmessage_gpg_homedir") -# 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 (full path, 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. - """ - - # Detect static resources. - - htdocs_dir = get_htdocs(request) - - if htdocs_dir: - prefix = request.cfg.url_prefix_static - - # Normalise the - - if not prefix.endswith("/"): - prefix += "/" - - if url.startswith(prefix): - filename = url[len(prefix):] - - # Obtain the resource path. - - path = abspath(join(htdocs_dir, filename)) - - if exists(path): - return path, taintfilename(filename) - - # Detect attachments and other resources. - - script = request.getScriptname() - - # Normalise the URL. - - if not script.endswith("/"): - script += "/" - - # Reject URLs outside the wiki. - - 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 {} - - filename = qs.get("target") or qs.get("drawing") - filename = taintfilename(filename) - - # Obtain the attachment path. - - path = AttachFile.getFilename(request, pagename, filename) - return path, filename - -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. def execute(pagename, request):