1.1 --- a/actions/SendMessage.py Fri Apr 19 15:15:40 2013 +0200
1.2 +++ b/actions/SendMessage.py Fri May 17 01:45:12 2013 +0200
1.3 @@ -6,12 +6,18 @@
1.4 @license: GNU GPL (v2 or later), see COPYING.txt for details.
1.5 """
1.6
1.7 -from MoinMoin.action import ActionBase
1.8 +from MoinMoin.action import ActionBase, AttachFile
1.9 +from MoinMoin.formatter import text_html
1.10 from MoinMoin.log import getLogger
1.11 +from MoinMoin import config
1.12 from MoinMessage import GPG, MoinMessageError, Message, sendMessage
1.13 from MoinSupport import *
1.14 -from MoinMoin.wikiutil import escape
1.15 +from MoinMoin.wikiutil import escape, MimeType, parseQueryString, taintfilename
1.16 +
1.17 +from email.mime.image import MIMEImage
1.18 +from email.mime.multipart import MIMEMultipart
1.19 from email.mime.text import MIMEText
1.20 +import urllib
1.21
1.22 Dependencies = []
1.23
1.24 @@ -29,6 +35,7 @@
1.25
1.26 message = form.get("message", [""])[0]
1.27 recipient = form.get("recipient", [""])[0]
1.28 + preview = form.get("preview", [""])[0]
1.29
1.30 # Get a list of potential recipients.
1.31
1.32 @@ -43,6 +50,11 @@
1.33
1.34 recipients_list.sort()
1.35
1.36 + # Prepare any preview.
1.37 +
1.38 + request.formatter.setPage(self.page)
1.39 + preview_output = preview and formatText(message, request, request.formatter) or ""
1.40 +
1.41 # Fill in the fields and labels.
1.42
1.43 d = {
1.44 @@ -50,7 +62,9 @@
1.45 "recipient_label" : _("Recipient"),
1.46 "recipients_list" : "\n".join(recipients_list),
1.47 "message_label" : _("Message text"),
1.48 - "message_default" : escattr(message),
1.49 + "message_default" : escape(message),
1.50 + "preview_label" : _("Preview message"),
1.51 + "preview_output" : preview_output,
1.52 }
1.53
1.54 # Prepare the output HTML.
1.55 @@ -67,13 +81,25 @@
1.56 </tr>
1.57 <tr>
1.58 <td class="label"><label>%(message_label)s</label></td>
1.59 - <td colspan="2">
1.60 - <input name="message" type="text" size="40" value="%(message_default)s" />
1.61 + <td>
1.62 + <textarea name="message" cols="60" rows="10">%(message_default)s</textarea>
1.63 </td>
1.64 </tr>
1.65 <tr>
1.66 <td></td>
1.67 - <td colspan="2" class="buttons">
1.68 + <td class="buttons">
1.69 + <input name="preview" type="submit" value="%(preview_label)s" />
1.70 + </td>
1.71 + </tr>
1.72 + <tr>
1.73 + <td></td>
1.74 + <td class="moinmessage-preview">
1.75 +%(preview_output)s
1.76 + </td>
1.77 + </tr>
1.78 + <tr>
1.79 + <td></td>
1.80 + <td class="buttons">
1.81 %(buttons_html)s
1.82 </td>
1.83 </tr>
1.84 @@ -107,13 +133,58 @@
1.85 # Construct a message from the request.
1.86
1.87 message = Message()
1.88 - message.add_update(MIMEText(text, "moin"))
1.89 +
1.90 + container = MIMEMultipart("related")
1.91 + container["Update-Action"] = "store"
1.92 +
1.93 + # Add the message body and any attachments.
1.94 +
1.95 + fmt = OutgoingHTMLFormatter(request)
1.96 + fmt.setPage(request.page)
1.97 + body = formatText(text, request, fmt)
1.98 +
1.99 + container.attach(MIMEText(body, "html"))
1.100 +
1.101 + for pos, (pagename, filename) in enumerate(fmt.attachments):
1.102 +
1.103 + # Obtain the attachment path.
1.104 +
1.105 + filename = taintfilename(filename)
1.106 + path = AttachFile.getFilename(request, pagename, filename)
1.107 +
1.108 + # Obtain the attachment content.
1.109 +
1.110 + f = open(path, "rb")
1.111 + try:
1.112 + body = f.read()
1.113 + finally:
1.114 + f.close()
1.115 +
1.116 + # Determine the attachment type.
1.117 +
1.118 + mimetype = MimeType(filename=filename)
1.119 +
1.120 + # NOTE: Support a limited set of explicit part types for now.
1.121 +
1.122 + if mimetype.major == "image":
1.123 + part = MIMEImage(body, mimetype.minor, **mimetype.params)
1.124 + elif mimetype.major == "text":
1.125 + part = MIMEText(body, mimetype.minor, mimetype.charset, **mimetype.params)
1.126 + else:
1.127 + part = MIMEApplication(body, mimetype.minor, **mimetype.params)
1.128 +
1.129 + # Label the attachment and include it in the message.
1.130 +
1.131 + part["Content-ID"] = "attachment%d" % pos
1.132 + container.attach(part)
1.133 +
1.134 + message.add_update(container)
1.135
1.136 # Get the sender details for signing messages.
1.137 # This is not the same as the details for authenticating users in the
1.138 # PostMessage action since the fingerprints refer to public keys.
1.139
1.140 - signing_users = getWikiDict(getattr(request.cfg, "moinmessage_gpg_signing_users_page", "MoinMessageSigningUserDict"), request)
1.141 + signing_users = self.get_signing_users()
1.142 signer = signing_users and signing_users.get(request.user.name)
1.143
1.144 # Get the recipient details.
1.145 @@ -153,7 +224,89 @@
1.146 return getattr(self.request.cfg, "moinmessage_gpg_homedir")
1.147
1.148 def get_recipients(self):
1.149 - return getWikiDict(getattr(self.request.cfg, "moinmessage_gpg_recipients_page", "MoinMessageRecipientsDict"), self.request)
1.150 + return getWikiDict(
1.151 + getattr(self.request.cfg, "moinmessage_gpg_recipients_page", "MoinMessageRecipientsDict"),
1.152 + self.request)
1.153 +
1.154 + def get_signing_users(self):
1.155 + return getWikiDict(
1.156 + getattr(self.request.cfg, "moinmessage_gpg_signing_users_page", "MoinMessageSigningUserDict"),
1.157 + self.request)
1.158 +
1.159 +# Special message formatters.
1.160 +
1.161 +def unquoteWikinameURL(url, charset=config.charset):
1.162 +
1.163 + """
1.164 + The inverse of wikiutil.quoteWikinameURL, returning the page name referenced
1.165 + by the given 'url', with the page name assumed to be encoded using the given
1.166 + 'charset' (or default charset if omitted).
1.167 + """
1.168 +
1.169 + return unicode(urllib.unquote(url), encoding=charset)
1.170 +
1.171 +def getAttachmentFromURL(url, request):
1.172 +
1.173 + """
1.174 + Return a (page name, attachment filename) tuple for the attachment
1.175 + referenced by the given 'url', using the 'request' to interpret the
1.176 + structure of 'url'.
1.177 +
1.178 + If 'url' does not refer to an attachment on this wiki, None is returned.
1.179 + """
1.180 +
1.181 + script = request.getScriptname()
1.182 + if not url.startswith(script):
1.183 + return None
1.184 +
1.185 + path = url[len(script):].lstrip("/")
1.186 + try:
1.187 + qpagename, qs = path.split("?", 1)
1.188 + except ValueError:
1.189 + qpagename = path
1.190 + qs = None
1.191 +
1.192 + pagename = unquoteWikinameURL(qpagename)
1.193 + qs = qs and parseQueryString(qs) or {}
1.194 + return pagename, qs.get("target") or qs.get("drawing")
1.195 +
1.196 +class OutgoingHTMLFormatter(text_html.Formatter):
1.197 +
1.198 + """
1.199 + Handle outgoing HTML content by identifying attachments and rewriting their
1.200 + locations. References to bundled attachments are done using RFC 2111:
1.201 +
1.202 + https://tools.ietf.org/html/rfc2111
1.203 +
1.204 + Messages employing references between parts are meant to comply with RFC
1.205 + 2387:
1.206 +
1.207 + https://tools.ietf.org/html/rfc2387
1.208 + """
1.209 +
1.210 + def __init__(self, request, **kw):
1.211 + text_html.Formatter.__init__(self, request, **kw)
1.212 + self.attachments = []
1.213 +
1.214 + def add_attachment(self, location):
1.215 + details = getAttachmentFromURL(location, self.request)
1.216 + if details:
1.217 + pos = len(self.attachments)
1.218 + self.attachments.append(details)
1.219 + return "cid:attachment%d" % pos
1.220 + else:
1.221 + return None
1.222 +
1.223 + def image(self, src=None, **kw):
1.224 + src = src or kw.get("src")
1.225 + ref = src and self.add_attachment(src)
1.226 + return text_html.Formatter.image(self, ref or src, **kw)
1.227 +
1.228 + def transclusion(self, on, **kw):
1.229 + if on:
1.230 + data = kw.get("data")
1.231 + kw["data"] = data and self.add_attachment(data)
1.232 + return text_html.Formatter.transclusion(self, on, **kw)
1.233
1.234 # Action function.
1.235