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