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 # Communications functions. 150 151 def timestamp(message): 152 153 """ 154 Timestamp the given 'message' so that its validity can be assessed by the 155 recipient. 156 """ 157 158 datestr = formatdate() 159 160 if not message.has_key("Date"): 161 message.add_header("Date", datestr) 162 else: 163 message.replace_header("Date", datestr) 164 165 def _getConnection(scheme): 166 167 "Return the connection class for the given 'scheme'." 168 169 if scheme == "http": 170 return httplib.HTTPConnection 171 elif scheme == "https": 172 return httplib.HTTPSConnection 173 else: 174 raise MoinMessageError, "Communications protocol not supported: %s" % scheme 175 176 def sendMessageOpener(message, url, method="PUT"): 177 178 """ 179 Send 'message' to the given 'url' using the given 'method' (using PUT as the 180 default if omitted). 181 """ 182 183 scheme, host, port, path = parseURL(url) 184 text = as_string(message) 185 186 req = _getConnection(scheme)(host, port) 187 req.request(method, path, text) 188 resp = req.getresponse() 189 190 if resp.status >= 400: 191 raise MoinMessageTransferError(resp.status, "Message sending failed (%s)" % resp.status, resp.read()) 192 193 return resp 194 195 def sendMessage(message, url, method="PUT"): 196 197 """ 198 Send 'message' to the given 'url' using the given 'method' (using PUT as the 199 default if omitted). 200 """ 201 202 resp = sendMessageOpener(message, url, method) 203 return resp.read() 204 205 def parseURL(url): 206 207 "Return the scheme, host, port and path for the given 'url'." 208 209 scheme, host_port, path, query, fragment = urlsplit(url) 210 host_port = host_port.split(":") 211 212 if query: 213 path += "?" + query 214 215 if len(host_port) > 1: 216 host = host_port[0] 217 port = int(host_port[1]) 218 else: 219 host = host_port[0] 220 port = 80 221 222 return scheme, host, port, path 223 224 # Message handling. 225 226 class MessageInterface: 227 228 "A command-based interface to a message store, inspired by RFC 1939 (POP3)." 229 230 def __init__(self, store): 231 self.store = store 232 233 def execute(self, commands): 234 235 """ 236 Access messages according to the given 'commands' script, acting on the 237 store provided during initialisation and returning a message object 238 containing the results. 239 """ 240 241 # Build a container for the responses. 242 243 message = Message() 244 245 # Process each command. 246 247 for command in commands.split("\n"): 248 command = command.strip() 249 250 # Get the command and arguments. 251 252 command_parts = command.split(None, 1) 253 cmd = command_parts[0] 254 255 try: 256 if cmd in self.commands: 257 getattr(self, cmd)(command_parts, message) 258 else: 259 self.add_result(cmd, command, "ERR", message) 260 except Exception, exc: 261 self.add_result(cmd, "\n".join([command, str(exc)]), "ERR", message) 262 263 return message 264 265 def get_count(self, command_parts): 266 267 # Select all messages by default. 268 269 count = None 270 271 if len(command_parts) > 1: 272 count = int(command_parts[1]) 273 274 return count 275 276 def add_result(self, cmd, result, status, message): 277 part = MIMEText(result, "x-moinmessage-fetch-result") 278 part["Request-Type"] = cmd 279 part["Request-Status"] = status 280 message.add_update(part) 281 282 def add_messages(self, resources, message): 283 container = Message() 284 285 for message_text in resources: 286 message_item = Parser().parsestr(message_text) 287 container.add_update(message_item) 288 289 # Convert the container to a proper multipart section. 290 291 message.add_update(container.get_payload()) 292 293 def STAT(self, command_parts, message): 294 295 # A request to count the messages is returned in a part. 296 297 self.add_result("STAT", str(len(self.store)), "OK", message) 298 299 def RETR(self, command_parts, message): 300 301 # A request for specific messages returns each message 302 # in its own part within a collection part. 303 304 count = self.get_count(command_parts) 305 306 self.add_messages(islice(iter(self.store), count), message) 307 308 def DELE(self, command_parts, message): 309 310 # A request to delete messages is performed immediately. 311 312 count = self.get_count(command_parts) 313 314 keys = self.store.keys()[:count] 315 316 for key in keys: 317 del self.store[key] 318 319 self.add_result("DELE", str(len(keys)), "OK", message) 320 321 # Command manifest, may be extended by subclasses. 322 323 commands = "STAT", "RETR", "DELE" 324 325 # vim: tabstop=4 expandtab shiftwidth=4