# HG changeset patch # User Paul Boddie # Date 1358465773 -3600 # Node ID 48e5623c0724c0bf354bb8fd90b4a4b9432bffd3 # Parent 0ffbf71dd873af0abbc71e25380f26db29608d67 Added support for message stores associated with pages. Changed the add_update method on the Message class to use MIME parts directly, adding a get_update method to provide multipart objects for use in encoding updates with alternative representations. Exposed the update type and action in the test programs. diff -r 0ffbf71dd873 -r 48e5623c0724 MoinMessage.py --- a/MoinMessage.py Mon Oct 29 22:36:16 2012 +0100 +++ b/MoinMessage.py Fri Jan 18 00:36:13 2013 +0100 @@ -2,7 +2,7 @@ """ MoinMoin - MoinMessage library - @copyright: 2012 by Paul Boddie + @copyright: 2012, 2013 by Paul Boddie @license: GNU GPL (v2 or later), see COPYING.txt for details. """ @@ -14,6 +14,7 @@ from subprocess import Popen, PIPE from tempfile import mkstemp from urlparse import urlsplit +from MoinMoin.util import lock import httplib import os @@ -50,21 +51,34 @@ else: self.updates.append(message) - def add_update(self, alternatives): + def add_updates(self, parts): """ - Add an update fragment to a message, providing alternative forms of the - update content in the given 'alternatives': a list of MIME message - parts, each encoding the content according to different MIME types. + Add the given 'parts' to a message. """ - if len(alternatives) > 1: - part = MIMEMultipart() - for alternative in alternatives: - part.attach(alternative) - self.updates.append(part) - else: - self.updates.append(alternatives[0]) + for part in updates: + self.add_update(part) + + def add_update(self, part): + + """ + Add an update 'part' to a message. + """ + + self.updates.append(part) + + def get_update(self, alternatives): + + """ + Return a suitable multipart object containing the supplied + 'alternatives'. + """ + + part = MIMEMultipart() + for alternative in alternatives: + part.attach(alternative) + return part def get_payload(self): @@ -117,6 +131,83 @@ return mailbox +class MessageStore: + + "A page-specific message store." + + def __init__(self, page): + + "Initialise a message store for the given 'page'." + + self.path = page.getPagePath("messages") + self.next_path = os.path.join(self.path, "next") + lock_dir = page.getPagePath("message-locks") + self.lock = lock.WriteLock(lock_dir) + + def get_next(self): + + "Return the next message number." + + next = self.read_next() + if not next: + next = self.deduce_next() + self.write_next(next) + return next + + def deduce_next(self): + + "Deduce the next message number from the existing message files." + + return max([int(filename) for filename in os.listdir(self.path) if filename.isdigit()] or [0]) + 1 + + def read_next(self): + + "Read the next message number from a special file." + + if not os.path.exists(self.next_path): + return 0 + + f = open(self.next_path) + try: + try: + return int(f.read()) + except ValueError: + return 0 + finally: + f.close() + + def write_next(self, next): + + "Write the 'next' message number to a special file." + + f = open(self.next_path, "w") + try: + f.write(str(next)) + finally: + f.close() + + def write_message(self, message, next): + + "Write the given 'message' to a file with the given 'next' message number." + + f = open(os.path.join(self.path, str(next)), "w") + try: + f.write(message.as_string()) + finally: + f.close() + + def append(self, message): + + "Append the given 'message' to the store." + + self.lock.acquire() + try: + next = self.get_next() + self.write_message(message, next) + self.write_next(next + 1) + finally: + self.lock.release() + class MoinMessageError(Exception): pass diff -r 0ffbf71dd873 -r 48e5623c0724 README.txt --- a/README.txt Mon Oct 29 22:36:16 2012 +0100 +++ b/README.txt Fri Jan 18 00:36:13 2013 +0100 @@ -127,7 +127,7 @@ To send a message signed and encrypted to a resource on localhost: python tests/test_send.py 1C1AAF83 0891463A http://localhost/wiki/ShareTest \ - 'An update to the Wiki.' 'Another update.' + collection update 'An update to the Wiki.' 'Another update.' Here, the first identifier is a reference to the signing key (over which you have complete control), and the second identifier is a reference to the @@ -146,12 +146,14 @@ Prepare a message signed with a "detached signature" (note that this does not seem to be what gpg calls a detached signature with the --detach-sig option): - python tests/test_message.py 'An update to the Wiki.' 'Another update.' \ + python tests/test_message.py collection update 'An update to the Wiki.' \ + 'Another update.' \ | python tests/test_sign.py 1C1AAF83 The complicated recipe based on the individual operations is as follows: - python tests/test_message.py 'An update to the Wiki.' 'Another update.' \ + python tests/test_message.py collection update 'An update to the Wiki.' \ + 'Another update.' \ > test.txt \ && cat test.txt \ | gpg --armor -u 1C1AAF83 --detach-sig \ @@ -162,12 +164,14 @@ Prepare a message with an encrypted payload using the above key: - python tests/test_message.py 'An update to the Wiki.' 'Another update.' \ + python tests/test_message.py collection update 'An update to the Wiki.' \ + 'Another update.' \ | python tests/test_encrypt.py 0891463A The complicated recipe based on the individual operations is as follows: - python tests/test_message.py 'An update to the Wiki.' 'Another update.' \ + python tests/test_message.py collection update 'An update to the Wiki.' \ + 'Another update.' \ > test.txt \ && cat test.txt \ | gpg --armor -r 0891463A --encrypt --trust-model always \ @@ -180,13 +184,15 @@ Sign and encrypt a message: - python tests/test_message.py 'An update to the Wiki.' 'Another update.' \ + python tests/test_message.py collection update 'An update to the Wiki.' \ + 'Another update.' \ | python tests/test_sign.py 1C1AAF83 \ | python tests/test_encrypt.py 0891463A The complicated recipe based on the individual operations is as follows: - python tests/test_message.py 'An update to the Wiki.' 'Another update.' \ + python tests/test_message.py collection update 'An update to the Wiki.' \ + 'Another update.' \ > test.txt \ && cat test.txt \ | gpg --armor -u 1C1AAF83 --detach-sig \ @@ -204,3 +210,21 @@ Here, the resource "/wiki/ShareTest" on localhost is presented with the message. + +The Message Format +------------------ + +Messages are MIME-encoded and consist of one or more update fragments. Where +the "Update-Type" header is present and set to a value of "collection", a +multipart message describes as many updates as there are parts. Otherwise, +only a single update is described by the message. + +For each update, the "Update-Action" header indicates the action to be taken +with the update content. Where it is absent, the content is inserted into the +Wiki page specified in the request; where it is present and set to "replace", +the content replaces all content on the Wiki page; where it is set to "store", +the content is stored in a message store associated with the Wiki page. + +Each update may describe multiple representations of some content by employing +a multipart section containing parts for each of the representations. +Alternatively, a single message part may describe a single representation. diff -r 0ffbf71dd873 -r 48e5623c0724 actions/PostMessage.py --- a/actions/PostMessage.py Mon Oct 29 22:36:16 2012 +0100 +++ b/actions/PostMessage.py Fri Jan 18 00:36:13 2013 +0100 @@ -2,15 +2,16 @@ """ MoinMoin - PostMessage Action - @copyright: 2012 by Paul Boddie + @copyright: 2012, 2013 by Paul Boddie @license: GNU GPL (v2 or later), see COPYING.txt for details. """ +from MoinMoin.Page import Page from MoinMoin.PageEditor import PageEditor from MoinMoin.log import getLogger from MoinMoin.user import User from MoinSupport import * -from MoinMessage import GPG, Message, MoinMessageError +from MoinMessage import GPG, Message, MessageStore, MoinMessageError from email.parser import Parser try: @@ -215,47 +216,59 @@ # Handle a single part. if not update.is_multipart(): - self.handle_message_parts([update], to_replace(update)) + self.handle_message_parts([update], update) # Or a collection of alternative representations for a single # update. else: - self.handle_message_parts(update.get_payload(), to_replace(update)) + self.handle_message_parts(update.get_payload(), update) # Default output. writeHeaders(request, "text/plain", getMetadata(self.page), "204 No Content") - def handle_message_parts(self, parts, replace): + def handle_message_parts(self, parts, update): """ - Handle the given message 'parts', replacing the page content if - 'replace' is set to a true value. + Handle the given message 'parts', using the original 'update' to + determine whether the content is to replace or update page content, or + whether it will be placed in a message store. """ - # NOTE: Should either choose preferred content types or somehow retain them - # NOTE: all but present one at a time. + # Handle the different update actions. + # Update a message store for the page. + + if to_store(update): + store = MessageStore(self.page) + store.append(update) - body = [] + # Update the page. + + else: + # NOTE: Should either choose preferred content types or somehow retain them + # NOTE: all but present one at a time. + + body = [] + replace = to_replace(update) - for part in parts: - mimetype = part.get_content_type() - encoding = part.get_content_charset() - if mimetype == "text/moin": - body.append(part.get_payload()) - if replace: - break + for part in parts: + mimetype = part.get_content_type() + encoding = part.get_content_charset() + if mimetype == "text/moin": + body.append(part.get_payload()) + if replace: + break - if not replace: - body.append(self.page.get_raw_body()) + if not replace: + body.append(self.page.get_raw_body()) - page_editor = PageEditor(self.request, self.pagename) - page_editor.saveText("\n\n".join(body), 0) + page_editor = PageEditor(self.request, self.pagename) + page_editor.saveText("\n\n".join(body), 0) - # Refresh the page. + # Refresh the page. - self.page = Page(self.request, self.pagename) + self.page = Page(self.request, self.pagename) def get_homedir(self): @@ -267,12 +280,12 @@ request.write("Encoded data cannot currently be understood. Please notify the site administrator.") return homedir -def is_collection(message): - return message.get("Update-Type") == "collection" - def to_replace(message): return message.get("Update-Action") == "replace" +def to_store(message): + return message.get("Update-Action") == "store" + # Action function. def execute(pagename, request): diff -r 0ffbf71dd873 -r 48e5623c0724 actions/SendMessage.py --- a/actions/SendMessage.py Mon Oct 29 22:36:16 2012 +0100 +++ b/actions/SendMessage.py Fri Jan 18 00:36:13 2013 +0100 @@ -2,7 +2,7 @@ """ MoinMoin - SendMessage Action - @copyright: 2012 by Paul Boddie + @copyright: 2012, 2013 by Paul Boddie @license: GNU GPL (v2 or later), see COPYING.txt for details. """ @@ -107,7 +107,7 @@ # Construct a message from the request. message = Message() - message.add_update([MIMEText(text, "moin")]) + message.add_update(MIMEText(text, "moin")) # Get the sender details for signing messages. # This is not the same as the details for authenticating users in the diff -r 0ffbf71dd873 -r 48e5623c0724 tests/test_message.py --- a/tests/test_message.py Mon Oct 29 22:36:16 2012 +0100 +++ b/tests/test_message.py Fri Jan 18 00:36:13 2013 +0100 @@ -1,14 +1,40 @@ #!/usr/bin/env python from MoinMessage import Message +from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText import sys if __name__ == "__main__": + try: + type = sys.argv[1] + action = sys.argv[2] + args = sys.argv[3:] + except IndexError: + args = None + + if not args: + print >>sys.stderr, "Need an update type, update action and some updates as arguments to this program." + sys.exit(1) + message = Message() + parts = [] - for arg in sys.argv[1:]: - message.add_update([MIMEText(arg, "moin")]) + # Add each content fragment as either a separate update to a collection of + # updates or as an alternative part to a single update. + + for arg in args: + part = MIMEText(arg, "moin", sys.stdin.encoding) + if type == "collection": + part["Update-Action"] = action + message.add_update(part) + else: + parts.append(part) + + if type != "collection": + multipart = message.get_update(parts) + multipart["Update-Action"] = action + message.add_update(multipart) text = message.get_payload() print text diff -r 0ffbf71dd873 -r 48e5623c0724 tests/test_send.py --- a/tests/test_send.py Mon Oct 29 22:36:16 2012 +0100 +++ b/tests/test_send.py Fri Jan 18 00:36:13 2013 +0100 @@ -5,19 +5,35 @@ import sys if __name__ == "__main__": - signer = sys.argv[1] - recipient = sys.argv[2] - url = sys.argv[3] + "?action=PostMessage" - args = sys.argv[4:] + try: + signer = sys.argv[1] + recipient = sys.argv[2] + url = sys.argv[3] + "?action=PostMessage" + type = sys.argv[4] + action = sys.argv[5] + args = sys.argv[6:] + except IndexError: + args = None if not args: - print >>sys.stderr, "Need some updates as arguments to this program." + print >>sys.stderr, "Need a signer, recipient, URL, update type, action and some updates as arguments to this program." sys.exit(1) message = Message() + parts = [] for arg in args: - message.add_update([MIMEText(arg, "moin")]) + part = MIMEText(arg, "moin", sys.stdin.encoding) + if type == "collection": + part["Update-Action"] = action + message.add_update(part) + else: + parts.append(part) + + if type != "collection": + multipart = message.get_update(parts) + multipart["Update-Action"] = action + message.add_update(multipart) email_message = message.get_payload() gpg = GPG()