0011556: sending mails to multiple recipients fails
[tine20] / tine20 / Calendar / Frontend / ActiveSync.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Calendar
6  * @subpackage  Frontend
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2009-2014 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Cornelius Weiss <c.weiss@metaways.de>
10  */
11
12 /**
13  * ActiveSync frontend class
14  * 
15  * @package     Calendar
16  * @subpackage  Frontend
17  */
18 class Calendar_Frontend_ActiveSync extends ActiveSync_Frontend_Abstract implements Syncroton_Data_IDataCalendar
19 {
20     /**
21      * available filters
22      * 
23      * @var array
24      */
25     protected $_filterArray = array(
26         Syncroton_Command_Sync::FILTER_2_WEEKS_BACK,
27         Syncroton_Command_Sync::FILTER_1_MONTH_BACK,
28         Syncroton_Command_Sync::FILTER_3_MONTHS_BACK,
29         Syncroton_Command_Sync::FILTER_6_MONTHS_BACK
30     );
31     
32     /**
33      * mapping of attendee status
34      *
35      * NOTE: not surjektive
36      * @var array
37      */
38     protected $_attendeeStatusMapping = array(
39         Syncroton_Model_EventAttendee::ATTENDEE_STATUS_UNKNOWN       => Calendar_Model_Attender::STATUS_NEEDSACTION,
40         Syncroton_Model_EventAttendee::ATTENDEE_STATUS_TENTATIVE     => Calendar_Model_Attender::STATUS_TENTATIVE,
41         Syncroton_Model_EventAttendee::ATTENDEE_STATUS_ACCEPTED      => Calendar_Model_Attender::STATUS_ACCEPTED,
42         Syncroton_Model_EventAttendee::ATTENDEE_STATUS_DECLINED      => Calendar_Model_Attender::STATUS_DECLINED,
43         //self::ATTENDEE_STATUS_NOTRESPONDED  => Calendar_Model_Attender::STATUS_NEEDSACTION
44     );
45     
46     /**
47      * mapping of attendee status in meeting response
48      * @var array
49      */
50     protected $_meetingResponseAttendeeStatusMapping = array(
51         Syncroton_Model_MeetingResponse::RESPONSE_ACCEPTED    => Calendar_Model_Attender::STATUS_ACCEPTED,
52         Syncroton_Model_MeetingResponse::RESPONSE_TENTATIVE   => Calendar_Model_Attender::STATUS_TENTATIVE,
53         Syncroton_Model_MeetingResponse::RESPONSE_DECLINED    => Calendar_Model_Attender::STATUS_DECLINED,
54     );
55     
56     /**
57      * mapping of busy status
58      *
59      * NOTE: not surjektive
60      * @var array
61      */
62     protected $_busyStatusMapping = array(
63         Syncroton_Model_Event::BUSY_STATUS_FREE      => Calendar_Model_Attender::STATUS_DECLINED,
64         Syncroton_Model_Event::BUSY_STATUS_TENATTIVE => Calendar_Model_Attender::STATUS_TENTATIVE,
65         Syncroton_Model_Event::BUSY_STATUS_BUSY      => Calendar_Model_Attender::STATUS_ACCEPTED
66     );
67     
68     /**
69      * mapping of attendee types
70      * 
71      * NOTE: recources need extra handling!
72      * @var array
73      */
74     protected $_attendeeTypeMapping = array(
75         Syncroton_Model_EventAttendee::ATTENDEE_TYPE_REQUIRED => Calendar_Model_Attender::ROLE_REQUIRED,
76         Syncroton_Model_EventAttendee::ATTENDEE_TYPE_OPTIONAL => Calendar_Model_Attender::ROLE_OPTIONAL,
77         Syncroton_Model_EventAttendee::ATTENDEE_TYPE_RESOURCE => Calendar_Model_Attender::USERTYPE_RESOURCE
78     );
79     
80     /**
81      * mapping of recur types
82      *
83      * NOTE: not surjektive
84      * @var array
85      */
86     protected $_recurTypeMapping = array(
87         Syncroton_Model_EventRecurrence::TYPE_DAILY          => Calendar_Model_Rrule::FREQ_DAILY,
88         Syncroton_Model_EventRecurrence::TYPE_WEEKLY         => Calendar_Model_Rrule::FREQ_WEEKLY,
89         Syncroton_Model_EventRecurrence::TYPE_MONTHLY        => Calendar_Model_Rrule::FREQ_MONTHLY,
90         Syncroton_Model_EventRecurrence::TYPE_MONTHLY_DAYN   => Calendar_Model_Rrule::FREQ_MONTHLY,
91         Syncroton_Model_EventRecurrence::TYPE_YEARLY         => Calendar_Model_Rrule::FREQ_YEARLY,
92         Syncroton_Model_EventRecurrence::TYPE_YEARLY_DAYN    => Calendar_Model_Rrule::FREQ_YEARLY,
93     );
94     
95     /**
96      * mapping of weekdays
97      * 
98      * NOTE: ActiveSync uses a bitmask
99      * @var array
100      */
101     protected $_recurDayMapping = array(
102         Calendar_Model_Rrule::WDAY_SUNDAY       => Syncroton_Model_EventRecurrence::RECUR_DOW_SUNDAY,
103         Calendar_Model_Rrule::WDAY_MONDAY       => Syncroton_Model_EventRecurrence::RECUR_DOW_MONDAY,
104         Calendar_Model_Rrule::WDAY_TUESDAY      => Syncroton_Model_EventRecurrence::RECUR_DOW_TUESDAY,
105         Calendar_Model_Rrule::WDAY_WEDNESDAY    => Syncroton_Model_EventRecurrence::RECUR_DOW_WEDNESDAY,
106         Calendar_Model_Rrule::WDAY_THURSDAY     => Syncroton_Model_EventRecurrence::RECUR_DOW_THURSDAY,
107         Calendar_Model_Rrule::WDAY_FRIDAY       => Syncroton_Model_EventRecurrence::RECUR_DOW_FRIDAY,
108         Calendar_Model_Rrule::WDAY_SATURDAY     => Syncroton_Model_EventRecurrence::RECUR_DOW_SATURDAY
109     );
110     
111     /**
112      * trivial mapping
113      *
114      * @var array
115      */
116     protected $_mapping = array(
117         //'Timezone'          => 'timezone',
118         'allDayEvent'       => 'is_all_day_event',
119         //'BusyStatus'        => 'transp',
120         //'OrganizerName'     => 'organizer',
121         //'OrganizerEmail'    => 'organizer',
122         //'DtStamp'           => 'last_modified_time',  // not used outside from Tine 2.0
123         'endTime'           => 'dtend',
124         'location'          => 'location',
125         'reminder'          => 'alarms',
126         'sensitivity'       => 'class',
127         'subject'           => 'summary',
128         'body'              => 'description',
129         'startTime'         => 'dtstart',
130         //'UID'               => 'uid',             // not used outside from Tine 2.0
131         //'MeetingStatus'     => 'status_id',
132         'attendees'         => 'attendee',
133         'categories'        => 'tags',
134         'recurrence'        => 'rrule',
135         'exceptions'        => 'exdate',
136     );
137     
138     /**
139      * name of Tine 2.0 backend application
140      * 
141      * @var string
142      */
143     protected $_applicationName     = 'Calendar';
144     
145     /**
146      * name of Tine 2.0 model to use
147      * 
148      * @var string
149      */
150     protected $_modelName           = 'Event';
151     
152     /**
153      * type of the default folder
154      *
155      * @var int
156      */
157     protected $_defaultFolderType   = Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR;
158     
159     /**
160      * default container for new entries
161      * 
162      * @var string
163      */
164     protected $_defaultFolder       = ActiveSync_Preference::DEFAULTCALENDAR;
165     
166     /**
167      * type of user created folders
168      *
169      * @var int
170      */
171     protected $_folderType          = Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR_USER_CREATED;
172     
173     /**
174      * name of property which defines the filterid for different content classes
175      * 
176      * @var string
177      */
178     protected $_filterProperty      = 'calendarfilterId';
179     
180     /**
181      * name of the contentcontoller class
182      * 
183      * @var string
184      */
185     protected $_contentControllerName = 'Calendar_Controller_MSEventFacade';
186     
187     protected $_defaultContainerPreferenceName = Calendar_Preference::DEFAULTCALENDAR;
188     
189     /**
190      * list of devicetypes with wrong busy status default (0 = FREE)
191      * 
192      * @var array
193      */
194     protected $_devicesWithWrongBusyStatusDefault = array(
195         'samsunggti9100', // Samsung Galaxy S-2
196         'samsunggtn7000', // Samsung Galaxy Note 
197         'samsunggti9300', // Samsung Galaxy S-3
198     );
199     
200     /**
201      * (non-PHPdoc)
202      * @see ActiveSync_Frontend_Abstract::__construct()
203      */
204     public function __construct(Syncroton_Model_IDevice $_device, DateTime $_syncTimeStamp)
205     {
206         parent::__construct($_device, $_syncTimeStamp);
207         
208         $this->_contentController->setEventFilter($this->_getContentFilter(0));
209     }
210     
211     /**
212      * (non-PHPdoc)
213      * @see Syncroton_Data_IDataCalendar::setAttendeeStatus()
214      */
215     public function setAttendeeStatus(Syncroton_Model_MeetingResponse $response)
216     {
217         $event = $instance = $this->_contentController->get($response->requestId);
218         $method = 'attenderStatusUpdate';
219         
220         if ($response->instanceId instanceof DateTime) {
221             $instance = $event->exdate->filter('recurid', $event->uid . '-' . $response->instanceId->format(Tinebase_Record_Abstract::ISO8601LONG))->getFirstRecord();
222             if (! $instance) {
223                 $exceptions = $event->exdate;
224                 $event->exdate = $exceptions->getOriginalDtStart();
225                 
226                 $instance = Calendar_Model_Rrule::computeNextOccurrence($event, $exceptions, new Tinebase_DateTime($response->instanceId));
227             }
228             
229             $method = 'attenderStatusCreateRecurException';
230         }
231         
232         $attendee = Calendar_Model_Attender::getOwnAttender($instance->attendee);
233         if (! $attendee) {
234             throw new Syncroton_Exception_Status_MeetingResponse("party crushing not allowed", Syncroton_Exception_Status_MeetingResponse::INVALID_REQUEST);
235         }
236         $attendee->status = $this->_meetingResponseAttendeeStatusMapping[$response->userResponse];
237         
238         Calendar_Controller_Event::getInstance()->$method($instance, $attendee, $attendee->status_authkey);
239         
240         // return id of calendar event
241         return $response->requestId;
242     }
243     
244     /**
245      * (non-PHPdoc)
246      * @see ActiveSync_Frontend_Abstract::toSyncrotonModel()
247      * @todo handle BusyStatus
248      */
249     public function toSyncrotonModel($entry, array $options = array())
250     {
251         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(
252             __METHOD__ . '::' . __LINE__ . " calendar data " . print_r($entry->toArray(), true));
253         
254         $syncrotonEvent = new Syncroton_Model_Event();
255         
256         foreach ($this->_mapping as $syncrotonProperty => $tine20Property) {
257             if (empty($entry->$tine20Property) && $entry->$tine20Property != '0' || count($entry->$tine20Property) === 0) {
258                 continue;
259             }
260             
261             switch($tine20Property) {
262                 case 'alarms':
263                     $entry->$tine20Property->sort('alarm_time');
264                     $alarm = $entry->alarms->getFirstRecord();
265                     
266                     if($alarm instanceof Tinebase_Model_Alarm) {
267                         // NOTE: option minutes_before is always calculated by Calendar_Controller_Event::_inspectAlarmSet
268                         $minutesBefore = (int) $alarm->getOption('minutes_before');
269                         
270                         // avoid negative alarms which may break phones
271                         if ($minutesBefore >= 0) {
272                             $syncrotonEvent->$syncrotonProperty = $minutesBefore;
273                         }
274                     }
275                     
276                     break;
277                     
278                 case 'attendee':
279                     if ($this->_device->devicetype === Syncroton_Model_Device::TYPE_IPHONE &&
280                         $entry->container_id       !== $this->_getDefaultContainerId()) {
281                         
282                         continue;
283                     }
284                     
285                     // fill attendee cache
286                     Calendar_Model_Attender::resolveAttendee($entry->$tine20Property, FALSE);
287                     
288                     $attendees = array();
289                 
290                     foreach($entry->$tine20Property as $attenderObject) {
291                         $attendee = new Syncroton_Model_EventAttendee();
292                         $attendee->name = $attenderObject->getName();
293                         $attendee->email = $attenderObject->getEmail();
294                         
295                         $acsType = array_search($attenderObject->role, $this->_attendeeTypeMapping);
296                         $attendee->attendeeType = $acsType ? $acsType : Syncroton_Model_EventAttendee::ATTENDEE_TYPE_REQUIRED;
297             
298                         $acsStatus = array_search($attenderObject->status, $this->_attendeeStatusMapping);
299                         $attendee->attendeeStatus = $acsStatus ? $acsStatus : Syncroton_Model_EventAttendee::ATTENDEE_STATUS_UNKNOWN;
300                         
301                         $attendees[] = $attendee;
302                     }
303                     
304                     $syncrotonEvent->$syncrotonProperty = $attendees;
305                     
306                     // set own status
307                     if (($ownAttendee = Calendar_Model_Attender::getOwnAttender($entry->attendee)) !== null && ($busyType = array_search($ownAttendee->status, $this->_busyStatusMapping)) !== false) {
308                         $syncrotonEvent->busyStatus = $busyType;
309                     }
310                     
311                     break;
312                     
313                 case 'class':
314                     $syncrotonEvent->$syncrotonProperty = $entry->$tine20Property == Calendar_Model_Event::CLASS_PRIVATE ? 2 : 0;
315                     
316                     break;
317                     
318                 case 'description':
319                     $syncrotonEvent->$syncrotonProperty = new Syncroton_Model_EmailBody(array(
320                         'type' => Syncroton_Model_EmailBody::TYPE_PLAINTEXT,
321                         'data' => $entry->$tine20Property
322                     ));
323                     
324                     break;
325                     
326                 case 'dtend':
327                     if($entry->$tine20Property instanceof DateTime) {
328                         if ($entry->is_all_day_event == true) {
329                             // whole day events ends at 23:59:59 in Tine 2.0 but 00:00 the next day in AS
330                             $dtend = clone $entry->$tine20Property;
331                             $dtend->addSecond($dtend->get('s') == 59 ? 1 : 0);
332                             $dtend->addMinute($dtend->get('i') == 59 ? 1 : 0);
333
334                             $syncrotonEvent->$syncrotonProperty = $dtend;
335                         } else {
336                             $syncrotonEvent->$syncrotonProperty = $entry->$tine20Property;
337                         }
338                     }
339                     
340                     break;
341                     
342                 case 'dtstart':
343                     if($entry->$tine20Property instanceof DateTime) {
344                         $syncrotonEvent->$syncrotonProperty = $entry->$tine20Property;
345                     }
346                     
347                     break;
348                     
349                 case 'exdate':
350                     // handle exceptions of repeating events
351                     if($entry->$tine20Property instanceof Tinebase_Record_RecordSet && $entry->$tine20Property->count() > 0) {
352                         $exceptions = array();
353                     
354                         foreach ($entry->exdate as $exdate) {
355                             $exception = new Syncroton_Model_EventException();
356                             
357                             // send the Deleted element only, when needed
358                             // HTC devices ignore the value(0 or 1) of the Deleted element
359                             if ((int)$exdate->is_deleted === 1) { 
360                                 $exception->deleted        = 1;
361                             }
362                             $exception->exceptionStartTime = $exdate->getOriginalDtStart();
363                             
364                             if ((int)$exdate->is_deleted === 0) {
365                                 $exceptionSyncrotonEvent = $this->toSyncrotonModel($exdate);
366                                 foreach ($exception->getProperties() as $property) {
367                                     if (isset($exceptionSyncrotonEvent->$property)) {
368                                         $exception->$property = $exceptionSyncrotonEvent->$property;
369                                     }
370                                 }
371                                 unset($exceptionSyncrotonEvent);
372                             }
373                             
374                             $exceptions[] = $exception;
375                         }
376                         
377                         $syncrotonEvent->$syncrotonProperty = $exceptions;
378                     }
379                     
380                     break;
381                     
382                 case 'rrule':
383                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
384                         __METHOD__ . '::' . __LINE__ . " calendar rrule " . $entry->$tine20Property);
385                         
386                     $rrule = Calendar_Model_Rrule::getRruleFromString($entry->$tine20Property);
387                     
388                     $recurrence = new Syncroton_Model_EventRecurrence();
389                     
390                     // required fields
391                     switch($rrule->freq) {
392                         case Calendar_Model_Rrule::FREQ_DAILY:
393                             $recurrence->type = Syncroton_Model_EventRecurrence::TYPE_DAILY;
394                             
395                             break;
396                     
397                         case Calendar_Model_Rrule::FREQ_WEEKLY:
398                             $recurrence->type      = Syncroton_Model_EventRecurrence::TYPE_WEEKLY;
399                             $recurrence->dayOfWeek = $this->_convertDayToBitMask($rrule->byday);
400                             
401                             break;
402                     
403                         case Calendar_Model_Rrule::FREQ_MONTHLY:
404                             if(!empty($rrule->bymonthday)) {
405                                 $recurrence->type       = Syncroton_Model_EventRecurrence::TYPE_MONTHLY;
406                                 $recurrence->dayOfMonth = $rrule->bymonthday;
407                             } else {
408                                 $weekOfMonth = (int) substr($rrule->byday, 0, -2);
409                                 $weekOfMonth = ($weekOfMonth == -1) ? 5 : $weekOfMonth;
410                                 $dayOfWeek   = substr($rrule->byday, -2);
411                     
412                                 $recurrence->type        = Syncroton_Model_EventRecurrence::TYPE_MONTHLY_DAYN;
413                                 $recurrence->weekOfMonth = $weekOfMonth;
414                                 $recurrence->dayOfWeek   = $this->_convertDayToBitMask($dayOfWeek);
415                             }
416                             
417                             break;
418                     
419                         case Calendar_Model_Rrule::FREQ_YEARLY:
420                             if(!empty($rrule->bymonthday)) {
421                                 $recurrence->type        = Syncroton_Model_EventRecurrence::TYPE_YEARLY;
422                                 $recurrence->dayOfMonth  = $rrule->bymonthday;
423                                 $recurrence->monthOfYear = $rrule->bymonth;
424                             } else {
425                                 $weekOfMonth = (int) substr($rrule->byday, 0, -2);
426                                 $weekOfMonth = ($weekOfMonth == -1) ? 5 : $weekOfMonth;
427                                 $dayOfWeek   = substr($rrule->byday, -2);
428                     
429                                 $recurrence->type        = Syncroton_Model_EventRecurrence::TYPE_YEARLY_DAYN;
430                                 $recurrence->weekOfMonth = $weekOfMonth;
431                                 $recurrence->dayOfWeek   = $this->_convertDayToBitMask($dayOfWeek);
432                                 $recurrence->monthOfYear = $rrule->bymonth;
433                             }
434                             
435                             break;
436                     }
437                     
438                     // required field
439                     $recurrence->interval = $rrule->interval ? $rrule->interval : 1;
440                     
441                     if($rrule->count) {
442                         $recurrence->occurrences = $rrule->count;
443                     } else if($rrule->until instanceof DateTime) {
444                         $recurrence->until = $rrule->until;
445                     }
446                     
447                     $syncrotonEvent->$syncrotonProperty = $recurrence;
448                     
449                     break;
450                     
451                 case 'tags':
452                     $syncrotonEvent->$syncrotonProperty = $entry->$tine20Property->name;;
453                     
454                     break;
455                     
456                 default:
457                     $syncrotonEvent->$syncrotonProperty = $entry->$tine20Property;
458                     
459                     break;
460             }
461         }
462         
463         $timeZoneConverter = ActiveSync_TimezoneConverter::getInstance(
464             Tinebase_Core::getLogger(),
465             Tinebase_Core::get(Tinebase_Core::CACHE)
466         );
467         
468         $syncrotonEvent->timezone = $timeZoneConverter->encodeTimezone(Tinebase_Core::getUserTimezone());
469         
470         $syncrotonEvent->meetingStatus = 1;
471         $syncrotonEvent->dtStamp = $entry->creation_time;
472         $syncrotonEvent->uID = $entry->uid;
473         
474         $this->_addOrganizer($syncrotonEvent, $entry);
475         
476         return $syncrotonEvent;
477     }
478     
479     /**
480      * convert string of days (TU,TH) to bitmask used by ActiveSync
481      *  
482      * @param $_days
483      * @return int
484      */
485     protected function _convertDayToBitMask($_days)
486     {
487         $daysArray = explode(',', $_days);
488         
489         $result = 0;
490         
491         foreach($daysArray as $dayString) {
492             $result = $result + $this->_recurDayMapping[$dayString];
493         }
494         
495         return $result;
496     }
497     
498     /**
499      * convert bitmask used by ActiveSync to string of days (TU,TH) 
500      *  
501      * @param int $_days
502      * @return string
503      */
504     protected function _convertBitMaskToDay($_days)
505     {
506         $daysArray = array();
507         
508         for($bitmask = 1; $bitmask <= Syncroton_Model_EventRecurrence::RECUR_DOW_SATURDAY; $bitmask = $bitmask << 1) {
509             $dayMatch = $_days & $bitmask;
510             if($dayMatch === $bitmask) {
511                 $daysArray[] = array_search($bitmask, $this->_recurDayMapping);
512             }
513         }
514         $result = implode(',', $daysArray);
515         
516         return $result;
517     }
518     
519     /**
520      * (non-PHPdoc)
521      * @see ActiveSync_Frontend_Abstract::toTineModel()
522      */
523     public function toTineModel(Syncroton_Model_IEntry $data, $entry = null)
524     {
525         if ($entry instanceof Calendar_Model_Event) {
526             $event = $entry;
527         } else {
528             $event = new Calendar_Model_Event(array(), true);
529         }
530
531         if ($data instanceof Syncroton_Model_Event) {
532             $data->copyFieldsFromParent();
533         }
534         
535         // Update seq to entries seq to prevent concurrent update
536         $event->seq = $entry['seq'];
537         
538         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(
539             __METHOD__ . '::' . __LINE__ . " Event before mapping: " . print_r($event->toArray(), true));
540         
541         foreach ($this->_mapping as $syncrotonProperty => $tine20Property) {
542             if (! isset($data->$syncrotonProperty)) {
543                 if ($tine20Property === 'description' && $this->_device->devicetype == Syncroton_Model_Device::TYPE_IPHONE) {
544                     // @see #8230: added alarm to event on iOS 6.1 -> description removed
545                     // this should be removed when Tine 2.0 / Syncroton supports ghosted properties
546                     if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(
547                         __METHOD__ . '::' . __LINE__ . ' Unsetting description');
548                     unset($event->$tine20Property);
549                 } else {
550                     if ($tine20Property === 'attendee' && $entry &&
551                         $this->_device->devicetype === Syncroton_Model_Device::TYPE_IPHONE &&
552                         $entry->container_id       !== $this->_getDefaultContainerId()) {
553                             // keep attendees as the are / they were not sent to the device before
554                     } else {
555                         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(
556                             __METHOD__ . '::' . __LINE__ . ' Removing ' . $tine20Property);
557                         $event->$tine20Property = null;
558                     }
559                 }
560                 continue;
561             }
562             
563             switch ($tine20Property) {
564                 case 'alarms':
565                     // handled after switch statement
566                     
567                     break;
568                     
569                 case 'attendee':
570                     if ($entry && 
571                         $this->_device->devicetype === Syncroton_Model_Device::TYPE_IPHONE &&
572                         $entry->container_id       !== $this->_getDefaultContainerId()) {
573                             // keep attendees as the are / they were not sent to the device before
574                             continue;
575                     }
576
577                     $newAttendees = array();
578                     
579                     foreach($data->$syncrotonProperty as $attendee) {
580                         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
581                             __METHOD__ . '::' . __LINE__ . " attendee email " . $attendee->email);
582                         
583                         if(isset($attendee->attendeeType) && (isset($this->_attendeeTypeMapping[$attendee->attendeeType]) || array_key_exists($attendee->attendeeType, $this->_attendeeTypeMapping))) {
584                             $role = $this->_attendeeTypeMapping[$attendee->attendeeType];
585                         } else {
586                             $role = Calendar_Model_Attender::ROLE_REQUIRED;
587                         }
588                         
589                         // AttendeeStatus send only on repsonse
590                         if (preg_match('/(?P<firstName>\S*) (?P<lastNameName>\S*)/', $attendee->name, $matches)) {
591                             $firstName = $matches['firstName'];
592                             $lastName  = $matches['lastNameName'];
593                         } else {
594                             $firstName = null;
595                             $lastName  = $attendee->name;
596                         }
597                         
598                         // @todo handle resources
599                         $newAttendees[] = array(
600                             'userType'  => Calendar_Model_Attender::USERTYPE_USER,
601                             'firstName' => $firstName,
602                             'lastName'  => $lastName,
603                             #'partStat'  => $status,
604                             'role'      => $role,
605                             'email'     => $attendee->email
606                         );
607                     }
608                     
609                     Calendar_Model_Attender::emailsToAttendee($event, $newAttendees);
610                     
611                     break;
612                     
613                 case 'class':
614                     $event->$tine20Property = $data->$syncrotonProperty == 2 ? Calendar_Model_Event::CLASS_PRIVATE : Calendar_Model_Event::CLASS_PUBLIC;
615                     
616                     break;
617                     
618                 case 'exdate':
619                     // handle exceptions from recurrence
620                     $exdates = new Tinebase_Record_RecordSet('Calendar_Model_Event');
621                     $oldExdates = $event->exdate instanceof Tinebase_Record_RecordSet ? $event->exdate : new Tinebase_Record_RecordSet('Calendar_Model_Event');
622                     
623                     foreach ($data->$syncrotonProperty as $exception) {
624                         $eventException = $this->_getRecurException($oldExdates, $exception);
625
626                         if ($exception->deleted == 0) {
627                             $eventException = $this->toTineModel($exception, $eventException);
628                             $eventException->last_modified_time = new Tinebase_DateTime($this->_syncTimeStamp);
629                         }
630
631                         $eventException->is_deleted = (bool) $exception->deleted;
632                         $eventException->seq = $entry['seq'];
633                         $exdates->addRecord($eventException);
634                     }
635                     
636                     $event->$tine20Property = $exdates;
637                     
638                     break;
639                     
640                 case 'description':
641                     // @todo check $data->$fieldName->Type and convert to/from HTML if needed
642                     if ($data->$syncrotonProperty instanceof Syncroton_Model_EmailBody) {
643                         $event->$tine20Property = $data->$syncrotonProperty->data;
644                     } else {
645                         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(
646                             __METHOD__ . '::' . __LINE__ . ' Removing description.');
647                         $event->$tine20Property = null;
648                     }
649                 
650                     break;
651                     
652                 case 'rrule':
653                     // handle recurrence
654                     if ($data->$syncrotonProperty instanceof Syncroton_Model_EventRecurrence && isset($data->$syncrotonProperty->type)) {
655                         $rrule = new Calendar_Model_Rrule();
656                     
657                         switch ($data->$syncrotonProperty->type) {
658                             case Syncroton_Model_EventRecurrence::TYPE_DAILY:
659                                 $rrule->freq = Calendar_Model_Rrule::FREQ_DAILY;
660                                 
661                                 break;
662                     
663                             case Syncroton_Model_EventRecurrence::TYPE_WEEKLY:
664                                 $rrule->freq  = Calendar_Model_Rrule::FREQ_WEEKLY;
665                                 $rrule->byday = $this->_convertBitMaskToDay($data->$syncrotonProperty->dayOfWeek);
666                                 
667                                 break;
668                                  
669                             case Syncroton_Model_EventRecurrence::TYPE_MONTHLY:
670                                 $rrule->freq       = Calendar_Model_Rrule::FREQ_MONTHLY;
671                                 $rrule->bymonthday = $data->$syncrotonProperty->dayOfMonth;
672                                 
673                                 break;
674                                  
675                             case Syncroton_Model_EventRecurrence::TYPE_MONTHLY_DAYN:
676                                 $rrule->freq = Calendar_Model_Rrule::FREQ_MONTHLY;
677                     
678                                 $week   = $data->$syncrotonProperty->weekOfMonth;
679                                 $day    = $data->$syncrotonProperty->dayOfWeek;
680                                 $byDay  = $week == 5 ? -1 : $week;
681                                 $byDay .= $this->_convertBitMaskToDay($day);
682                     
683                                 $rrule->byday = $byDay;
684                                 
685                                 break;
686                                  
687                             case Syncroton_Model_EventRecurrence::TYPE_YEARLY:
688                                 $rrule->freq       = Calendar_Model_Rrule::FREQ_YEARLY;
689                                 $rrule->bymonth    = $data->$syncrotonProperty->monthOfYear;
690                                 $rrule->bymonthday = $data->$syncrotonProperty->dayOfMonth;
691                                 
692                                 break;
693                                  
694                             case Syncroton_Model_EventRecurrence::TYPE_YEARLY_DAYN:
695                                 $rrule->freq    = Calendar_Model_Rrule::FREQ_YEARLY;
696                                 $rrule->bymonth = $data->$syncrotonProperty->monthOfYear;
697                     
698                                 $week = $data->$syncrotonProperty->weekOfMonth;
699                                 $day  = $data->$syncrotonProperty->dayOfWeek;
700                                 $byDay  = $week == 5 ? -1 : $week;
701                                 $byDay .= $this->_convertBitMaskToDay($day);
702                     
703                                 $rrule->byday = $byDay;
704                                 
705                                 break;
706                         }
707                         
708                         $rrule->interval = isset($data->$syncrotonProperty->interval) ? $data->$syncrotonProperty->interval : 1;
709                     
710                         if(isset($data->$syncrotonProperty->occurrences)) {
711                             $rrule->count = $data->$syncrotonProperty->occurrences;
712                             $rrule->until = null;
713                         } else if(isset($data->$syncrotonProperty->until)) {
714                             $rrule->count = null;
715                             $rrule->until = new Tinebase_DateTime($data->$syncrotonProperty->until);
716                         } else {
717                             $rrule->count = null;
718                             $rrule->until = null;
719                         }
720                         
721                         $event->rrule = $rrule;
722                     }
723                     
724                     break;
725                     
726                     
727                 default:
728                     if ($data->$syncrotonProperty instanceof DateTime) {
729                         $event->$tine20Property = new Tinebase_DateTime($data->$syncrotonProperty);
730                     } else {
731                         $event->$tine20Property = $data->$syncrotonProperty;
732                     }
733                     
734                     break;
735             }
736         }
737         
738         // whole day events ends at 23:59:59 in Tine 2.0 but 00:00 the next day in AS
739         if (isset($event->is_all_day_event) && $event->is_all_day_event == 1) {
740             $event->dtend->subSecond(1);
741         }
742         
743         // decode timezone data
744         if (isset($data->timezone)) {
745             $timeZoneConverter = ActiveSync_TimezoneConverter::getInstance(
746                 Tinebase_Core::getLogger(),
747                 Tinebase_Core::get(Tinebase_Core::CACHE)
748             );
749         
750             try {
751                 $timezone = $timeZoneConverter->getTimezone(
752                     $data->timezone,
753                     Tinebase_Core::getUserTimezone()
754                 );
755                 $event->originator_tz = $timezone;
756             } catch (ActiveSync_TimezoneNotFoundException $e) {
757                 Tinebase_Core::getLogger()->crit(__METHOD__ . '::' . __LINE__ . " timezone data not found " . $data->timezone);
758                 $event->originator_tz = Tinebase_Core::getUserTimezone();
759             }
760         
761             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
762                     __METHOD__ . '::' . __LINE__ . " timezone data " . $event->originator_tz);
763         }
764         
765         $this->_handleAlarms($data, $event);
766         
767         $this->_handleBusyStatus($data, $event);
768         
769         // event should be valid now
770         $event->isValid();
771         
772         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
773             __METHOD__ . '::' . __LINE__ . " eventData " . print_r($event->toArray(), true));
774
775         return $event;
776     }
777     
778     /**
779      * handle alarms / Reminder
780      * 
781      * @param SimpleXMLElement $xmlData
782      * @param Calendar_Model_Event $event
783      */
784     protected function _handleAlarms($data, $event)
785     {
786         // NOTE: existing alarms are already filtered for CU by MSEF
787         $event->alarms = $event->alarms instanceof Tinebase_Record_RecordSet ? $event->alarms : new Tinebase_Record_RecordSet('Tinebase_Model_Alarm');
788         $event->alarms->sort('alarm_time');
789         
790         $currentAlarm = $event->alarms->getFirstRecord();
791         $alarm = NULL;
792         
793         if (isset($data->reminder)) {
794             $dtstart = clone $event->dtstart;
795             
796             $alarm = new Tinebase_Model_Alarm(array(
797                 'alarm_time'        => $dtstart->subMinute($data->reminder),
798                 'minutes_before'    => in_array($data->reminder, array(0, 5, 15, 30, 60, 120, 720, 1440, 2880)) ? $data->reminder : 'custom',
799                 'model'             => 'Calendar_Model_Event'
800             ));
801             
802             $alarmUpdate = Calendar_Controller_Alarm::getMatchingAlarm($event->alarms, $alarm);
803             if (!$alarmUpdate) {
804                 // alarm not existing -> add it
805                 $event->alarms->addRecord($alarm);
806                 
807                 if ($currentAlarm) {
808                     // ActiveSync supports one alarm only -> current got deleted
809                     $event->alarms->removeRecord($currentAlarm);
810                 }
811             }
812         } else if ($currentAlarm) {
813             // current alarm got removed
814             $event->alarms->removeRecord($currentAlarm);
815         }
816     }
817
818     /**
819      * find a matching exdate or return an empty event record
820      *
821      * @param  Tinebase_Record_RecordSet        $oldExdates
822      * @param  Syncroton_Model_EventException   $sevent
823      * @return Calendar_Model_Event
824      */
825     protected function _getRecurException(Tinebase_Record_RecordSet $oldExdates, Syncroton_Model_EventException $sevent)
826     {
827         // we need to use the user timezone here if this is a DATE (like this: RECURRENCE-ID;VALUE=DATE:20140429)
828         $originalDtStart = new Tinebase_DateTime($sevent->exceptionStartTime);
829
830         foreach ($oldExdates as $id => $oldExdate) {
831             if ($originalDtStart == $oldExdate->getOriginalDtStart()) {
832                 return $oldExdate;
833             }
834         }
835
836         return new Calendar_Model_Event(array(
837             'recurid'    => $originalDtStart,
838         ));
839     }
840
841     /**
842      * append organizer name and email
843      *
844      * @param Syncroton_Model_Event $syncrotonEvent
845      * @param Calendar_Model_Event $event
846      */
847     protected function _addOrganizer(Syncroton_Model_Event $syncrotonEvent, Calendar_Model_Event $event)
848     {
849         $organizer = NULL;
850         
851         if(! empty($event->organizer)) {
852             try {
853                 $organizer = $event->resolveOrganizer();
854             } catch (Tinebase_Exception_AccessDenied $tead) {
855                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " " . $tead);
856             }
857         }
858     
859         if ($organizer instanceof Addressbook_Model_Contact) {
860             $organizerName = $organizer->n_fileas;
861             $organizerEmail = $organizer->getPreferedEmailAddress();
862         } else {
863             // set the current account as organizer
864             // if organizer is not set, you can not edit the event on the Motorola Milestone
865             $organizerName = Tinebase_Core::getUser()->accountFullName;
866             $organizerEmail = Tinebase_Core::getUser()->accountEmailAddress;
867         }
868     
869         $syncrotonEvent->organizerName = $organizerName;
870         if ($organizerEmail) {
871             $syncrotonEvent->organizerEmail = $organizerEmail;
872         }
873     }
874     
875     /**
876      * set status of own attender depending on BusyStatus
877      * 
878      * @param SimpleXMLElement $xmlData
879      * @param Calendar_Model_Event $event
880      * 
881      * @todo move detection of special handling / device type to device library
882      */
883     protected function _handleBusyStatus($data, $event)
884     {
885         if (! isset($data->busyStatus)) {
886             return;
887         }
888         
889         $ownAttender = Calendar_Model_Attender::getOwnAttender($event->attendee);
890         if ($ownAttender === NULL) {
891             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
892                 . ' No own attender found.');
893             return;
894         }
895         
896         $busyStatus = $data->busyStatus;
897         if (in_array(strtolower($this->_device->devicetype), $this->_devicesWithWrongBusyStatusDefault)) {
898             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
899                 . ' Device uses a bad default setting. BUSY and FREE are mapped to ACCEPTED.');
900             $busyStatusMapping = array(
901                 Syncroton_Model_Event::BUSY_STATUS_BUSY      => Calendar_Model_Attender::STATUS_ACCEPTED,
902                 Syncroton_Model_Event::BUSY_STATUS_TENATTIVE => Calendar_Model_Attender::STATUS_TENTATIVE,
903                 Syncroton_Model_Event::BUSY_STATUS_FREE      => Calendar_Model_Attender::STATUS_ACCEPTED
904             );
905         } else {
906             $busyStatusMapping = $this->_busyStatusMapping;
907         }
908         
909         if (isset($busyStatusMapping[$busyStatus])) {
910             $ownAttender->status = $busyStatusMapping[$busyStatus];
911         } else {
912             $ownAttender->status = Calendar_Model_Attender::STATUS_NEEDSACTION;
913         }
914     }
915     
916     /**
917      * convert contact from xml to Calendar_Model_EventFilter
918      *
919      * @param SimpleXMLElement $_data
920      * @return array
921      */
922     protected function _toTineFilterArray(SimpleXMLElement $_data)
923     {
924         $xmlData = $_data->children('uri:Calendar');
925         
926         $filterArray = array();
927         
928         foreach($this->_mapping as $fieldName => $field) {
929             if(isset($xmlData->$fieldName)) {
930                 switch ($field) {
931                     case 'dtend':
932                     case 'dtstart':
933                         $value = new Tinebase_DateTime((string)$xmlData->$fieldName);
934                         break;
935                         
936                     default:
937                         $value = (string)$xmlData->$fieldName;
938                         break;
939                         
940                 }
941                 $filterArray[] = array(
942                     'field'     => $field,
943                     'operator'  => 'equals',
944                     'value'     => $value
945                 );
946             }
947         }
948         
949         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " filterData " . print_r($filterArray, true));
950         
951         return $filterArray;
952     }
953     
954     /**
955      * return contentfilter array
956      * 
957      * @param  int $_filterType
958      * @return Tinebase_Model_Filter_FilterGroup
959      */
960     protected function _getContentFilter($_filterType)
961     {
962         $filter = parent::_getContentFilter($_filterType);
963         
964         // no persistent filter set -> add default filter
965         // NOTE: we use attender+status as devices always show declined events
966         if ($filter->isEmpty()) {
967             $attendeeFilter = $filter->createFilter('attender', 'equals', array(
968                 'user_type'    => Calendar_Model_Attender::USERTYPE_USER,
969                 'user_id'      => Tinebase_Core::getUser()->contact_id,
970             ));
971             $statusFilter = $filter->createFilter('attender_status', 'notin', array(
972                 Calendar_Model_Attender::STATUS_DECLINED
973             ));
974             $containerFilter = $filter->createFilter('container_id', 'equals', array(
975                 'path' => '/personal/' . Tinebase_Core::getUser()->getId()
976             ));
977             
978             $filter->addFilter($attendeeFilter);
979             $filter->addFilter($statusFilter);
980             $filter->addFilter($containerFilter);
981         }
982         
983         if (in_array($_filterType, $this->_filterArray)) {
984             switch($_filterType) {
985                 case Syncroton_Command_Sync::FILTER_2_WEEKS_BACK:
986                     $from = Tinebase_DateTime::now()->subWeek(2);
987                     break;
988                 case Syncroton_Command_Sync::FILTER_1_MONTH_BACK:
989                     $from = Tinebase_DateTime::now()->subMonth(2);
990                     break;
991                 case Syncroton_Command_Sync::FILTER_3_MONTHS_BACK:
992                     $from = Tinebase_DateTime::now()->subMonth(3);
993                     break;
994                 case Syncroton_Command_Sync::FILTER_6_MONTHS_BACK:
995                     $from = Tinebase_DateTime::now()->subMonth(6);
996                     break;
997             }
998         } else {
999             // don't return more than the previous 6 months
1000             $from = Tinebase_DateTime::now()->subMonth(6);
1001         }
1002         
1003         // next 10 years
1004         $to = Tinebase_DateTime::now()->addYear(10);
1005         
1006         // remove all 'old' period filters
1007         $filter->removeFilter('period');
1008         
1009         // add period filter
1010         $filter->addFilter(new Calendar_Model_PeriodFilter('period', 'within', array(
1011             'from'  => $from,
1012             'until' => $to
1013         )));
1014         
1015         return $filter;
1016     }
1017
1018     /**
1019      * 
1020      * @return int     Syncroton_Command_Sync::FILTER...
1021      */
1022     public function getMaxFilterType()
1023     {
1024         return ActiveSync_Config::getInstance()->get(ActiveSync_Config::MAX_FILTER_TYPE_CALENDAR);
1025     }
1026
1027     /**
1028      * NOTE: calendarFilter is based on contentFilter for ActiveSync
1029      *
1030      * @param $folderId
1031      */
1032     protected function _assertContentControllerParams($folderId)
1033     {
1034         try {
1035             $container = Tinebase_Container::getInstance()->getContainerById($folderId);
1036         } catch (Exception $e) {
1037             $containerId = Tinebase_Core::getPreference('ActiveSync')->{$this->_defaultFolder};
1038             $container = Tinebase_Container::getInstance()->getContainerById($containerId);
1039         }
1040         Calendar_Controller_MSEventFacade::getInstance()->assertEventFacadeParams($container, false);
1041     }
1042 }