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