diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 403f00c6..de6fbc00 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -38,6 +38,7 @@ class calendar extends rcube_plugin public $home; // declare public to be used in other classes public $urlbase; public $timezone; + public $gmt_offset; public $ical; public $ui; @@ -62,6 +63,9 @@ class calendar extends rcube_plugin 'Family' => '00ff00', 'Holiday' => 'ff6600', ); + + private $ics_parts = array(); + /** * Plugin initialization. @@ -102,10 +106,6 @@ class calendar extends rcube_plugin if ($this->rc->task == 'calendar' && $this->rc->action != 'save-pref') { if ($this->rc->action != 'upload') { $this->load_driver(); - - // load iCalendar functions - require($this->home . '/lib/calendar_ical.php'); - $this->ical = new calendar_ical($this->rc, $this->driver); } // register calendar actions @@ -137,6 +137,10 @@ class calendar extends rcube_plugin $this->add_hook('preferences_list', array($this, 'preferences_list')); $this->add_hook('preferences_save', array($this, 'preferences_save')); } + else if ($this->rc->task == 'mail' && ($this->rc->action == 'show' || $this->rc->action == 'preview')) { + $this->add_hook('message_load', array($this, 'mail_message_load')); + $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html')); + } // add hook to display alarms $this->add_hook('keep_alive', array($this, 'keep_alive')); @@ -165,6 +169,20 @@ class calendar extends rcube_plugin } } + /** + * Load iCalendar functions + */ + private function load_ical() + { + if (!$this->ical) { + require($this->home . '/lib/calendar_ical.php'); + $this->ical = new calendar_ical($this); + } + + return $this->ical; + } + + /** * Render the main calendar view from skin template */ @@ -641,6 +659,7 @@ class calendar extends rcube_plugin header("Content-Type: text/calendar"); header("Content-Disposition: inline; filename=".$calendar_name); + $this->load_ical(); echo $this->ical->export($events); exit; } @@ -1250,7 +1269,7 @@ class calendar extends rcube_plugin $message->setParam('text_encoding', 'quoted-printable'); $message->setParam('head_encoding', 'quoted-printable'); $message->setParam('head_charset', RCMAIL_CHARSET); - $message->setParam('text_charset', RCMAIL_CHARSET); + $message->setParam('text_charset', RCMAIL_CHARSET . ";\r\n format=flowed"); // compose common headers array $headers = array( @@ -1264,6 +1283,7 @@ class calendar extends rcube_plugin // attach ics file for this event + $this->load_ical(); $vcal = $this->ical->export(array($event), 'REQUEST'); $message->addAttachment($vcal, 'text/calendar', 'event.ics', false, '8bit', 'attachment', RCMAIL_CHARSET); @@ -1307,7 +1327,7 @@ class calendar extends rcube_plugin )); $message->headers($headers); - $message->setTXTBody(rc_wordwrap($body, 75, "\r\n")); + $message->setTXTBody(rcube_message::format_flowed($body, 79)); // finally send the message if (rcmail_deliver_message($message, $from, $mailto, $smtp_error)) @@ -1326,14 +1346,15 @@ class calendar extends rcube_plugin { $fromto = ''; $duration = $event['end'] - $event['start']; - $date_format = self::to_php_date_format($this->rc->config->get('calendar_date_format')); - $time_format = self::to_php_date_format($this->rc->config->get('calendar_time_format')); + + $date_format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])); + $time_format = self::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format'])); if ($event['allday']) { $fromto = format_date($event['start'], $date_format) . - ($duration > 86400 || date('d', $event['start']) != date('d', $event['end']) ? ' - ' . format_date($event['end'], $date_format) : ''); + ($duration > 86400 || gmdate('d', $event['start']) != gmdate('d', $event['end']) ? ' - ' . format_date($event['end'], $date_format) : ''); } - else if ($duration < 86400 && date('d', $event['start']) == date('d', $event['end'])) { + else if ($duration < 86400 && gmdate('d', $event['start']) == gmdate('d', $event['end'])) { $fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) . ' - ' . format_date($event['end'], $time_format); } @@ -1483,4 +1504,82 @@ class calendar extends rcube_plugin return $diff; } + + /**** Event invitation plugin hooks ****/ + + /** + * Check mail message structure of there are .ics files attached + */ + public function mail_message_load($p) + { + $this->message = $p['object']; + + // check all message parts for .ics files + foreach ((array)$this->message->mime_parts as $idx => $part) { + if ($this->is_vcalendar($part)) { + $this->ics_parts[] = $part->mime_id; + } + } + } + + /** + * Add UI elements to copy event invitations or updates to the calendar + */ + public function mail_messagebody_html($p) + { + // load iCalendar functions (if necessary) + if (!empty($this->ics_parts)) { + require($this->home . '/lib/calendar_ical.php'); + $this->ical = new calendar_ical($this->rc); + } + + $html = ''; + foreach ($this->ics_parts as $part) { + $this->load_ical(); + $events = $this->ical->import($this->message->get_part_content($part)); + + // successfully parsed events? + if (empty($events)) + continue; + + foreach ($events as $idx => $event) { + // add box below messsage body + $html .= html::p('calendar-invitebox', + html::span('eventtitle', Q($event['title'])) . ' ' . + html::span('eventdate', Q('(' . $this->event_date_text($event) . ')')) . ' ' . + html::tag('input', array( + 'type' => 'button', + 'class' => 'button', + # 'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($part.':'.$idx) . "', '" . JQ($event['title']) . "')", + 'value' => $this->gettext('importtocalendar'), + )) + ); + } + } + + // prepend event boxes to message body + if ($html) { + $this->ui->init(); + $p['content'] = $html . $p['content']; + } + + return $p; + } + + /** + * Checks if specified message part is a vcalendar data + * + * @param rcube_message_part Part object + * @return boolean True if part is of type vcard + */ + private function is_vcalendar($part) + { + return ( + $part->mimetype == 'text/calendar' || + $part->mimetype == 'text/x-vcalendar' || + // Apple sends files as application/x-any (!?) + ($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename)) + ); + } + } diff --git a/plugins/calendar/lib/calendar_ical.php b/plugins/calendar/lib/calendar_ical.php index 704f772f..b5d6cb0e 100644 --- a/plugins/calendar/lib/calendar_ical.php +++ b/plugins/calendar/lib/calendar_ical.php @@ -19,36 +19,92 @@ | | +-------------------------------------------------------------------------+ | Author: Lazlo Westerhof | - | Bogomil "Bogo" Shopov | + | Thomas Bruederli | + | Bogomil "Bogo" Shopov | +-------------------------------------------------------------------------+ */ + +/** + * Class to parse and build vCalendar (iCalendar) files + * + * Uses the Horde:iCalendar class for parsing. To install: + * > pear channel-discover pear.horde.org + * > pear install horde/Horde_Icalendar + * + */ class calendar_ical { const EOL = "\r\n"; private $rc; - private $driver; + private $cal; - function __construct($rc, $driver) + + function __construct($cal) { - $this->rc = $rc; - $this->driver = $driver; + $this->cal = $cal; + $this->rc = $cal->rc; } - + /** * Import events from iCalendar format * - * @param array Associative events array - * @access public + * @param string vCalendar input + * @return array List of events extracted from the input */ - public function import($events) + public function import($vcal) { - //TODO - // for ($events as $event) - // $this->backend->newEvent(...); + // use Horde:iCalendar to parse vcalendar file format + require_once 'Horde/iCalendar.php'; + + $parser = new Horde_iCalendar; + $parser->parsevCalendar($vcal); + $events = array(); + if ($data = $parser->getComponents()) { + foreach ($data as $comp) { + if ($comp->getType() == 'vEvent') + $events[] = $this->_to_rcube_format($comp); + } + } + + return $events; } - + + /** + * Convert the given File_IMC_Parse_Vcalendar_Event object to the internal event format + */ + private function _to_rcube_format($ve) + { + $event = array( + 'uid' => $ve->getAttributeDefault('UID'), + 'title' => $ve->getAttributeDefault('SUMMARY'), + 'description' => $ve->getAttributeDefault('DESCRIPTION'), + 'location' => $ve->getAttributeDefault('LOCATION'), + 'start' => $ve->getAttribute('DTSTART'), + 'end' => $ve->getAttribute('DTEND'), + ); + + // check for all-day dates + if (is_array($event['start'])) { + $event['start'] = gmmktime(0, 0, 0, $event['start']['month'], $event['start']['mday'], $event['start']['year']) + $this->cal->gmt_offset; + $event['allday'] = true; + } + if (is_array($event['end'])) { + $event['end'] = gmmktime(0, 0, 0, $event['end']['month'], $event['end']['mday'], $event['end']['year']) + $this->cal->gmt_offset - 60; + } + + // TODO: complete this + + + // make sure event has an UID + if (!$event['uid']) + $event['uid'] = $this->cal->$this->generate_uid(); + + return $event; + } + + /** * Export events to iCalendar format * @@ -69,8 +125,15 @@ class calendar_ical foreach ($events as $event) { $ical .= "BEGIN:VEVENT" . self::EOL; $ical .= "UID:" . self::escpape($event['uid']) . self::EOL; - $ical .= "DTSTART:" . gmdate('Ymd\THis\Z', $event['start']) . self::EOL; - $ical .= "DTEND:" . gmdate('Ymd\THis\Z', $event['end']) . self::EOL; + // correctly set all-day dates + if ($event['allday']) { + $ical .= "DTSTART;VALUE=DATE:" . gmdate('Ymd', $event['start'] + $this->cal->gmt_offset) . self::EOL; + $ical .= "DTEND;VALUE=DATE:" . gmdate('Ymd', $event['end'] + $this->cal->gmt_offset + 60) . self::EOL; // ends the next day + } + else { + $ical .= "DTSTART:" . gmdate('Ymd\THis\Z', $event['start']) . self::EOL; + $ical .= "DTEND:" . gmdate('Ymd\THis\Z', $event['end']) . self::EOL; + } $ical .= "SUMMARY:" . self::escpape($event['title']) . self::EOL; $ical .= "DESCRIPTION:" . self::escpape($event['description']) . self::EOL; diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php index 7f826513..77f5cc5e 100644 --- a/plugins/calendar/lib/calendar_ui.php +++ b/plugins/calendar/lib/calendar_ui.php @@ -26,6 +26,7 @@ class calendar_ui { private $rc; private $calendar; + private $ready = false; public $screen; function __construct($calendar) @@ -40,6 +41,9 @@ class calendar_ui */ public function init() { + if ($this->ready) // already done + return; + // add taskbar button $this->calendar->add_button(array( 'name' => 'calendar', @@ -54,6 +58,8 @@ class calendar_ui $skin = $this->rc->config->get('skin'); $this->calendar->include_stylesheet('skins/' . $skin . '/calendar.css'); + + $this->ready = true; } /** diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/default/calendar.css index 56bd9034..92e03a97 100644 --- a/plugins/calendar/skins/default/calendar.css +++ b/plugins/calendar/skins/default/calendar.css @@ -993,3 +993,19 @@ div.fc-event-location { fieldset #calendarcategories div { margin-bottom: 0.3em; } + + +/* Invitation UI in mail */ + +p.calendar-invitebox { + min-height: 20px; + margin: 5px 8px; + padding: 6px 6px 6px 34px; + border: 1px solid #C2D071; + background: url('images/calendar.png') 6px 3px no-repeat #F7FDCB; +} + +p.calendar-invitebox input.button { + margin-left: 1em; + font-size: 11px; +}