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