1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - MoinMessage library 4 5 @copyright: 2012, 2013, 2014, 2015 by Paul Boddie <paul@boddie.org.uk> 6 @license: GNU GPL (v2 or later), see COPYING.txt for details. 7 """ 8 9 from email import message_from_string 10 from email.encoders import encode_noop 11 from email.generator import Generator 12 from email.mime.multipart import MIMEMultipart 13 from email.mime.application import MIMEApplication 14 from email.mime.base import MIMEBase 15 from email.mime.text import MIMEText 16 from email.parser import Parser 17 from email.utils import formatdate 18 from itertools import islice 19 from subprocess import Popen, PIPE 20 from tempfile import mkstemp 21 from urlparse import urlsplit 22 from DateSupport import getDateTimeFromRFC2822 23 from GPGUtils import GPG, GPGError, GPGDecodingError, GPGMissingPart, GPGBadContent, \ 24 as_string, is_signed, is_encrypted, getContentAndSignature 25 import httplib 26 import os 27 28 try: 29 from cStringIO import StringIO 30 except ImportError: 31 from StringIO import StringIO 32 33 # Message inspection functions. 34 35 def is_collection(message): 36 return message.get("Update-Type") == "collection" 37 38 def to_replace(message): 39 return message.get("Update-Action") == "replace" 40 41 def to_store(message): 42 return message.get("Update-Action") == "store" 43 44 def get_update_action(message): 45 return message.get("Update-Action", "update") 46 47 # Core abstractions. 48 49 class Message: 50 51 "An update message." 52 53 def __init__(self, text=None): 54 self.date = None 55 self.updates = [] 56 if text: 57 self.parse_text(text) 58 59 def init_date(self, message): 60 61 "Obtain the date of the given 'message'." 62 63 if message.has_key("Date"): 64 self.date = getDateTimeFromRFC2822(message["Date"]) 65 else: 66 self.date = None 67 68 def parse_text(self, text): 69 70 "Parse the given 'text' as a message." 71 72 self.handle_message(message_from_string(text)) 73 74 def handle_message(self, message): 75 76 "Handle the given 'message', recording the separate updates." 77 78 self.init_date(message) 79 80 # The message either consists of a collection of updates. 81 82 if message.is_multipart() and is_collection(message): 83 for part in message.get_payload(): 84 self.updates.append(part) 85 86 # Or the message is a single update. 87 88 else: 89 self.updates.append(message) 90 91 def add_updates(self, parts): 92 93 """ 94 Add the given 'parts' to a message. 95 """ 96 97 for part in updates: 98 self.add_update(part) 99 100 def add_update(self, part): 101 102 """ 103 Add an update 'part' to a message. 104 """ 105 106 self.updates.append(part) 107 108 def get_update(self, alternatives): 109 110 """ 111 Return a suitable multipart object containing the supplied 112 'alternatives'. 113 """ 114 115 part = MIMEMultipart("alternative") 116 for alternative in alternatives: 117 part.attach(alternative) 118 return part 119 120 def get_payload(self, subtype="mixed", timestamped=True): 121 122 """ 123 Get the multipart payload for the message. If the 'timestamped' 124 parameter is omitted or set to a true value, the payload will be given a 125 date header set to the current date and time that can be used to assess 126 the validity of a message and to determine whether it has already been 127 received by a recipient. 128 """ 129 130 if len(self.updates) == 1: 131 message = self.updates[0] 132 else: 133 message = MIMEMultipart(subtype) 134 message.add_header("Update-Type", "collection") 135 for update in self.updates: 136 message.attach(update) 137 138 if timestamped: 139 timestamp(message) 140 self.init_date(message) 141 142 return message 143 144 MoinMessageError = GPGError 145 MoinMessageDecodingError = GPGDecodingError 146 MoinMessageMissingPart = GPGMissingPart 147 MoinMessageBadContent = GPGBadContent 148 149 class MoinMessageTransferError(MoinMessageError): 150 def __init__(self, code, message, body): 151 MoinMessageError.__init__(self, message) 152 self.code = code 153 self.body = body 154 155 # Communications functions. 156 157 def timestamp(message): 158 159 """ 160 Timestamp the given 'message' so that its validity can be assessed by the 161 recipient. 162 """ 163 164 datestr = formatdate() 165 166 if not message.has_key("Date"): 167 message.add_header("Date", datestr) 168 else: 169 message.replace_header("Date", datestr) 170 171 def _getConnection(scheme): 172 173 "Return the connection class for the given 'scheme'." 174 175 if scheme == "http": 176 return httplib.HTTPConnection 177 elif scheme == "https": 178 return httplib.HTTPSConnection 179 else: 180 raise MoinMessageError, "Communications protocol not supported: %s" % scheme 181 182 def sendMessageOpener(message, url, method="PUT"): 183 184 """ 185 Send 'message' to the given 'url' using the given 'method' (using PUT as the 186 default if omitted). 187 """ 188 189 scheme, host, port, path = parseURL(url) 190 text = as_string(message) 191 192 req = _getConnection(scheme)(host, port) 193 req.request(method, path, text) 194 resp = req.getresponse() 195 196 if resp.status >= 400: 197 raise MoinMessageTransferError(resp.status, "Message sending failed (%s)" % resp.status, resp.read()) 198 199 return resp 200 201 def sendMessage(message, url, method="PUT"): 202 203 """ 204 Send 'message' to the given 'url' using the given 'method' (using PUT as the 205 default if omitted). 206 """ 207 208 resp = sendMessageOpener(message, url, method) 209 return resp.read() 210 211 def parseURL(url): 212 213 "Return the scheme, host, port and path for the given 'url'." 214 215 scheme, host_port, path, query, fragment = urlsplit(url) 216 host_port = host_port.split(":") 217 218 if query: 219 path += "?" + query 220 221 if len(host_port) > 1: 222 host = host_port[0] 223 port = int(host_port[1]) 224 else: 225 host = host_port[0] 226 port = 80 227 228 return scheme, host, port, path 229 230 # Message handling. 231 232 class MessageInterface: 233 234 "A command-based interface to a message store, inspired by RFC 1939 (POP3)." 235 236 def __init__(self, store): 237 self.store = store 238 239 def execute(self, commands): 240 241 """ 242 Access messages according to the given 'commands' script, acting on the 243 store provided during initialisation and returning a message object 244 containing the results. 245 """ 246 247 # Build a container for the responses. 248 249 message = Message() 250 251 # Process each command. 252 253 for command in commands.split("\n"): 254 command = command.strip() 255 256 # Get the command and arguments. 257 258 command_parts = command.split(None, 1) 259 cmd = command_parts[0] 260 261 try: 262 if cmd in self.commands: 263 getattr(self, cmd)(command_parts, message) 264 else: 265 self.add_result(cmd, command, "ERR", message) 266 except Exception, exc: 267 self.add_result(cmd, "\n".join([command, str(exc)]), "ERR", message) 268 269 return message 270 271 def get_count(self, command_parts): 272 273 # Select all messages by default. 274 275 count = None 276 277 if len(command_parts) > 1: 278 count = int(command_parts[1]) 279 280 return count 281 282 def add_result(self, cmd, result, status, message): 283 part = MIMEText(result, "x-moinmessage-fetch-result") 284 part["Request-Type"] = cmd 285 part["Request-Status"] = status 286 message.add_update(part) 287 288 def add_messages(self, resources, message): 289 container = Message() 290 291 for message_text in resources: 292 message_item = Parser().parsestr(message_text) 293 container.add_update(message_item) 294 295 # Convert the container to a proper multipart section. 296 297 message.add_update(container.get_payload()) 298 299 def STAT(self, command_parts, message): 300 301 # A request to count the messages is returned in a part. 302 303 self.add_result("STAT", str(len(self.store)), "OK", message) 304 305 def RETR(self, command_parts, message): 306 307 # A request for specific messages returns each message 308 # in its own part within a collection part. 309 310 count = self.get_count(command_parts) 311 312 self.add_messages(islice(iter(self.store), count), message) 313 314 def DELE(self, command_parts, message): 315 316 # A request to delete messages is performed immediately. 317 318 count = self.get_count(command_parts) 319 320 keys = self.store.keys()[:count] 321 322 for key in keys: 323 del self.store[key] 324 325 self.add_result("DELE", str(len(keys)), "OK", message) 326 327 # Command manifest, may be extended by subclasses. 328 329 commands = "STAT", "RETR", "DELE" 330 331 # vim: tabstop=4 expandtab shiftwidth=4