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