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