1.1 --- a/MoinMessage.py Sat Jan 11 20:20:48 2014 +0100
1.2 +++ b/MoinMessage.py Sun Jan 12 18:09:20 2014 +0100
1.3 @@ -12,8 +12,10 @@
1.4 from email.mime.multipart import MIMEMultipart
1.5 from email.mime.application import MIMEApplication
1.6 from email.mime.base import MIMEBase
1.7 +from email.mime.text import MIMEText
1.8 from email.parser import Parser
1.9 from email.utils import formatdate
1.10 +from itertools import islice
1.11 from subprocess import Popen, PIPE
1.12 from tempfile import mkstemp
1.13 from urlparse import urlsplit
1.14 @@ -26,6 +28,8 @@
1.15 except ImportError:
1.16 from StringIO import StringIO
1.17
1.18 +# Message inspection functions.
1.19 +
1.20 def is_collection(message):
1.21 return message.get("Update-Type") == "collection"
1.22
1.23 @@ -38,6 +42,8 @@
1.24 def get_update_action(message):
1.25 return message.get("Update-Action", "update")
1.26
1.27 +# Core abstractions.
1.28 +
1.29 class Message:
1.30
1.31 "An update message."
1.32 @@ -104,7 +110,7 @@
1.33 'alternatives'.
1.34 """
1.35
1.36 - part = MIMEMultipart()
1.37 + part = MIMEMultipart("alternative")
1.38 for alternative in alternatives:
1.39 part.attach(alternative)
1.40 return part
1.41 @@ -133,43 +139,6 @@
1.42
1.43 return message
1.44
1.45 -class Mailbox:
1.46 -
1.47 - "A collection of messages within a multipart message."
1.48 -
1.49 - def __init__(self, text=None):
1.50 - self.messages = []
1.51 - if text:
1.52 - self.parse_text(text)
1.53 -
1.54 - def parse_text(self, text):
1.55 -
1.56 - "Parse the given 'text' as a mailbox."
1.57 -
1.58 - message = message_from_string(text)
1.59 -
1.60 - if message.is_multipart():
1.61 - for part in message.get_payload():
1.62 - self.messages.append(part)
1.63 - else:
1.64 - self.messages.append(message)
1.65 -
1.66 - def add_message(self, message):
1.67 -
1.68 - "Add the given 'message' to the mailbox."
1.69 -
1.70 - self.messages.append(message)
1.71 -
1.72 - def get_payload(self):
1.73 -
1.74 - "Get the multipart payload for the mailbox."
1.75 -
1.76 - mailbox = MIMEMultipart()
1.77 - for message in self.messages:
1.78 - mailbox.attach(message)
1.79 -
1.80 - return mailbox
1.81 -
1.82 class MoinMessageError(Exception):
1.83 pass
1.84
1.85 @@ -577,4 +546,105 @@
1.86
1.87 return scheme, host, port, path
1.88
1.89 +# Message handling.
1.90 +
1.91 +class MessageInterface:
1.92 +
1.93 + "A command-based interface to a message store, inspired by RFC 1939 (POP3)."
1.94 +
1.95 + def __init__(self, store):
1.96 + self.store = store
1.97 +
1.98 + def execute(self, commands):
1.99 +
1.100 + """
1.101 + Access messages according to the given 'commands' script, acting on the
1.102 + store provided during initialisation and returning a message object
1.103 + containing the results.
1.104 + """
1.105 +
1.106 + # Build a container for the responses.
1.107 +
1.108 + message = Message()
1.109 +
1.110 + # Process each command.
1.111 +
1.112 + for command in commands.split("\n"):
1.113 + command = command.strip()
1.114 +
1.115 + # Get the command and arguments.
1.116 +
1.117 + command_parts = command.split(None, 1)
1.118 + cmd = command_parts[0]
1.119 +
1.120 + try:
1.121 + if cmd in self.commands:
1.122 + getattr(self, cmd)(command_parts, message)
1.123 + else:
1.124 + self.add_result(cmd, command, "ERR", message)
1.125 + except Exception, exc:
1.126 + self.add_result(cmd, "\n".join([command, str(exc)]), "ERR", message)
1.127 +
1.128 + return message
1.129 +
1.130 + def get_count(self, command_parts):
1.131 +
1.132 + # Select all messages by default.
1.133 +
1.134 + count = None
1.135 +
1.136 + if len(command_parts) > 1:
1.137 + count = int(command_parts[1])
1.138 +
1.139 + return count
1.140 +
1.141 + def add_result(self, cmd, result, status, message):
1.142 + part = MIMEText(result, "x-moinmessage-fetch-result")
1.143 + part["Request-Type"] = cmd
1.144 + part["Request-Status"] = status
1.145 + message.add_update(part)
1.146 +
1.147 + def add_messages(self, resources, message):
1.148 + container = Message()
1.149 +
1.150 + for message_text in resources:
1.151 + message_item = Parser().parsestr(message_text)
1.152 + container.add_update(message_item)
1.153 +
1.154 + # Convert the container to a proper multipart section.
1.155 +
1.156 + message.add_update(container.get_payload())
1.157 +
1.158 + def STAT(self, command_parts, message):
1.159 +
1.160 + # A request to count the messages is returned in a part.
1.161 +
1.162 + self.add_result("STAT", str(len(self.store)), "OK", message)
1.163 +
1.164 + def RETR(self, command_parts, message):
1.165 +
1.166 + # A request for specific messages returns each message
1.167 + # in its own part within a collection part.
1.168 +
1.169 + count = self.get_count(command_parts)
1.170 +
1.171 + self.add_messages(islice(iter(self.store), count), message)
1.172 +
1.173 + def DELE(self, command_parts, message):
1.174 +
1.175 + # A request to delete messages is performed immediately.
1.176 +
1.177 + count = self.get_count(command_parts)
1.178 +
1.179 + keys = self.store.keys()[:count]
1.180 +
1.181 + for key in keys:
1.182 + del self.store[key]
1.183 +
1.184 + self.add_result("DELE", str(len(keys)), "OK", message)
1.185 +
1.186 + # Command manifest, may be extended by subclasses.
1.187 +
1.188 + commands = "STAT", "RETR", "DELE"
1.189 +
1.190 # vim: tabstop=4 expandtab shiftwidth=4