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