1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - MoinMessage library 4 5 @copyright: 2012 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.mime.multipart import MIMEMultipart 12 from email.mime.application import MIMEApplication 13 from email.mime.base import MIMEBase 14 from subprocess import Popen, PIPE 15 from tempfile import mkstemp 16 from urlparse import urlsplit 17 import httplib 18 import os 19 20 def is_collection(message): 21 return message.get("Update-Type") == "collection" 22 23 class Message: 24 25 "An update message." 26 27 def __init__(self, text=None): 28 self.updates = [] 29 if text: 30 self.parse_text(text) 31 32 def parse_text(self, text): 33 34 "Parse the given 'text' as a message." 35 36 self.handle_message(message_from_string(text)) 37 38 def handle_message(self, message): 39 40 "Handle the given 'message', recording the separate updates." 41 42 # The message either consists of a collection of updates. 43 44 if message.is_multipart() and is_collection(message): 45 for part in message.get_payload(): 46 self.updates.append(part) 47 48 # Or the message is a single update. 49 50 else: 51 self.updates.append(message) 52 53 def add_update(self, alternatives): 54 55 """ 56 Add an update fragment to a message, providing alternative forms of the 57 update content in the given 'alternatives': a list of MIME message 58 parts, each encoding the content according to different MIME types. 59 """ 60 61 if len(alternatives) > 1: 62 part = MIMEMultipart() 63 for alternative in alternatives: 64 part.attach(alternative) 65 self.updates.append(part) 66 else: 67 self.updates.append(alternatives[0]) 68 69 def get_payload(self): 70 71 "Get the multipart payload for the message." 72 73 if len(self.updates) == 1: 74 message = self.updates[0] 75 else: 76 message = MIMEMultipart() 77 message.add_header("Update-Type", "collection") 78 for update in self.updates: 79 message.attach(update) 80 81 return message 82 83 class Mailbox: 84 85 "A collection of messages within a multipart message." 86 87 def __init__(self, text=None): 88 self.messages = [] 89 if text: 90 self.parse_text(text) 91 92 def parse_text(self, text): 93 94 "Parse the given 'text' as a mailbox." 95 96 message = message_from_string(text) 97 98 if message.is_multipart(): 99 for part in message.get_payload(): 100 self.messages.append(part) 101 else: 102 self.messages.append(message) 103 104 def add_message(self, message): 105 106 "Add the given 'message' to the mailbox." 107 108 self.messages.append(message) 109 110 def get_payload(self): 111 112 "Get the multipart payload for the mailbox." 113 114 mailbox = MIMEMultipart() 115 for message in self.messages: 116 mailbox.attach(message) 117 118 return mailbox 119 120 class MoinMessageError(Exception): 121 pass 122 123 class GPG: 124 125 "A wrapper around the gpg command using a particular configuration." 126 127 def __init__(self, homedir=None): 128 self.conf_args = [] 129 130 if homedir: 131 self.conf_args += ["--homedir", homedir] 132 133 self.errors = None 134 135 def run(self, args, text=None): 136 137 """ 138 Invoke gpg with the given 'args', supplying the given 'text' to the 139 command directly or, if 'text' is omitted, using a file provided as part 140 of the 'args' if appropriate. 141 142 Failure to complete the operation will result in a MoinMessageError 143 being raised. 144 """ 145 146 cmd = Popen(["gpg"] + self.conf_args + list(args), stdin=PIPE, stdout=PIPE, stderr=PIPE) 147 148 try: 149 # Attempt to write input to the command and to read output from the 150 # command. 151 152 try: 153 if text: 154 cmd.stdin.write(text) 155 cmd.stdin.close() 156 157 text = cmd.stdout.read() 158 159 # I/O errors can indicate the failure of the command. 160 161 except IOError: 162 pass 163 164 self.errors = cmd.stderr.read() 165 166 # Test for a zero result. 167 168 if not cmd.wait(): 169 return text 170 else: 171 raise MoinMessageError, self.errors 172 173 finally: 174 cmd.stdout.close() 175 cmd.stderr.close() 176 177 def verifyMessage(self, signature, content): 178 179 "Using the given 'signature', verify the given message 'content'." 180 181 # Write the detached signature and content to files. 182 183 signature_fd, signature_filename = mkstemp() 184 content_fd, content_filename = mkstemp() 185 186 try: 187 signature_fp = os.fdopen(signature_fd, "w") 188 content_fp = os.fdopen(content_fd, "w") 189 try: 190 signature_fp.write(signature) 191 content_fp.write(content) 192 finally: 193 signature_fp.close() 194 content_fp.close() 195 196 # Verify the message text. 197 198 text = self.run(["--status-fd", "1", "--verify", signature_filename, content_filename]) 199 200 # Return the details of the signing key. 201 202 identity = None 203 fingerprint = None 204 205 for line in text.split("\n"): 206 try: 207 prefix, msgtype, digest, details = line.strip().split(" ", 3) 208 except ValueError: 209 continue 210 211 # Return the fingerprint and identity details. 212 213 if msgtype == "GOODSIG": 214 identity = details 215 elif msgtype == "VALIDSIG": 216 fingerprint = digest 217 218 if identity and fingerprint: 219 return fingerprint, identity 220 221 return None 222 223 finally: 224 os.remove(signature_filename) 225 os.remove(content_filename) 226 227 def signMessage(self, message, keyid): 228 229 """ 230 Return a signed version of 'message' using the given 'keyid'. 231 """ 232 233 text = message.as_string() 234 signature = self.run(["--armor", "-u", keyid, "--detach-sig"], text) 235 236 # Make the container for the message. 237 238 signed_message = MIMEMultipart("signed", protocol="application/pgp-signature") 239 signed_message.attach(message) 240 241 signature_part = MIMEBase("application", "pgp-signature") 242 signature_part.set_payload(signature) 243 signed_message.attach(signature_part) 244 245 return signed_message 246 247 def decryptMessage(self, message): 248 249 "Return a decrypted version of 'message'." 250 251 return self.run(["--decrypt"], message) 252 253 def encryptMessage(self, message, keyid): 254 255 """ 256 Return an encrypted version of 'message' using the given 'keyid'. 257 """ 258 259 text = message.as_string() 260 encrypted = self.run(["--armor", "-r", keyid, "--encrypt", "--trust-model", "always"], text) 261 262 # Make the container for the message. 263 264 encrypted_message = MIMEMultipart("encrypted", protocol="application/pgp-encrypted") 265 266 # For encrypted content, add the declaration and content. 267 268 declaration = MIMEBase("application", "pgp-encrypted") 269 declaration.set_payload("Version: 1") 270 encrypted_message.attach(declaration) 271 272 content = MIMEApplication(encrypted, "octet-stream", encode_noop) 273 encrypted_message.attach(content) 274 275 return encrypted_message 276 277 # Communications functions. 278 279 def sendMessage(message, url): 280 281 "Send 'message' to the given 'url." 282 283 scheme, host, port, path = parseURL(url) 284 text = message.as_string() 285 286 if scheme == "http": 287 cls = httplib.HTTPConnection 288 elif scheme == "https": 289 cls = httplib.HTTPSConnection 290 else: 291 raise MoinMessageError, "Communications protocol not supported: %s" % scheme 292 293 req = cls(host, port) 294 req.request("PUT", path, text) 295 resp = req.getresponse() 296 return resp.read() 297 298 def parseURL(url): 299 300 "Return the scheme, host, port and path for the given 'url'." 301 302 scheme, host_port, path, query, fragment = urlsplit(url) 303 host_port = host_port.split(":") 304 305 if query: 306 path += "?" + query 307 308 if len(host_port) > 1: 309 host = host_port[0] 310 port = int(host_port[1]) 311 else: 312 host = host_port[0] 313 port = 80 314 315 return scheme, host, port, path 316 317 # vim: tabstop=4 expandtab shiftwidth=4