1.1 --- a/MoinMessage.py Mon Jan 27 19:20:33 2014 +0100
1.2 +++ b/MoinMessage.py Mon Jan 27 21:57:58 2014 +0100
1.3 @@ -151,6 +151,12 @@
1.4 class MoinMessageBadContent(MoinMessageDecodingError):
1.5 pass
1.6
1.7 +class MoinMessageTransferError(MoinMessageError):
1.8 + def __init__(self, code, message, body):
1.9 + MoinMessageError.__init__(self, message)
1.10 + self.code = code
1.11 + self.body = body
1.12 +
1.13 class GPG:
1.14
1.15 "A wrapper around the gpg command using a particular configuration."
1.16 @@ -521,7 +527,7 @@
1.17 resp = req.getresponse()
1.18
1.19 if resp.status >= 400:
1.20 - raise MoinMessageError, "Message sending failed (%s): %s" % (resp.status, resp.read())
1.21 + raise MoinMessageTransferError(resp.status, "Message sending failed (%s)" % resp.status, resp.read())
1.22
1.23 return resp
1.24
2.1 --- a/MoinMessageSupport.py Mon Jan 27 19:20:33 2014 +0100
2.2 +++ b/MoinMessageSupport.py Mon Jan 27 21:57:58 2014 +0100
2.3 @@ -6,19 +6,27 @@
2.4 @license: GNU GPL (v2 or later), see COPYING.txt for details.
2.5 """
2.6
2.7 +from MoinMoin import config
2.8 from MoinMoin.Page import Page
2.9 +from MoinMoin.action import AttachFile
2.10 +from MoinMoin.formatter import text_html
2.11 from MoinMoin.log import getLogger
2.12 from MoinMoin.user import User
2.13 -from MoinMoin import wikiutil
2.14 -from MoinSupport import getHeader, getMetadata, getWikiDict, writeHeaders, \
2.15 - parseDictEntry
2.16 -from ItemSupport import ItemStore
2.17 -from TokenSupport import getIdentifiers
2.18 +from MoinMoin.wikiutil import parseQueryString, taintfilename, \
2.19 + version2timestamp, getInterwikiHomePage
2.20 +
2.21 from MoinMessage import GPG, Message, MoinMessageError, \
2.22 MoinMessageMissingPart, MoinMessageBadContent, \
2.23 is_signed, is_encrypted, getContentAndSignature
2.24 +from MoinSupport import getHeader, getMetadata, getWikiDict, writeHeaders, \
2.25 + parseDictEntry, getStaticContentDirectory
2.26 +from ItemSupport import ItemStore
2.27 +from TokenSupport import getIdentifiers
2.28 +
2.29 from email.parser import Parser
2.30 +from os.path import abspath, exists, join
2.31 import time
2.32 +import urllib
2.33
2.34 RECIPIENT_PARAMETERS = ("type", "location", "fingerprint")
2.35
2.36 @@ -63,16 +71,21 @@
2.37 try:
2.38 parameters = get_recipient_details(request, message["To"], main=True)
2.39 except MoinMessageRecipientError, exc:
2.40 - writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
2.41 - request.write("The recipient indicated in the message is not known to this site. "
2.42 - "Details: %s" % exc.message)
2.43 - return
2.44 +
2.45 + # Reject missing recipients if being strict and not relying only
2.46 + # on signatures and user actions.
2.47 +
2.48 + if getattr(request, "moinmessage_reject_missing_global_recipients", False):
2.49 + writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
2.50 + request.write("The recipient indicated in the message is not known to this site. "
2.51 + "Details: %s" % exc.message)
2.52 + return
2.53 else:
2.54 if parameters["type"] == "page":
2.55 self.page = Page(request, parameters["location"])
2.56 self.init_store()
2.57
2.58 - # NOTE: Support "url".
2.59 + # NOTE: Support "url" for message forwarding.
2.60
2.61 # Handle the parsed message.
2.62
2.63 @@ -137,7 +150,7 @@
2.64 request.write("Encrypted data must be provided as application/octet-stream.")
2.65 return
2.66
2.67 - # Reject any unencryptable message.
2.68 + # Reject any undecryptable message.
2.69
2.70 except MoinMessageError:
2.71 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
2.72 @@ -190,7 +203,7 @@
2.73
2.74 if message.date:
2.75 store_date = time.gmtime(self.store.mtime())
2.76 - page_date = time.gmtime(wikiutil.version2timestamp(self.page.mtime_usecs()))
2.77 + page_date = time.gmtime(version2timestamp(self.page.mtime_usecs()))
2.78 last_date = max(store_date, page_date)
2.79
2.80 # Reject messages older than the page date.
2.81 @@ -308,7 +321,7 @@
2.82 subpage = getattr(request.cfg, "moinmessage_gpg_recipients_page", "MoinMessageRecipientsDict")
2.83
2.84 if not main:
2.85 - homedetails = wikiutil.getInterwikiHomePage(request)
2.86 + homedetails = getInterwikiHomePage(request)
2.87
2.88 if homedetails:
2.89 homewiki, homepage = homedetails
2.90 @@ -424,4 +437,137 @@
2.91
2.92 return result
2.93
2.94 +# Access to static Moin content.
2.95 +
2.96 +htdocs = None
2.97 +
2.98 +def get_htdocs(request):
2.99 +
2.100 + "Use the 'request' to find the htdocs directory."
2.101 +
2.102 + global htdocs
2.103 + htdocs = getStaticContentDirectory(request)
2.104 +
2.105 + if not htdocs:
2.106 + htdocs_in_cfg = getattr(request.cfg, "moinmessage_static_files", None)
2.107 + if htdocs_in_cfg and exists(htdocs_in_cfg):
2.108 + htdocs = htdocs_in_cfg
2.109 + return htdocs
2.110 +
2.111 + return htdocs
2.112 +
2.113 +# Special message formatters.
2.114 +
2.115 +def unquoteWikinameURL(url, charset=config.charset):
2.116 +
2.117 + """
2.118 + The inverse of wikiutil.quoteWikinameURL, returning the page name referenced
2.119 + by the given 'url', with the page name assumed to be encoded using the given
2.120 + 'charset' (or default charset if omitted).
2.121 + """
2.122 +
2.123 + return unicode(urllib.unquote(url), encoding=charset)
2.124 +
2.125 +def getAttachmentFromURL(url, request):
2.126 +
2.127 + """
2.128 + Return a (full path, attachment filename) tuple for the attachment
2.129 + referenced by the given 'url', using the 'request' to interpret the
2.130 + structure of 'url'.
2.131 +
2.132 + If 'url' does not refer to an attachment on this wiki, None is returned.
2.133 + """
2.134 +
2.135 + # Detect static resources.
2.136 +
2.137 + htdocs_dir = get_htdocs(request)
2.138 +
2.139 + if htdocs_dir:
2.140 + prefix = request.cfg.url_prefix_static
2.141 +
2.142 + # Normalise the
2.143 +
2.144 + if not prefix.endswith("/"):
2.145 + prefix += "/"
2.146 +
2.147 + if url.startswith(prefix):
2.148 + filename = url[len(prefix):]
2.149 +
2.150 + # Obtain the resource path.
2.151 +
2.152 + path = abspath(join(htdocs_dir, filename))
2.153 +
2.154 + if exists(path):
2.155 + return path, taintfilename(filename)
2.156 +
2.157 + # Detect attachments and other resources.
2.158 +
2.159 + script = request.getScriptname()
2.160 +
2.161 + # Normalise the URL.
2.162 +
2.163 + if not script.endswith("/"):
2.164 + script += "/"
2.165 +
2.166 + # Reject URLs outside the wiki.
2.167 +
2.168 + if not url.startswith(script):
2.169 + return None
2.170 +
2.171 + path = url[len(script):].lstrip("/")
2.172 + try:
2.173 + qpagename, qs = path.split("?", 1)
2.174 + except ValueError:
2.175 + qpagename = path
2.176 + qs = None
2.177 +
2.178 + pagename = unquoteWikinameURL(qpagename)
2.179 + qs = qs and parseQueryString(qs) or {}
2.180 +
2.181 + filename = qs.get("target") or qs.get("drawing")
2.182 + filename = taintfilename(filename)
2.183 +
2.184 + # Obtain the attachment path.
2.185 +
2.186 + path = AttachFile.getFilename(request, pagename, filename)
2.187 + return path, filename
2.188 +
2.189 +class OutgoingHTMLFormatter(text_html.Formatter):
2.190 +
2.191 + """
2.192 + Handle outgoing HTML content by identifying attachments and rewriting their
2.193 + locations. References to bundled attachments are done using RFC 2111:
2.194 +
2.195 + https://tools.ietf.org/html/rfc2111
2.196 +
2.197 + Messages employing references between parts are meant to comply with RFC
2.198 + 2387:
2.199 +
2.200 + https://tools.ietf.org/html/rfc2387
2.201 + """
2.202 +
2.203 + def __init__(self, request, **kw):
2.204 + text_html.Formatter.__init__(self, request, **kw)
2.205 + self.attachments = []
2.206 +
2.207 + def add_attachment(self, location):
2.208 + details = getAttachmentFromURL(location, self.request)
2.209 + if details:
2.210 + pos = len(self.attachments)
2.211 + self.attachments.append(details)
2.212 + return "cid:attachment%d" % pos
2.213 + else:
2.214 + return None
2.215 +
2.216 + def image(self, src=None, **kw):
2.217 + src = src or kw.get("src")
2.218 + ref = src and self.add_attachment(src)
2.219 + return text_html.Formatter.image(self, ref or src, **kw)
2.220 +
2.221 + def transclusion(self, on, **kw):
2.222 + if on:
2.223 + data = kw.get("data")
2.224 + kw["data"] = data and self.add_attachment(data)
2.225 + return text_html.Formatter.transclusion(self, on, **kw)
2.226 +
2.227 # vim: tabstop=4 expandtab shiftwidth=4
3.1 --- a/README.txt Mon Jan 27 19:20:33 2014 +0100
3.2 +++ b/README.txt Mon Jan 27 21:57:58 2014 +0100
3.3 @@ -127,6 +127,13 @@
3.4 This causes messages sent to a wiki using the PostMessage action to be
3.5 rejected if date information is missing.
3.6
3.7 + moinmessage_reject_missing_global_recipients (optional, default is False)
3.8 + This causes messages sent to a wiki using the PostMessage action to be
3.9 + rejected if the global recipients mapping does not contain the recipient.
3.10 + This is potentially useful as an extra measure to reject unsolicited
3.11 + messages in addition to defining user actions and requiring messages to be
3.12 + signed by a known identity.
3.13 +
3.14 moinmessage_static_files (optional, may refer to the built-in htdocs directory)
3.15 This explicitly defines the path to static resources used by Moin, enabling
3.16 such resources to be attached to messages. When set, the path must refer to
3.17 @@ -303,6 +310,14 @@
3.18 MoinMessageRecipientsDict unless overridden by the configuration, as a subpage
3.19 of their own home page.
3.20
3.21 +The Recipients Mapping and Incoming Messages
3.22 +--------------------------------------------
3.23 +
3.24 +The recipients mapping can also be used to route incoming messages, and if the
3.25 +moinmessage_reject_missing_global_recipients setting is enabled, any message
3.26 +recipient specified in the "To" header of a message that is not present in the
3.27 +recipients mapping will cause the message to be rejected.
3.28 +
3.29 The Relays Mapping
3.30 ------------------
3.31
4.1 --- a/actions/SendMessage.py Mon Jan 27 19:20:33 2014 +0100
4.2 +++ b/actions/SendMessage.py Mon Jan 27 21:57:58 2014 +0100
4.3 @@ -6,52 +6,22 @@
4.4 @license: GNU GPL (v2 or later), see COPYING.txt for details.
4.5 """
4.6
4.7 -from MoinMoin.action import ActionBase, AttachFile
4.8 -from MoinMoin.formatter import text_html
4.9 -from MoinMoin.log import getLogger
4.10 +from MoinMoin.action import ActionBase
4.11 from MoinMoin.Page import Page
4.12 -from MoinMoin import config
4.13 +from MoinMoin.wikiutil import escape, MimeType
4.14 +
4.15 from MoinMessage import GPG, MoinMessageError, Message, sendMessage, timestamp, \
4.16 as_string
4.17 from MoinMessageSupport import get_signing_users, get_recipients, get_relays, \
4.18 - get_recipient_details, MoinMessageRecipientError
4.19 + get_recipient_details, \
4.20 + MoinMessageRecipientError, OutgoingHTMLFormatter
4.21 from MoinSupport import *
4.22 from ItemSupport import ItemStore
4.23 -from MoinMoin.wikiutil import escape, MimeType, parseQueryString, \
4.24 - taintfilename
4.25
4.26 from email.mime.base import MIMEBase
4.27 from email.mime.image import MIMEImage
4.28 from email.mime.multipart import MIMEMultipart
4.29 from email.mime.text import MIMEText
4.30 -from os.path import abspath, exists, join
4.31 -import urllib
4.32 -
4.33 -try:
4.34 - from MoinMoin.web import static
4.35 - htdocs = abspath(join(static.__file__, "htdocs"))
4.36 -except ImportError:
4.37 - htdocs = None
4.38 -
4.39 -Dependencies = []
4.40 -
4.41 -def get_htdocs(request):
4.42 -
4.43 - "Use the 'request' to find the htdocs directory."
4.44 -
4.45 - global htdocs
4.46 -
4.47 - if not htdocs:
4.48 - htdocs_in_cfg = getattr(request.cfg, "moinmessage_static_files", None)
4.49 - if htdocs_in_cfg and exists(htdocs_in_cfg):
4.50 - htdocs = htdocs_in_cfg
4.51 - return htdocs
4.52 - htdocs_in_data = abspath(join(request.cfg.data_dir, "../htdocs"))
4.53 - if exists(htdocs_in_data):
4.54 - htdocs = htdocs_in_data
4.55 - return htdocs
4.56 -
4.57 - return htdocs
4.58
4.59 class SendMessage(ActionBase, ActionSupport):
4.60
4.61 @@ -347,120 +317,6 @@
4.62
4.63 return getattr(self.request.cfg, "moinmessage_gpg_homedir")
4.64
4.65 -# Special message formatters.
4.66 -
4.67 -def unquoteWikinameURL(url, charset=config.charset):
4.68 -
4.69 - """
4.70 - The inverse of wikiutil.quoteWikinameURL, returning the page name referenced
4.71 - by the given 'url', with the page name assumed to be encoded using the given
4.72 - 'charset' (or default charset if omitted).
4.73 - """
4.74 -
4.75 - return unicode(urllib.unquote(url), encoding=charset)
4.76 -
4.77 -def getAttachmentFromURL(url, request):
4.78 -
4.79 - """
4.80 - Return a (full path, attachment filename) tuple for the attachment
4.81 - referenced by the given 'url', using the 'request' to interpret the
4.82 - structure of 'url'.
4.83 -
4.84 - If 'url' does not refer to an attachment on this wiki, None is returned.
4.85 - """
4.86 -
4.87 - # Detect static resources.
4.88 -
4.89 - htdocs_dir = get_htdocs(request)
4.90 -
4.91 - if htdocs_dir:
4.92 - prefix = request.cfg.url_prefix_static
4.93 -
4.94 - # Normalise the
4.95 -
4.96 - if not prefix.endswith("/"):
4.97 - prefix += "/"
4.98 -
4.99 - if url.startswith(prefix):
4.100 - filename = url[len(prefix):]
4.101 -
4.102 - # Obtain the resource path.
4.103 -
4.104 - path = abspath(join(htdocs_dir, filename))
4.105 -
4.106 - if exists(path):
4.107 - return path, taintfilename(filename)
4.108 -
4.109 - # Detect attachments and other resources.
4.110 -
4.111 - script = request.getScriptname()
4.112 -
4.113 - # Normalise the URL.
4.114 -
4.115 - if not script.endswith("/"):
4.116 - script += "/"
4.117 -
4.118 - # Reject URLs outside the wiki.
4.119 -
4.120 - if not url.startswith(script):
4.121 - return None
4.122 -
4.123 - path = url[len(script):].lstrip("/")
4.124 - try:
4.125 - qpagename, qs = path.split("?", 1)
4.126 - except ValueError:
4.127 - qpagename = path
4.128 - qs = None
4.129 -
4.130 - pagename = unquoteWikinameURL(qpagename)
4.131 - qs = qs and parseQueryString(qs) or {}
4.132 -
4.133 - filename = qs.get("target") or qs.get("drawing")
4.134 - filename = taintfilename(filename)
4.135 -
4.136 - # Obtain the attachment path.
4.137 -
4.138 - path = AttachFile.getFilename(request, pagename, filename)
4.139 - return path, filename
4.140 -
4.141 -class OutgoingHTMLFormatter(text_html.Formatter):
4.142 -
4.143 - """
4.144 - Handle outgoing HTML content by identifying attachments and rewriting their
4.145 - locations. References to bundled attachments are done using RFC 2111:
4.146 -
4.147 - https://tools.ietf.org/html/rfc2111
4.148 -
4.149 - Messages employing references between parts are meant to comply with RFC
4.150 - 2387:
4.151 -
4.152 - https://tools.ietf.org/html/rfc2387
4.153 - """
4.154 -
4.155 - def __init__(self, request, **kw):
4.156 - text_html.Formatter.__init__(self, request, **kw)
4.157 - self.attachments = []
4.158 -
4.159 - def add_attachment(self, location):
4.160 - details = getAttachmentFromURL(location, self.request)
4.161 - if details:
4.162 - pos = len(self.attachments)
4.163 - self.attachments.append(details)
4.164 - return "cid:attachment%d" % pos
4.165 - else:
4.166 - return None
4.167 -
4.168 - def image(self, src=None, **kw):
4.169 - src = src or kw.get("src")
4.170 - ref = src and self.add_attachment(src)
4.171 - return text_html.Formatter.image(self, ref or src, **kw)
4.172 -
4.173 - def transclusion(self, on, **kw):
4.174 - if on:
4.175 - data = kw.get("data")
4.176 - kw["data"] = data and self.add_attachment(data)
4.177 - return text_html.Formatter.transclusion(self, on, **kw)
4.178 -
4.179 # Action function.
4.180
4.181 def execute(pagename, request):