1 #!/usr/bin/env python 2 3 """ 4 Handlers for a person for whom scheduling is performed. 5 """ 6 7 from email.mime.text import MIMEText 8 from imiptools.config import MANAGER_PATH, MANAGER_URL 9 from imiptools.content import Handler, get_address, get_uri, to_part, uri_dict, uri_items 10 from imiptools.handlers.common import CommonFreebusy 11 from socket import gethostname 12 from vCalendar import to_node 13 14 def get_manager_url(): 15 url_base = MANAGER_URL or "http://%s/" % gethostname() 16 return "%s/%s" % (url_base.rstrip("/"), MANAGER_PATH.lstrip("/")) 17 18 def get_object_url(uid): 19 return "%s/%s" % (get_manager_url().rstrip("/"), uid) 20 21 class PersonHandler(Handler): 22 23 "Handling mechanisms specific to people." 24 25 def _record_and_deliver(self, objtype, from_organiser=True, queue=False, cancel=False): 26 27 oa = self.require_organiser_and_attendees(from_organiser) 28 if not oa: 29 return False 30 31 (organiser, organiser_attr), attendees = organiser_item, attendees = oa 32 33 # Validate the organiser or attendee, ignoring spoofed requests. 34 35 if not self.validate_identities(from_organiser and [organiser_item] or attendees.items()): 36 return False 37 38 # Handle notifications and invitations. 39 40 if from_organiser: 41 42 # Process each attendee separately. 43 44 for attendee, attendee_attr in attendees.items(): 45 46 if not self.have_new_object(attendee, objtype): 47 continue 48 49 # Store the object and queue any request. 50 51 self.store.set_event(attendee, self.uid, to_node( 52 {objtype : [(self.details, {})]} 53 )) 54 55 if queue: 56 self.store.queue_request(attendee, self.uid) 57 elif cancel: 58 self.store.cancel_event(attendee, self.uid) 59 60 # As organiser, update attendance. 61 62 else: 63 obj = self.get_object(organiser, objtype) 64 65 if obj and self.have_new_object(organiser, objtype, obj): 66 67 # Get attendee details in a usable form. 68 69 attendee_map = uri_dict(self.get_value_map("ATTENDEE")) 70 71 for attendee, attendee_attr in attendees.items(): 72 73 # Update attendance in the loaded object. 74 75 attendee_map[attendee] = attendee_attr 76 77 # Set the new details and store the object. 78 79 obj["ATTENDEE"] = attendee_map.items() 80 81 self.store.set_event(organiser, self.uid, to_node( 82 {objtype : [(obj, {})]} 83 )) 84 85 return True 86 87 def _record_freebusy(self, from_organiser=True): 88 89 "Record free/busy information for the received information." 90 91 freebusy = [] 92 93 for value in self.get_values("FREEBUSY") or []: 94 if not isinstance(value, list): 95 value = [value] 96 for v in value: 97 try: 98 start, end = v.split("/", 1) 99 freebusy.append((start, end)) 100 except ValueError: 101 pass 102 103 for sender, sender_attr in uri_items(self.get_items(from_organiser and "ORGANIZER" or "ATTENDEE")): 104 for recipient in self.recipients: 105 self.store.set_freebusy_for_other(get_uri(recipient), freebusy, sender) 106 107 def wrap(self, method, text, from_organiser=True, link=True): 108 109 "Wrap any valid message and pass it on to the recipient." 110 111 texts = [] 112 texts.append(text) 113 if link: 114 texts.append("If your mail program cannot handle this " 115 "message, you may view the details here:\n\n%s" % 116 get_object_url(self.uid)) 117 118 return method, MIMEText("\n".join(texts)) 119 120 class Event(PersonHandler): 121 122 "An event handler." 123 124 def add(self): 125 126 # NOTE: Queue a suggested modification to any active event. 127 128 # The message is now wrapped and passed on to the recipient. 129 130 return "ADD", MIMEText("An addition to an event has been received.") 131 132 def cancel(self): 133 134 "Queue a cancellation of any active event." 135 136 self._record_and_deliver("VEVENT", from_organiser=True, queue=False, cancel=True) 137 return self.wrap("CANCEL", "A cancellation has been received.", from_organiser=True, link=True) 138 139 def counter(self): 140 141 # NOTE: Queue a suggested modification to any active event. 142 143 # The message is now wrapped and passed on to the recipient. 144 145 return "COUNTER", MIMEText("A counter proposal has been received.") 146 147 def declinecounter(self): 148 149 # NOTE: Queue a suggested modification to any active event. 150 151 # The message is now wrapped and passed on to the recipient. 152 153 return "DECLINECOUNTER", MIMEText("A declining counter proposal has been received.") 154 155 def publish(self): 156 157 "Register details of any relevant event." 158 159 self._record_and_deliver("VEVENT", from_organiser=True, queue=False) 160 return self.wrap("PUBLISH", "Details of an event have been received.", from_organiser=True, link=True) 161 162 def refresh(self): 163 164 "Update details of any active event." 165 166 self._record_and_deliver("VEVENT", from_organiser=True, queue=False) 167 return self.wrap("REFRESH", "An event update has been received.", from_organiser=True, link=True) 168 169 def reply(self): 170 171 "Record replies and notify the recipient." 172 173 self._record_and_deliver("VEVENT", from_organiser=False, queue=False) 174 return self.wrap("REPLY", "A reply has been received.", from_organiser=False, link=True) 175 176 def request(self): 177 178 "Hold requests and notify the recipient." 179 180 self._record_and_deliver("VEVENT", from_organiser=True, queue=True) 181 182 # The message is now wrapped and passed on to the recipient. 183 184 return "REQUEST", MIMEText("A request has been queued and can be viewed here: %s" % get_object_url(self.uid)) 185 186 class Freebusy(PersonHandler, CommonFreebusy): 187 188 "A free/busy handler." 189 190 def publish(self): 191 192 "Register free/busy information." 193 194 # NOTE: This could be configured to not produce a message. 195 196 self._record_freebusy(from_organiser=True) 197 return self.wrap("PUBLISH", "A free/busy update has been received.", from_organiser=True, link=False) 198 199 def reply(self): 200 201 "Record replies and notify the recipient." 202 203 # NOTE: This could be configured to not produce a message. 204 205 self._record_freebusy(from_organiser=False) 206 return self.wrap("REPLY", "A reply has been received.", from_organiser=False, link=False) 207 208 def request(self): 209 210 """ 211 Respond to a request by preparing a reply containing free/busy 212 information for each indicated attendee. 213 """ 214 215 # NOTE: This should be subject to policy/preferences. 216 217 return CommonFreebusy.request(self) 218 219 class Journal(PersonHandler): 220 221 "A journal entry handler." 222 223 def add(self): 224 225 # NOTE: Queue a suggested modification to any active entry. 226 227 # The message is now wrapped and passed on to the recipient. 228 229 return "ADD", MIMEText("An addition to a journal entry has been received.") 230 231 def cancel(self): 232 233 # NOTE: Queue a suggested modification to any active entry. 234 235 # The message is now wrapped and passed on to the recipient. 236 237 return "CANCEL", MIMEText("A cancellation has been received.") 238 239 def publish(self): 240 241 # NOTE: Register details of any relevant entry. 242 243 # The message is now wrapped and passed on to the recipient. 244 245 self._record_and_deliver("VJOURNAL", from_organiser=True, queue=False) 246 return self.wrap("PUBLISH", "Details of a journal entry have been received.", from_organiser=True, link=False) 247 248 class Todo(PersonHandler): 249 250 "A to-do item handler." 251 252 def add(self): 253 254 # NOTE: Queue a suggested modification to any active item. 255 256 # The message is now wrapped and passed on to the recipient. 257 258 return "ADD", MIMEText("An addition to an item has been received.") 259 260 def cancel(self): 261 262 # NOTE: Queue a suggested modification to any active item. 263 264 # The message is now wrapped and passed on to the recipient. 265 266 return "CANCEL", MIMEText("A cancellation has been received.") 267 268 def counter(self): 269 270 # NOTE: Queue a suggested modification to any active item. 271 272 # The message is now wrapped and passed on to the recipient. 273 274 return "COUNTER", MIMEText("A counter proposal has been received.") 275 276 def declinecounter(self): 277 278 # NOTE: Queue a suggested modification to any active item. 279 280 # The message is now wrapped and passed on to the recipient. 281 282 return "DECLINECOUNTER", MIMEText("A declining counter proposal has been received.") 283 284 def publish(self): 285 286 "Register details of any relevant item." 287 288 self._record_and_deliver("VTODO", from_organiser=True, queue=False) 289 return self.wrap("PUBLISH", "Details of an item have been received.", from_organiser=True, link=True) 290 291 def refresh(self): 292 293 "Update details of any active item." 294 295 self._record_and_deliver("VTODO", from_organiser=True, queue=False) 296 return self.wrap("REFRESH", "An item update has been received.", from_organiser=True, link=True) 297 298 def reply(self): 299 300 "Record replies and notify the recipient." 301 302 self._record_and_deliver("VTODO", from_organiser=False, queue=False) 303 return self.wrap("REPLY", "A reply has been received.", from_organiser=False, link=True) 304 305 def request(self): 306 307 "Hold requests and notify the recipient." 308 309 self._record_and_deliver("VTODO", from_organiser=True, queue=True) 310 311 # The message is now wrapped and passed on to the recipient. 312 313 return "REQUEST", MIMEText("A request has been queued.") 314 315 # Handler registry. 316 317 handlers = [ 318 ("VFREEBUSY", Freebusy), 319 ("VEVENT", Event), 320 ("VTODO", Todo), 321 ("VJOURNAL", Journal), 322 ] 323 324 # vim: tabstop=4 expandtab shiftwidth=4