paul@12 | 1 | # -*- coding: iso-8859-1 -*- |
paul@12 | 2 | """ |
paul@12 | 3 | MoinMoin - SendMessage Action |
paul@12 | 4 | |
paul@105 | 5 | @copyright: 2012, 2013, 2014 by Paul Boddie <paul@boddie.org.uk> |
paul@12 | 6 | @license: GNU GPL (v2 or later), see COPYING.txt for details. |
paul@12 | 7 | """ |
paul@12 | 8 | |
paul@120 | 9 | from MoinMoin.action import ActionBase |
paul@27 | 10 | from MoinMoin.Page import Page |
paul@120 | 11 | from MoinMoin.wikiutil import escape, MimeType |
paul@120 | 12 | |
paul@105 | 13 | from MoinMessage import GPG, MoinMessageError, Message, sendMessage, timestamp, \ |
paul@105 | 14 | as_string |
paul@83 | 15 | from MoinMessageSupport import get_signing_users, get_recipients, get_relays, \ |
paul@120 | 16 | get_recipient_details, \ |
paul@120 | 17 | MoinMessageRecipientError, OutgoingHTMLFormatter |
paul@12 | 18 | from MoinSupport import * |
paul@73 | 19 | from ItemSupport import ItemStore |
paul@21 | 20 | |
paul@41 | 21 | from email.mime.base import MIMEBase |
paul@21 | 22 | from email.mime.image import MIMEImage |
paul@21 | 23 | from email.mime.multipart import MIMEMultipart |
paul@12 | 24 | from email.mime.text import MIMEText |
paul@40 | 25 | |
paul@12 | 26 | class SendMessage(ActionBase, ActionSupport): |
paul@12 | 27 | |
paul@12 | 28 | "An action that can send a message to another site." |
paul@12 | 29 | |
paul@12 | 30 | def get_form_html(self, buttons_html): |
paul@12 | 31 | |
paul@12 | 32 | "Present an interface for message sending." |
paul@12 | 33 | |
paul@12 | 34 | _ = self._ |
paul@12 | 35 | request = self.request |
paul@12 | 36 | form = self.get_form() |
paul@12 | 37 | |
paul@12 | 38 | message = form.get("message", [""])[0] |
paul@12 | 39 | recipient = form.get("recipient", [""])[0] |
paul@41 | 40 | format = form.get("format", ["wiki"])[0] |
paul@25 | 41 | preview = form.get("preview") |
paul@53 | 42 | action = form.get("send-action", ["send"])[0] |
paul@12 | 43 | |
paul@12 | 44 | # Get a list of potential recipients. |
paul@12 | 45 | |
paul@84 | 46 | recipients = get_recipients(request, fetching=False) |
paul@12 | 47 | |
paul@12 | 48 | # Prepare the recipients list, selecting the specified recipients. |
paul@12 | 49 | |
paul@12 | 50 | recipients_list = [] |
paul@12 | 51 | |
paul@12 | 52 | if recipients: |
paul@12 | 53 | recipients_list += self.get_option_list(recipient, recipients) or [] |
paul@12 | 54 | |
paul@12 | 55 | recipients_list.sort() |
paul@12 | 56 | |
paul@21 | 57 | # Prepare any preview. |
paul@21 | 58 | |
paul@41 | 59 | parser_cls = getParserClass(request, format) |
paul@21 | 60 | request.formatter.setPage(self.page) |
paul@41 | 61 | preview_output = preview and formatText(message, request, request.formatter, inhibit_p=False, parser_cls=parser_cls) or "" |
paul@21 | 62 | |
paul@12 | 63 | # Fill in the fields and labels. |
paul@12 | 64 | |
paul@12 | 65 | d = { |
paul@12 | 66 | "buttons_html" : buttons_html, |
paul@41 | 67 | "format_label" : escape(_("Message format")), |
paul@41 | 68 | "format" : escattr(format), |
paul@25 | 69 | "recipient_label" : escape(_("Recipient")), |
paul@12 | 70 | "recipients_list" : "\n".join(recipients_list), |
paul@25 | 71 | "message_label" : escape(_("Message text")), |
paul@21 | 72 | "message_default" : escape(message), |
paul@25 | 73 | "preview_label" : escattr(_("Preview message")), |
paul@21 | 74 | "preview_output" : preview_output, |
paul@53 | 75 | "send_label" : escape(_("Send message immediately")), |
paul@53 | 76 | "send_selected" : self._get_selected("send", action), |
paul@26 | 77 | "queue_label" : escape(_("Queue message for sending")), |
paul@53 | 78 | "queue_selected" : self._get_selected("queue", action), |
paul@12 | 79 | } |
paul@12 | 80 | |
paul@12 | 81 | # Prepare the output HTML. |
paul@12 | 82 | |
paul@12 | 83 | html = ''' |
paul@12 | 84 | <table> |
paul@12 | 85 | <tr> |
paul@12 | 86 | <td class="label"><label>%(recipient_label)s</label></td> |
paul@12 | 87 | <td> |
paul@12 | 88 | <select name="recipient"> |
paul@12 | 89 | %(recipients_list)s |
paul@12 | 90 | </select> |
paul@12 | 91 | </td> |
paul@12 | 92 | </tr> |
paul@12 | 93 | <tr> |
paul@41 | 94 | <td class="label"><label>%(format_label)s</label></td> |
paul@41 | 95 | <td> |
paul@41 | 96 | <input name="format" type="text" value="%(format)s" size="20" /> |
paul@41 | 97 | </td> |
paul@41 | 98 | </tr> |
paul@41 | 99 | <tr> |
paul@12 | 100 | <td class="label"><label>%(message_label)s</label></td> |
paul@21 | 101 | <td> |
paul@21 | 102 | <textarea name="message" cols="60" rows="10">%(message_default)s</textarea> |
paul@12 | 103 | </td> |
paul@12 | 104 | </tr> |
paul@12 | 105 | <tr> |
paul@12 | 106 | <td></td> |
paul@21 | 107 | <td class="buttons"> |
paul@21 | 108 | <input name="preview" type="submit" value="%(preview_label)s" /> |
paul@21 | 109 | </td> |
paul@21 | 110 | </tr> |
paul@21 | 111 | <tr> |
paul@21 | 112 | <td></td> |
paul@21 | 113 | <td class="moinmessage-preview"> |
paul@21 | 114 | %(preview_output)s |
paul@21 | 115 | </td> |
paul@21 | 116 | </tr> |
paul@21 | 117 | <tr> |
paul@53 | 118 | <td></td> |
paul@26 | 119 | <td> |
paul@53 | 120 | <select name="send-action"> |
paul@53 | 121 | <option value="send" %(send_selected)s>%(send_label)s</option> |
paul@53 | 122 | <option value="queue" %(queue_selected)s>%(queue_label)s</option> |
paul@53 | 123 | </select> |
paul@26 | 124 | </td> |
paul@52 | 125 | </tr> |
paul@26 | 126 | <tr> |
paul@21 | 127 | <td></td> |
paul@21 | 128 | <td class="buttons"> |
paul@12 | 129 | %(buttons_html)s |
paul@12 | 130 | </td> |
paul@12 | 131 | </tr> |
paul@12 | 132 | </table>''' % d |
paul@12 | 133 | |
paul@12 | 134 | return html |
paul@12 | 135 | |
paul@12 | 136 | def do_action(self): |
paul@12 | 137 | |
paul@12 | 138 | "Attempt to send the message." |
paul@12 | 139 | |
paul@12 | 140 | _ = self._ |
paul@12 | 141 | request = self.request |
paul@12 | 142 | form = self.get_form() |
paul@12 | 143 | |
paul@12 | 144 | text = form.get("message", [None])[0] |
paul@12 | 145 | recipient = form.get("recipient", [None])[0] |
paul@41 | 146 | format = form.get("format", ["wiki"])[0] |
paul@53 | 147 | action = form.get("send-action", ["send"])[0] |
paul@53 | 148 | |
paul@53 | 149 | queue = action == "queue" |
paul@12 | 150 | |
paul@12 | 151 | if not text: |
paul@12 | 152 | return 0, _("A message must be given.") |
paul@12 | 153 | |
paul@12 | 154 | if not recipient: |
paul@12 | 155 | return 0, _("A recipient must be given.") |
paul@12 | 156 | |
paul@12 | 157 | homedir = self.get_homedir() |
paul@12 | 158 | if not homedir: |
paul@12 | 159 | return 0, _("MoinMessage has not been set up: a GPG homedir is not defined.") |
paul@12 | 160 | |
paul@12 | 161 | gpg = GPG(homedir) |
paul@12 | 162 | |
paul@12 | 163 | # Construct a message from the request. |
paul@12 | 164 | |
paul@12 | 165 | message = Message() |
paul@21 | 166 | |
paul@21 | 167 | container = MIMEMultipart("related") |
paul@21 | 168 | container["Update-Action"] = "store" |
paul@26 | 169 | container["To"] = recipient |
paul@21 | 170 | |
paul@21 | 171 | # Add the message body and any attachments. |
paul@21 | 172 | |
paul@41 | 173 | parser_cls = getParserClass(request, format) |
paul@41 | 174 | |
paul@41 | 175 | # Determine whether alternative output types are produced and, if so, |
paul@41 | 176 | # bundle them in a multipart/alternative part. |
paul@41 | 177 | |
paul@41 | 178 | output_types = getParserOutputTypes(parser_cls) |
paul@41 | 179 | |
paul@41 | 180 | if len(output_types) > 1: |
paul@41 | 181 | alternatives = MIMEMultipart("alternative") |
paul@41 | 182 | container.attach(alternatives) |
paul@41 | 183 | else: |
paul@41 | 184 | alternatives = container |
paul@41 | 185 | |
paul@41 | 186 | # Produce each of the representations. |
paul@41 | 187 | |
paul@41 | 188 | for output_type in output_types: |
paul@21 | 189 | |
paul@41 | 190 | # HTML must be processed to identify attachments. |
paul@41 | 191 | |
paul@41 | 192 | if output_type == "text/html": |
paul@41 | 193 | fmt = OutgoingHTMLFormatter(request) |
paul@41 | 194 | fmt.setPage(request.page) |
paul@41 | 195 | body = formatText(text, request, fmt, inhibit_p=False, parser_cls=parser_cls) |
paul@41 | 196 | else: |
paul@41 | 197 | body = formatTextForOutputType(text, request, parser_cls, output_type) |
paul@41 | 198 | |
paul@41 | 199 | maintype, subtype = output_type.split("/", 1) |
paul@41 | 200 | if maintype == "text": |
paul@41 | 201 | part = MIMEText(body.encode("utf-8"), subtype, "utf-8") |
paul@41 | 202 | else: |
paul@41 | 203 | part = MIMEBase(maintype, subtype) |
paul@41 | 204 | part.set_payload(body) |
paul@41 | 205 | |
paul@41 | 206 | alternatives.attach(part) |
paul@41 | 207 | |
paul@41 | 208 | # Produce any identified attachments. |
paul@21 | 209 | |
paul@40 | 210 | for pos, (path, filename) in enumerate(fmt.attachments): |
paul@21 | 211 | |
paul@21 | 212 | # Obtain the attachment content. |
paul@21 | 213 | |
paul@21 | 214 | f = open(path, "rb") |
paul@21 | 215 | try: |
paul@21 | 216 | body = f.read() |
paul@21 | 217 | finally: |
paul@21 | 218 | f.close() |
paul@21 | 219 | |
paul@21 | 220 | # Determine the attachment type. |
paul@21 | 221 | |
paul@21 | 222 | mimetype = MimeType(filename=filename) |
paul@21 | 223 | |
paul@21 | 224 | # NOTE: Support a limited set of explicit part types for now. |
paul@21 | 225 | |
paul@21 | 226 | if mimetype.major == "image": |
paul@21 | 227 | part = MIMEImage(body, mimetype.minor, **mimetype.params) |
paul@21 | 228 | elif mimetype.major == "text": |
paul@21 | 229 | part = MIMEText(body, mimetype.minor, mimetype.charset, **mimetype.params) |
paul@21 | 230 | else: |
paul@21 | 231 | part = MIMEApplication(body, mimetype.minor, **mimetype.params) |
paul@21 | 232 | |
paul@21 | 233 | # Label the attachment and include it in the message. |
paul@21 | 234 | |
paul@21 | 235 | part["Content-ID"] = "attachment%d" % pos |
paul@21 | 236 | container.attach(part) |
paul@21 | 237 | |
paul@21 | 238 | message.add_update(container) |
paul@12 | 239 | |
paul@12 | 240 | # Get the sender details for signing messages. |
paul@12 | 241 | # This is not the same as the details for authenticating users in the |
paul@12 | 242 | # PostMessage action since the fingerprints refer to public keys. |
paul@12 | 243 | |
paul@60 | 244 | signing_users = get_signing_users(request) |
paul@12 | 245 | signer = signing_users and signing_users.get(request.user.name) |
paul@12 | 246 | |
paul@12 | 247 | # Get the recipient details. |
paul@12 | 248 | |
paul@83 | 249 | try: |
paul@83 | 250 | parameters = get_recipient_details(request, recipient) |
paul@83 | 251 | except MoinMessageRecipientError, exc: |
paul@83 | 252 | return 0, exc.message |
paul@12 | 253 | |
paul@68 | 254 | type = parameters["type"] |
paul@68 | 255 | location = parameters["location"] |
paul@68 | 256 | |
paul@68 | 257 | # Obtain the actual location if a relay is specified. |
paul@68 | 258 | |
paul@68 | 259 | if parameters["type"] == "relay": |
paul@68 | 260 | relays = get_relays(request) |
paul@68 | 261 | if not relays: |
paul@68 | 262 | return 0, _("No relays are defined for MoinMessage, but one is specified for the recipient.") |
paul@68 | 263 | if not relays.has_key(location): |
paul@68 | 264 | return 0, _("The relay specified for the recipient is not defined.") |
paul@68 | 265 | |
paul@68 | 266 | location = relays[location] |
paul@68 | 267 | |
paul@12 | 268 | # Sign, encrypt and send the message. |
paul@12 | 269 | |
paul@26 | 270 | message = message.get_payload() |
paul@26 | 271 | |
paul@68 | 272 | if not queue and type in ("url", "relay"): |
paul@26 | 273 | try: |
paul@26 | 274 | if signer: |
paul@26 | 275 | message = gpg.signMessage(message, signer) |
paul@12 | 276 | |
paul@27 | 277 | message = gpg.encryptMessage(message, parameters["fingerprint"]) |
paul@53 | 278 | |
paul@68 | 279 | # Send relayed messages with an extra signature. |
paul@65 | 280 | |
paul@68 | 281 | if type == "relay": |
paul@127 | 282 | relaying_user = getattr(request.cfg, "moinmessage_gpg_relaying_user") |
paul@65 | 283 | |
paul@68 | 284 | # Signing with the same identity if no special relaying user is |
paul@68 | 285 | # defined. |
paul@65 | 286 | |
paul@68 | 287 | if relaying_user: |
paul@68 | 288 | signer = signing_users and signing_users.get(relaying_user) |
paul@53 | 289 | |
paul@53 | 290 | timestamp(message) |
paul@53 | 291 | message["Update-Action"] = "store" |
paul@53 | 292 | message = gpg.signMessage(message, signer) |
paul@53 | 293 | |
paul@68 | 294 | sendMessage(message, location) |
paul@26 | 295 | |
paul@26 | 296 | except MoinMessageError, exc: |
paul@39 | 297 | return 0, "%s: %s" % (_("The message could not be prepared and sent"), exc) |
paul@12 | 298 | |
paul@27 | 299 | # Or queue the message on the specified page. |
paul@27 | 300 | |
paul@43 | 301 | elif type == "page": |
paul@68 | 302 | page = Page(request, location) |
paul@27 | 303 | outbox = ItemStore(page, "messages", "message-locks") |
paul@105 | 304 | outbox.append(as_string(message)) |
paul@27 | 305 | |
paul@27 | 306 | # Or queue the message in a special outbox. |
paul@26 | 307 | |
paul@26 | 308 | else: |
paul@26 | 309 | outbox = ItemStore(request.page, "outgoing-messages", "outgoing-message-locks") |
paul@105 | 310 | outbox.append(as_string(message)) |
paul@12 | 311 | |
paul@31 | 312 | return 1, _("Message sent!") |
paul@12 | 313 | |
paul@12 | 314 | def get_homedir(self): |
paul@12 | 315 | |
paul@12 | 316 | "Locate the GPG home directory." |
paul@12 | 317 | |
paul@12 | 318 | return getattr(self.request.cfg, "moinmessage_gpg_homedir") |
paul@12 | 319 | |
paul@12 | 320 | # Action function. |
paul@12 | 321 | |
paul@12 | 322 | def execute(pagename, request): |
paul@12 | 323 | SendMessage(pagename, request).render() |
paul@12 | 324 | |
paul@12 | 325 | # vim: tabstop=4 expandtab shiftwidth=4 |