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