1<?php
2/***********************************************
3* File      :   mime_calendar.php
4* Project   :   Z-Push
5* Descr     :   Functions for using within the IMAP backend
6*
7* Created   :   2015
8*
9* Copyright 2015 - 2016 Zarafa Deutschland GmbH
10*
11* This program is free software: you can redistribute it and/or modify
12* it under the terms of the GNU Affero General Public License, version 3,
13* as published by the Free Software Foundation.
14*
15* This program is distributed in the hope that it will be useful,
16* but WITHOUT ANY WARRANTY; without even the implied warranty of
17* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18* GNU Affero General Public License for more details.
19*
20* You should have received a copy of the GNU Affero General Public License
21* along with this program.  If not, see <http://www.gnu.org/licenses/>.
22*
23* Consult LICENSE file for details
24************************************************/
25
26function create_calendar_dav($data) {
27    ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->create_calendar_dav(): Creating calendar event");
28
29    if (defined('IMAP_MEETING_USE_CALDAV') && IMAP_MEETING_USE_CALDAV) {
30        $caldav = new BackendCalDAV();
31        if ($caldav->Logon(Request::GetAuthUser(), Request::GetAuthDomain(), Request::GetAuthPassword())) {
32            $etag = $caldav->CreateUpdateCalendar($data);
33            ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->create_calendar_dav(): Calendar created with etag '%s' and data <%s>", $etag, $data));
34            $caldav->Logoff();
35        }
36        else {
37            ZLog::Write(LOGLEVEL_ERROR, "BackendIMAP->create_calendar_dav(): Error connecting with BackendCalDAV");
38        }
39    }
40}
41
42function delete_calendar_dav($uid) {
43    ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->delete_calendar_dav('%s'): Deleting calendar event", $uid));
44
45    if ($uid === false) {
46        ZLog::Write(LOGLEVEL_WARN, "BackendIMAP->delete_calendar_dav(): UID not found; report the full calendar object to developers");
47    }
48    else {
49        if (defined('IMAP_MEETING_USE_CALDAV') && IMAP_MEETING_USE_CALDAV) {
50            $caldav = new BackendCalDAV();
51            if ($caldav->Logon(Request::GetAuthUser(), Request::GetAuthDomain(), Request::GetAuthPassword())) {
52                $events = $caldav->FindCalendar($uid);
53                if (count($events) == 1) {
54                    $href = $events[0]["href"];
55                    ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->delete_calendar_dav(): found event with href '%s', deleting", $href));
56                    // Delete event
57                    $res = $caldav->DeleteCalendar($href);
58                    if ($res) {
59                        ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->delete_calendar_dav(): event deleted");
60                    }
61                    else {
62                        ZLog::Write(LOGLEVEL_ERROR, "BackendIMAP->delete_calendar_dav(): error removing event, we will end with zombie events");
63                    }
64                    $caldav->Logoff();
65                }
66                else {
67                    ZLog::Write(LOGLEVEL_ERROR, "BackendIMAP->delete_calendar_dav(): event not found, we will end with zombie events");
68                }
69            }
70            else {
71                ZLog::Write(LOGLEVEL_ERROR, "BackendIMAP->delete_calendar_dav(): Error connecting with BackendCalDAV");
72            }
73        }
74    }
75}
76
77
78function update_calendar_attendee($uid, $mailto, $status) {
79    ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->update_calendar_attendee('%s', '%s', '%s'): Updating calendar event attendee", $uid, $mailto, $status));
80    $updated = false;
81
82    if ($uid === false) {
83        ZLog::Write(LOGLEVEL_WARN, "BackendIMAP->update_calendar_attendee(): UID not found; report the full calendar object to developers");
84    }
85    else {
86        if (defined('IMAP_MEETING_USE_CALDAV') && IMAP_MEETING_USE_CALDAV) {
87            $caldav = new BackendCalDAV();
88            if ($caldav->Logon(Request::GetAuthUser(), Request::GetAuthDomain(), Request::GetAuthPassword())) {
89                $events = $caldav->FindCalendar($uid);
90                if (count($events) == 1) {
91                    $href = $events[0]["href"];
92                    $etag = $events[0]["etag"];
93                    ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->update_calendar_attendee(): found event with href '%s' etag '%s'; updating", $href, $etag));
94
95                    // Get Attendee status
96                    $old_status = "";
97
98                    if (strcasecmp($old_status, $status) != 0) {
99                        ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->update_calendar_attendee(): Before <%s>", $events[0]["data"]));
100                        $ical = new iCalComponent();
101                        $ical->ParseFrom($events[0]["data"]);
102                        $ical->SetCPParameterValue("VEVENT", "ATTENDEE", "PARTSTAT", strtoupper($status), $mailto);
103                        ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->update_calendar_attendee(): After <%s>", $ical->Render()));
104                        $etag = $caldav->CreateUpdateCalendar($ical->Render(), $href, $etag);
105                        ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->update_calendar_attendee(): Calendar updated with etag '%s'", $etag));
106                        // Update new status
107                        $updated = true;
108                    }
109
110                    $caldav->Logoff();
111                }
112                else {
113                    ZLog::Write(LOGLEVEL_ERROR, "BackendIMAP->update_calendar_attendee(): event not found or duplicated event");
114                }
115            }
116            else {
117                ZLog::Write(LOGLEVEL_ERROR, "BackendIMAP->update_calendar_attendee(): Error connecting with BackendCalDAV");
118            }
119        }
120    }
121
122    return $updated;
123}
124
125/**
126 * Detect if one message has one VCALENDAR part
127 *
128 * @param Mail_mimeDecode $message
129 * @return boolean
130 */
131function has_calendar_object($message) {
132    if (is_calendar($message)) {
133        return true;
134    }
135    else {
136        if(isset($message->parts)) {
137            for ($i = 0; $i < count($message->parts); $i++) {
138                if (is_calendar($message->parts[$i])) {
139                    return true;
140                }
141            }
142        }
143    }
144
145    return false;
146}
147
148
149/**
150 * Detect if the message-part is VCALENDAR
151 * Content-Type: text/calendar;
152 *
153 * @param Mail_mimeDecode $message
154 * @return boolean
155 */
156function is_calendar($message) {
157    return isset($message->ctype_primary) && isset($message->ctype_secondary) && $message->ctype_primary == "text" && $message->ctype_secondary == "calendar";
158}
159
160
161/**
162 * Converts a text/calendar part into SyncMeetingRequest
163 * This is called on received messages, it's not called for events generated from the mobile
164 *
165 * @param $part             MIME part
166 * @param $output           SyncMail object
167 * @param $is_sent_folder   boolean
168 */
169function parse_meeting_calendar($part, &$output, $is_sent_folder) {
170    $ical = new iCalComponent();
171    $ical->ParseFrom($part->body);
172    ZLog::Write(LOGLEVEL_WBXML, sprintf("BackendIMAP->parse_meeting_calendar(): %s", $part->body));
173
174    // Get UID
175    $uid = false;
176    $props = $ical->GetPropertiesByPath("VEVENT/UID");
177    if (count($props) > 0) {
178        $uid = $props[0]->Value();
179    }
180
181    $method = false;
182    $props = $ical->GetPropertiesByPath("VCALENDAR/METHOD");
183    if (count($props) > 0) {
184        $method = strtolower($props[0]->Value());
185        ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->parse_meeting_calendar(): Using method from vcalendar object: %s", $method));
186    }
187    else {
188        if (isset($part->ctype_parameters["method"])) {
189            $method = strtolower($part->ctype_parameters["method"]);
190            ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->parse_meeting_calendar(): Using method from mime part object: %s", $method));
191        }
192    }
193
194    if ($method === false) {
195        ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->parse_meeting_calendar() - No method header, please report it to the developers"));
196        $output->messageclass = "IPM.Appointment";
197    }
198    else {
199        switch ($method) {
200            case "cancel":
201                $output->messageclass = "IPM.Schedule.Meeting.Canceled";
202                ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->parse_meeting_calendar(): Event canceled, removing calendar object");
203                delete_calendar_dav($uid);
204                break;
205            case "counter":
206                ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->parse_meeting_calendar(): Counter received");
207                $output->messageclass = "IPM.Schedule.Meeting.Resp.Tent";
208                $output->meetingrequest->disallownewtimeproposal = 0;
209                break;
210            case "reply":
211                ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->parse_meeting_calendar(): Reply received");
212                $props = $ical->GetPropertiesByPath('VEVENT/ATTENDEE');
213
214                for ($i = 0; $i < count($props); $i++) {
215                    $mailto = $props[$i]->Value();
216                    $props_params = $props[$i]->Parameters();
217                    $status = strtolower($props_params["PARTSTAT"]);
218                    if (!$is_sent_folder) {
219                        // Only evaluate received replies, not sent
220                        $res = update_calendar_attendee($uid, $mailto, $status);
221                    }
222                    else {
223                        $res = true;
224                    }
225                    if ($res) {
226                        // Only set messageclass for replies changing my calendar object
227                        switch ($status) {
228                            case "accepted":
229                                $output->messageclass = "IPM.Schedule.Meeting.Resp.Pos";
230                                ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->parse_meeting_calendar(): Update attendee -> accepted");
231                                break;
232                            case "needs-action":
233                                $output->messageclass = "IPM.Schedule.Meeting.Resp.Tent";
234                                ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->parse_meeting_calendar(): Update attendee -> needs-action");
235                                break;
236                            case "tentative":
237                                $output->messageclass = "IPM.Schedule.Meeting.Resp.Tent";
238                                ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->parse_meeting_calendar(): Update attendee -> tentative");
239                                break;
240                            case "declined":
241                                $output->messageclass = "IPM.Schedule.Meeting.Resp.Neg";
242                                ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->parse_meeting_calendar(): Update attendee -> declined");
243                                break;
244                            default:
245                                ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->parse_meeting_calendar() - Unknown reply status <%s>, please report it to the developers", $status));
246                                $output->messageclass = "IPM.Appointment";
247                                break;
248                        }
249                    }
250                }
251                $output->meetingrequest->disallownewtimeproposal = 1;
252                break;
253            case "request":
254                $output->messageclass = "IPM.Schedule.Meeting.Request";
255                $output->meetingrequest->disallownewtimeproposal = 0;
256                ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->parse_meeting_calendar(): New request");
257                // New meeting, we don't create it now, because we need to confirm it first, but if we don't create it we won't see it in the calendar
258                break;
259            default:
260                ZLog::Write(LOGLEVEL_WARN, sprintf("BackendIMAP->parse_meeting_calendar() - Unknown method <%s>, please report it to the developers", strtolower($part->headers["method"])));
261                $output->messageclass = "IPM.Appointment";
262                $output->meetingrequest->disallownewtimeproposal = 0;
263                break;
264        }
265    }
266
267    $props = $ical->GetPropertiesByPath('VEVENT/DTSTAMP');
268    if (count($props) == 1) {
269        $output->meetingrequest->dtstamp = TimezoneUtil::MakeUTCDate($props[0]->Value());
270    }
271    $props = $ical->GetPropertiesByPath('VEVENT/UID');
272    if (count($props) == 1) {
273        $output->meetingrequest->globalobjid = $props[0]->Value();
274    }
275    $props = $ical->GetPropertiesByPath('VEVENT/DTSTART');
276    if (count($props) == 1) {
277        $output->meetingrequest->starttime = TimezoneUtil::MakeUTCDate($props[0]->Value());
278        if (strlen($props[0]->Value()) == 8) {
279            $output->meetingrequest->alldayevent = 1;
280        }
281    }
282    $props = $ical->GetPropertiesByPath('VEVENT/DTEND');
283    if (count($props) == 1) {
284        $output->meetingrequest->endtime = TimezoneUtil::MakeUTCDate($props[0]->Value());
285        if (strlen($props[0]->Value()) == 8) {
286            $output->meetingrequest->alldayevent = 1;
287        }
288    }
289    $props = $ical->GetPropertiesByPath('VEVENT/ORGANIZER');
290    if (count($props) == 1) {
291        $output->meetingrequest->organizer = str_ireplace("MAILTO:", "", $props[0]->Value());
292    }
293    $props = $ical->GetPropertiesByPath('VEVENT/LOCATION');
294    if (count($props) == 1) {
295        $output->meetingrequest->location = $props[0]->Value();
296    }
297    $props = $ical->GetPropertiesByPath('VEVENT/CLASS');
298    if (count($props) == 1) {
299        switch ($props[0]->Value()) {
300            case "PUBLIC":
301                $output->meetingrequest->sensitivity = "0";
302                break;
303            case "PRIVATE":
304                $output->meetingrequest->sensitivity = "2";
305                break;
306            case "CONFIDENTIAL":
307                $output->meetingrequest->sensitivity = "3";
308                break;
309            default:
310                ZLog::Write(LOGLEVEL_DEBUG, sprintf("BackendIMAP->parse_meeting_calendar() - Unknown VEVENT/CLASS '%s'. Using 0", $props[0]->Value()));
311                $output->meetingrequest->sensitivity = "0";
312                break;
313        }
314    }
315    else {
316        ZLog::Write(LOGLEVEL_DEBUG, "BackendIMAP->parse_meeting_calendar() - No sensitivity class. Using 0");
317        $output->meetingrequest->sensitivity = "0";
318    }
319
320    // Get $tz from first timezone
321    $props = $ical->GetPropertiesByPath("VTIMEZONE/TZID");
322    if (count($props) > 0) {
323        // TimeZones shouldn't have dots
324        $tzname = str_replace(".", "", $props[0]->Value());
325        $tz = TimezoneUtil::GetFullTZFromTZName($tzname);
326    }
327    else {
328        $tz = TimezoneUtil::GetFullTZ();
329    }
330    $output->meetingrequest->timezone = base64_encode(TimezoneUtil::GetSyncBlobFromTZ($tz));
331
332    // Fixed values
333    $output->meetingrequest->instancetype = 0;
334    $output->meetingrequest->responserequested = 1;
335    $output->meetingrequest->busystatus = 2;
336
337    // TODO: reminder
338    $output->meetingrequest->reminder = "";
339}
340
341
342
343/**
344 * Modify a text/calendar part to transform it in a reply
345 *
346 * @param $part             MIME part
347 * @param $response         Response numeric value
348 * @param $condition_value  string
349 * @return string MIME text/calendar
350 */
351function reply_meeting_calendar($part, $response, $emailaddress) {
352    $status_attendee = "ACCEPTED"; // 1 or default is ACCEPTED
353    $status_event = "CONFIRMED";
354    switch ($response) {
355        case 1:
356            $status_attendee = "ACCEPTED";
357            $status_event = "CONFIRMED";
358            break;
359        case 2:
360            $status_attendee = $status_event = "TENTATIVE";
361            break;
362        case 3:
363            // We won't hit this case ever, because we won't create an event if we are rejecting it
364            $status_attendee = "DECLINED";
365            $status_event = "CANCELLED";
366            break;
367    }
368
369    $ical = new iCalComponent();
370    $ical->ParseFrom($part->body);
371
372    $ical->SetPValue("METHOD", "REPLY");
373    $ical->SetCPParameterValue("VEVENT", "STATUS", $status_event, null);
374    // Update my information as attendee, but only mine
375    $ical->SetCPParameterValue("VEVENT", "ATTENDEE", "PARTSTAT", $status_attendee, sprintf("MAILTO:%s", $emailaddress));
376    $ical->SetCPParameterValue("VEVENT", "ATTENDEE", "RSVP", null, sprintf("MAILTO:%s", $emailaddress));
377
378    return $ical->Render();
379}
380