# HG changeset patch # User Paul Boddie # Date 1389546560 -3600 # Node ID d9c2cb62387da6ff74b099a7bafdf809788cfe8c # Parent 1214baa89569918422879d0f69916300d0cb94eb Added a MessageInterface class for POP3-like (RFC 1939) command-based access to message stores. Enforced usage of multipart/alternative for updates with multiple representations. Removed the Mailbox class. diff -r 1214baa89569 -r d9c2cb62387d MoinMessage.py --- a/MoinMessage.py Sat Jan 11 20:20:48 2014 +0100 +++ b/MoinMessage.py Sun Jan 12 18:09:20 2014 +0100 @@ -12,8 +12,10 @@ from email.mime.multipart import MIMEMultipart from email.mime.application import MIMEApplication from email.mime.base import MIMEBase +from email.mime.text import MIMEText from email.parser import Parser from email.utils import formatdate +from itertools import islice from subprocess import Popen, PIPE from tempfile import mkstemp from urlparse import urlsplit @@ -26,6 +28,8 @@ except ImportError: from StringIO import StringIO +# Message inspection functions. + def is_collection(message): return message.get("Update-Type") == "collection" @@ -38,6 +42,8 @@ def get_update_action(message): return message.get("Update-Action", "update") +# Core abstractions. + class Message: "An update message." @@ -104,7 +110,7 @@ 'alternatives'. """ - part = MIMEMultipart() + part = MIMEMultipart("alternative") for alternative in alternatives: part.attach(alternative) return part @@ -133,43 +139,6 @@ return message -class Mailbox: - - "A collection of messages within a multipart message." - - def __init__(self, text=None): - self.messages = [] - if text: - self.parse_text(text) - - def parse_text(self, text): - - "Parse the given 'text' as a mailbox." - - message = message_from_string(text) - - if message.is_multipart(): - for part in message.get_payload(): - self.messages.append(part) - else: - self.messages.append(message) - - def add_message(self, message): - - "Add the given 'message' to the mailbox." - - self.messages.append(message) - - def get_payload(self): - - "Get the multipart payload for the mailbox." - - mailbox = MIMEMultipart() - for message in self.messages: - mailbox.attach(message) - - return mailbox - class MoinMessageError(Exception): pass @@ -577,4 +546,105 @@ return scheme, host, port, path +# Message handling. + +class MessageInterface: + + "A command-based interface to a message store, inspired by RFC 1939 (POP3)." + + def __init__(self, store): + self.store = store + + def execute(self, commands): + + """ + Access messages according to the given 'commands' script, acting on the + store provided during initialisation and returning a message object + containing the results. + """ + + # Build a container for the responses. + + message = Message() + + # Process each command. + + for command in commands.split("\n"): + command = command.strip() + + # Get the command and arguments. + + command_parts = command.split(None, 1) + cmd = command_parts[0] + + try: + if cmd in self.commands: + getattr(self, cmd)(command_parts, message) + else: + self.add_result(cmd, command, "ERR", message) + except Exception, exc: + self.add_result(cmd, "\n".join([command, str(exc)]), "ERR", message) + + return message + + def get_count(self, command_parts): + + # Select all messages by default. + + count = None + + if len(command_parts) > 1: + count = int(command_parts[1]) + + return count + + def add_result(self, cmd, result, status, message): + part = MIMEText(result, "x-moinmessage-fetch-result") + part["Request-Type"] = cmd + part["Request-Status"] = status + message.add_update(part) + + def add_messages(self, resources, message): + container = Message() + + for message_text in resources: + message_item = Parser().parsestr(message_text) + container.add_update(message_item) + + # Convert the container to a proper multipart section. + + message.add_update(container.get_payload()) + + def STAT(self, command_parts, message): + + # A request to count the messages is returned in a part. + + self.add_result("STAT", str(len(self.store)), "OK", message) + + def RETR(self, command_parts, message): + + # A request for specific messages returns each message + # in its own part within a collection part. + + count = self.get_count(command_parts) + + self.add_messages(islice(iter(self.store), count), message) + + def DELE(self, command_parts, message): + + # A request to delete messages is performed immediately. + + count = self.get_count(command_parts) + + keys = self.store.keys()[:count] + + for key in keys: + del self.store[key] + + self.add_result("DELE", str(len(keys)), "OK", message) + + # Command manifest, may be extended by subclasses. + + commands = "STAT", "RETR", "DELE" + # vim: tabstop=4 expandtab shiftwidth=4