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 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 attendees = [] 149 for address in valid: 150 attendees += identities[get_uri(address)] 151 return attendees 152 153 # Rely on the mapping keys being accessible as a sequence. 154 155 else: 156 return mapping 157 158 def filter_by_recipient(self, mapping): 159 160 """ 161 Return a list of items from 'mapping' filtered using recipient 162 information. 163 """ 164 165 if self.recipient: 166 addresses = set(map(get_address, mapping)) 167 return map(get_uri, addresses.intersection([self.recipient])) 168 169 # Rely on the mapping keys being accessible as a sequence. 170 171 else: 172 return mapping 173 174 def is_delegation(self): 175 176 """ 177 Return whether delegation is occurring by returning any delegator. 178 """ 179 180 attendee_map = self.obj.get_uri_map("ATTENDEE") 181 attendee_attr = attendee_map.get(self.user) 182 return check_delegation(attendee_map, self.user, attendee_attr) 183 184 def require_organiser(self, from_organiser=True): 185 186 """ 187 Return the normalised organiser for the current object, filtered for the 188 sender or recipient of interest. Return None if no identities are 189 eligible. 190 191 If the sender is not the organiser but is delegating to the recipient, 192 the actual organiser is returned. 193 """ 194 195 organiser, organiser_attr = organiser_item = self.obj.get_uri_item("ORGANIZER") 196 197 if not organiser: 198 return None 199 200 # Check the delegate status of the recipient. 201 202 delegated = from_organiser and self.is_delegation() 203 204 # Only provide details for an organiser who sent/receives the message or 205 # is presiding over a delegation. 206 207 organiser_filter_fn = from_organiser and self.filter_by_senders or self.filter_by_recipient 208 209 if not delegated and not organiser_filter_fn(dict([organiser_item])): 210 return None 211 212 # Test against any previously-received organiser details. 213 214 if not self.is_recognised_organiser(organiser): 215 replacement = self.get_organiser_replacement() 216 217 # Allow any organiser as a replacement where indicated. 218 219 if replacement == "any": 220 pass 221 222 # Allow any recognised attendee as a replacement where indicated. 223 224 elif replacement != "attendee" or not self.is_recognised_attendee(organiser): 225 return None 226 227 return organiser_item 228 229 def require_attendees(self, from_organiser=True): 230 231 """ 232 Return the attendees for the current object, filtered for the sender or 233 recipient of interest. Return None if no identities are eligible. 234 235 The attendee identities are normalized. 236 """ 237 238 attendee_map = self.obj.get_uri_map("ATTENDEE") 239 240 # Only provide details for attendees who sent/receive the message. 241 242 attendee_filter_fn = from_organiser and self.filter_by_recipient or self.filter_by_senders 243 244 attendees = {} 245 for attendee in attendee_filter_fn(attendee_map): 246 if attendee: 247 attendees[attendee] = attendee_map[attendee] 248 249 return attendees 250 251 def require_organiser_and_attendees(self, from_organiser=True): 252 253 """ 254 Return the organiser and attendees for the current object, filtered for 255 the recipient of interest. Return None if no identities are eligible. 256 257 Organiser and attendee identities are normalized. 258 """ 259 260 organiser_item = self.require_organiser(from_organiser) 261 attendees = self.require_attendees(from_organiser) 262 263 if not attendees or not organiser_item: 264 return None 265 266 return organiser_item, attendees 267 268 # vim: tabstop=4 expandtab shiftwidth=4