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