From 978c9023e5a2a94be4a1e1569d134a64ba9e474b Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Thu, 31 Jul 2014 18:21:53 +0200 Subject: [PATCH] Move iTip message parsing functionality to libcalendaring. Only parse iCal attachments once although used by calendar and tasks --- plugins/calendar/calendar.php | 134 ++++--------------- plugins/libcalendaring/libcalendaring.php | 138 ++++++++++++++++++++ plugins/tasklist/tasklist.php | 149 ++++------------------ 3 files changed, 189 insertions(+), 232 deletions(-) diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index ae1c4cea..9bc6be5f 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -173,7 +173,6 @@ class calendar extends rcube_plugin else if ($args['task'] == 'mail') { // hooks to catch event invitations on incoming mails if ($args['action'] == 'show' || $args['action'] == 'preview') { - $this->add_hook('message_load', array($this, 'mail_message_load')); $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html')); } @@ -2287,7 +2286,7 @@ class calendar extends rcube_plugin foreach ($p['messages'] as $i => $header) { $part = new StdClass; $part->mimetype = $header->ctype; - if ($this->is_vcalendar($part)) { + if (libcalendaring::part_is_vcalendar($part)) { $header->list_flags['attachmentClass'] = 'ical'; } else if (in_array($header->ctype, array('multipart/alternative', 'multipart/mixed'))) { @@ -2295,7 +2294,7 @@ class calendar extends rcube_plugin if (!empty($header->structure) && is_array($header->structure->parts)) { foreach ($header->structure->parts as $part) { - if ($this->is_vcalendar($part) && !empty($part->ctype_parameters['method'])) { + if (libcalendaring::part_is_vcalendar($part) && !empty($part->ctype_parameters['method'])) { $header->list_flags['attachmentClass'] = 'ical'; break; } @@ -2305,29 +2304,6 @@ class calendar extends rcube_plugin } } } - - /** - * Check mail message structure of there are .ics files attached - */ - public function mail_message_load($p) - { - $this->message = $p['object']; - $itip_part = null; - - // check all message parts for .ics files - foreach ((array)$this->message->mime_parts as $part) { - if ($this->is_vcalendar($part)) { - if ($part->ctype_parameters['method']) - $itip_part = $part->mime_id; - else - $this->ics_parts[] = $part->mime_id; - } - } - - // priorize part with method parameter - if ($itip_part) - $this->ics_parts = array($itip_part); - } /** * Add UI element to copy event invitations or updates to the calendar @@ -2335,47 +2311,38 @@ class calendar extends rcube_plugin public function mail_messagebody_html($p) { // load iCalendar functions (if necessary) - if (!empty($this->ics_parts)) { + if (!empty($this->lib->ical_parts)) { $this->get_ical(); $this->load_itip(); } $html = ''; $has_events = false; - foreach ($this->ics_parts as $mime_id) { - $part = $this->message->mime_parts[$mime_id]; - $charset = $part->ctype_parameters['charset'] ? $part->ctype_parameters['charset'] : RCMAIL_CHARSET; - $events = $this->ical->import($this->message->get_part_content($mime_id), $charset); - $title = $this->gettext('title'); + $ical_objects = $this->lib->get_mail_ical_objects(); - // successfully parsed events? - if (empty($events)) - continue; + // show a box for every event in the file + foreach ($ical_objects as $idx => $event) { + if ($event['_type'] != 'event') // skip non-event objects (#2928) + continue; - // show a box for every event in the file - foreach ($events as $idx => $event) { - if ($event['_type'] != 'event') // skip non-event objects (#2928) - continue; + $has_events = true; - $has_events = true; - - // get prepared inline UI for this event object - if ($this->ical->method) { - $html .= html::div('calendar-invitebox', - $this->itip->mail_itip_inline_ui( - $event, - $this->ical->method, - $mime_id.':'.$idx, - 'calendar', - rcube_utils::anytodatetime($this->message->headers->date) - ) - ); - } - - // limit listing - if ($idx >= 3) - break; + // get prepared inline UI for this event object + if ($ical_objects->method) { + $html .= html::div('calendar-invitebox', + $this->itip->mail_itip_inline_ui( + $event, + $ical_objects->method, + $ical_objects->mime_id . ':' . $idx, + 'calendar', + rcube_utils::anytodatetime($ical_objects->message_date) + ) + ); } + + // limit listing + if ($idx >= 3) + break; } // prepend event boxes to message body @@ -2403,40 +2370,6 @@ class calendar extends rcube_plugin return $p; } - /** - * Read the given mime message from IMAP and parse ical data - */ - private function mail_get_itip_event($mbox, $uid, $mime_id) - { - $charset = RCMAIL_CHARSET; - - // establish imap connection - $imap = $this->rc->get_storage(); - $imap->set_mailbox($mbox); - - if ($uid && $mime_id) { - list($mime_id, $index) = explode(':', $mime_id); - $part = $imap->get_message_part($uid, $mime_id); - if ($part->ctype_parameters['charset']) - $charset = $part->ctype_parameters['charset']; - $headers = $imap->get_message_headers($uid); - - if ($part) { - $events = $this->get_ical()->import($part, $charset); - } - } - - // successfully parsed events? - if (!empty($events) && ($event = $events[$index])) { - // store the message's sender address for comparisons - $event['_sender'] = preg_match('/([a-z0-9][a-z0-9\-\.\+\_]*@[^&@"\'.][^@&"\']*\\.([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,}))/', $headers->from, $m) ? $m[1] : ''; - $event['_sender_utf'] = rcube_idn_to_utf8($event['_sender']); - - return $event; - } - - return null; - } /** * Handler for POST request to import an event attached to a mail message @@ -2454,7 +2387,7 @@ class calendar extends rcube_plugin $success = false; // successfully parsed events? - if ($event = $this->mail_get_itip_event($mbox, $uid, $mime_id)) { + if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) { // find writeable calendar to store event $cal_id = !empty($_REQUEST['_folder']) ? get_input_value('_folder', RCUBE_INPUT_POST) : null; $calendars = $this->driver->list_calendars(false, true); @@ -2635,7 +2568,7 @@ class calendar extends rcube_plugin $mbox = get_input_value('_mbox', RCUBE_INPUT_POST); $mime_id = get_input_value('_part', RCUBE_INPUT_POST); - if (($event = $this->mail_get_itip_event($mbox, $uid, $mime_id)) && $this->ical->method == 'REPLY') { + if (($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) && $event['_method'] == 'REPLY') { $event['comment'] = get_input_value('_comment', RCUBE_INPUT_POST); foreach ($event['attendees'] as $_attendee) { @@ -2807,21 +2740,6 @@ class calendar extends rcube_plugin return $args; } - /** - * 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 ( - in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) || - // Apple sends files as application/x-any (!?) - ($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename)) - ); - } - /** * Get a list of email addresses of the current user (from login and identities) diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php index a8b70cb3..6eae2df1 100644 --- a/plugins/libcalendaring/libcalendaring.php +++ b/plugins/libcalendaring/libcalendaring.php @@ -35,6 +35,8 @@ class libcalendaring extends rcube_plugin public $gmt_offset; public $dst_active; public $timezone_offset; + public $ical_parts = array(); + public $ical_message; public $defaults = array( 'calendar_date_format' => "yyyy-MM-dd", @@ -57,6 +59,8 @@ class libcalendaring extends rcube_plugin private static $instance; + private $mail_ical_parser; + /** * Singleton getter to allow direct access from other plugins */ @@ -102,6 +106,21 @@ class libcalendaring extends rcube_plugin $this->add_hook('refresh', array($this, 'refresh')); $this->register_action('plugin.alarms', array($this, 'alarms_action')); } + + // proceed initialization in startup hook + $this->add_hook('startup', array($this, 'startup')); + } + + /** + * Startup hook + */ + public function startup($args) + { + if ($args['task'] == 'mail') { + if ($args['action'] == 'show' || $args['action'] == 'preview') { + $this->add_hook('message_load', array($this, 'mail_message_load')); + } + } } /** @@ -1220,6 +1239,125 @@ class libcalendaring extends rcube_plugin } + /********* iTip message detection *********/ + + /** + * Check mail message structure of there are .ics files attached + */ + public function mail_message_load($p) + { + $this->ical_message = $p['object']; + $itip_part = null; + + // check all message parts for .ics files + foreach ((array)$this->ical_message->mime_parts as $part) { + if (self::part_is_vcalendar($part)) { + if ($part->ctype_parameters['method']) + $itip_part = $part->mime_id; + else + $this->ical_parts[] = $part->mime_id; + } + } + + // priorize part with method parameter + if ($itip_part) { + $this->ical_parts = array($itip_part); + } + } + + /** + * Getter for the parsed iCal objects attached to the current email message + * + * @return object libvcalendar parser instance with the parsed objects + */ + public function get_mail_ical_objects() + { + // create parser and load ical objects + if (!$this->mail_ical_parser) { + $this->mail_ical_parser = $this->get_ical(); + + foreach ($this->ical_parts as $mime_id) { + $part = $this->ical_message->mime_parts[$mime_id]; + $charset = $part->ctype_parameters['charset'] ?: RCMAIL_CHARSET; + $this->mail_ical_parser->import($this->ical_message->get_part_content($mime_id), $charset); + + // stop on the part that has an iTip method specified + if (count($this->mail_ical_parser->objects) && $this->mail_ical_parser->method) { + $this->mail_ical_parser->message_date = $this->ical_message->headers->date; + $this->mail_ical_parser->mime_id = $mime_id; + break; + } + } + } + + return $this->mail_ical_parser; + } + + /** + * Read the given mime message from IMAP and parse ical data + * + * @param string Mailbox name + * @param string Message UID + * @param string Message part ID and object index (e.g. '1.2:0') + * @param string Object type filter (optional) + * + * @return array Hash array with the parsed iCal + */ + public function mail_get_itip_object($mbox, $uid, $mime_id, $type = null) + { + $charset = RCMAIL_CHARSET; + + // establish imap connection + $imap = $this->rc->get_storage(); + $imap->set_mailbox($mbox); + + if ($uid && $mime_id) { + list($mime_id, $index) = explode(':', $mime_id); + + $part = $imap->get_message_part($uid, $mime_id); + $headers = $imap->get_message_headers($uid); + $parser = $this->get_ical(); + + if ($part->ctype_parameters['charset']) { + $charset = $part->ctype_parameters['charset']; + } + + if ($part) { + $objects = $parser->import($part, $charset); + } + } + + // successfully parsed events/tasks? + if (!empty($objects) && ($object = $objects[$index]) && (!$type || $object['_type'] == $type)) { + if ($parser->method) + $object['_method'] = $parser->method; + + // store the message's sender address for comparisons + $object['_sender'] = preg_match('/([a-z0-9][a-z0-9\-\.\+\_]*@[^&@"\'.][^@&"\']*\\.([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,}))/', $headers->from, $m) ? $m[1] : ''; + $object['_sender_utf'] = rcube_idn_to_utf8($object['_sender']); + + return $object; + } + + return null; + } + + /** + * Checks if specified message part is a vcalendar data + * + * @param rcube_message_part Part object + * @return boolean True if part is of type vcard + */ + public static function part_is_vcalendar($part) + { + return ( + in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) || + // Apple sends files as application/x-any (!?) + ($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename)) + ); + } + + /********* Static utility functions *********/ /** diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php index 9aca0df7..29d30c8a 100644 --- a/plugins/tasklist/tasklist.php +++ b/plugins/tasklist/tasklist.php @@ -123,7 +123,6 @@ class tasklist extends rcube_plugin } else if ($args['task'] == 'mail') { if ($args['action'] == 'show' || $args['action'] == 'preview') { - $this->add_hook('message_load', array($this, 'mail_message_load')); $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html')); } @@ -1337,84 +1336,44 @@ class tasklist extends rcube_plugin } /** - * Check mail message structure of there are .ics files attached - * - * @todo move to libcalendaring - */ - public function mail_message_load($p) - { - $this->message = $p['object']; - $itip_part = null; - - // check all message parts for .ics files - foreach ((array)$this->message->mime_parts as $part) { - if ($this->is_vcalendar($part)) { - if ($part->ctype_parameters['method']) - $itip_part = $part->mime_id; - else - $this->ics_parts[] = $part->mime_id; - } - } - - // priorize part with method parameter - if ($itip_part) { - $this->ics_parts = array($itip_part); - } - } - - /** - * Add UI element to copy event invitations or updates to the calendar - * - * @todo move to libcalendaring + * Add UI element to copy task invitations or updates to the tasklist */ public function mail_messagebody_html($p) { // load iCalendar functions (if necessary) - if (!empty($this->ics_parts)) { + if (!empty($this->lib->ical_parts)) { $this->get_ical(); $this->load_itip(); } - // @todo: Calendar plugin does the same, which means the - // attachment body is fetched twice, this is not optimal $html = ''; $has_tasks = false; - foreach ($this->ics_parts as $mime_id) { - $part = $this->message->mime_parts[$mime_id]; - $charset = $part->ctype_parameters['charset'] ? $part->ctype_parameters['charset'] : RCMAIL_CHARSET; - $objects = $this->ical->import($this->message->get_part_content($mime_id), $charset); - $title = $this->gettext('title'); + $ical_objects = $this->lib->get_mail_ical_objects(); - // successfully parsed events? - if (empty($objects)) { + // show a box for every task in the file + foreach ($ical_objects as $idx => $task) { + if ($task['_type'] != 'task') { continue; } - // show a box for every task in the file - foreach ($objects as $idx => $task) { - if ($task['_type'] != 'task') { - continue; - } + $has_tasks = true; - $has_tasks = true; + // get prepared inline UI for this event object + if ($ical_objects->method) { + $html .= html::div('tasklist-invitebox', + $this->itip->mail_itip_inline_ui( + $task, + $ical_objects->method, + $ical_objects->mime_id . ':' . $idx, + 'tasks', + rcube_utils::anytodatetime($ical_objects->message_date) + ) + ); + } - // get prepared inline UI for this event object - if ($this->ical->method) { - $html .= html::div('tasklist-invitebox', - $this->itip->mail_itip_inline_ui( - $task, - $this->ical->method, - $mime_id . ':' . $idx, - 'tasks', - rcube_utils::anytodatetime($this->message->headers->date) - ) - ); - } - - // limit listing - if ($idx >= 3) { - break; - } + // limit listing + if ($idx >= 3) { + break; } } @@ -1446,66 +1405,6 @@ class tasklist extends rcube_plugin return $p; } - /** - * Read the given mime message from IMAP and parse ical data - * - * @todo move to libcalendaring - */ - private function mail_get_itip_task($mbox, $uid, $mime_id) - { - $charset = RCMAIL_CHARSET; - - // establish imap connection - $imap = $this->rc->get_storage(); - $imap->set_mailbox($mbox); - - if ($uid && $mime_id) { - list($mime_id, $index) = explode(':', $mime_id); - - $part = $imap->get_message_part($uid, $mime_id); - $headers = $imap->get_message_headers($uid); - - if ($part->ctype_parameters['charset']) { - $charset = $part->ctype_parameters['charset']; - } - - if ($part) { - $tasks = $this->get_ical()->import($part, $charset); - } - } - - // successfully parsed events? - if (!empty($tasks) && ($task = $tasks[$index])) { - $task = $this->from_ical($task); - - // store the message's sender address for comparisons - $task['_sender'] = preg_match('/([a-z0-9][a-z0-9\-\.\+\_]*@[^&@"\'.][^@&"\']*\\.([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,}))/', $headers->from, $m) ? $m[1] : ''; - $askt['_sender_utf'] = rcube_idn_to_utf8($task['_sender']); - - return $task; - } - - return null; - } - - /** - * Checks if specified message part is a vcalendar data - * - * @param rcube_message_part Part object - * - * @return boolean True if part is of type vcard - * - * @todo move to libcalendaring - */ - private function is_vcalendar($part) - { - return ( - in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) || - // Apple sends files as application/x-any (!?) - ($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename)) - ); - } - /** * Load iCalendar functions */ @@ -1624,7 +1523,9 @@ class tasklist extends rcube_plugin $success = false; // successfully parsed tasks? - if ($task = $this->mail_get_itip_task($mbox, $uid, $mime_id)) { + if ($task = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'task')) { + $task = $this->from_ical($task); + // find writeable list to store the task $list_id = !empty($_REQUEST['_list']) ? rcube_utils::get_input_value('_list', rcube_utils::INPUT_POST) : null; $lists = $this->driver->get_lists();