paul@48 | 1 | #!/usr/bin/env python |
paul@48 | 2 | |
paul@48 | 3 | """ |
paul@596 | 4 | The handler invocation mechanism. |
paul@146 | 5 | |
paul@1210 | 6 | Copyright (C) 2014, 2015, 2017 Paul Boddie <paul@boddie.org.uk> |
paul@146 | 7 | |
paul@146 | 8 | This program is free software; you can redistribute it and/or modify it under |
paul@146 | 9 | the terms of the GNU General Public License as published by the Free Software |
paul@146 | 10 | Foundation; either version 3 of the License, or (at your option) any later |
paul@146 | 11 | version. |
paul@146 | 12 | |
paul@146 | 13 | This program is distributed in the hope that it will be useful, but WITHOUT |
paul@146 | 14 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
paul@146 | 15 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
paul@146 | 16 | details. |
paul@146 | 17 | |
paul@146 | 18 | You should have received a copy of the GNU General Public License along with |
paul@146 | 19 | this program. If not, see <http://www.gnu.org/licenses/>. |
paul@48 | 20 | """ |
paul@48 | 21 | |
paul@1210 | 22 | from imiptools.config import settings |
paul@418 | 23 | from imiptools.data import Object, parse_object, get_value |
paul@48 | 24 | |
paul@48 | 25 | try: |
paul@48 | 26 | from cStringIO import StringIO |
paul@48 | 27 | except ImportError: |
paul@48 | 28 | from StringIO import StringIO |
paul@48 | 29 | |
paul@1210 | 30 | IMIP_COUNTER_AS_REQUEST = settings["IMIP_COUNTER_AS_REQUEST"] |
paul@1210 | 31 | |
paul@1286 | 32 | # Permitted iTIP content types. |
paul@1286 | 33 | |
paul@1286 | 34 | itip_content_types = [ |
paul@1286 | 35 | "text/calendar", # from RFC 6047 |
paul@1286 | 36 | "text/x-vcalendar", "application/ics", # other possibilities |
paul@1286 | 37 | ] |
paul@1286 | 38 | |
paul@1286 | 39 | def have_itip_part(part): |
paul@1286 | 40 | |
paul@1286 | 41 | "Return whether 'part' provides iTIP content." |
paul@1286 | 42 | |
paul@1286 | 43 | return part.get_content_type() in itip_content_types and \ |
paul@1286 | 44 | part.get_param("method") |
paul@1286 | 45 | |
paul@1286 | 46 | def is_returned_message(message): |
paul@1286 | 47 | |
paul@1286 | 48 | """ |
paul@1286 | 49 | Return whether 'message' contains a returned message that should not be |
paul@1286 | 50 | handled because it may have originated from the current recipient. |
paul@1286 | 51 | """ |
paul@1286 | 52 | |
paul@1286 | 53 | for part in message.walk(): |
paul@1286 | 54 | if part.get_content_type() == "message/delivery-status": |
paul@1286 | 55 | return True |
paul@1286 | 56 | return False |
paul@1286 | 57 | |
paul@1286 | 58 | def consistent_methods(itip_method, part_method): |
paul@1286 | 59 | |
paul@1286 | 60 | """ |
paul@1286 | 61 | Return whether 'itip_method' (from content) and 'part_method' (from |
paul@1286 | 62 | metadata) are consistent. |
paul@1286 | 63 | """ |
paul@1286 | 64 | |
paul@1286 | 65 | return itip_method == part_method or \ |
paul@1286 | 66 | IMIP_COUNTER_AS_REQUEST and itip_method == "COUNTER" and \ |
paul@1286 | 67 | part_method == "REQUEST" |
paul@1286 | 68 | |
paul@1286 | 69 | def parse_itip_part(part): |
paul@1286 | 70 | |
paul@1286 | 71 | """ |
paul@1286 | 72 | Parse the given message 'part' and return a dictionary mapping calendar |
paul@1289 | 73 | object type names to lists of fragments. |
paul@1286 | 74 | |
paul@1286 | 75 | If no iTIP content is found, None is returned. |
paul@1286 | 76 | """ |
paul@1286 | 77 | |
paul@1289 | 78 | # Ignore the part if not iTIP-related. |
paul@1289 | 79 | |
paul@1289 | 80 | if not have_itip_part(part): |
paul@1289 | 81 | return None |
paul@1289 | 82 | |
paul@1286 | 83 | # Decode the data and parse it. |
paul@1286 | 84 | |
paul@1286 | 85 | f = StringIO(part.get_payload(decode=True)) |
paul@1286 | 86 | itip = parse_object(f, part.get_content_charset(), "VCALENDAR") |
paul@1286 | 87 | |
paul@1286 | 88 | # Ignore the part if not a calendar object. |
paul@1286 | 89 | |
paul@1286 | 90 | if not itip: |
paul@1286 | 91 | return None |
paul@1286 | 92 | |
paul@1286 | 93 | # Require consistency between declared and employed methods. |
paul@1286 | 94 | |
paul@1286 | 95 | itip_method = get_value(itip, "METHOD") |
paul@1286 | 96 | method = part.get_param("method") |
paul@1286 | 97 | method = method and method.upper() |
paul@1286 | 98 | |
paul@1286 | 99 | if not consistent_methods(itip_method, method): |
paul@1286 | 100 | return None |
paul@1286 | 101 | |
paul@1289 | 102 | return itip |
paul@1289 | 103 | |
paul@1289 | 104 | def get_objects_from_itip(itip, handlers): |
paul@1289 | 105 | |
paul@1289 | 106 | """ |
paul@1289 | 107 | Return all objects provided by the given 'itip' mapping supported by the |
paul@1289 | 108 | given 'handlers'. |
paul@1289 | 109 | """ |
paul@1286 | 110 | |
paul@1289 | 111 | objects = [] |
paul@1289 | 112 | |
paul@1289 | 113 | # Look for different kinds of sections. |
paul@1289 | 114 | |
paul@1289 | 115 | if itip: |
paul@1289 | 116 | for name, items in itip.items(): |
paul@1289 | 117 | if name in handlers: |
paul@1289 | 118 | for item in items: |
paul@1289 | 119 | objects.append(Object({name : item})) |
paul@1289 | 120 | |
paul@1289 | 121 | return objects |
paul@1286 | 122 | |
paul@224 | 123 | def handle_itip_part(part, handlers): |
paul@48 | 124 | |
paul@48 | 125 | """ |
paul@227 | 126 | Handle the given iTIP 'part' using the given 'handlers' dictionary. |
paul@1289 | 127 | Responses are set in each handler used to handle a message. Return whether |
paul@1289 | 128 | the part could be handled. |
paul@48 | 129 | """ |
paul@48 | 130 | |
paul@1289 | 131 | itip = parse_itip_part(part) |
paul@1289 | 132 | method = itip and get_value(itip, "METHOD") |
paul@918 | 133 | |
paul@1289 | 134 | for obj in get_objects_from_itip(itip, handlers): |
paul@48 | 135 | |
paul@1289 | 136 | # Get a handler for the given object type. |
paul@48 | 137 | |
paul@1289 | 138 | handler = handlers.get(obj.objtype) |
paul@1286 | 139 | if not handler: |
paul@1286 | 140 | continue |
paul@226 | 141 | |
paul@1289 | 142 | # Dispatch to a handler and obtain any response. |
paul@226 | 143 | |
paul@1289 | 144 | handler.set_object(obj) |
paul@1289 | 145 | handler.set_identity(method) |
paul@48 | 146 | |
paul@1289 | 147 | if handler.is_usable(method): |
paul@48 | 148 | |
paul@1289 | 149 | # Perform the method in a critical section. |
paul@730 | 150 | |
paul@1289 | 151 | handler.acquire_lock() |
paul@1289 | 152 | try: |
paul@1289 | 153 | methods[method](handler)() |
paul@1289 | 154 | finally: |
paul@1289 | 155 | handler.release_lock() |
paul@1289 | 156 | |
paul@1289 | 157 | return not not itip |
paul@48 | 158 | |
paul@48 | 159 | # Handler registry. |
paul@48 | 160 | |
paul@48 | 161 | methods = { |
paul@48 | 162 | "ADD" : lambda handler: handler.add, |
paul@48 | 163 | "CANCEL" : lambda handler: handler.cancel, |
paul@48 | 164 | "COUNTER" : lambda handler: handler.counter, |
paul@48 | 165 | "DECLINECOUNTER" : lambda handler: handler.declinecounter, |
paul@48 | 166 | "PUBLISH" : lambda handler: handler.publish, |
paul@48 | 167 | "REFRESH" : lambda handler: handler.refresh, |
paul@48 | 168 | "REPLY" : lambda handler: handler.reply, |
paul@48 | 169 | "REQUEST" : lambda handler: handler.request, |
paul@48 | 170 | } |
paul@48 | 171 | |
paul@48 | 172 | # vim: tabstop=4 expandtab shiftwidth=4 |