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