1 #!/usr/bin/env python 2 3 """ 4 The handler invocation mechanism. 5 6 Copyright (C) 2014, 2015, 2017 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 imiptools.config import settings 23 from imiptools.data import Object, parse_object, get_value 24 25 try: 26 from cStringIO import StringIO 27 except ImportError: 28 from StringIO import StringIO 29 30 IMIP_COUNTER_AS_REQUEST = settings["IMIP_COUNTER_AS_REQUEST"] 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 def have_itip_part(part): 40 41 "Return whether 'part' provides iTIP content." 42 43 return part.get_content_type() in itip_content_types and \ 44 part.get_param("method") 45 46 def is_returned_message(message): 47 48 """ 49 Return whether 'message' contains a returned message that should not be 50 handled because it may have originated from the current recipient. 51 """ 52 53 for part in message.walk(): 54 if part.get_content_type() == "message/delivery-status": 55 return True 56 return False 57 58 def consistent_methods(itip_method, part_method): 59 60 """ 61 Return whether 'itip_method' (from content) and 'part_method' (from 62 metadata) are consistent. 63 """ 64 65 return itip_method == part_method or \ 66 IMIP_COUNTER_AS_REQUEST and itip_method == "COUNTER" and \ 67 part_method == "REQUEST" 68 69 def parse_itip_part(part): 70 71 """ 72 Parse the given message 'part' and return a dictionary mapping calendar 73 object type names to lists of fragments. 74 75 If no iTIP content is found, None is returned. 76 """ 77 78 # Ignore the part if not iTIP-related. 79 80 if not have_itip_part(part): 81 return None 82 83 # Decode the data and parse it. 84 85 f = StringIO(part.get_payload(decode=True)) 86 itip = parse_object(f, part.get_content_charset(), "VCALENDAR") 87 88 # Ignore the part if not a calendar object. 89 90 if not itip: 91 return None 92 93 # Require consistency between declared and employed methods. 94 95 itip_method = get_value(itip, "METHOD") 96 method = part.get_param("method") 97 method = method and method.upper() 98 99 if not consistent_methods(itip_method, method): 100 return None 101 102 return itip 103 104 def get_objects_from_itip(itip, handlers): 105 106 """ 107 Return all objects provided by the given 'itip' mapping supported by the 108 given 'handlers'. 109 """ 110 111 objects = [] 112 113 # Look for different kinds of sections. 114 115 if itip: 116 for name, items in itip.items(): 117 if name in handlers: 118 for item in items: 119 objects.append(Object({name : item})) 120 121 return objects 122 123 def handle_itip_part(part, handlers): 124 125 """ 126 Handle the given iTIP 'part' using the given 'handlers' dictionary. 127 Responses are set in each handler used to handle a message. Return whether 128 the part could be handled. 129 """ 130 131 itip = parse_itip_part(part) 132 if itip: 133 handle_calendar_data(itip, handlers) 134 135 return not not itip 136 137 def is_cancel_itip(itip): 138 139 "Return whether 'itip' is cancelling objects." 140 141 return get_value(itip, "METHOD") == "CANCEL" 142 143 def handle_calendar_data(itip, handlers): 144 145 """ 146 Handle the calendar data provided by 'itip' using the given 'handlers' 147 dictionary. Responses are set in each handler used to handle a message. 148 Return all handled objects. 149 """ 150 151 method = itip and get_value(itip, "METHOD") 152 handled = [] 153 154 for obj in get_objects_from_itip(itip, handlers): 155 handle_calendar_object(obj, handlers, method) 156 handled.append(obj) 157 158 return handled 159 160 def handle_calendar_object(obj, handlers, method): 161 162 """ 163 Handle the calendar data provided by 'obj' using the given 'handlers' 164 dictionary and 'method' information. Responses are set in each handler used 165 to handle a message. 166 """ 167 168 # Get a handler for the given object type. 169 170 handler = handlers.get(obj.objtype) 171 if not handler: 172 return 173 174 # Dispatch to a handler and obtain any response. 175 176 handler.set_object(obj) 177 handler.set_identity(method) 178 179 if handler.is_usable(method): 180 181 # Perform the method in a critical section. 182 183 handler.acquire_lock() 184 try: 185 methods[method](handler)() 186 finally: 187 handler.release_lock() 188 189 # Handler registry. 190 191 methods = { 192 "ADD" : lambda handler: handler.add, 193 "CANCEL" : lambda handler: handler.cancel, 194 "COUNTER" : lambda handler: handler.counter, 195 "DECLINECOUNTER" : lambda handler: handler.declinecounter, 196 "PUBLISH" : lambda handler: handler.publish, 197 "REFRESH" : lambda handler: handler.refresh, 198 "REPLY" : lambda handler: handler.reply, 199 "REQUEST" : lambda handler: handler.request, 200 } 201 202 # vim: tabstop=4 expandtab shiftwidth=4