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