1.1 --- a/MoinMessage.py Mon Oct 29 22:36:16 2012 +0100
1.2 +++ b/MoinMessage.py Fri Jan 18 00:36:13 2013 +0100
1.3 @@ -2,7 +2,7 @@
1.4 """
1.5 MoinMoin - MoinMessage library
1.6
1.7 - @copyright: 2012 by Paul Boddie <paul@boddie.org.uk>
1.8 + @copyright: 2012, 2013 by Paul Boddie <paul@boddie.org.uk>
1.9 @license: GNU GPL (v2 or later), see COPYING.txt for details.
1.10 """
1.11
1.12 @@ -14,6 +14,7 @@
1.13 from subprocess import Popen, PIPE
1.14 from tempfile import mkstemp
1.15 from urlparse import urlsplit
1.16 +from MoinMoin.util import lock
1.17 import httplib
1.18 import os
1.19
1.20 @@ -50,21 +51,34 @@
1.21 else:
1.22 self.updates.append(message)
1.23
1.24 - def add_update(self, alternatives):
1.25 + def add_updates(self, parts):
1.26
1.27 """
1.28 - Add an update fragment to a message, providing alternative forms of the
1.29 - update content in the given 'alternatives': a list of MIME message
1.30 - parts, each encoding the content according to different MIME types.
1.31 + Add the given 'parts' to a message.
1.32 """
1.33
1.34 - if len(alternatives) > 1:
1.35 - part = MIMEMultipart()
1.36 - for alternative in alternatives:
1.37 - part.attach(alternative)
1.38 - self.updates.append(part)
1.39 - else:
1.40 - self.updates.append(alternatives[0])
1.41 + for part in updates:
1.42 + self.add_update(part)
1.43 +
1.44 + def add_update(self, part):
1.45 +
1.46 + """
1.47 + Add an update 'part' to a message.
1.48 + """
1.49 +
1.50 + self.updates.append(part)
1.51 +
1.52 + def get_update(self, alternatives):
1.53 +
1.54 + """
1.55 + Return a suitable multipart object containing the supplied
1.56 + 'alternatives'.
1.57 + """
1.58 +
1.59 + part = MIMEMultipart()
1.60 + for alternative in alternatives:
1.61 + part.attach(alternative)
1.62 + return part
1.63
1.64 def get_payload(self):
1.65
1.66 @@ -117,6 +131,83 @@
1.67
1.68 return mailbox
1.69
1.70 +class MessageStore:
1.71 +
1.72 + "A page-specific message store."
1.73 +
1.74 + def __init__(self, page):
1.75 +
1.76 + "Initialise a message store for the given 'page'."
1.77 +
1.78 + self.path = page.getPagePath("messages")
1.79 + self.next_path = os.path.join(self.path, "next")
1.80 + lock_dir = page.getPagePath("message-locks")
1.81 + self.lock = lock.WriteLock(lock_dir)
1.82 +
1.83 + def get_next(self):
1.84 +
1.85 + "Return the next message number."
1.86 +
1.87 + next = self.read_next()
1.88 + if not next:
1.89 + next = self.deduce_next()
1.90 + self.write_next(next)
1.91 + return next
1.92 +
1.93 + def deduce_next(self):
1.94 +
1.95 + "Deduce the next message number from the existing message files."
1.96 +
1.97 + return max([int(filename) for filename in os.listdir(self.path) if filename.isdigit()] or [0]) + 1
1.98 +
1.99 + def read_next(self):
1.100 +
1.101 + "Read the next message number from a special file."
1.102 +
1.103 + if not os.path.exists(self.next_path):
1.104 + return 0
1.105 +
1.106 + f = open(self.next_path)
1.107 + try:
1.108 + try:
1.109 + return int(f.read())
1.110 + except ValueError:
1.111 + return 0
1.112 + finally:
1.113 + f.close()
1.114 +
1.115 + def write_next(self, next):
1.116 +
1.117 + "Write the 'next' message number to a special file."
1.118 +
1.119 + f = open(self.next_path, "w")
1.120 + try:
1.121 + f.write(str(next))
1.122 + finally:
1.123 + f.close()
1.124 +
1.125 + def write_message(self, message, next):
1.126 +
1.127 + "Write the given 'message' to a file with the given 'next' message number."
1.128 +
1.129 + f = open(os.path.join(self.path, str(next)), "w")
1.130 + try:
1.131 + f.write(message.as_string())
1.132 + finally:
1.133 + f.close()
1.134 +
1.135 + def append(self, message):
1.136 +
1.137 + "Append the given 'message' to the store."
1.138 +
1.139 + self.lock.acquire()
1.140 + try:
1.141 + next = self.get_next()
1.142 + self.write_message(message, next)
1.143 + self.write_next(next + 1)
1.144 + finally:
1.145 + self.lock.release()
1.146 +
1.147 class MoinMessageError(Exception):
1.148 pass
1.149
2.1 --- a/README.txt Mon Oct 29 22:36:16 2012 +0100
2.2 +++ b/README.txt Fri Jan 18 00:36:13 2013 +0100
2.3 @@ -127,7 +127,7 @@
2.4 To send a message signed and encrypted to a resource on localhost:
2.5
2.6 python tests/test_send.py 1C1AAF83 0891463A http://localhost/wiki/ShareTest \
2.7 - 'An update to the Wiki.' 'Another update.'
2.8 + collection update 'An update to the Wiki.' 'Another update.'
2.9
2.10 Here, the first identifier is a reference to the signing key (over which you
2.11 have complete control), and the second identifier is a reference to the
2.12 @@ -146,12 +146,14 @@
2.13 Prepare a message signed with a "detached signature" (note that this does not
2.14 seem to be what gpg calls a detached signature with the --detach-sig option):
2.15
2.16 - python tests/test_message.py 'An update to the Wiki.' 'Another update.' \
2.17 + python tests/test_message.py collection update 'An update to the Wiki.' \
2.18 + 'Another update.' \
2.19 | python tests/test_sign.py 1C1AAF83
2.20
2.21 The complicated recipe based on the individual operations is as follows:
2.22
2.23 - python tests/test_message.py 'An update to the Wiki.' 'Another update.' \
2.24 + python tests/test_message.py collection update 'An update to the Wiki.' \
2.25 + 'Another update.' \
2.26 > test.txt \
2.27 && cat test.txt \
2.28 | gpg --armor -u 1C1AAF83 --detach-sig \
2.29 @@ -162,12 +164,14 @@
2.30
2.31 Prepare a message with an encrypted payload using the above key:
2.32
2.33 - python tests/test_message.py 'An update to the Wiki.' 'Another update.' \
2.34 + python tests/test_message.py collection update 'An update to the Wiki.' \
2.35 + 'Another update.' \
2.36 | python tests/test_encrypt.py 0891463A
2.37
2.38 The complicated recipe based on the individual operations is as follows:
2.39
2.40 - python tests/test_message.py 'An update to the Wiki.' 'Another update.' \
2.41 + python tests/test_message.py collection update 'An update to the Wiki.' \
2.42 + 'Another update.' \
2.43 > test.txt \
2.44 && cat test.txt \
2.45 | gpg --armor -r 0891463A --encrypt --trust-model always \
2.46 @@ -180,13 +184,15 @@
2.47
2.48 Sign and encrypt a message:
2.49
2.50 - python tests/test_message.py 'An update to the Wiki.' 'Another update.' \
2.51 + python tests/test_message.py collection update 'An update to the Wiki.' \
2.52 + 'Another update.' \
2.53 | python tests/test_sign.py 1C1AAF83 \
2.54 | python tests/test_encrypt.py 0891463A
2.55
2.56 The complicated recipe based on the individual operations is as follows:
2.57
2.58 - python tests/test_message.py 'An update to the Wiki.' 'Another update.' \
2.59 + python tests/test_message.py collection update 'An update to the Wiki.' \
2.60 + 'Another update.' \
2.61 > test.txt \
2.62 && cat test.txt \
2.63 | gpg --armor -u 1C1AAF83 --detach-sig \
2.64 @@ -204,3 +210,21 @@
2.65
2.66 Here, the resource "/wiki/ShareTest" on localhost is presented with the
2.67 message.
2.68 +
2.69 +The Message Format
2.70 +------------------
2.71 +
2.72 +Messages are MIME-encoded and consist of one or more update fragments. Where
2.73 +the "Update-Type" header is present and set to a value of "collection", a
2.74 +multipart message describes as many updates as there are parts. Otherwise,
2.75 +only a single update is described by the message.
2.76 +
2.77 +For each update, the "Update-Action" header indicates the action to be taken
2.78 +with the update content. Where it is absent, the content is inserted into the
2.79 +Wiki page specified in the request; where it is present and set to "replace",
2.80 +the content replaces all content on the Wiki page; where it is set to "store",
2.81 +the content is stored in a message store associated with the Wiki page.
2.82 +
2.83 +Each update may describe multiple representations of some content by employing
2.84 +a multipart section containing parts for each of the representations.
2.85 +Alternatively, a single message part may describe a single representation.
3.1 --- a/actions/PostMessage.py Mon Oct 29 22:36:16 2012 +0100
3.2 +++ b/actions/PostMessage.py Fri Jan 18 00:36:13 2013 +0100
3.3 @@ -2,15 +2,16 @@
3.4 """
3.5 MoinMoin - PostMessage Action
3.6
3.7 - @copyright: 2012 by Paul Boddie <paul@boddie.org.uk>
3.8 + @copyright: 2012, 2013 by Paul Boddie <paul@boddie.org.uk>
3.9 @license: GNU GPL (v2 or later), see COPYING.txt for details.
3.10 """
3.11
3.12 +from MoinMoin.Page import Page
3.13 from MoinMoin.PageEditor import PageEditor
3.14 from MoinMoin.log import getLogger
3.15 from MoinMoin.user import User
3.16 from MoinSupport import *
3.17 -from MoinMessage import GPG, Message, MoinMessageError
3.18 +from MoinMessage import GPG, Message, MessageStore, MoinMessageError
3.19 from email.parser import Parser
3.20
3.21 try:
3.22 @@ -215,47 +216,59 @@
3.23 # Handle a single part.
3.24
3.25 if not update.is_multipart():
3.26 - self.handle_message_parts([update], to_replace(update))
3.27 + self.handle_message_parts([update], update)
3.28
3.29 # Or a collection of alternative representations for a single
3.30 # update.
3.31
3.32 else:
3.33 - self.handle_message_parts(update.get_payload(), to_replace(update))
3.34 + self.handle_message_parts(update.get_payload(), update)
3.35
3.36 # Default output.
3.37
3.38 writeHeaders(request, "text/plain", getMetadata(self.page), "204 No Content")
3.39
3.40 - def handle_message_parts(self, parts, replace):
3.41 + def handle_message_parts(self, parts, update):
3.42
3.43 """
3.44 - Handle the given message 'parts', replacing the page content if
3.45 - 'replace' is set to a true value.
3.46 + Handle the given message 'parts', using the original 'update' to
3.47 + determine whether the content is to replace or update page content, or
3.48 + whether it will be placed in a message store.
3.49 """
3.50
3.51 - # NOTE: Should either choose preferred content types or somehow retain them
3.52 - # NOTE: all but present one at a time.
3.53 + # Handle the different update actions.
3.54 + # Update a message store for the page.
3.55 +
3.56 + if to_store(update):
3.57 + store = MessageStore(self.page)
3.58 + store.append(update)
3.59
3.60 - body = []
3.61 + # Update the page.
3.62 +
3.63 + else:
3.64 + # NOTE: Should either choose preferred content types or somehow retain them
3.65 + # NOTE: all but present one at a time.
3.66 +
3.67 + body = []
3.68 + replace = to_replace(update)
3.69
3.70 - for part in parts:
3.71 - mimetype = part.get_content_type()
3.72 - encoding = part.get_content_charset()
3.73 - if mimetype == "text/moin":
3.74 - body.append(part.get_payload())
3.75 - if replace:
3.76 - break
3.77 + for part in parts:
3.78 + mimetype = part.get_content_type()
3.79 + encoding = part.get_content_charset()
3.80 + if mimetype == "text/moin":
3.81 + body.append(part.get_payload())
3.82 + if replace:
3.83 + break
3.84
3.85 - if not replace:
3.86 - body.append(self.page.get_raw_body())
3.87 + if not replace:
3.88 + body.append(self.page.get_raw_body())
3.89
3.90 - page_editor = PageEditor(self.request, self.pagename)
3.91 - page_editor.saveText("\n\n".join(body), 0)
3.92 + page_editor = PageEditor(self.request, self.pagename)
3.93 + page_editor.saveText("\n\n".join(body), 0)
3.94
3.95 - # Refresh the page.
3.96 + # Refresh the page.
3.97
3.98 - self.page = Page(self.request, self.pagename)
3.99 + self.page = Page(self.request, self.pagename)
3.100
3.101 def get_homedir(self):
3.102
3.103 @@ -267,12 +280,12 @@
3.104 request.write("Encoded data cannot currently be understood. Please notify the site administrator.")
3.105 return homedir
3.106
3.107 -def is_collection(message):
3.108 - return message.get("Update-Type") == "collection"
3.109 -
3.110 def to_replace(message):
3.111 return message.get("Update-Action") == "replace"
3.112
3.113 +def to_store(message):
3.114 + return message.get("Update-Action") == "store"
3.115 +
3.116 # Action function.
3.117
3.118 def execute(pagename, request):
4.1 --- a/actions/SendMessage.py Mon Oct 29 22:36:16 2012 +0100
4.2 +++ b/actions/SendMessage.py Fri Jan 18 00:36:13 2013 +0100
4.3 @@ -2,7 +2,7 @@
4.4 """
4.5 MoinMoin - SendMessage Action
4.6
4.7 - @copyright: 2012 by Paul Boddie <paul@boddie.org.uk>
4.8 + @copyright: 2012, 2013 by Paul Boddie <paul@boddie.org.uk>
4.9 @license: GNU GPL (v2 or later), see COPYING.txt for details.
4.10 """
4.11
4.12 @@ -107,7 +107,7 @@
4.13 # Construct a message from the request.
4.14
4.15 message = Message()
4.16 - message.add_update([MIMEText(text, "moin")])
4.17 + message.add_update(MIMEText(text, "moin"))
4.18
4.19 # Get the sender details for signing messages.
4.20 # This is not the same as the details for authenticating users in the
5.1 --- a/tests/test_message.py Mon Oct 29 22:36:16 2012 +0100
5.2 +++ b/tests/test_message.py Fri Jan 18 00:36:13 2013 +0100
5.3 @@ -1,14 +1,40 @@
5.4 #!/usr/bin/env python
5.5
5.6 from MoinMessage import Message
5.7 +from email.mime.multipart import MIMEMultipart
5.8 from email.mime.text import MIMEText
5.9 import sys
5.10
5.11 if __name__ == "__main__":
5.12 + try:
5.13 + type = sys.argv[1]
5.14 + action = sys.argv[2]
5.15 + args = sys.argv[3:]
5.16 + except IndexError:
5.17 + args = None
5.18 +
5.19 + if not args:
5.20 + print >>sys.stderr, "Need an update type, update action and some updates as arguments to this program."
5.21 + sys.exit(1)
5.22 +
5.23 message = Message()
5.24 + parts = []
5.25
5.26 - for arg in sys.argv[1:]:
5.27 - message.add_update([MIMEText(arg, "moin")])
5.28 + # Add each content fragment as either a separate update to a collection of
5.29 + # updates or as an alternative part to a single update.
5.30 +
5.31 + for arg in args:
5.32 + part = MIMEText(arg, "moin", sys.stdin.encoding)
5.33 + if type == "collection":
5.34 + part["Update-Action"] = action
5.35 + message.add_update(part)
5.36 + else:
5.37 + parts.append(part)
5.38 +
5.39 + if type != "collection":
5.40 + multipart = message.get_update(parts)
5.41 + multipart["Update-Action"] = action
5.42 + message.add_update(multipart)
5.43
5.44 text = message.get_payload()
5.45 print text
6.1 --- a/tests/test_send.py Mon Oct 29 22:36:16 2012 +0100
6.2 +++ b/tests/test_send.py Fri Jan 18 00:36:13 2013 +0100
6.3 @@ -5,19 +5,35 @@
6.4 import sys
6.5
6.6 if __name__ == "__main__":
6.7 - signer = sys.argv[1]
6.8 - recipient = sys.argv[2]
6.9 - url = sys.argv[3] + "?action=PostMessage"
6.10 - args = sys.argv[4:]
6.11 + try:
6.12 + signer = sys.argv[1]
6.13 + recipient = sys.argv[2]
6.14 + url = sys.argv[3] + "?action=PostMessage"
6.15 + type = sys.argv[4]
6.16 + action = sys.argv[5]
6.17 + args = sys.argv[6:]
6.18 + except IndexError:
6.19 + args = None
6.20
6.21 if not args:
6.22 - print >>sys.stderr, "Need some updates as arguments to this program."
6.23 + print >>sys.stderr, "Need a signer, recipient, URL, update type, action and some updates as arguments to this program."
6.24 sys.exit(1)
6.25
6.26 message = Message()
6.27 + parts = []
6.28
6.29 for arg in args:
6.30 - message.add_update([MIMEText(arg, "moin")])
6.31 + part = MIMEText(arg, "moin", sys.stdin.encoding)
6.32 + if type == "collection":
6.33 + part["Update-Action"] = action
6.34 + message.add_update(part)
6.35 + else:
6.36 + parts.append(part)
6.37 +
6.38 + if type != "collection":
6.39 + multipart = message.get_update(parts)
6.40 + multipart["Update-Action"] = action
6.41 + message.add_update(multipart)
6.42
6.43 email_message = message.get_payload()
6.44 gpg = GPG()