paul@31 | 1 | # -*- coding: iso-8859-1 -*- |
paul@31 | 2 | """ |
paul@31 | 3 | MoinMoin - FetchMessages Action |
paul@31 | 4 | |
paul@31 | 5 | @copyright: 2012, 2013 by Paul Boddie <paul@boddie.org.uk> |
paul@31 | 6 | @license: GNU GPL (v2 or later), see COPYING.txt for details. |
paul@31 | 7 | """ |
paul@31 | 8 | |
paul@61 | 9 | from MoinSupport import getMetadata, writeHeaders, parseDictEntry |
paul@61 | 10 | from MoinMessage import Message, GPG |
paul@61 | 11 | from MoinMessageSupport import MoinMessageAction, \ |
paul@61 | 12 | get_signing_users, get_recipients |
paul@31 | 13 | from email.mime.text import MIMEText |
paul@31 | 14 | from email.parser import Parser |
paul@31 | 15 | from itertools import islice |
paul@31 | 16 | |
paul@31 | 17 | try: |
paul@31 | 18 | from cStringIO import StringIO |
paul@31 | 19 | except ImportError: |
paul@31 | 20 | from StringIO import StringIO |
paul@31 | 21 | |
paul@31 | 22 | Dependencies = ['pages'] |
paul@31 | 23 | |
paul@31 | 24 | class FetchMessages(MoinMessageAction): |
paul@31 | 25 | |
paul@31 | 26 | "A handler for requests accessing messages." |
paul@31 | 27 | |
paul@31 | 28 | def handle_message_content(self, content): |
paul@31 | 29 | |
paul@31 | 30 | "Handle the given message 'content'." |
paul@31 | 31 | |
paul@31 | 32 | request = self.request |
paul@31 | 33 | |
paul@31 | 34 | # NOTE: Could employ a more accurate content type. |
paul@31 | 35 | |
paul@31 | 36 | if not content.get_content_type() == "text/plain": |
paul@31 | 37 | writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") |
paul@31 | 38 | request.write("The content does not appear to be a request for messages.") |
paul@31 | 39 | return |
paul@31 | 40 | |
paul@61 | 41 | homedir = self.get_homedir() |
paul@61 | 42 | if not homedir: |
paul@61 | 43 | return |
paul@61 | 44 | |
paul@61 | 45 | gpg = GPG(homedir) |
paul@61 | 46 | |
paul@61 | 47 | # Get keys for signing and encrypting. |
paul@61 | 48 | # The signing key will be this wiki's signing key for the user |
paul@61 | 49 | # requesting the messages. |
paul@61 | 50 | # The encryption key will be the key associated with the user requesting |
paul@61 | 51 | # the messages, found in the recipients mapping. |
paul@61 | 52 | |
paul@61 | 53 | recipient = request.user.name |
paul@61 | 54 | |
paul@61 | 55 | signing_users = get_signing_users(request) |
paul@61 | 56 | signer = signing_users and signing_users.get(recipient) |
paul@61 | 57 | |
paul@61 | 58 | # Get the recipient details. |
paul@61 | 59 | |
paul@61 | 60 | recipients = get_recipients(request) |
paul@61 | 61 | if not recipient or not recipients or not recipients.has_key(recipient): |
paul@61 | 62 | writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") |
paul@61 | 63 | request.write("The recipient does not appear to be registered for message delivery.") |
paul@61 | 64 | return |
paul@61 | 65 | |
paul@61 | 66 | recipient_details = recipients[recipient] |
paul@61 | 67 | parameters = parseDictEntry(recipient_details, ("type", "location", "fingerprint",)) |
paul@61 | 68 | |
paul@61 | 69 | if not parameters.has_key("fingerprint"): |
paul@61 | 70 | writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") |
paul@61 | 71 | request.write("The recipient does not appear to be registered for message delivery.") |
paul@61 | 72 | return |
paul@61 | 73 | |
paul@31 | 74 | # Obtain commands from the payload, returning a collection of messages. |
paul@31 | 75 | |
paul@42 | 76 | commands = content.get_payload(decode=True) |
paul@31 | 77 | |
paul@31 | 78 | # Build a container for the responses. |
paul@31 | 79 | |
paul@31 | 80 | message = Message() |
paul@31 | 81 | |
paul@31 | 82 | # Process each command, using RFC 1939 (POP3) as inspiration. |
paul@31 | 83 | |
paul@31 | 84 | for command in commands.split("\n"): |
paul@31 | 85 | command = command.strip() |
paul@31 | 86 | |
paul@31 | 87 | # Get the command and arguments. |
paul@31 | 88 | |
paul@31 | 89 | command_parts = command.split(None, 1) |
paul@31 | 90 | cmd = command_parts[0] |
paul@31 | 91 | |
paul@31 | 92 | # A request to count the messages is returned in a part. |
paul@31 | 93 | |
paul@31 | 94 | if cmd == "STAT": |
paul@31 | 95 | result = str(len(self.store)) |
paul@31 | 96 | part = MIMEText(result, "plain") |
paul@31 | 97 | part["Request-Type"] = "STAT" |
paul@31 | 98 | part["Request-Status"] = "OK" |
paul@31 | 99 | message.add_update(part) |
paul@31 | 100 | |
paul@31 | 101 | # A request for specific messages returns each message in its own |
paul@31 | 102 | # part. |
paul@31 | 103 | |
paul@31 | 104 | elif cmd in ("RETR", "DELE"): |
paul@31 | 105 | |
paul@31 | 106 | try: |
paul@31 | 107 | # Either select all. |
paul@31 | 108 | |
paul@31 | 109 | if len(command_parts) == 1: |
paul@31 | 110 | count = None |
paul@31 | 111 | |
paul@31 | 112 | # Or select a particular number. |
paul@31 | 113 | |
paul@31 | 114 | else: |
paul@31 | 115 | count = int(parameters[1]) |
paul@31 | 116 | |
paul@31 | 117 | except ValueError: |
paul@31 | 118 | part = MIMEText(command, "plain") |
paul@31 | 119 | part["Request-Type"] = cmd |
paul@31 | 120 | part["Request-Status"] = "ERR" |
paul@31 | 121 | message.add_update(part) |
paul@31 | 122 | |
paul@31 | 123 | else: |
paul@31 | 124 | # A request for specific messages returns each message |
paul@31 | 125 | # in its own part within a collection part. |
paul@31 | 126 | |
paul@31 | 127 | if cmd == "RETR": |
paul@31 | 128 | container = Message() |
paul@31 | 129 | |
paul@31 | 130 | for message_text in islice(iter(self.store), count): |
paul@31 | 131 | message_item = Parser().parse(StringIO(message_text)) |
paul@31 | 132 | container.add_update(message_item) |
paul@31 | 133 | |
paul@31 | 134 | # Convert the container to a proper multipart section. |
paul@31 | 135 | |
paul@31 | 136 | message.add_update(container.get_payload()) |
paul@31 | 137 | |
paul@31 | 138 | # A request to delete messages is performed immediately. |
paul@31 | 139 | |
paul@31 | 140 | elif cmd == "DELE": |
paul@31 | 141 | keys = self.store.keys()[:count] |
paul@31 | 142 | keys.sort() |
paul@31 | 143 | |
paul@31 | 144 | for key in keys: |
paul@31 | 145 | del self.store[key] |
paul@31 | 146 | |
paul@31 | 147 | part = MIMEText(result, "plain") |
paul@31 | 148 | part["Request-Type"] = cmd |
paul@31 | 149 | part["Request-Status"] = "OK" |
paul@31 | 150 | message.add_update(part) |
paul@31 | 151 | |
paul@31 | 152 | # Handle invalid commands. |
paul@31 | 153 | |
paul@31 | 154 | elif cmd: |
paul@31 | 155 | part = MIMEText(result, "plain") |
paul@31 | 156 | part["Request-Type"] = cmd |
paul@31 | 157 | part["Request-Status"] = "ERR" |
paul@31 | 158 | message.add_update(part) |
paul@31 | 159 | |
paul@61 | 160 | # Sign and encrypt the message. |
paul@61 | 161 | |
paul@61 | 162 | message = message.get_payload() |
paul@61 | 163 | |
paul@61 | 164 | if signer: |
paul@61 | 165 | message = gpg.signMessage(message, signer) |
paul@61 | 166 | |
paul@61 | 167 | message = gpg.encryptMessage(message, parameters["fingerprint"]) |
paul@61 | 168 | |
paul@31 | 169 | # Write the response. |
paul@31 | 170 | |
paul@69 | 171 | writeHeaders(request, "text/plain", getMetadata(self.page)) |
paul@61 | 172 | request.write(message.as_string()) |
paul@31 | 173 | return 1, None |
paul@31 | 174 | |
paul@31 | 175 | # Action function. |
paul@31 | 176 | |
paul@31 | 177 | def execute(pagename, request): |
paul@31 | 178 | FetchMessages(pagename, request).do_action() # instead of render |
paul@31 | 179 | |
paul@31 | 180 | # vim: tabstop=4 expandtab shiftwidth=4 |