1.1 --- a/actions/PostMessage.py Fri Apr 19 15:15:40 2013 +0200
1.2 +++ b/actions/PostMessage.py Fri May 17 01:45:12 2013 +0200
1.3 @@ -12,7 +12,7 @@
1.4 from MoinMoin.user import User
1.5 from MoinMoin import wikiutil
1.6 from MoinSupport import ItemStore, getHeader, getMetadata, getWikiDict, writeHeaders
1.7 -from MoinMessage import GPG, Message, MoinMessageError
1.8 +from MoinMessage import GPG, Message, MoinMessageError, is_collection
1.9 from email.parser import Parser
1.10 import time
1.11
1.12 @@ -241,7 +241,7 @@
1.13
1.14 # Handle a single part.
1.15
1.16 - if not update.is_multipart():
1.17 + if not is_collection(update):
1.18 self.handle_message_parts([update], update)
1.19
1.20 # Or a collection of alternative representations for a single
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2.2 +++ b/actions/ReadMessage.py Fri May 17 01:45:12 2013 +0200
2.3 @@ -0,0 +1,144 @@
2.4 +# -*- coding: iso-8859-1 -*-
2.5 +"""
2.6 + MoinMoin - ReadMessage Action
2.7 +
2.8 + @copyright: 2012, 2013 by Paul Boddie <paul@boddie.org.uk>
2.9 + @license: GNU GPL (v2 or later), see COPYING.txt for details.
2.10 +"""
2.11 +
2.12 +from MoinMoin.action import ActionBase
2.13 +from MoinSupport import *
2.14 +from email.parser import Parser
2.15 +
2.16 +try:
2.17 + from cStringIO import StringIO
2.18 +except ImportError:
2.19 + from StringIO import StringIO
2.20 +
2.21 +Dependencies = []
2.22 +
2.23 +class ReadMessage(ActionBase, ActionSupport):
2.24 +
2.25 + "An action that can read a stored message component."
2.26 +
2.27 + def __init__(self, pagename, request):
2.28 +
2.29 + """
2.30 + On the page with the given 'pagename', use the given 'request' to access
2.31 + message components.
2.32 + """
2.33 +
2.34 + ActionBase.__init__(self, pagename, request)
2.35 + self.store = ItemStore(self.page, "messages", "message-locks")
2.36 +
2.37 + def get_form_html(self, buttons_html):
2.38 +
2.39 + "Present an interface for accessing a message component."
2.40 +
2.41 + _ = self._
2.42 + request = self.request
2.43 + form = self.get_form()
2.44 +
2.45 + message = form.get("message", [""])[0]
2.46 + part = form.get("part", [""])[0]
2.47 +
2.48 + # Fill in the fields and labels.
2.49 +
2.50 + d = {
2.51 + "buttons_html" : buttons_html,
2.52 + "message_label" : _("Message number"),
2.53 + "message_default" : escattr(message),
2.54 + "part_label" : _("Part identifier"),
2.55 + "part_default" : escattr(part),
2.56 + }
2.57 +
2.58 + # Prepare the output HTML.
2.59 +
2.60 + html = '''
2.61 +<table>
2.62 + <tr>
2.63 + <td class="label"><label>%(message_label)s</label></td>
2.64 + <td>
2.65 + <input name="message" type="text" value="%(message_default)s" />
2.66 + </td>
2.67 + </tr>
2.68 + <tr>
2.69 + <td class="label"><label>%(part_label)s</label></td>
2.70 + <td>
2.71 + <input name="part" type="text" value="%(part_default)s" />
2.72 + </td>
2.73 + </tr>
2.74 + <tr>
2.75 + <td></td>
2.76 + <td class="buttons">
2.77 + %(buttons_html)s
2.78 + </td>
2.79 + </tr>
2.80 +</table>''' % d
2.81 +
2.82 + return html
2.83 +
2.84 + def do_action(self):
2.85 +
2.86 + "Attempt to send the message."
2.87 +
2.88 + _ = self._
2.89 + request = self.request
2.90 + form = self.get_form()
2.91 +
2.92 + message_number = form.get("message", [None])[0]
2.93 + part_identifier = form.get("part", [None])[0]
2.94 +
2.95 + if not message_number:
2.96 + return 0, _("A message number must be given.")
2.97 +
2.98 + if not part_identifier:
2.99 + return 0, _("A part identifier must be given.")
2.100 +
2.101 + # Obtain the message.
2.102 +
2.103 + try:
2.104 + message_text = self.store[int(message_number)]
2.105 + except (IndexError, ValueError):
2.106 + return 0, _("No such message is stored on this page.")
2.107 +
2.108 + # Visit the message parts, looking for the indicated component.
2.109 +
2.110 + message = Parser().parse(StringIO(message_text))
2.111 +
2.112 + if message.is_multipart():
2.113 + for part in message.get_payload():
2.114 +
2.115 + # For the selected component, return the content as a response
2.116 + # to the current request.
2.117 +
2.118 + if part.get("Content-ID") == part_identifier:
2.119 + charset = part.get_content_charset()
2.120 + headers = [
2.121 + "Content-Type: %s%s" % (
2.122 + part.get_content_type(),
2.123 + charset and ("; charset=%s" % charset) or ""
2.124 + )
2.125 + ]
2.126 + get_send_headers(request)(headers)
2.127 + request.write(part.get_payload(decode=True))
2.128 + return 1, None
2.129 +
2.130 + return 0, _("No such component in the indicated message.")
2.131 +
2.132 + def render_success(self, msg, msgtype=None):
2.133 +
2.134 + """
2.135 + Render neither 'msg' nor 'msgtype' since a resource has already been
2.136 + produced.
2.137 + NOTE: msgtype is optional because MoinMoin 1.5.x does not support it.
2.138 + """
2.139 +
2.140 + pass
2.141 +
2.142 +# Action function.
2.143 +
2.144 +def execute(pagename, request):
2.145 + ReadMessage(pagename, request).render()
2.146 +
2.147 +# vim: tabstop=4 expandtab shiftwidth=4
3.1 --- a/actions/SendMessage.py Fri Apr 19 15:15:40 2013 +0200
3.2 +++ b/actions/SendMessage.py Fri May 17 01:45:12 2013 +0200
3.3 @@ -6,12 +6,18 @@
3.4 @license: GNU GPL (v2 or later), see COPYING.txt for details.
3.5 """
3.6
3.7 -from MoinMoin.action import ActionBase
3.8 +from MoinMoin.action import ActionBase, AttachFile
3.9 +from MoinMoin.formatter import text_html
3.10 from MoinMoin.log import getLogger
3.11 +from MoinMoin import config
3.12 from MoinMessage import GPG, MoinMessageError, Message, sendMessage
3.13 from MoinSupport import *
3.14 -from MoinMoin.wikiutil import escape
3.15 +from MoinMoin.wikiutil import escape, MimeType, parseQueryString, taintfilename
3.16 +
3.17 +from email.mime.image import MIMEImage
3.18 +from email.mime.multipart import MIMEMultipart
3.19 from email.mime.text import MIMEText
3.20 +import urllib
3.21
3.22 Dependencies = []
3.23
3.24 @@ -29,6 +35,7 @@
3.25
3.26 message = form.get("message", [""])[0]
3.27 recipient = form.get("recipient", [""])[0]
3.28 + preview = form.get("preview", [""])[0]
3.29
3.30 # Get a list of potential recipients.
3.31
3.32 @@ -43,6 +50,11 @@
3.33
3.34 recipients_list.sort()
3.35
3.36 + # Prepare any preview.
3.37 +
3.38 + request.formatter.setPage(self.page)
3.39 + preview_output = preview and formatText(message, request, request.formatter) or ""
3.40 +
3.41 # Fill in the fields and labels.
3.42
3.43 d = {
3.44 @@ -50,7 +62,9 @@
3.45 "recipient_label" : _("Recipient"),
3.46 "recipients_list" : "\n".join(recipients_list),
3.47 "message_label" : _("Message text"),
3.48 - "message_default" : escattr(message),
3.49 + "message_default" : escape(message),
3.50 + "preview_label" : _("Preview message"),
3.51 + "preview_output" : preview_output,
3.52 }
3.53
3.54 # Prepare the output HTML.
3.55 @@ -67,13 +81,25 @@
3.56 </tr>
3.57 <tr>
3.58 <td class="label"><label>%(message_label)s</label></td>
3.59 - <td colspan="2">
3.60 - <input name="message" type="text" size="40" value="%(message_default)s" />
3.61 + <td>
3.62 + <textarea name="message" cols="60" rows="10">%(message_default)s</textarea>
3.63 </td>
3.64 </tr>
3.65 <tr>
3.66 <td></td>
3.67 - <td colspan="2" class="buttons">
3.68 + <td class="buttons">
3.69 + <input name="preview" type="submit" value="%(preview_label)s" />
3.70 + </td>
3.71 + </tr>
3.72 + <tr>
3.73 + <td></td>
3.74 + <td class="moinmessage-preview">
3.75 +%(preview_output)s
3.76 + </td>
3.77 + </tr>
3.78 + <tr>
3.79 + <td></td>
3.80 + <td class="buttons">
3.81 %(buttons_html)s
3.82 </td>
3.83 </tr>
3.84 @@ -107,13 +133,58 @@
3.85 # Construct a message from the request.
3.86
3.87 message = Message()
3.88 - message.add_update(MIMEText(text, "moin"))
3.89 +
3.90 + container = MIMEMultipart("related")
3.91 + container["Update-Action"] = "store"
3.92 +
3.93 + # Add the message body and any attachments.
3.94 +
3.95 + fmt = OutgoingHTMLFormatter(request)
3.96 + fmt.setPage(request.page)
3.97 + body = formatText(text, request, fmt)
3.98 +
3.99 + container.attach(MIMEText(body, "html"))
3.100 +
3.101 + for pos, (pagename, filename) in enumerate(fmt.attachments):
3.102 +
3.103 + # Obtain the attachment path.
3.104 +
3.105 + filename = taintfilename(filename)
3.106 + path = AttachFile.getFilename(request, pagename, filename)
3.107 +
3.108 + # Obtain the attachment content.
3.109 +
3.110 + f = open(path, "rb")
3.111 + try:
3.112 + body = f.read()
3.113 + finally:
3.114 + f.close()
3.115 +
3.116 + # Determine the attachment type.
3.117 +
3.118 + mimetype = MimeType(filename=filename)
3.119 +
3.120 + # NOTE: Support a limited set of explicit part types for now.
3.121 +
3.122 + if mimetype.major == "image":
3.123 + part = MIMEImage(body, mimetype.minor, **mimetype.params)
3.124 + elif mimetype.major == "text":
3.125 + part = MIMEText(body, mimetype.minor, mimetype.charset, **mimetype.params)
3.126 + else:
3.127 + part = MIMEApplication(body, mimetype.minor, **mimetype.params)
3.128 +
3.129 + # Label the attachment and include it in the message.
3.130 +
3.131 + part["Content-ID"] = "attachment%d" % pos
3.132 + container.attach(part)
3.133 +
3.134 + message.add_update(container)
3.135
3.136 # Get the sender details for signing messages.
3.137 # This is not the same as the details for authenticating users in the
3.138 # PostMessage action since the fingerprints refer to public keys.
3.139
3.140 - signing_users = getWikiDict(getattr(request.cfg, "moinmessage_gpg_signing_users_page", "MoinMessageSigningUserDict"), request)
3.141 + signing_users = self.get_signing_users()
3.142 signer = signing_users and signing_users.get(request.user.name)
3.143
3.144 # Get the recipient details.
3.145 @@ -153,7 +224,89 @@
3.146 return getattr(self.request.cfg, "moinmessage_gpg_homedir")
3.147
3.148 def get_recipients(self):
3.149 - return getWikiDict(getattr(self.request.cfg, "moinmessage_gpg_recipients_page", "MoinMessageRecipientsDict"), self.request)
3.150 + return getWikiDict(
3.151 + getattr(self.request.cfg, "moinmessage_gpg_recipients_page", "MoinMessageRecipientsDict"),
3.152 + self.request)
3.153 +
3.154 + def get_signing_users(self):
3.155 + return getWikiDict(
3.156 + getattr(self.request.cfg, "moinmessage_gpg_signing_users_page", "MoinMessageSigningUserDict"),
3.157 + self.request)
3.158 +
3.159 +# Special message formatters.
3.160 +
3.161 +def unquoteWikinameURL(url, charset=config.charset):
3.162 +
3.163 + """
3.164 + The inverse of wikiutil.quoteWikinameURL, returning the page name referenced
3.165 + by the given 'url', with the page name assumed to be encoded using the given
3.166 + 'charset' (or default charset if omitted).
3.167 + """
3.168 +
3.169 + return unicode(urllib.unquote(url), encoding=charset)
3.170 +
3.171 +def getAttachmentFromURL(url, request):
3.172 +
3.173 + """
3.174 + Return a (page name, attachment filename) tuple for the attachment
3.175 + referenced by the given 'url', using the 'request' to interpret the
3.176 + structure of 'url'.
3.177 +
3.178 + If 'url' does not refer to an attachment on this wiki, None is returned.
3.179 + """
3.180 +
3.181 + script = request.getScriptname()
3.182 + if not url.startswith(script):
3.183 + return None
3.184 +
3.185 + path = url[len(script):].lstrip("/")
3.186 + try:
3.187 + qpagename, qs = path.split("?", 1)
3.188 + except ValueError:
3.189 + qpagename = path
3.190 + qs = None
3.191 +
3.192 + pagename = unquoteWikinameURL(qpagename)
3.193 + qs = qs and parseQueryString(qs) or {}
3.194 + return pagename, qs.get("target") or qs.get("drawing")
3.195 +
3.196 +class OutgoingHTMLFormatter(text_html.Formatter):
3.197 +
3.198 + """
3.199 + Handle outgoing HTML content by identifying attachments and rewriting their
3.200 + locations. References to bundled attachments are done using RFC 2111:
3.201 +
3.202 + https://tools.ietf.org/html/rfc2111
3.203 +
3.204 + Messages employing references between parts are meant to comply with RFC
3.205 + 2387:
3.206 +
3.207 + https://tools.ietf.org/html/rfc2387
3.208 + """
3.209 +
3.210 + def __init__(self, request, **kw):
3.211 + text_html.Formatter.__init__(self, request, **kw)
3.212 + self.attachments = []
3.213 +
3.214 + def add_attachment(self, location):
3.215 + details = getAttachmentFromURL(location, self.request)
3.216 + if details:
3.217 + pos = len(self.attachments)
3.218 + self.attachments.append(details)
3.219 + return "cid:attachment%d" % pos
3.220 + else:
3.221 + return None
3.222 +
3.223 + def image(self, src=None, **kw):
3.224 + src = src or kw.get("src")
3.225 + ref = src and self.add_attachment(src)
3.226 + return text_html.Formatter.image(self, ref or src, **kw)
3.227 +
3.228 + def transclusion(self, on, **kw):
3.229 + if on:
3.230 + data = kw.get("data")
3.231 + kw["data"] = data and self.add_attachment(data)
3.232 + return text_html.Formatter.transclusion(self, on, **kw)
3.233
3.234 # Action function.
3.235