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 if link and self.have_manager(): 78 texts.append(_("If your mail program cannot handle this " 79 "message, you may view the details here:\n\n%s") % 80 get_object_url(self.uid, self.recurrenceid)) 81 82 return self.add_result(None, None, MIMEText("\n".join(texts))) 83 84 # Result registration. 85 86 def add_result(self, method, outgoing_recipients, part): 87 88 """ 89 Record a result having the given 'method', 'outgoing_recipients' and 90 message 'part'. 91 """ 92 93 if outgoing_recipients: 94 self.outgoing_methods.add(method) 95 self.results.append((outgoing_recipients, part)) 96 97 def add_results(self, methods, outgoing_recipients, parts): 98 99 """ 100 Record results having the given 'methods', 'outgoing_recipients' and 101 message 'parts'. 102 """ 103 104 if outgoing_recipients: 105 self.outgoing_methods.update(methods) 106 for part in parts: 107 self.results.append((outgoing_recipients, part)) 108 109 def get_results(self): 110 return self.results 111 112 def get_outgoing_methods(self): 113 return self.outgoing_methods 114 115 # Logic, filtering and access to calendar structures and other data. 116 117 def filter_by_senders(self, mapping): 118 119 """ 120 Return a list of items from 'mapping' filtered using sender information. 121 """ 122 123 if self.senders: 124 125 # Get a mapping from senders to identities. 126 127 identities = get_sender_identities(mapping) 128 129 # Find the senders that are valid. 130 131 senders = map(get_address, identities) 132 valid = self.senders.intersection(senders) 133 134 # Return the true identities. 135 136 return reduce(lambda a, b: a + b, [identities[get_uri(address)] for address in valid], []) 137 else: 138 return mapping 139 140 def filter_by_recipient(self, mapping): 141 142 """ 143 Return a list of items from 'mapping' filtered using recipient 144 information. 145 """ 146 147 if self.recipient: 148 addresses = set(map(get_address, mapping)) 149 return map(get_uri, addresses.intersection([self.recipient])) 150 else: 151 return mapping 152 153 def is_delegation(self): 154 155 """ 156 Return whether delegation is occurring by returning any delegator. 157 """ 158 159 attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE")) 160 attendee_attr = attendee_map.get(self.user) 161 return check_delegation(attendee_map, self.user, attendee_attr) 162 163 def require_organiser(self, from_organiser=True): 164 165 """ 166 Return the normalised organiser for the current object, filtered for the 167 sender or recipient of interest. Return None if no identities are 168 eligible. 169 170 If the sender is not the organiser but is delegating to the recipient, 171 the actual organiser is returned. 172 """ 173 174 organiser, organiser_attr = organiser_item = uri_item(self.obj.get_item("ORGANIZER")) 175 176 if not organiser: 177 return None 178 179 # Check the delegate status of the recipient. 180 181 delegated = from_organiser and self.is_delegation() 182 183 # Only provide details for an organiser who sent/receives the message or 184 # is presiding over a delegation. 185 186 organiser_filter_fn = from_organiser and self.filter_by_senders or self.filter_by_recipient 187 188 if not delegated and not organiser_filter_fn(dict([organiser_item])): 189 return None 190 191 # Test against any previously-received organiser details. 192 193 if not self.is_recognised_organiser(organiser): 194 replacement = self.get_organiser_replacement() 195 196 # Allow any organiser as a replacement where indicated. 197 198 if replacement == "any": 199 pass 200 201 # Allow any recognised attendee as a replacement where indicated. 202 203 elif replacement != "attendee" or not self.is_recognised_attendee(organiser): 204 return None 205 206 return organiser_item 207 208 def require_attendees(self, from_organiser=True): 209 210 """ 211 Return the attendees for the current object, filtered for the sender or 212 recipient of interest. Return None if no identities are eligible. 213 214 The attendee identities are normalized. 215 """ 216 217 attendee_map = uri_dict(self.obj.get_value_map("ATTENDEE")) 218 219 # Only provide details for attendees who sent/receive the message. 220 221 attendee_filter_fn = from_organiser and self.filter_by_recipient or self.filter_by_senders 222 223 attendees = {} 224 for attendee in attendee_filter_fn(attendee_map): 225 if attendee: 226 attendees[attendee] = attendee_map[attendee] 227 228 return attendees 229 230 def require_organiser_and_attendees(self, from_organiser=True): 231 232 """ 233 Return the organiser and attendees for the current object, filtered for 234 the recipient of interest. Return None if no identities are eligible. 235 236 Organiser and attendee identities are normalized. 237 """ 238 239 organiser_item = self.require_organiser(from_organiser) 240 attendees = self.require_attendees(from_organiser) 241 242 if not attendees or not organiser_item: 243 return None 244 245 return organiser_item, attendees 246 247 # vim: tabstop=4 expandtab shiftwidth=4