1 #!/usr/bin/env python 2 3 """ 4 General handler support for incoming calendar objects. 5 6 Copyright (C) 2014, 2015, 2016, 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 email.mime.text import MIMEText 23 from imiptools.client import ClientForObject 24 from imiptools.config import settings 25 from imiptools.data import check_delegation, get_address, get_uri, \ 26 get_sender_identities, uri_dict, uri_item 27 from socket import gethostname 28 29 MANAGER_PATH = settings["MANAGER_PATH"] 30 MANAGER_URL = settings["MANAGER_URL"] 31 MANAGER_URL_SCHEME = settings["MANAGER_URL_SCHEME"] 32 33 # References to the Web interface. 34 35 def get_manager_url(): 36 url_base = MANAGER_URL or \ 37 "%s%s/" % (MANAGER_URL_SCHEME or "https://", gethostname()) 38 return "%s/%s" % (url_base.rstrip("/"), MANAGER_PATH.lstrip("/")) 39 40 def get_object_url(uid, recurrenceid=None): 41 return "%s/%s%s" % ( 42 get_manager_url().rstrip("/"), uid, 43 recurrenceid and "/%s" % recurrenceid or "" 44 ) 45 46 class Handler(ClientForObject): 47 48 "General handler support." 49 50 def __init__(self, senders=None, recipient=None, messenger=None, store=None, 51 publisher=None, journal=None, preferences_dir=None): 52 53 """ 54 Initialise the handler with any specifically indicated 'senders' and 55 'recipient' of a calendar object. The object is initially undefined. 56 57 The optional 'messenger' provides a means of interacting with the mail 58 system. 59 60 The optional 'store', 'publisher' and 'journal' can be specified to 61 override the default store and publisher objects. 62 """ 63 64 ClientForObject.__init__(self, None, recipient and get_uri(recipient), 65 messenger, store, publisher, journal, preferences_dir) 66 67 self.senders = senders and set(map(get_address, senders)) 68 self.recipient = recipient and get_address(recipient) 69 70 self.results = [] 71 self.outgoing_methods = set() 72 73 def wrap(self, text, link=True): 74 75 "Wrap any valid message for passing to the recipient." 76 77 _ = self.get_translator() 78 79 texts = [] 80 texts.append(text) 81 82 # Add a link to the manager application if available and requested. 83 84 if link and self.have_manager(): 85 texts.append(_("If your mail program cannot handle this " 86 "message, you may view the details here:\n\n%s\n") % 87 get_object_url(self.uid, self.recurrenceid)) 88 89 # Create the text part, tagging it with a header that allows this part 90 # to be merged with other calendar information. 91 92 text_part = MIMEText("\n\n".join(texts)) 93 text_part["X-IMIP-Agent"] = "info" 94 return self.add_result(None, None, text_part) 95 96 # Result registration. 97 98 def add_result(self, method, outgoing_recipients, part): 99 100 """ 101 Record a result having the given 'method', 'outgoing_recipients' and 102 message 'part'. 103 """ 104 105 if outgoing_recipients: 106 self.outgoing_methods.add(method) 107 self.results.append((outgoing_recipients, part)) 108 109 def add_results(self, methods, outgoing_recipients, parts): 110 111 """ 112 Record results having the given 'methods', 'outgoing_recipients' and 113 message 'parts'. 114 """ 115 116 if outgoing_recipients: 117 self.outgoing_methods.update(methods) 118 for part in parts: 119 self.results.append((outgoing_recipients, part)) 120 121 def get_results(self): 122 return self.results 123 124 def get_outgoing_methods(self): 125 return self.outgoing_methods 126 127 # Logic, filtering and access to calendar structures and other data. 128 129 def filter_by_senders(self, mapping): 130 131 """ 132 Return a list of items from 'mapping' filtered using sender information. 133 """ 134 135 if self.senders: 136 137 # Get a mapping from senders to identities. 138 139 identities = get_sender_identities(mapping) 140 141 # Find the senders that are valid. 142 143 senders = map(get_address, identities) 144 valid = self.senders.intersection(senders) 145 146 # Return the true identities. 147 148 return reduce(lambda a, b: a + b, [identities[get_uri(address)] for address in valid], []) 149 else: 150 return mapping 151 152 def filter_by_recipient(self, mapping): 153 154 """ 155 Return a list of items from 'mapping' filtered using recipient 156 information. 157 """ 158 159 if self.recipient: 160 addresses = set(map(get_address, mapping)) 161 return map(get_uri, addresses.intersection([self.recipient])) 162 else: 163 return mapping 164 165 def is_delegation(self): 166 167 """ 168 Return whether delegation is occurring by returning any delegator. 169 """ 170 171 attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE")) 172 attendee_attr = attendee_map.get(self.user) 173 return check_delegation(attendee_map, self.user, attendee_attr) 174 175 def require_organiser(self, from_organiser=True): 176 177 """ 178 Return the normalised organiser for the current object, filtered for the 179 sender or recipient of interest. Return None if no identities are 180 eligible. 181 182 If the sender is not the organiser but is delegating to the recipient, 183 the actual organiser is returned. 184 """ 185 186 organiser, organiser_attr = organiser_item = uri_item(self.obj.get_item("ORGANIZER")) 187 188 if not organiser: 189 return None 190 191 # Check the delegate status of the recipient. 192 193 delegated = from_organiser and self.is_delegation() 194 195 # Only provide details for an organiser who sent/receives the message or 196 # is presiding over a delegation. 197 198 organiser_filter_fn = from_organiser and self.filter_by_senders or self.filter_by_recipient 199 200 if not delegated and not organiser_filter_fn(dict([organiser_item])): 201 return None 202 203 # Test against any previously-received organiser details. 204 205 if not self.is_recognised_organiser(organiser): 206 replacement = self.get_organiser_replacement() 207 208 # Allow any organiser as a replacement where indicated. 209 210 if replacement == "any": 211 pass 212 213 # Allow any recognised attendee as a replacement where indicated. 214 215 elif replacement != "attendee" or not self.is_recognised_attendee(organiser): 216 return None 217 218 return organiser_item 219 220 def require_attendees(self, from_organiser=True): 221 222 """ 223 Return the attendees for the current object, filtered for the sender or 224 recipient of interest. Return None if no identities are eligible. 225 226 The attendee identities are normalized. 227 """ 228 229 attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE")) 230 231 # Only provide details for attendees who sent/receive the message. 232 233 attendee_filter_fn = from_organiser and self.filter_by_recipient or self.filter_by_senders 234 235 attendees = {} 236 for attendee in attendee_filter_fn(attendee_map): 237 if attendee: 238 attendees[attendee] = attendee_map[attendee] 239 240 return attendees 241 242 def require_organiser_and_attendees(self, from_organiser=True): 243 244 """ 245 Return the organiser and attendees for the current object, filtered for 246 the recipient of interest. Return None if no identities are eligible. 247 248 Organiser and attendee identities are normalized. 249 """ 250 251 organiser_item = self.require_organiser(from_organiser) 252 attendees = self.require_attendees(from_organiser) 253 254 if not attendees or not organiser_item: 255 return None 256 257 return organiser_item, attendees 258 259 # vim: tabstop=4 expandtab shiftwidth=4