1 #!/usr/bin/env python 2 3 """ 4 A processing framework for iMIP content. 5 6 Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from email import message_from_file 23 from imiptools.content import get_addresses, get_uri, handle_itip_part 24 from imiptools.mail import Messenger 25 from imiptools.profile import Preferences 26 import sys 27 28 # Postfix exit codes. 29 30 EX_TEMPFAIL = 75 31 32 # Permitted iTIP content types. 33 34 itip_content_types = [ 35 "text/calendar", # from RFC 6047 36 "text/x-vcalendar", "application/ics", # other possibilities 37 ] 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, messenger=None): 52 self.handlers = handlers 53 self.messenger = messenger or Messenger() 54 self.lmtp_socket = None 55 56 def process(self, f, original_recipients, recipients, outgoing_only): 57 58 """ 59 Process content from the stream 'f' accompanied by the given 60 'original_recipients' and 'recipients'. 61 """ 62 63 msg = message_from_file(f) 64 senders = get_addresses(msg.get_all("Reply-To") or msg.get_all("From") or []) 65 original_recipients = original_recipients or get_addresses(get_all_values(msg, "To") or []) 66 67 # Handle messages with iTIP parts. 68 69 for recipient in original_recipients: 70 self.process_for_recipient(msg, recipient, senders, outgoing_only) 71 72 def process_for_recipient(self, msg, recipient, senders, outgoing_only): 73 74 """ 75 Process the given 'msg' for a single 'recipient', having the given 76 'senders', and with the given 'outgoing_only' status. 77 78 Processing individually means that contributions to resulting messages 79 may be constructed according to individual preferences. 80 """ 81 82 all_responses = [] 83 handled = False 84 85 for part in msg.walk(): 86 if part.get_content_type() in itip_content_types and \ 87 part.get_param("method"): 88 89 all_responses += handle_itip_part(part, senders, recipient, self.handlers, self.messenger) 90 handled = True 91 92 # When processing outgoing messages, no replies or deliveries are 93 # performed. 94 95 if outgoing_only: 96 return 97 98 # Pack any returned parts into messages. 99 100 if all_responses: 101 outgoing_parts = [] 102 forwarded_parts = [] 103 104 for outgoing, part in all_responses: 105 if outgoing: 106 outgoing_parts.append(part) 107 else: 108 forwarded_parts.append(part) 109 110 # Reply using any outgoing parts in a new message. 111 112 if outgoing_parts: 113 message = self.messenger.make_outgoing_message(outgoing_parts, senders) 114 115 if "-d" in sys.argv: 116 print >>sys.stderr, "Outgoing parts..." 117 print message 118 else: 119 self.messenger.sendmail(senders, message.as_string()) 120 121 # Forward messages to their recipients either wrapping the existing 122 # message, accompanying it or replacing it. 123 124 if forwarded_parts: 125 126 # Determine whether to wrap, accompany or replace the message. 127 128 preferences = Preferences(get_uri(recipient)) 129 130 incoming = preferences.get("incoming") 131 132 if incoming == "message-only": 133 messages = [msg] 134 else: 135 summary = self.messenger.make_summary_message(msg, forwarded_parts) 136 if incoming == "summary-then-message": 137 messages = [summary, msg] 138 elif incoming == "message-then-summary": 139 messages = [msg, summary] 140 elif incoming == "summary-only": 141 messages = [summary] 142 else: # incoming == "summary-wraps-message": 143 messages = [self.messenger.wrap_message(msg, forwarded_parts)] 144 145 for message in messages: 146 if "-d" in sys.argv: 147 print >>sys.stderr, "Forwarded parts..." 148 print message 149 elif self.lmtp_socket: 150 self.messenger.sendmail(recipient, message.as_string(), lmtp_socket=self.lmtp_socket) 151 152 # Unhandled messages are delivered as they are. 153 154 if not handled: 155 if "-d" in sys.argv: 156 print >>sys.stderr, "Unhandled parts..." 157 print msg 158 elif self.lmtp_socket: 159 self.messenger.sendmail(recipient, msg.as_string(), lmtp_socket=self.lmtp_socket) 160 161 def process_args(self, args, stream): 162 163 """ 164 Interpret the given program arguments 'args' and process input from the 165 given 'stream'. 166 """ 167 168 # Obtain the different kinds of recipients plus sender address. 169 170 original_recipients = [] 171 recipients = [] 172 senders = [] 173 lmtp = [] 174 outgoing_only = False 175 176 l = [] 177 178 for arg in args: 179 180 # Detect outgoing processing mode. 181 182 if arg == "-O": 183 outgoing_only = True 184 185 # Switch to collecting recipients. 186 187 if arg == "-o": 188 l = original_recipients 189 elif arg == "-r": 190 l = recipients 191 192 # Switch to collecting senders. 193 194 elif arg == "-s": 195 l = senders 196 197 # Switch to getting the LMTP socket. 198 199 elif arg == "-l": 200 l = lmtp 201 202 # Ignore debugging options. 203 204 elif arg == "-d": 205 pass 206 else: 207 l.append(arg) 208 209 self.messenger.sender = senders and senders[0] or self.messenger.sender 210 self.lmtp_socket = lmtp and lmtp[0] or None 211 self.process(stream, original_recipients, recipients, outgoing_only) 212 213 def __call__(self): 214 215 """ 216 Obtain arguments from the command line to initialise the processor 217 before invoking it. 218 """ 219 220 args = sys.argv[1:] 221 222 if "-d" in args: 223 self.process_args(args, sys.stdin) 224 else: 225 try: 226 self.process_args(args, sys.stdin) 227 except SystemExit, value: 228 sys.exit(value) 229 except Exception, exc: 230 if "-v" in args: 231 raise 232 type, value, tb = sys.exc_info() 233 print >>sys.stderr, "Exception %s at %d" % (exc, tb.tb_lineno) 234 #import traceback 235 #traceback.print_exc(file=open("/tmp/mail.log", "a")) 236 sys.exit(EX_TEMPFAIL) 237 sys.exit(0) 238 239 # vim: tabstop=4 expandtab shiftwidth=4