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