1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - SendMessage Action 4 5 @copyright: 2012, 2013 by Paul Boddie <paul@boddie.org.uk> 6 @license: GNU GPL (v2 or later), see COPYING.txt for details. 7 """ 8 9 from MoinMoin.action import ActionBase, AttachFile 10 from MoinMoin.formatter import text_html 11 from MoinMoin.log import getLogger 12 from MoinMoin import config 13 from MoinMessage import GPG, MoinMessageError, Message, sendMessage 14 from MoinSupport import * 15 from MoinMoin.wikiutil import escape, MimeType, parseQueryString, taintfilename 16 17 from email.mime.image import MIMEImage 18 from email.mime.multipart import MIMEMultipart 19 from email.mime.text import MIMEText 20 import urllib 21 22 Dependencies = [] 23 24 class SendMessage(ActionBase, ActionSupport): 25 26 "An action that can send a message to another site." 27 28 def get_form_html(self, buttons_html): 29 30 "Present an interface for message sending." 31 32 _ = self._ 33 request = self.request 34 form = self.get_form() 35 36 message = form.get("message", [""])[0] 37 recipient = form.get("recipient", [""])[0] 38 preview = form.get("preview", [""])[0] 39 40 # Get a list of potential recipients. 41 42 recipients = self.get_recipients() 43 44 # Prepare the recipients list, selecting the specified recipients. 45 46 recipients_list = [] 47 48 if recipients: 49 recipients_list += self.get_option_list(recipient, recipients) or [] 50 51 recipients_list.sort() 52 53 # Prepare any preview. 54 55 request.formatter.setPage(self.page) 56 preview_output = preview and formatText(message, request, request.formatter, inhibit_p=False) or "" 57 58 # Fill in the fields and labels. 59 60 d = { 61 "buttons_html" : buttons_html, 62 "recipient_label" : _("Recipient"), 63 "recipients_list" : "\n".join(recipients_list), 64 "message_label" : _("Message text"), 65 "message_default" : escape(message), 66 "preview_label" : _("Preview message"), 67 "preview_output" : preview_output, 68 } 69 70 # Prepare the output HTML. 71 72 html = ''' 73 <table> 74 <tr> 75 <td class="label"><label>%(recipient_label)s</label></td> 76 <td> 77 <select name="recipient"> 78 %(recipients_list)s 79 </select> 80 </td> 81 </tr> 82 <tr> 83 <td class="label"><label>%(message_label)s</label></td> 84 <td> 85 <textarea name="message" cols="60" rows="10">%(message_default)s</textarea> 86 </td> 87 </tr> 88 <tr> 89 <td></td> 90 <td class="buttons"> 91 <input name="preview" type="submit" value="%(preview_label)s" /> 92 </td> 93 </tr> 94 <tr> 95 <td></td> 96 <td class="moinmessage-preview"> 97 %(preview_output)s 98 </td> 99 </tr> 100 <tr> 101 <td></td> 102 <td class="buttons"> 103 %(buttons_html)s 104 </td> 105 </tr> 106 </table>''' % d 107 108 return html 109 110 def do_action(self): 111 112 "Attempt to send the message." 113 114 _ = self._ 115 request = self.request 116 form = self.get_form() 117 118 text = form.get("message", [None])[0] 119 recipient = form.get("recipient", [None])[0] 120 121 if not text: 122 return 0, _("A message must be given.") 123 124 if not recipient: 125 return 0, _("A recipient must be given.") 126 127 homedir = self.get_homedir() 128 if not homedir: 129 return 0, _("MoinMessage has not been set up: a GPG homedir is not defined.") 130 131 gpg = GPG(homedir) 132 133 # Construct a message from the request. 134 135 message = Message() 136 137 container = MIMEMultipart("related") 138 container["Update-Action"] = "store" 139 140 # Add the message body and any attachments. 141 142 fmt = OutgoingHTMLFormatter(request) 143 fmt.setPage(request.page) 144 body = formatText(text, request, fmt, inhibit_p=False) 145 146 container.attach(MIMEText(body, "html")) 147 148 for pos, (pagename, filename) in enumerate(fmt.attachments): 149 150 # Obtain the attachment path. 151 152 filename = taintfilename(filename) 153 path = AttachFile.getFilename(request, pagename, filename) 154 155 # Obtain the attachment content. 156 157 f = open(path, "rb") 158 try: 159 body = f.read() 160 finally: 161 f.close() 162 163 # Determine the attachment type. 164 165 mimetype = MimeType(filename=filename) 166 167 # NOTE: Support a limited set of explicit part types for now. 168 169 if mimetype.major == "image": 170 part = MIMEImage(body, mimetype.minor, **mimetype.params) 171 elif mimetype.major == "text": 172 part = MIMEText(body, mimetype.minor, mimetype.charset, **mimetype.params) 173 else: 174 part = MIMEApplication(body, mimetype.minor, **mimetype.params) 175 176 # Label the attachment and include it in the message. 177 178 part["Content-ID"] = "attachment%d" % pos 179 container.attach(part) 180 181 message.add_update(container) 182 183 # Get the sender details for signing messages. 184 # This is not the same as the details for authenticating users in the 185 # PostMessage action since the fingerprints refer to public keys. 186 187 signing_users = self.get_signing_users() 188 signer = signing_users and signing_users.get(request.user.name) 189 190 # Get the recipient details. 191 192 recipients = self.get_recipients() 193 if not recipients: 194 return 0, _("No recipients page is defined for MoinMessage.") 195 196 recipient_details = recipients.get(recipient) 197 if not recipient_details: 198 return 0, _("The specified recipient is not present in the list of known contacts.") 199 200 try: 201 url, fingerprint = recipient_details.split() 202 except ValueError: 203 return 0, _("The recipient details were not in the correct format: url, fingerprint.") 204 205 # Sign, encrypt and send the message. 206 207 try: 208 message = message.get_payload() 209 if signer: 210 message = gpg.signMessage(message, signer) 211 212 message = gpg.encryptMessage(message, fingerprint) 213 sendMessage(message, url) 214 215 except MoinMessageError, exc: 216 return 0, "%s: %s" % (_("The message could not be prepared and sent:"), exc) 217 218 return 1, None 219 220 def get_homedir(self): 221 222 "Locate the GPG home directory." 223 224 return getattr(self.request.cfg, "moinmessage_gpg_homedir") 225 226 def get_recipients(self): 227 return getWikiDict( 228 getattr(self.request.cfg, "moinmessage_gpg_recipients_page", "MoinMessageRecipientsDict"), 229 self.request) 230 231 def get_signing_users(self): 232 return getWikiDict( 233 getattr(self.request.cfg, "moinmessage_gpg_signing_users_page", "MoinMessageSigningUserDict"), 234 self.request) 235 236 # Special message formatters. 237 238 def unquoteWikinameURL(url, charset=config.charset): 239 240 """ 241 The inverse of wikiutil.quoteWikinameURL, returning the page name referenced 242 by the given 'url', with the page name assumed to be encoded using the given 243 'charset' (or default charset if omitted). 244 """ 245 246 return unicode(urllib.unquote(url), encoding=charset) 247 248 def getAttachmentFromURL(url, request): 249 250 """ 251 Return a (page name, attachment filename) tuple for the attachment 252 referenced by the given 'url', using the 'request' to interpret the 253 structure of 'url'. 254 255 If 'url' does not refer to an attachment on this wiki, None is returned. 256 """ 257 258 script = request.getScriptname() 259 if not url.startswith(script): 260 return None 261 262 path = url[len(script):].lstrip("/") 263 try: 264 qpagename, qs = path.split("?", 1) 265 except ValueError: 266 qpagename = path 267 qs = None 268 269 pagename = unquoteWikinameURL(qpagename) 270 qs = qs and parseQueryString(qs) or {} 271 return pagename, qs.get("target") or qs.get("drawing") 272 273 class OutgoingHTMLFormatter(text_html.Formatter): 274 275 """ 276 Handle outgoing HTML content by identifying attachments and rewriting their 277 locations. References to bundled attachments are done using RFC 2111: 278 279 https://tools.ietf.org/html/rfc2111 280 281 Messages employing references between parts are meant to comply with RFC 282 2387: 283 284 https://tools.ietf.org/html/rfc2387 285 """ 286 287 def __init__(self, request, **kw): 288 text_html.Formatter.__init__(self, request, **kw) 289 self.attachments = [] 290 291 def add_attachment(self, location): 292 details = getAttachmentFromURL(location, self.request) 293 if details: 294 pos = len(self.attachments) 295 self.attachments.append(details) 296 return "cid:attachment%d" % pos 297 else: 298 return None 299 300 def image(self, src=None, **kw): 301 src = src or kw.get("src") 302 ref = src and self.add_attachment(src) 303 return text_html.Formatter.image(self, ref or src, **kw) 304 305 def transclusion(self, on, **kw): 306 if on: 307 data = kw.get("data") 308 kw["data"] = data and self.add_attachment(data) 309 return text_html.Formatter.transclusion(self, on, **kw) 310 311 # Action function. 312 313 def execute(pagename, request): 314 SendMessage(pagename, request).render() 315 316 # vim: tabstop=4 expandtab shiftwidth=4