/** * Variables required in form input: * - identity (TODO: ? Code uses it, but it is never set anywhere) * - imple_submit: itip_action(s) * - mime_id * - muid * * @return boolean True on success. */ protected function _handle(Horde_Variables $vars) { global $injector, $notification, $registry; $actions = (array) $vars->imple_submit; $result = false; $vCal = new Horde_Icalendar(); /* Retrieve the calendar data from the message. */ try { $contents = $injector->getInstance('IMP_Factory_Contents')->create(new IMP_Indices_Mailbox($vars)); $mime_part = $contents->getMIMEPart($vars->mime_id); if (empty($mime_part)) { throw new IMP_Exception(_("Cannot retrieve calendar data from message.")); } elseif (!$vCal->parsevCalendar($mime_part->getContents(), 'VCALENDAR', $mime_part->getCharset())) { throw new IMP_Exception(_("The calendar data is invalid")); } $components = $vCal->getComponents(); } catch (Exception $e) { $notification->push($e, 'horde.error'); $actions = array(); } foreach ($actions as $key => $action) { $pos = strpos($key, '['); $key = substr($key, $pos + 1, strlen($key) - $pos - 2); switch ($action) { case 'delete': // vEvent cancellation. if ($registry->hasMethod('calendar/delete')) { $guid = $components[$key]->getAttribute('UID'); $recurrenceId = null; try { // This is a cancellation of a recurring event instance. $recurrenceId = $components[$key]->getAttribute('RECURRENCE-ID'); $atts = $components[$key]->getAttribute('RECURRENCE-ID', true); $range = null; foreach ($atts as $att) { if (array_key_exists('RANGE', $att)) { $range = $att['RANGE']; } } } catch (Horde_Icalendar_Exception $e) { } try { $registry->call('calendar/delete', array($guid, $recurrenceId, $range)); $notification->push(_("Event successfully deleted."), 'horde.success'); $result = true; } catch (Horde_Exception $e) { $notification->push(sprintf(_("There was an error deleting the event: %s"), $e->getMessage()), 'horde.error'); } } else { $notification->push(_("This action is not supported."), 'horde.warning'); } break; case 'update': // vEvent reply. if ($registry->hasMethod('calendar/updateAttendee')) { try { $from = $contents->getHeader()->getOb('from'); $registry->call('calendar/updateAttendee', array($components[$key], $from[0]->bare_address)); $notification->push(_("Respondent Status Updated."), 'horde.success'); $result = true; } catch (Horde_Exception $e) { $notification->push(sprintf(_("There was an error updating the event: %s"), $e->getMessage()), 'horde.error'); } } else { $notification->push(_("This action is not supported."), 'horde.warning'); } break; case 'import': case 'accept-import': // vFreebusy reply. // vFreebusy publish. // vEvent request. // vEvent publish. // vTodo publish. // vJournal publish. switch ($components[$key]->getType()) { case 'vEvent': $result = $this->_handlevEvent($key, $components, $mime_part); // Must check for exceptions. foreach ($components as $k => $component) { try { if ($component->getType() == 'vEvent' && $component->getAttribute('RECURRENCE-ID')) { $uid = $component->getAttribute('UID'); if ($uid == $components[$key]->getAttribute('UID')) { $this->_handlevEvent($k, $components, $mime_part); } } } catch (Horde_Icalendar_Exception $e) { } } break; case 'vFreebusy': // Import into Kronolith. if ($registry->hasMethod('calendar/import_vfreebusy')) { try { $registry->call('calendar/import_vfreebusy', array($components[$key])); $notification->push(_("The user's free/busy information was sucessfully stored."), 'horde.success'); $result = true; } catch (Horde_Exception $e) { $notification->push(sprintf(_("There was an error importing user's free/busy information: %s"), $e->getMessage()), 'horde.error'); } } else { $notification->push(_("This action is not supported."), 'horde.warning'); } break; case 'vTodo': // Import into Nag. if ($registry->hasMethod('tasks/import')) { try { $guid = $registry->call('tasks/import', array($components[$key], $mime_part->getType())); $url = Horde::url($registry->link('tasks/show', array('uid' => $guid))); $notification->push(_("The task has been added to your tasklist.") . ' ' . Horde::link($url, _("View task"), null, '_blank') . Horde_Themes_Image::tag('mime/icalendar.png', array('alt' => _("View task"))) . '</a>', 'horde.success', array('content.raw')); $result = true; } catch (Horde_Exception $e) { $notification->push(sprintf(_("There was an error importing the task: %s"), $e->getMessage()), 'horde.error'); } } else { $notification->push(_("This action is not supported."), 'horde.warning'); } break; case 'vJournal': default: $notification->push(_("This action is not supported."), 'horde.warning'); } if ($action == 'import') { break; } // Fall-through for 'accept-import' // Fall-through for 'accept-import' case 'accept': case 'deny': case 'tentative': // vEvent request. if (isset($components[$key]) && $components[$key]->getType() == 'vEvent') { $vEvent = $components[$key]; $resource = new Horde_Itip_Resource_Identity($injector->getInstance('IMP_Identity'), $vEvent->getAttribute('ATTENDEE'), $vars->identity); switch ($action) { case 'accept': case 'accept-import': $type = new Horde_Itip_Response_Type_Accept($resource); break; case 'deny': $type = new Horde_Itip_Response_Type_Decline($resource); break; case 'tentative': $type = new Horde_Itip_Response_Type_Tentative($resource); break; } try { // Send the reply. Horde_Itip::factory($vEvent, $resource)->sendMultiPartResponse($type, new Horde_Core_Itip_Response_Options_Horde('UTF-8', array()), $injector->getInstance('IMP_Mail')); $notification->push(_("Reply Sent."), 'horde.success'); $result = true; } catch (Horde_Itip_Exception $e) { $notification->push(sprintf(_("Error sending reply: %s."), $e->getMessage()), 'horde.error'); } } else { $notification->push(_("This action is not supported."), 'horde.warning'); } break; case 'send': case 'reply': case 'reply2m': // vfreebusy request. if (isset($components[$key]) && $components[$key]->getType() == 'vFreebusy') { $vFb = $components[$key]; // Get the organizer details. try { $organizer = parse_url($vFb->getAttribute('ORGANIZER')); } catch (Horde_Icalendar_Exception $e) { break; } $organizerEmail = $organizer['path']; $organizer = $vFb->getAttribute('ORGANIZER', true); $organizerFullEmail = new Horde_Mail_Rfc822_Address($organizerEmail); if (isset($organizer['cn'])) { $organizerFullEmail->personal = $organizer['cn']; } if ($action == 'reply2m') { $startStamp = time(); $endStamp = $startStamp + 60 * 24 * 3600; } else { try { $startStamp = $vFb->getAttribute('DTSTART'); } catch (Horde_Icalendar_Exception $e) { $startStamp = time(); } try { $endStamp = $vFb->getAttribute('DTEND'); } catch (Horde_Icalendar_Exception $e) { } if (!$endStamp) { try { $duration = $vFb->getAttribute('DURATION'); $endStamp = $startStamp + $duration; } catch (Horde_Icalendar_Exception $e) { $endStamp = $startStamp + 60 * 24 * 3600; } } } $vfb_reply = $registry->call('calendar/getFreeBusy', array($startStamp, $endStamp)); // Find out who we are and update status. $identity = $injector->getInstance('IMP_Identity'); $email = $identity->getFromAddress(); // Build the reply. $msg_headers = new Horde_Mime_Headers(); $vCal = new Horde_Icalendar(); $vCal->setAttribute('PRODID', '-//The Horde Project//' . $msg_headers->getUserAgent() . '//EN'); $vCal->setAttribute('METHOD', 'REPLY'); $vCal->addComponent($vfb_reply); $message = _("Attached is a reply to a calendar request you sent."); $body = new Horde_Mime_Part(); $body->setType('text/plain'); $body->setCharset('UTF-8'); $body->setContents(Horde_String::wrap($message, 76)); $ics = new Horde_Mime_Part(); $ics->setType('text/calendar'); $ics->setCharset('UTF-8'); $ics->setContents($vCal->exportvCalendar()); $ics->setName('icalendar.ics'); $ics->setContentTypeParameter('METHOD', 'REPLY'); $mime = new Horde_Mime_Part(); $mime->addPart($body); $mime->addPart($ics); // Build the reply headers. $msg_headers->addReceivedHeader(array('dns' => $injector->getInstance('Net_DNS2_Resolver'), 'server' => $conf['server']['name'])); $msg_headers->addMessageIdHeader(); $msg_headers->addHeader('Date', date('r')); $msg_headers->addHeader('From', $email); $msg_headers->addHeader('To', $organizerFullEmail); $identity->setDefault($vars->identity); $replyto = $identity->getValue('replyto_addr'); if (!empty($replyto) && !$email->match($replyto)) { $msg_headers->addHeader('Reply-To', $replyto); } $msg_headers->addHeader('Subject', _("Free/Busy Request Response")); // Send the reply. try { $mime->send($organizerEmail, $msg_headers, $injector->getInstance('IMP_Mail')); $notification->push(_("Reply Sent."), 'horde.success'); $result = true; } catch (Exception $e) { $notification->push(sprintf(_("Error sending reply: %s."), $e->getMessage()), 'horde.error'); } } else { $notification->push(_("Invalid Action selected for this component."), 'horde.warning'); } break; case 'nosup': // vFreebusy request. // vFreebusy request. default: $notification->push(_("This action is not supported."), 'horde.warning'); break; } } return $result; }
private function _getItip($invitation = null) { if ($invitation === null) { $invitation = $this->_getInvitation(); } return Horde_Itip::factory($invitation, $this->_getResource()); }
/** * Sends out iTip event notifications to all attendees of a specific * event. * * Can be used to send event invitations, event updates as well as event * cancellations. * * @param Kronolith_Event $event * The event in question. * @param Horde_Notification_Handler $notification * A notification object used to show result status. * @param integer $action * The type of notification to send. One of the Kronolith::ITIP_* * values. * @param Horde_Date $instance * If cancelling a single instance of a recurring event, the date of * this instance. * @param string $range The range parameter if this is a recurring event. * Possible values are self::RANGE_THISANDFUTURE * @param Kronolith_Attendee_List $cancellations If $action is 'CANCEL', * but it is due to removing * attendees and not * canceling the entire * event, these are the * uninvited attendees and * are the ONLY people that * will receive the CANCEL * iTIP. @since 4.2.10 */ public static function sendITipNotifications(Kronolith_Event $event, Horde_Notification_Handler $notification, $action, Horde_Date $instance = null, $range = null, Kronolith_Attendee_List $cancellations = null) { global $injector, $prefs, $registry; if (!count($event->attendees) || $prefs->getValue('itip_silent')) { return; } $ident = $injector->getInstance('Horde_Core_Factory_Identity')->create($event->creator); if (!$ident->getValue('from_addr')) { $notification->push(sprintf(_("You do not have an email address configured in your Personal Information Preferences. You must set one %shere%s before event notifications can be sent."), $registry->getServiceLink('prefs', 'kronolith')->add(array('app' => 'horde', 'group' => 'identities'))->link(), '</a>'), 'horde.error', array('content.raw')); return; } // Generate image mime part first and only once, because we // need the Content-ID. $image = self::getImagePart('big_invitation.png'); $share = $injector->getInstance('Kronolith_Shares')->getShare($event->calendar); $view = new Horde_View(array('templatePath' => KRONOLITH_TEMPLATES . '/itip')); new Horde_View_Helper_Text($view); $view->identity = $ident; $view->event = $event; $view->imageId = $image->getContentId(); if ($action == self::ITIP_CANCEL && count($cancellations)) { $mail_attendees = $cancellations; } elseif ($event->organizer && !self::isUserEmail($event->creator, $event->organizer)) { /* Only send updates to organizer if the user is not the * organizer */ if (isset($event->attendees['email:' . $event->organizer])) { $organizer = $event->attendees['email:' . $event->organizer]; } else { $organizer = new Kronolith_Attendee(array('email' => $event->organizer)); } $mail_attendees = new Kronolith_Attendee_List(array($organizer)); } else { $mail_attendees = $event->attendees; } foreach ($mail_attendees as $attendee) { /* Don't send notifications to the ORGANIZER if this is the * ORGANIZER's copy of the event. */ if (!$event->organizer && Kronolith::isUserEmail($event->creator, $attendee->email)) { continue; } /* Don't bother sending an invitation/update if the recipient does * not need to participate, or has declined participating, or * doesn't have an email address. */ if (strpos($attendee->email, '@') === false || $attendee->response == self::RESPONSE_DECLINED) { continue; } /* Determine all notification-specific strings. */ switch ($action) { case self::ITIP_CANCEL: /* Cancellation. */ $method = 'CANCEL'; $filename = 'event-cancellation.ics'; $view->subject = sprintf(_("Cancelled: %s"), $event->getTitle()); if (empty($instance)) { $view->header = sprintf(_("%s has cancelled \"%s\"."), $ident->getName(), $event->getTitle()); } else { $view->header = sprintf(_("%s has cancelled an instance of the recurring \"%s\"."), $ident->getName(), $event->getTitle()); } break; case self::ITIP_REPLY: $filename = 'event-reply.ics'; $events = $event->toiCalendar(new Horde_Icalendar()); $vEvent = array_shift($events); $itipIdentity = new Horde_Itip_Resource_Identity($ident, $vEvent->getAttribute('ATTENDEE'), (string) $ident->getFromAddress()); /* Find which of the creator's mail addresses is used here */ foreach ($event->attendees as $attendee) { if (self::isUserEmail($event->creator, $attendee->email)) { switch ($attendee->response) { case self::RESPONSE_ACCEPTED: $type = new Horde_Itip_Response_Type_Accept($itipIdentity); break; case self::RESPONSE_DECLINED: $type = new Horde_Itip_Response_Type_Decline($itipIdentity); break; case self::RESPONSE_TENTATIVE: $type = new Horde_Itip_Response_Type_Tentative($itipIdentity); break; default: return; } try { // Send the reply. Horde_Itip::factory($vEvent, $itipIdentity)->sendMultiPartResponse($type, new Horde_Core_Itip_Response_Options_Horde('UTF-8', array()), $injector->getInstance('Horde_Mail')); } catch (Horde_Itip_Exception $e) { $notification->push(sprintf(_("Error sending reply: %s."), $e->getMessage()), 'horde.error'); } } } return; case self::ITIP_REQUEST: default: $method = 'REQUEST'; if ($attendee->response == self::RESPONSE_NONE) { /* Invitation. */ $filename = 'event-invitation.ics'; $view->subject = $event->getTitle(); $view->header = sprintf(_("%s wishes to make you aware of \"%s\"."), $ident->getName(), $event->getTitle()); } else { /* Update. */ $filename = 'event-update.ics'; $view->subject = sprintf(_("Updated: %s."), $event->getTitle()); $view->header = sprintf(_("%s wants to notify you about changes of \"%s\"."), $ident->getName(), $event->getTitle()); } break; } $view->organizer = $registry->convertUserName($event->creator, false); if ($action == self::ITIP_REQUEST) { $attend_link = Horde::url('attend.php', true, -1)->add(array('c' => $event->calendar, 'e' => $event->id, 'u' => $attendee->email)); $view->linkAccept = (string) $attend_link->add('a', 'accept'); $view->linkTentative = (string) $attend_link->add('a', 'tentative'); $view->linkDecline = (string) $attend_link->add('a', 'decline'); } /* Build the iCalendar data */ $iCal = new Horde_Icalendar(); $iCal->setAttribute('METHOD', $method); $iCal->setAttribute('X-WR-CALNAME', $share->get('name')); $vevent = $event->toiCalendar($iCal); if ($action == self::ITIP_CANCEL && !empty($instance)) { // Recurring event instance deletion, need to specify the // RECURRENCE-ID but NOT the EXDATE. foreach ($vevent as &$ve) { try { $uid = $ve->getAttribute('UID'); } catch (Horde_Icalendar_Exception $e) { continue; } if ($event->uid == $uid) { $ve->setAttribute('RECURRENCE-ID', $instance); if (!empty($range)) { $ve->setParameter('RECURRENCE-ID', array('RANGE' => $range)); } $ve->setAttribute('DTSTART', $instance, array(), false); $diff = $event->end->timestamp() - $event->start->timestamp(); $end = clone $instance; $end->sec += $diff; $ve->setAttribute('DTEND', $end, array(), false); $ve->removeAttribute('EXDATE'); break; } } } $iCal->addComponent($vevent); /* text/calendar part */ $ics = new Horde_Mime_Part(); $ics->setType('text/calendar'); $ics->setContents($iCal->exportvCalendar()); $ics->setName($filename); $ics->setContentTypeParameter('method', $method); $ics->setCharset('UTF-8'); $ics->setEOL("\r\n"); /* application/ics part */ $ics2 = clone $ics; $ics2->setType('application/ics'); /* multipart/mixed part */ $multipart = new Horde_Mime_Part(); $multipart->setType('multipart/mixed'); $inner = self::buildMimeMessage($view, 'notification', $image); $inner->addPart($ics); $multipart->addPart($inner); $multipart->addPart($ics2); $recipient = $attendee->addressObject; $mail = new Horde_Mime_Mail(array('Subject' => $view->subject, 'To' => $recipient, 'From' => $ident->getDefaultFromAddress(true), 'User-Agent' => 'Kronolith ' . $registry->getVersion())); $mail->setBasePart($multipart); try { $mail->send($injector->getInstance('Horde_Mail')); $notification->push(sprintf(_("The event notification to %s was successfully sent."), $recipient), 'horde.success'); } catch (Horde_Mime_Exception $e) { $notification->push(sprintf(_("There was an error sending an event notification to %s: %s"), $recipient, $e->getMessage(), $e->getCode()), 'horde.error'); } } }
/** * Handle meeting responses. * * @param array $response The response data. Contains: * - requestid: The identifier of the meeting request. Used by the server * to fetch the original meeting request details. * - response: The user's response to the request. One of the response * code constants. * - folderid: The collection id that contains the meeting request. * - * * @return string The UID of any created calendar entries, otherwise false. * @throws Horde_ActiveSync_Exception, Horde_Exception_NotFound */ public function meetingResponse(array $response) { global $injector; if (empty($response['folderid']) || empty($response['requestid']) || empty($response['response'])) { throw new Horde_ActiveSync_Exception('Invalid meeting response.'); } // First thing we need is to obtain the meeting request. $imap_message = $this->_imap->getImapMessage($response['folderid'], $response['requestid']); if (empty($imap_message)) { throw new Horde_Exception_NotFound(); } $imap_message = $imap_message[$response['requestid']]; // Find the request if (!($part = $imap_message->hasiCalendar())) { $this->_logger->err('Unable to find the meeting request.'); throw new Horde_Exception_NotFound(); } // Parse the vCal $vCal = new Horde_Icalendar(); $data = $part->getContents(); if (!$vCal->parsevCalendar($data, 'VCALENDAR', $part->getCharset())) { throw new Horde_ActiveSync_Exception('Unknown error parsing vCal data.'); } if (!($vEvent = $vCal->findComponent('vEvent'))) { throw new Horde_ActiveSync_Exception('Unknown error locating vEvent.'); } // Update the vCal so the response will be reflected when imported. $ident = $injector->getInstance('Horde_Core_Factory_Identity')->create($this->_user); $cn = $ident->getValue('fullname'); $email = $ident->getValue('from_addr'); switch ($response['response']) { case Horde_ActiveSync_Request_MeetingResponse::RESPONSE_ACCEPTED: $itip_response = 'ACCEPTED'; break; case Horde_ActiveSync_Request_MeetingResponse::RESPONSE_TENTATIVE: $itip_response = 'TENTATIVE'; break; case Horde_ActiveSync_Request_MeetingResponse::RESPONSE_DECLINED: $itip_response = 'DECLINED'; } $vEvent->updateAttendee($email, $itip_response); // Create an event from the vEvent. // Note we don't use self::changeMessage since we don't want to treat // this as an incoming message addition from the PIM. Otherwise, the // message may not get synched back to the PIM. try { $uid = $this->_connector->calendar_import_vevent($vEvent); } catch (Horde_Exception $e) { $this->_logger->err($e->getMessage()); throw new Horde_ActiveSync_Exception($e); } if (!empty($response['sendresponse'])) { if ($response['sendresponse'] !== true) { $comment = $response['sendresponse']->data; if ($response['sendresponse']->type == Horde_ActiveSync::BODYPREF_TYPE_HTML) { $comment = Horde_Text_Filter::filter($comment, 'Html2text', array('charset' => 'UTF-8', 'nestingLimit' => 1000)); } } else { $comment = ''; } // Start building the iTip response email. try { $organizer = parse_url($vEvent->getAttribute('ORGANIZER')); $organizer = $organizer['path']; } catch (Horde_Icalendar_Exception $e) { $this->_logger->err('Unable to find organizer.'); throw new Horde_ActiveSync_Exception($e); } $ident = $injector->getInstance('Horde_Core_Factory_Identity')->create($event->creator); if (!$ident->getValue('from_addr')) { throw new Horde_ActiveSync_Exception(_("You do not have an email address configured in your Personal Information Preferences.")); } $resource = new Horde_Itip_Resource_Identity($ident, $vEvent->getAttribute('ATTENDEE'), (string) $ident->getFromAddress()); switch ($response['response']) { case Horde_ActiveSync_Request_MeetingResponse::RESPONSE_ACCEPTED: $type = new Horde_Itip_Response_Type_Accept($resource, $comment); break; case Horde_ActiveSync_Request_MeetingResponse::RESPONSE_DECLINED: $type = new Horde_Itip_Response_Type_Decline($resource, $comment); break; case Horde_ActiveSync_Request_MeetingResponse::RESPONSE_TENTATIVE: $type = new Horde_Itip_Response_Type_Tentative($resource, $comment); break; } try { // Send the reply. Horde_Itip::factory($vEvent, $resource)->sendMultiPartResponse($type, new Horde_Core_Itip_Response_Options_Horde('UTF-8', array()), $injector->getInstance('Horde_Mail')); $this->_logger->info('Reply sent.'); } catch (Horde_Itip_Exception $e) { $this->_logger->err(sprintf(_("Error sending reply: %s."), $e->getMessage()), 'horde.error'); } } // Delete the original request. EAS Specs require this. Most clients // will remove the email from the UI as soon as the response is sent. // Failure to remove it from the server will result in an inconsistent // sync state. try { $this->_imap->deleteMessages(array($response['requestid']), $response['folderid']); } catch (Horde_ActiveSync_Exception $e) { $this->_logger->err($e->getMessage()); } return $uid; }