1 #!/usr/bin/env python 2 3 """ 4 Interaction with the mail system for the manager interface. 5 6 Copyright (C) 2014, 2015 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 imiptools.client import Client 23 from imiptools.data import get_address, get_uri, make_freebusy, \ 24 to_part, uri_item, uri_items, uri_values 25 from imiptools.dates import format_datetime, get_timestamp 26 from imiptools.handlers import Handler 27 from imiptools.period import update_freebusy 28 from imipweb.data import event_period_from_period 29 30 class ManagerHandler(Handler): 31 32 """ 33 A content handler for use by the manager, as opposed to operating within the 34 mail processing pipeline. 35 """ 36 37 def __init__(self, obj, user, messenger): 38 Handler.__init__(self, messenger=messenger) 39 Client.__init__(self, user) # this redefines the Handler initialisation 40 41 self.set_object(obj) 42 43 # Communication methods. 44 45 def send_message(self, method, sender, from_organiser, parts=None): 46 47 """ 48 Create a full calendar object employing the given 'method', and send it 49 to the appropriate recipients, also sending a copy to the 'sender'. The 50 'from_organiser' value indicates whether the organiser is sending this 51 message. 52 """ 53 54 parts = parts or [self.obj.to_part(method)] 55 56 # As organiser, send an invitation to attendees, excluding oneself if 57 # also attending. The updated event will be saved by the outgoing 58 # handler. 59 60 organiser = get_uri(self.obj.get_value("ORGANIZER")) 61 attendees = uri_values(self.obj.get_values("ATTENDEE")) 62 63 if from_organiser: 64 recipients = [get_address(attendee) for attendee in attendees if attendee != self.user] 65 else: 66 recipients = [get_address(organiser)] 67 68 # Bundle free/busy information if appropriate. 69 70 if self.is_sharing() and self.is_bundling(): 71 72 # Invent a unique identifier. 73 74 utcnow = get_timestamp() 75 uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user)) 76 77 freebusy = self.store.get_freebusy(self.user) 78 79 # Replace the non-updated free/busy details for this event with 80 # newer details (since the outgoing handler updates this user's 81 # free/busy details). 82 83 self.update_freebusy(freebusy, 84 self.obj.get_periods(self.get_tzid(), self.get_window_end())) 85 86 user_attr = self.messenger and self.messenger.sender != get_address(self.user) and \ 87 {"SENT-BY" : get_uri(self.messenger.sender)} or {} 88 89 parts.append(to_part("PUBLISH", [ 90 make_freebusy(freebusy, uid, self.user, user_attr) 91 ])) 92 93 # Explicitly specify the outgoing BCC recipient since we are sending as 94 # the generic calendar user. 95 96 message = self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender) 97 self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender) 98 99 # Action methods. 100 101 def process_received_request(self): 102 103 """ 104 Process the current request for the given 'user'. Return whether any 105 action was taken. 106 """ 107 108 # Reply only on behalf of this user. 109 110 attendee_attr = self.update_participation(self.obj) 111 112 if attendee_attr: 113 self.obj["ATTENDEE"] = [(self.user, attendee_attr)] 114 self.update_dtstamp() 115 self.set_sequence(False) 116 self.send_message("REPLY", get_address(self.user), from_organiser=False) 117 return True 118 119 return False 120 121 def process_created_request(self, method, to_cancel=None, to_unschedule=None): 122 123 """ 124 Process the current request, sending a created request of the given 125 'method' to attendees. Return whether any action was taken. 126 127 If 'to_cancel' is specified, a list of participants to be sent cancel 128 messages is provided. 129 """ 130 131 organiser, organiser_attr = uri_item(self.obj.get_item("ORGANIZER")) 132 133 if self.messenger and self.messenger.sender != get_address(organiser): 134 organiser_attr["SENT-BY"] = get_uri(self.messenger.sender) 135 136 self.update_dtstamp() 137 self.set_sequence(True) 138 139 parts = [self.obj.to_part(method)] 140 141 # Add message parts with cancelled occurrence information. 142 # NOTE: This could probably be merged with the updated event message. 143 144 if to_unschedule: 145 obj = self.obj.copy() 146 obj.remove_all(["RRULE", "RDATE", "DTSTART", "DTEND", "DURATION"]) 147 148 for p in to_unschedule: 149 if not p.origin: 150 continue 151 obj["RECURRENCE-ID"] = [(format_datetime(p.get_start()), {})] 152 parts.append(obj.to_part("CANCEL")) 153 154 # Send the updated event, along with a cancellation for each of the 155 # unscheduled occurrences. 156 157 self.send_message("CANCEL", get_address(organiser), from_organiser=True, parts=parts) 158 159 # When cancelling, replace the attendees with those for whom the event 160 # is now cancelled. 161 162 if to_cancel: 163 obj = self.obj.copy() 164 obj["ATTENDEE"] = to_cancel 165 166 # Send a cancellation to all uninvited attendees. 167 168 self.send_message("CANCEL", get_address(organiser), from_organiser=True) 169 170 return True 171 172 # vim: tabstop=4 expandtab shiftwidth=4