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