1 #!/usr/bin/env python 2 3 from email import message_from_file 4 from email.mime.message import MIMEMessage 5 from email.mime.multipart import MIMEMultipart 6 from email.mime.text import MIMEText 7 from smtplib import LMTP, SMTP 8 from imiptools.content import handle_itip_part 9 import sys 10 11 MESSAGE_SENDER = "resources+agent@example.com" 12 13 MESSAGE_SUBJECT = "Calendar system message" 14 15 MESSAGE_TEXT = """\ 16 This is a response to a calendar message sent by your calendar program. 17 """ 18 19 # Postfix exit codes. 20 21 EX_TEMPFAIL = 75 22 23 # Permitted iTIP content types. 24 25 itip_content_types = [ 26 "text/calendar", # from RFC 6047 27 "text/x-vcalendar", "application/ics", # other possibilities 28 ] 29 30 # Sending of outgoing messages. 31 32 def sendmail(sender, recipients, data, lmtp_socket=None): 33 if lmtp_socket: 34 smtp = LMTP(lmtp_socket) 35 else: 36 smtp = SMTP("localhost") 37 smtp.sendmail(sender, recipients, data) 38 smtp.quit() 39 40 def make_message(parts, recipients, sender, subject, body_text): 41 message = MIMEMultipart("mixed", _subparts=parts) 42 message.preamble = body_text 43 payload = message.get_payload() 44 payload.insert(0, MIMEText(body_text)) 45 46 message["From"] = sender 47 for recipient in recipients: 48 message["To"] = recipient 49 message["Subject"] = subject 50 51 return message 52 53 # Processing of incoming messages. 54 55 def get_all_values(msg, key): 56 l = [] 57 for v in msg.get_all(key) or []: 58 l += [s.strip() for s in v.split(",")] 59 return l 60 61 class Processor: 62 63 "The processing framework." 64 65 def __init__(self, handlers, sender=None, subject=None, body_text=None): 66 self.handlers = handlers 67 self.sender = sender or MESSAGE_SENDER 68 self.subject = subject or MESSAGE_SUBJECT 69 self.body_text = body_text or MESSAGE_TEXT 70 self.lmtp_socket = None 71 72 def process(self, f, original_recipients, recipients): 73 74 """ 75 Process content from the stream 'f' accompanied by the given 76 'original_recipients' and 'recipients'. 77 """ 78 79 msg = message_from_file(f) 80 senders = msg.get_all("Reply-To") or msg.get_all("From") 81 original_recipients = original_recipients or get_all_values(msg, "To") 82 83 # Handle messages with iTIP parts. 84 85 all_responses = [] 86 handled = False 87 88 for part in msg.walk(): 89 if part.get_content_type() in itip_content_types and \ 90 part.get_param("method"): 91 92 all_responses += handle_itip_part(part, original_recipients, self.handlers) 93 handled = True 94 95 # Pack any returned parts into a single message. 96 97 if all_responses: 98 outgoing_parts = [] 99 forwarded_parts = [] 100 101 for outgoing, part in all_responses: 102 if outgoing: 103 outgoing_parts.append(part) 104 else: 105 forwarded_parts.append(part) 106 107 # Reply using any outgoing parts in a new message. 108 109 if outgoing_parts: 110 message = self.make_message(outgoing_parts, senders) 111 112 if "-d" in sys.argv: 113 print message 114 else: 115 sendmail(self.sender, senders, message.as_string()) 116 117 # Forward messages to their recipients using the existing message. 118 119 if forwarded_parts: 120 message = self.wrap_message(msg, forwarded_parts) 121 122 if "-d" in sys.argv: 123 print message 124 elif self.lmtp_socket: 125 sendmail(self.sender, original_recipients, message.as_string(), self.lmtp_socket) 126 127 # Unhandled messages are delivered as they are. 128 129 if not handled: 130 if "-d" in sys.argv: 131 print msg 132 elif self.lmtp_socket: 133 sendmail(self.sender, original_recipients, msg.as_string(), self.lmtp_socket) 134 135 def make_message(self, parts, recipients): 136 137 "Make a message from the given 'parts' for the given 'recipients'." 138 139 return make_message(parts, recipients, self.sender, self.subject, self.body_text) 140 141 def wrap_message(self, msg, parts): 142 143 "Wrap 'msg' and provide the given 'parts' as the primary content." 144 145 message = MIMEMultipart("mixed", _subparts=parts) 146 message.preamble = self.body_text 147 message.get_payload().append(MIMEMessage(msg)) 148 149 message["From"] = msg["From"] 150 message["To"] = msg["To"] 151 message["Subject"] = msg["Subject"] 152 153 return message 154 155 def process_args(self, args, stream): 156 157 """ 158 Interpret the given program arguments 'args' and process input from the 159 given 'stream'. 160 """ 161 162 # Obtain the different kinds of recipients plus sender address. 163 164 original_recipients = [] 165 recipients = [] 166 senders = [] 167 lmtp = [] 168 169 l = [] 170 171 for arg in args: 172 173 # Switch to collecting recipients. 174 175 if arg == "-o": 176 l = original_recipients 177 elif arg == "-r": 178 l = recipients 179 180 # Switch to collecting senders. 181 182 elif arg == "-s": 183 l = senders 184 185 # Switch to getting the LMTP socket. 186 187 elif arg == "-l": 188 l = lmtp 189 190 # Ignore debugging options. 191 192 elif arg == "-d": 193 pass 194 else: 195 l.append(arg) 196 197 self.sender = senders and senders[0] or self.sender 198 self.lmtp_socket = lmtp and lmtp[0] or None 199 self.process(stream, original_recipients, recipients) 200 201 def __call__(self): 202 203 """ 204 Obtain arguments from the command line to initialise the processor 205 before invoking it. 206 """ 207 208 args = sys.argv[1:] 209 210 if "-d" in args: 211 self.process_args(args, sys.stdin) 212 else: 213 try: 214 self.process_args(args, sys.stdin) 215 except SystemExit, value: 216 sys.exit(value) 217 except Exception, exc: 218 if "-v" in args: 219 raise 220 type, value, tb = sys.exc_info() 221 print >>sys.stderr, "Exception %s at %d" % (exc, tb.tb_lineno) 222 sys.exit(EX_TEMPFAIL) 223 sys.exit(0) 224 225 # vim: tabstop=4 expandtab shiftwidth=4