0013358: triggerAsyncEvents: After SMTP error alarm sent_status is on 'success'
[tine20] / tine20 / Calendar / Controller / EventNotifications.php
1 <?php
2 /**
3  * Calendar Event Notifications
4  * 
5  * @package     Calendar
6  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
7  * @author      Cornelius Weiss <c.weiss@metaways.de>
8  * @copyright   Copyright (c) 2009-2013 Metaways Infosystems GmbH (http://www.metaways.de)
9  */
10
11 /**
12  * Calendar Event Notifications
13  *
14  * @package     Calendar
15  */
16  class Calendar_Controller_EventNotifications
17  {
18      const NOTIFICATION_LEVEL_NONE                      =  0;
19      const NOTIFICATION_LEVEL_INVITE_CANCEL             = 10;
20      const NOTIFICATION_LEVEL_EVENT_RESCHEDULE          = 20;
21      const NOTIFICATION_LEVEL_EVENT_UPDATE              = 30;
22      const NOTIFICATION_LEVEL_ATTENDEE_STATUS_UPDATE    = 40;
23      
24      const INVITATION_ATTACHMENT_MAX_FILESIZE           = 2097152; // 2 MB
25      
26     /**
27      * @var Calendar_Controller_EventNotifications
28      */
29     private static $_instance = NULL;
30     
31     /**
32      * don't clone. Use the singleton.
33      *
34      */
35     private function __clone() 
36     {
37     }
38     
39     /**
40      * the singleton pattern
41      *
42      * @return Calendar_Controller_EventNotifications
43      */
44     public static function getInstance() 
45     {
46         if (self::$_instance === NULL) {
47             self::$_instance = new Calendar_Controller_EventNotifications();
48         }
49         
50         return self::$_instance;
51     }
52     
53     /**
54      * constructor
55      * 
56      */
57     private function __construct()
58     {
59         
60     }
61     
62     /**
63      * get updates of human interest
64      * 
65      * @param  Calendar_Model_Event $_event
66      * @param  Calendar_Model_Event $_oldEvent
67      * @return array
68      */
69     protected function _getUpdates($_event, $_oldEvent)
70     {
71         // check event details
72         $diff = $_event->diff($_oldEvent)->diff;
73         
74         $orderedUpdateFieldOfInterest = array(
75             'dtstart', 'dtend', 'rrule', 'summary', 'location', 'description',
76             'transp', 'priority', 'status', 'class',
77             'url', 'is_all_day_event', 'originator_tz', /*'tags', 'notes',*/
78         );
79         
80         $updates = array();
81         foreach ($orderedUpdateFieldOfInterest as $field) {
82             if ((isset($diff[$field]) || array_key_exists($field, $diff))) {
83                 $updates[$field] = $diff[$field];
84             }
85         }
86         
87         // rrule legacy
88         if ((isset($updates['rrule']) || array_key_exists('rrule', $updates))) {
89             $updates['rrule'] = $_oldEvent->rrule;
90         }
91         
92         // check for organizer update
93         if (Tinebase_Record_Abstract::convertId($_event['organizer'], 'Addressbook_Model_Contact') != 
94             Tinebase_Record_Abstract::convertId($_oldEvent['organizer'], 'Addressbook_Model_Contact')) {
95             
96             $updates['organizer'] = $_event->resolveOrganizer();
97         }
98         
99         // check attendee updates
100         $attendeeMigration = Calendar_Model_Attender::getMigration($_oldEvent->attendee, $_event->attendee);
101         foreach ($attendeeMigration['toUpdate'] as $attendee) {
102             $oldAttendee = Calendar_Model_Attender::getAttendee($_oldEvent->attendee, $attendee);
103             if ($attendee->status == $oldAttendee->status) {
104                 $attendeeMigration['toUpdate']->removeRecord($attendee);
105             }
106         }
107         
108         foreach($attendeeMigration as $action => $migration) {
109             Calendar_Model_Attender::resolveAttendee($migration, FALSE);
110             if (! count($migration)) {
111                 unset($attendeeMigration[$action]);
112             }
113         }
114         
115         if (! empty($attendeeMigration)) {
116             $updates['attendee'] = $attendeeMigration;
117         }
118         
119         return $updates;
120     }
121     
122     /**
123      * send notifications 
124      * 
125      * @param Calendar_Model_Event       $_event
126      * @param Tinebase_Model_FullUser    $_updater
127      * @param String                     $_action
128      * @param Tinebase_Record_Interface  $_oldEvent
129      * @param Array                      $_additionalData
130      */
131     public function doSendNotifications(Calendar_Model_Event $_event, $_updater, $_action, Tinebase_Record_Interface $_oldEvent = NULL, array $_additionalData = array())
132     {
133         if (isset($_additionalData['alarm']))
134         {
135             $_alarm = $_additionalData['alarm'];
136         } else {
137             $_alarm = null;
138         }
139
140         // we only send notifications to attendee
141         if (! $_event->attendee instanceof Tinebase_Record_RecordSet && 'tentative' !== $_action) {
142             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
143                 . " Event has no attendee");
144             return;
145         }
146
147         if ($_event->dtend === NULL) {
148             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
149                 . " ". print_r($_event->toArray(), TRUE));
150             throw new Tinebase_Exception_UnexpectedValue('no dtend set in event');
151         }
152         
153         if (Tinebase_DateTime::now()->subHour(1)->isLater($_event->dtend)) {
154             if ($_action == 'alarm' || ! ($_event->isRecurException() || $_event->rrule)) {
155                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
156                     . " Skip notifications to past events");
157                 return;
158             }
159         }
160         
161         $notificationPeriodConfig = Calendar_Config::getInstance()->get(Calendar_Config::MAX_NOTIFICATION_PERIOD_FROM);
162         if (Tinebase_DateTime::now()->subWeek($notificationPeriodConfig)->isLater($_event->dtend)) {
163             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
164                 . " Skip notifications to past events (MAX_NOTIFICATION_PERIOD_FROM: " . $notificationPeriodConfig . " week(s))");
165             return;
166         }
167         
168         // lets resolve attendee once as batch to fill cache
169         if (null !== $_event->attendee) {
170             $attendee = clone $_event->attendee;
171             Calendar_Model_Attender::resolveAttendee($attendee);
172         }
173         
174         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
175             . " " . print_r($_event->toArray(), true));
176         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
177             . " Notification action: " . $_action);
178         
179         switch ($_action) {
180             case 'alarm':
181                 foreach($_event->attendee as $attender) {
182                     if (Calendar_Model_Attender::isAlarmForAttendee($attender, $_alarm)) {
183                         $this->sendNotificationToAttender($attender, $_event, $_updater, $_action, self::NOTIFICATION_LEVEL_NONE);
184                     }
185                 }
186                 break;
187             case 'booked':
188             case 'created':
189             case 'deleted':
190                 foreach($_event->attendee as $attender) {
191                     $this->sendNotificationToAttender($attender, $_event, $_updater, $_action, self::NOTIFICATION_LEVEL_INVITE_CANCEL);
192                 }
193                 break;
194             case 'changed':
195                 $attendeeMigration = Calendar_Model_Attender::getMigration($_oldEvent->attendee, $_event->attendee);
196                 
197                 foreach ($attendeeMigration['toCreate'] as $attender) {
198                     $this->sendNotificationToAttender($attender, $_event, $_updater, 'created', self::NOTIFICATION_LEVEL_INVITE_CANCEL);
199                 }
200                 
201                 foreach ($attendeeMigration['toDelete'] as $attender) {
202                     $this->sendNotificationToAttender($attender, $_oldEvent, $_updater, 'deleted', self::NOTIFICATION_LEVEL_INVITE_CANCEL);
203                 }
204                 
205                 // NOTE: toUpdate are all attendee to be notified
206                 if (count($attendeeMigration['toUpdate']) > 0) {
207                     $updates = $this->_getUpdates($_event, $_oldEvent);
208                     
209                     if (empty($updates)) {
210                         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " empty update, nothing to notify about");
211                         return;
212                     }
213                     
214                     // compute change type
215                     if (count(array_intersect(array('dtstart', 'dtend'), array_keys($updates))) > 0) {
216                         $notificationLevel = self::NOTIFICATION_LEVEL_EVENT_RESCHEDULE;
217                     } else if (count(array_diff(array_keys($updates), array('attendee'))) > 0) {
218                         $notificationLevel = self::NOTIFICATION_LEVEL_EVENT_UPDATE;
219                     } else {
220                         $notificationLevel = self::NOTIFICATION_LEVEL_ATTENDEE_STATUS_UPDATE;
221                     }
222                     
223                     // send notifications
224                     foreach ($attendeeMigration['toUpdate'] as $attender) {
225                         $this->sendNotificationToAttender($attender, $_event, $_updater, 'changed', $notificationLevel, $updates);
226                     }
227                 }
228                 
229                 break;
230
231             case 'tentative':
232                 $attendee = new Calendar_Model_Attender(array(
233                     'cal_event_id'      => $_event->getId(),
234                     'user_type'         => Calendar_Model_Attender::USERTYPE_USER,
235                     'user_id'           => $_event->organizer,
236                 ), true);
237                 $this->sendNotificationToAttender($attendee, $_event, $_updater, 'tentative', self::NOTIFICATION_LEVEL_NONE);
238                 break;
239
240             default:
241                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " unknown action '$_action'");
242                 break;
243                 
244         }
245         
246         // SEND REPLY/COUNTER to external organizer
247         if ($_action == 'changed' && $_event->organizer && ! $_event->resolveOrganizer()->account_id) {
248             $updates = array('attendee' => array('toUpdate' => $_event->attendee));
249             $organizer = new Calendar_Model_Attender(array(
250                 'user_type'  => Calendar_Model_Attender::USERTYPE_USER,
251                 'user_id'    => $_event->resolveOrganizer()
252             ));
253             $this->sendNotificationToAttender($organizer, $_event, $_updater, 'changed', self::NOTIFICATION_LEVEL_ATTENDEE_STATUS_UPDATE, $updates);
254         }
255     }
256
257     /**
258      * send notification to a single attender
259      * 
260      * @param Calendar_Model_Attender    $_attender
261      * @param Calendar_Model_Event       $_event
262      * @param Tinebase_Model_FullUser    $_updater
263      * @param string                     $_action
264      * @param string                     $_notificationLevel
265      * @param array                      $_updates
266      * @return void
267      *
268      * @throws Exception
269      *
270      * TODO this needs major refactoring
271      */
272     public function sendNotificationToAttender(Calendar_Model_Attender $_attender, $_event, $_updater, $_action, $_notificationLevel, $_updates = NULL)
273     {
274         try {
275             $organizer = $_event->resolveOrganizer();
276             $organizerAccountId = $organizer->account_id;
277             $attendee = $_attender->getResolvedUser();
278
279             if ($attendee instanceof Addressbook_Model_List) {
280                 // don't send notification to lists as we already resolved the list members for individual mails
281                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
282                     . " Skip notification for list " . $attendee->name);
283                 return;
284             }
285
286             $attendeeAccountId = $_attender->getUserAccountId();
287             
288             $prefUserId = $attendeeAccountId ? $attendeeAccountId :
289                           ($organizerAccountId ? $organizerAccountId : 
290                           ($_event->created_by));
291             
292             try {
293                 $prefUser = Tinebase_User::getInstance()->getFullUserById($prefUserId);
294             } catch (Exception $e) {
295                 $prefUser = Tinebase_Core::getUser();
296                 $prefUserId = $prefUser->getId();
297             }
298             
299             // get prefered language, timezone and notification level
300             $locale = Tinebase_Translation::getLocale(Tinebase_Core::getPreference()->getValueForUser(Tinebase_Preference::LOCALE, $prefUserId));
301             $timezone = Tinebase_Core::getPreference()->getValueForUser(Tinebase_Preference::TIMEZONE, $prefUserId);
302             $translate = Tinebase_Translation::getTranslation('Calendar', $locale);
303             $sendLevel        = Tinebase_Core::getPreference('Calendar')->getValueForUser(Calendar_Preference::NOTIFICATION_LEVEL, $prefUserId);
304             $sendOnOwnActions = Tinebase_Core::getPreference('Calendar')->getValueForUser(Calendar_Preference::SEND_NOTIFICATION_OF_OWN_ACTIONS, $prefUserId);
305             $sendAlarms = Tinebase_Core::getPreference('Calendar')->getValueForUser(Calendar_Preference::SEND_ALARM_NOTIFICATIONS, $prefUserId);
306
307             // external (non account) notification
308             if (! $attendeeAccountId) {
309                 // external organizer needs status updates
310                 $sendLevel = is_object($organizer) && $_attender->getEmail() == $organizer->getPreferredEmailAddress() ? 40 : 30;
311                 $sendOnOwnActions = false;
312                 $sendAlarms = false;
313             }
314
315             $recipients = array($attendee);
316
317             $this->_handleResourceEditors($_attender, $_notificationLevel, $recipients, $_action, $sendLevel, $_updates);
318
319             // check if user wants this notification NOTE: organizer gets mails unless she set notificationlevel to NONE
320             // NOTE prefUser is organizer for external notifications
321             if ((null !== $_updater && $attendeeAccountId == $_updater->getId() && ! $sendOnOwnActions)
322                 || ($sendLevel < $_notificationLevel && (
323                         ((is_object($organizer) && method_exists($attendee, 'getPreferredEmailAddress') && $attendee->getPreferredEmailAddress() != $organizer->getPreferredEmailAddress())
324                         || (is_object($organizer) && !method_exists($attendee, 'getPreferredEmailAddress') && $attendee->email != $organizer->getPreferredEmailAddress()))
325                         || $sendLevel == self::NOTIFICATION_LEVEL_NONE)
326                    )
327                 ) {
328                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
329                     . " Preferred notification level not reached -> skipping notification for {$_attender->getEmail()}");
330                 return;
331             }
332
333             if ($_action == 'alarm' && ! $sendAlarms) {
334                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
335                     . " User does not want alarm mails -> skipping notification for {$_attender->getEmail()}");
336                 return;
337             }
338
339             $method = NULL; // NOTE $method gets set in _getSubject as referenced param
340             $messageSubject = $this->_getSubject($_event, $_notificationLevel, $_action, $_updates, $timezone, $locale, $translate, $method, $_attender);
341
342             // we don't send iMIP parts to external attendee if config is active
343             if (Calendar_Config::getInstance()->get(Calendar_Config::DISABLE_EXTERNAL_IMIP) && ! $attendeeAccountId) {
344                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
345                     . " External iMIP is disabled.");
346                 $method = NULL;
347             }
348
349             $view = new Zend_View();
350             $view->setScriptPath(dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'views');
351             
352             $view->translate    = $translate;
353             $view->timezone     = $timezone;
354             
355             $view->event        = $_event;
356             $view->updater      = $_updater;
357             $view->updates      = $_updates;
358
359             $view->attendeeAccountId = $attendeeAccountId;
360             
361             $messageBody = $view->render('eventNotification.php');
362             
363             $calendarPart = null;
364             $attachments = $this->_getAttachments($method, $_event, $_action, $_updater, $calendarPart);
365             
366             $sender = $_action == 'alarm' ? $prefUser : $_updater;
367             if (!empty($recipients)) {
368                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " receiver: " . count($recipients));
369                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " subject: '$messageSubject'");
370                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " body: $messageBody");
371                 
372                 Tinebase_Notification::getInstance()->send($sender, $recipients, $messageSubject, $messageBody, $calendarPart, $attachments);
373             }
374         } catch (Exception $e) {
375             Tinebase_Exception::log($e);
376             if ($_action === 'alarm') {
377                 // throw exception in case of alarm as the exception is catched in \Tinebase_Alarm::sendPendingAlarms
378                 // and alarm sending is marked as failure
379                 throw $e;
380             }
381             return;
382         }
383     }
384
385      /**
386       * @param $attender
387       * @param $_notificationLevel
388       * @param $recipients
389       * @param $action
390       * @param $sendLevel
391       * @return bool
392       */
393      protected function _handleResourceEditors($attender, $_notificationLevel, &$recipients, &$action, &$sendLevel, $_updates)
394      {
395          // Add additional recipients for resources
396          if ($attender->user_type !== Calendar_Model_Attender::USERTYPE_RESOURCE
397          || ! Calendar_Config::getInstance()->get(Calendar_Config::RESOURCE_MAIL_FOR_EDITORS)) {
398              
399              return true;
400          }
401          
402          // Set custom startus booked
403          if ($action == 'created') {
404              $action = 'booked';
405          }
406          
407          $resource = Calendar_Controller_Resource::getInstance()->get($attender->user_id);
408          if ($resource->suppress_notification) {
409              if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
410                      . " Do not send Notifications for this resource: ". $resource->name);
411              // $recipients will still contain the resource itself
412              // Edit 13.12.2016 Remove resource as well and supress ALL notifications
413              $recipients = array();
414              return true;
415          }
416          
417          // The resource has no account there for the organizer preference (sendLevel) is used. We don't want that
418          $sendLevel = self::NOTIFICATION_LEVEL_EVENT_RESCHEDULE;
419          //handle attendee status change
420          if(! empty($_updates['attendee']) && ! empty($_updates['attendee']['toUpdate'])) {
421              foreach ($_updates['attendee']['toUpdate'] as $updatedAttendee) {
422                  if ($updatedAttendee->user_type == Calendar_Model_Attender::USERTYPE_RESOURCE && $resource->getId() == $updatedAttendee->user_id) {
423                      $sendLevel = self::NOTIFICATION_LEVEL_ATTENDEE_STATUS_UPDATE;
424                  }
425              }
426          }
427          
428          /*
429          if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
430                  . " Attender: ". $attender);
431          if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
432                  . " Action: ". $action);
433          if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
434                  . " Notification Level: ". $_notificationLevel);
435          if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
436                  . " Send Level: ". $sendLevel);
437          */
438          
439          $recipients = array_merge($recipients,
440              Calendar_Controller_Resource::getInstance()->getNotificationRecipients(
441                  Calendar_Controller_Resource::getInstance()->get($attender->user_id)
442              )
443          );
444      }
445     
446     /**
447      * get notification subject and method
448      * 
449      * @param Calendar_Model_Event $_event
450      * @param string $_notificationLevel
451      * @param string $_action
452      * @param array $_updates
453      * @param string $timezone
454      * @param Zend_Locale $locale
455      * @param Zend_Translate $translate
456      * @param atring $method
457      * @param Calendar_Model_Attender
458      * @return string
459      * @throws Tinebase_Exception_UnexpectedValue
460      */
461     protected function _getSubject($_event, $_notificationLevel, $_action, $_updates, $timezone, $locale, $translate, &$method, Calendar_Model_Attender $attender)
462     {
463         $startDateString = Tinebase_Translation::dateToStringInTzAndLocaleFormat($_event->dtstart, $timezone, $locale);
464         $endDateString = Tinebase_Translation::dateToStringInTzAndLocaleFormat($_event->dtend, $timezone, $locale);
465         
466         switch ($_action) {
467             case 'alarm':
468                 $messageSubject = sprintf($translate->_('Alarm for event "%1$s" at %2$s'), $_event->summary, $startDateString);
469                 break;
470             case 'created':
471                 $messageSubject = sprintf($translate->_('Event invitation "%1$s" at %2$s'), $_event->summary, $startDateString);
472                 $method = Calendar_Model_iMIP::METHOD_REQUEST;
473                 break;
474             case 'booked':
475                 if ($attender->user_type !== Calendar_Model_Attender::USERTYPE_RESOURCE) {
476                     throw new Tinebase_Exception_UnexpectedValue('not a resource');
477                 }
478                 $resource = Calendar_Controller_Resource::getInstance()->get($attender->user_id);
479                 $messageSubject = sprintf(
480                     $translate->_('Resource "%1$s" was booked for "%2$s" at %3$s'),
481                     $resource->name,
482                     $_event->summary,
483                     $startDateString
484                 );
485                 $method = Calendar_Model_iMIP::METHOD_REQUEST;
486                 break;
487             case 'deleted':
488                 $messageSubject = sprintf($translate->_('Event "%1$s" at %2$s has been canceled' ), $_event->summary, $startDateString);
489                 $method = Calendar_Model_iMIP::METHOD_CANCEL;
490                 break;
491             case 'changed':
492                 switch ($_notificationLevel) {
493                     case self::NOTIFICATION_LEVEL_EVENT_RESCHEDULE:
494                         if ((isset($_updates['dtstart']) || array_key_exists('dtstart', $_updates))) {
495                             $oldStartDateString = Tinebase_Translation::dateToStringInTzAndLocaleFormat($_updates['dtstart'], $timezone, $locale);
496                             $messageSubject = sprintf($translate->_('Event "%1$s" has been rescheduled from %2$s to %3$s' ), $_event->summary, $oldStartDateString, $startDateString);
497                             $method = Calendar_Model_iMIP::METHOD_REQUEST;
498                             break;
499                         }
500                         // fallthrough if dtstart didn't change
501                         
502                     case self::NOTIFICATION_LEVEL_EVENT_UPDATE:
503                         $messageSubject = sprintf($translate->_('Event "%1$s" at %2$s has been updated' ), $_event->summary, $startDateString);
504                         $method = Calendar_Model_iMIP::METHOD_REQUEST;
505                         break;
506                         
507                     case self::NOTIFICATION_LEVEL_ATTENDEE_STATUS_UPDATE:
508                         if(! empty($_updates['attendee']) && ! empty($_updates['attendee']['toUpdate']) && count($_updates['attendee']['toUpdate']) == 1) {
509                             // single attendee status update
510                             $attender = $_updates['attendee']['toUpdate']->getFirstRecord();
511                             
512                             switch ($attender->status) {
513                                 case Calendar_Model_Attender::STATUS_ACCEPTED:
514                                     $messageSubject = sprintf($translate->_('%1$s accepted event "%2$s" at %3$s' ), $attender->getName(), $_event->summary, $startDateString);
515                                     break;
516                                     
517                                 case Calendar_Model_Attender::STATUS_DECLINED:
518                                     $messageSubject = sprintf($translate->_('%1$s declined event "%2$s" at %3$s' ), $attender->getName(), $_event->summary, $startDateString);
519                                     break;
520                                     
521                                 case Calendar_Model_Attender::STATUS_TENTATIVE:
522                                     $messageSubject = sprintf($translate->_('Tentative response from %1$s for event "%2$s" at %3$s' ), $attender->getName(), $_event->summary, $startDateString);
523                                     break;
524                                     
525                                 case Calendar_Model_Attender::STATUS_NEEDSACTION:
526                                     $messageSubject = sprintf($translate->_('No response from %1$s for event "%2$s" at %3$s' ), $attender->getName(), $_event->summary, $startDateString);
527                                     break;
528                             }
529                         } else {
530                             $messageSubject = sprintf($translate->_('Attendee changes for event "%1$s" at %2$s' ), $_event->summary, $startDateString);
531                         }
532                         
533                         // we don't send iMIP parts to organizers with an account cause event is already up to date
534                         if ($_event->organizer && !$_event->resolveOrganizer()->account_id) {
535                             $method = Calendar_Model_iMIP::METHOD_REPLY;
536                         }
537                         break;
538                 }
539                 break;
540
541             case 'tentative':
542                 $messageSubject = sprintf($translate->_('Tentative event notification for event "%1$s" at %2$s' ), $_event->summary, $startDateString);
543                 break;
544
545             default:
546                 $messageSubject = 'unknown action';
547                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " unknown action '$_action'");
548                 break;
549         }
550         if ($attender->user_type === Calendar_Model_Attender::USERTYPE_RESOURCE) {
551             $messageSubject = '[' . $translate->_('Resource Management') . '] ' . $messageSubject;
552         }
553         return $messageSubject;
554     }
555     
556     /**
557      * get notification attachments
558      * 
559      * @param string $method
560      * @param Calendar_Model_Event $event
561      * @param string $_action
562      * @param Tinebase_Model_FullUser $updater
563      * @param Zend_Mime_Part $calendarPart
564      * @return array
565      */
566     protected function _getAttachments($method, $event, $_action, $updater, &$calendarPart)
567     {
568         if ($method === NULL) {
569             return array();
570         }
571         
572         $vcalendar = $this->_createVCalendar($event, $method, $updater);
573         
574         $calendarPart           = new Zend_Mime_Part($vcalendar->serialize());
575         $calendarPart->charset  = 'UTF-8';
576         $calendarPart->type     = 'text/calendar; method=' . $method;
577         $calendarPart->encoding = Zend_Mime::ENCODING_QUOTEDPRINTABLE;
578         
579         $attachment = new Zend_Mime_Part($vcalendar->serialize());
580         $attachment->type     = 'application/ics';
581         $attachment->encoding = Zend_Mime::ENCODING_QUOTEDPRINTABLE;
582         $attachment->disposition = Zend_Mime::DISPOSITION_ATTACHMENT;
583         $attachment->filename = 'event.ics';
584         
585         $attachments = array($attachment);
586         
587         // add other attachments (only on invitation)
588         if ($_action == 'created' || $_action == 'booked') {
589             $eventAttachments = $this->_getEventAttachments($event);
590             $attachments = array_merge($attachments, $eventAttachments);
591         }
592         
593         return $attachments;
594     }
595     
596     /**
597      * create iMIP VCALENDAR
598      * 
599      * @param Calendar_Model_Event $event
600      * @param string $method
601      * @param Tinebase_Model_FullAccount $updater
602      * @return Sabre\VObject\Component
603      */
604     protected function _createVCalendar($event, $method, $updater)
605     {
606         $converter = Calendar_Convert_Event_VCalendar_Factory::factory(Calendar_Convert_Event_VCalendar_Factory::CLIENT_GENERIC);
607         $converter->setMethod($method);
608         $vcalendar = $converter->fromTine20Model($event);
609         
610         foreach ($vcalendar->children() as $component) {
611             if ($component->name == 'VEVENT') {
612                 if ($method != Calendar_Model_iMIP::METHOD_REPLY && $event->organizer !== $updater->contact_id) {
613                     if (isset($component->{'ORGANIZER'})) {
614                         // in Tine 2.0 non organizers might be given the grant to update events
615                         // @see rfc6047 section 2.2.1 & rfc5545 section 3.2.18
616                         $component->{'ORGANIZER'}->add('SENT-BY', 'mailto:' . $updater->accountEmailAddress);
617                     }
618                 } else if ($method == Calendar_Model_iMIP::METHOD_REPLY) {
619                     // TODO in Tine 2.0 status updater might not be updater
620                     $component->{'REQUEST-STATUS'} = '2.0;Success';
621                 }
622             }
623         }
624         
625         return $vcalendar;
626     }
627     
628     /**
629      * get event attachments
630      * 
631      * @param Calendar_Model_Event $_event
632      * @return array of Zend_Mime_Part
633      */
634     protected function _getEventAttachments($_event)
635     {
636         $attachments = array();
637         foreach ($_event->attachments as $attachment) {
638             if ($attachment->size < self::INVITATION_ATTACHMENT_MAX_FILESIZE) {
639                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
640                     . " Adding attachment " . $attachment->name . ' to invitation mail');
641                 
642                 $path = Tinebase_Model_Tree_Node_Path::STREAMWRAPPERPREFIX
643                     . Tinebase_FileSystem_RecordAttachments::getInstance()->getRecordAttachmentPath($_event)
644                     . '/' . $attachment->name;
645                 
646                 $handle = fopen($path, 'r');
647                 $stream = fopen("php://temp", 'r+');
648                 stream_copy_to_stream($handle, $stream);
649                 rewind($stream);
650
651                 $part              = new Zend_Mime_Part($stream);
652                 $part->encoding    = Zend_Mime::ENCODING_BASE64; // ?
653                 $part->filename    = $attachment->name;
654                 $part->setTypeAndDispositionForAttachment($attachment->contenttype, $attachment->name);
655                 
656                 fclose($handle);
657                 
658                 $attachments[] = $part;
659                 
660             } else {
661                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
662                     . " Not adding attachment " . $attachment->name . ' to invitation mail (size: ' . Tinebase_Helper::convertToMegabytes($attachment-size) . ')');
663             }
664         }
665         
666         return $attachments;
667     }
668  }