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