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