MoinMessage

Annotated actions/SendMessage.py

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