paul@444 | 1 | #!/usr/bin/env python |
paul@444 | 2 | |
paul@444 | 3 | """ |
paul@444 | 4 | Interaction with the mail system for the manager interface. |
paul@444 | 5 | |
paul@444 | 6 | Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk> |
paul@444 | 7 | |
paul@444 | 8 | This program is free software; you can redistribute it and/or modify it under |
paul@444 | 9 | the terms of the GNU General Public License as published by the Free Software |
paul@444 | 10 | Foundation; either version 3 of the License, or (at your option) any later |
paul@444 | 11 | version. |
paul@444 | 12 | |
paul@444 | 13 | This program is distributed in the hope that it will be useful, but WITHOUT |
paul@444 | 14 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
paul@444 | 15 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
paul@444 | 16 | details. |
paul@444 | 17 | |
paul@444 | 18 | You should have received a copy of the GNU General Public License along with |
paul@444 | 19 | this program. If not, see <http://www.gnu.org/licenses/>. |
paul@444 | 20 | """ |
paul@444 | 21 | |
paul@601 | 22 | from imiptools.client import ClientForObject |
paul@608 | 23 | from imiptools.data import get_address, get_uri, uri_item, uri_values |
paul@608 | 24 | from imiptools.dates import format_datetime |
paul@444 | 25 | |
paul@601 | 26 | class ManagerClient(ClientForObject): |
paul@444 | 27 | |
paul@444 | 28 | """ |
paul@601 | 29 | A content client for use by the manager, as opposed to operating within the |
paul@444 | 30 | mail processing pipeline. |
paul@444 | 31 | """ |
paul@444 | 32 | |
paul@444 | 33 | # Communication methods. |
paul@444 | 34 | |
paul@526 | 35 | def send_message(self, method, sender, from_organiser, parts=None): |
paul@444 | 36 | |
paul@444 | 37 | """ |
paul@444 | 38 | Create a full calendar object employing the given 'method', and send it |
paul@444 | 39 | to the appropriate recipients, also sending a copy to the 'sender'. The |
paul@501 | 40 | 'from_organiser' value indicates whether the organiser is sending this |
paul@709 | 41 | message (and is thus equivalent to "as organiser"). |
paul@444 | 42 | """ |
paul@444 | 43 | |
paul@526 | 44 | parts = parts or [self.obj.to_part(method)] |
paul@444 | 45 | |
paul@444 | 46 | # As organiser, send an invitation to attendees, excluding oneself if |
paul@444 | 47 | # also attending. The updated event will be saved by the outgoing |
paul@444 | 48 | # handler. |
paul@444 | 49 | |
paul@444 | 50 | organiser = get_uri(self.obj.get_value("ORGANIZER")) |
paul@444 | 51 | attendees = uri_values(self.obj.get_values("ATTENDEE")) |
paul@444 | 52 | |
paul@501 | 53 | if from_organiser: |
paul@444 | 54 | recipients = [get_address(attendee) for attendee in attendees if attendee != self.user] |
paul@444 | 55 | else: |
paul@444 | 56 | recipients = [get_address(organiser)] |
paul@444 | 57 | |
paul@606 | 58 | # Since the outgoing handler updates this user's free/busy details, |
paul@606 | 59 | # the stored details will probably not have the updated details at |
paul@606 | 60 | # this point, so we update our copy for serialisation as the bundled |
paul@606 | 61 | # free/busy object. |
paul@606 | 62 | |
paul@606 | 63 | freebusy = self.store.get_freebusy(self.user) |
paul@606 | 64 | self.update_freebusy(freebusy, self.user, from_organiser) |
paul@606 | 65 | |
paul@444 | 66 | # Bundle free/busy information if appropriate. |
paul@444 | 67 | |
paul@606 | 68 | part = self.get_freebusy_part(freebusy) |
paul@604 | 69 | if part: |
paul@604 | 70 | parts.append(part) |
paul@444 | 71 | |
paul@445 | 72 | # Explicitly specify the outgoing BCC recipient since we are sending as |
paul@445 | 73 | # the generic calendar user. |
paul@445 | 74 | |
paul@444 | 75 | message = self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender) |
paul@444 | 76 | self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender) |
paul@444 | 77 | |
paul@444 | 78 | # Action methods. |
paul@444 | 79 | |
paul@512 | 80 | def process_received_request(self): |
paul@444 | 81 | |
paul@444 | 82 | """ |
paul@584 | 83 | Process the current request for the current user. Return whether any |
paul@444 | 84 | action was taken. |
paul@444 | 85 | """ |
paul@444 | 86 | |
paul@444 | 87 | # Reply only on behalf of this user. |
paul@444 | 88 | |
paul@582 | 89 | attendee_attr = self.update_participation(self.obj) |
paul@444 | 90 | |
paul@584 | 91 | if not attendee_attr: |
paul@584 | 92 | return False |
paul@444 | 93 | |
paul@584 | 94 | self.obj["ATTENDEE"] = [(self.user, attendee_attr)] |
paul@584 | 95 | self.update_dtstamp() |
paul@584 | 96 | self.set_sequence(False) |
paul@584 | 97 | self.send_message("REPLY", get_address(self.user), from_organiser=False) |
paul@584 | 98 | return True |
paul@444 | 99 | |
paul@526 | 100 | def process_created_request(self, method, to_cancel=None, to_unschedule=None): |
paul@444 | 101 | |
paul@444 | 102 | """ |
paul@473 | 103 | Process the current request, sending a created request of the given |
paul@473 | 104 | 'method' to attendees. Return whether any action was taken. |
paul@444 | 105 | |
paul@473 | 106 | If 'to_cancel' is specified, a list of participants to be sent cancel |
paul@473 | 107 | messages is provided. |
paul@675 | 108 | |
paul@675 | 109 | If 'to_unschedule' is specified, a list of periods to be unscheduled is |
paul@675 | 110 | provided. |
paul@444 | 111 | """ |
paul@444 | 112 | |
paul@584 | 113 | # Here, the organiser should be the current user. |
paul@584 | 114 | |
paul@444 | 115 | organiser, organiser_attr = uri_item(self.obj.get_item("ORGANIZER")) |
paul@444 | 116 | |
paul@584 | 117 | self.update_sender(organiser_attr) |
paul@444 | 118 | self.update_dtstamp() |
paul@512 | 119 | self.set_sequence(True) |
paul@444 | 120 | |
paul@526 | 121 | parts = [self.obj.to_part(method)] |
paul@526 | 122 | |
paul@526 | 123 | # Add message parts with cancelled occurrence information. |
paul@526 | 124 | # NOTE: This could probably be merged with the updated event message. |
paul@526 | 125 | |
paul@526 | 126 | if to_unschedule: |
paul@526 | 127 | obj = self.obj.copy() |
paul@526 | 128 | obj.remove_all(["RRULE", "RDATE", "DTSTART", "DTEND", "DURATION"]) |
paul@526 | 129 | |
paul@526 | 130 | for p in to_unschedule: |
paul@526 | 131 | if not p.origin: |
paul@526 | 132 | continue |
paul@627 | 133 | obj["RECURRENCE-ID"] = [p.get_start_item()] |
paul@526 | 134 | parts.append(obj.to_part("CANCEL")) |
paul@526 | 135 | |
paul@526 | 136 | # Send the updated event, along with a cancellation for each of the |
paul@526 | 137 | # unscheduled occurrences. |
paul@526 | 138 | |
paul@526 | 139 | self.send_message("CANCEL", get_address(organiser), from_organiser=True, parts=parts) |
paul@444 | 140 | |
paul@444 | 141 | # When cancelling, replace the attendees with those for whom the event |
paul@444 | 142 | # is now cancelled. |
paul@444 | 143 | |
paul@444 | 144 | if to_cancel: |
paul@526 | 145 | obj = self.obj.copy() |
paul@526 | 146 | obj["ATTENDEE"] = to_cancel |
paul@444 | 147 | |
paul@526 | 148 | # Send a cancellation to all uninvited attendees. |
paul@444 | 149 | |
paul@526 | 150 | self.send_message("CANCEL", get_address(organiser), from_organiser=True) |
paul@444 | 151 | |
paul@444 | 152 | return True |
paul@444 | 153 | |
paul@444 | 154 | # vim: tabstop=4 expandtab shiftwidth=4 |