diff --git a/modules/imap/hm-jmap.php b/modules/imap/hm-jmap.php index 62440e5679..440be0451c 100644 --- a/modules/imap/hm-jmap.php +++ b/modules/imap/hm-jmap.php @@ -9,12 +9,14 @@ /** * public interface to JMAP commands * @subpackage imap/lib - */ -class Hm_JMAP { + */ +class Hm_JMAP extends Hm_JMAP_Email { private $api; private $session; private $api_url; + private $download_url; + private $upload_url; private $account_id; private $headers; private $delim = '.'; @@ -22,6 +24,11 @@ class Hm_JMAP { private $requests = array(); private $responses = array(); private $folder_list = array(); + private $streaming_msg = ''; + private $msg_part_id = 0; + private $append_mailbox = false; + private $append_seen = false; + private $append_result = false; private $sorts = array( 'ARRIVAL' => 'receivedAt', 'FROM' => 'from', @@ -49,33 +56,139 @@ class Hm_JMAP { /** * PUBLIC INTERFACE */ - + public function __construct() { $this->api = new Hm_API_Curl(); } - - public function create_mailbox($mailbox) { + + public function get_mailbox_status($mailbox, $args=array('UNSEEN', 'UIDVALIDITY', 'UIDNEXT', 'MESSAGES', 'RECENT')) { } - public function delete_mailbox($mailbox) { + public function append_start($mailbox, $size, $seen=true) { + $this->append_mailbox = $mailbox; + $this->append_seen = $seen; + return true; } - public function start_message_stream($uid, $message_part) { + public function append_feed($string) { + $upload_url = str_replace('{accountId}', $this->account_id, $this->upload_url); + $res = $this->send_command($upload_url, array(), 'POST', $string, array(2 => 'Content-Type: multipart/form-data')); + if (!is_array($res) || !array_key_exists('blobId', $res)) { + return false; + } + $blob_id = $res['blobId']; + $methods = array(array( + 'Email/import', + array( + 'accountId' => $this->account_id, + 'emails' => array(1 => array( + 'mailboxIds' => array($this->folder_name_to_id($this->append_mailbox) => true), + 'blobId' => $blob_id + )) + ), + 'af' + )); + /* TODO: figure out blobId issue */ } - public function read_stream_line($size=1024) { + public function append_end() { + $res = $this->append_result; + $this->append_result = false; + $this->append_mailbox = false; + $this->append_seen = false; + return $res; } - public function append_start($mailbox, $size, $seen=true) { + public function start_message_stream($uid, $message_part) { + + $struct = $this->get_message_structure($uid); + $part_struct = $this->search_bodystructure($struct, array('imap_part_number' => $message_part)); + $blob_id = false; + $name = false; + $type = false; + if (is_array($part_struct) && array_key_exists($message_part, $part_struct)) { + if (array_key_exists('blob_id', $part_struct[$message_part])) { + $blob_id = $part_struct[$message_part]['blob_id']; + } + if (array_key_exists('name', $part_struct[$message_part])) { + $name = $part_struct[$message_part]['name']; + } + else { + $name = sprintf('message_%s', $message_part); + } + if (array_key_exists('type', $part_struct[$message_part])) { + $type = $part_struct[$message_part]['type']; + } + } + if (!$name || !$blob_id || !$type) { + return 0; + } + $download_url = str_replace( + array('{accountId}', '{blobId}', '{name}', '{type}'), + array( + urlencode($this->account_id), + urlencode($blob_id), + urlencode($name), + urlencode('application/octet-stream') + ), + $this->download_url + ); + $this->api->format = 'binary'; + $this->streaming_msg = $this->send_command($download_url, array(), 'GET'); + $this->api->format = 'json'; + return strlen($this->streaming_msg); } - public function append_feed($string) { + public function read_stream_line($size=1024) { + $res = $this->streaming_msg; + $this->streaming_msg = false; + return $res; } - public function append_end() { + public function create_mailbox($mailbox) { + list($name, $parent) = $this->split_name_and_parent($mailbox); + $methods = array(array( + 'Mailbox/set', + array( + 'accountId' => $this->account_id, + 'create' => array(NULL => array('parentId' => $parent, 'name' => $name)) + ), + 'cm' + )); + $created = $this->send_and_parse($methods, array(0, 1, 'created'), array()); + $this->reset_folders(); + return $created && count($created) > 0; } - public function get_mailbox_status($mailbox, $args=array('UNSEEN', 'UIDVALIDITY', 'UIDNEXT', 'MESSAGES', 'RECENT')) { + public function delete_mailbox($mailbox) { + $ids = array($this->folder_name_to_id($mailbox)); + $methods = array(array( + 'Mailbox/set', + array( + 'accountId' => $this->account_id, + 'destroy' => $ids + ), + 'dm' + )); + $destroyed = $this->send_and_parse($methods, array(0, 1, 'destroyed'), array()); + $this->reset_folders(); + return $destroyed && count($destroyed) > 0; + } + + public function rename_mailbox($mailbox, $new_mailbox) { + $id = $this->folder_name_to_id($mailbox); + list($name, $parent) = $this->split_name_and_parent($new_mailbox); + $methods = array(array( + 'Mailbox/set', + array( + 'accountId' => $this->account_id, + 'update' => array($id => array('parentId' => $parent, 'name' => $name)) + ), + 'rm' + )); + $updated = $this->send_and_parse($methods, array(0, 1, 'updated'), array()); + $this->reset_folders(); + return $updated && count($updated) > 0; } public function get_state() { @@ -103,15 +216,7 @@ public function get_first_message_part($uid, $type, $subtype=false, $struct=fals $subset = array_slice(array_keys($matches), 0, 1); $msg_part_num = $subset[0]; - $struct = array_slice($matches, 0, 1); - - if (isset($struct[$msg_part_num])) { - $struct = $struct[$msg_part_num]; - } - elseif (isset($struct[0])) { - $struct = $struct[0]; - } - return array($msg_part_num, $this->get_message_content($uid, $msg_part_num, false, $struct)); + return array($msg_part_num, $this->get_message_content($uid, $msg_part_num)); } public function get_mailbox_page($mailbox, $sort, $rev, $filter, $offset=0, $limit=0, $keyword=false) { @@ -134,7 +239,7 @@ public function get_mailbox_page($mailbox, $sort, $rev, $filter, $offset=0, $lim 'limit' => $limit, 'calculateTotal' => true ), - 'gmp' + 'gmp' ) ); $res = $this->send_command($this->api_url, $methods); @@ -149,7 +254,7 @@ public function get_folder_list_by_level($level=false) { $level = ''; } if (count($this->folder_list) == 0) { - $this->folder_list = $this->parse_imap_folders($this->get_folder_list()); + $this->reset_folders(); } return $this->parse_folder_list_by_level($level); } @@ -240,10 +345,9 @@ public function get_message_list($uids) { 'properties' => $flds, 'bodyProperties' => $body ), - 'gml' + 'gml' )); - $res = $this->send_command($this->api_url, $methods); - foreach ($this->search_response($res, array(0, 1, 'list'), array()) as $msg) { + foreach ($this->send_and_parse($methods, array(0, 1, 'list'), array()) as $msg) { $result[] = $this->normalize_headers($msg); } return $result; @@ -255,7 +359,7 @@ public function get_message_structure($uid) { return $converted; } - public function get_message_content($uid, $message_part, $max=false, $struct=true) { + public function get_message_content($uid, $message_part, $max=false, $struct=false) { $methods = array(array( 'Email/get', array( @@ -264,10 +368,9 @@ public function get_message_content($uid, $message_part, $max=false, $struct=tru 'fetchAllBodyValues' => true, 'properties' => array('bodyValues') ), - 'gmc' + 'gmc' )); - $res = $this->send_command($this->api_url, $methods); - return $this->search_response($res, array(0, 1, 'list', 0, 'bodyValues', $message_part, 'value')); + return $this->send_and_parse($methods, array(0, 1, 'list', 0, 'bodyValues', $message_part, 'value')); } public function search($target='ALL', $uids=false, $terms=array(), $esearch=array(), $exclude_deleted=true, $exclude_auto_bcc=true, $only_auto_bcc=false) { @@ -293,8 +396,7 @@ public function search($target='ALL', $uids=false, $terms=array(), $esearch=arra 's' ) ); - $res = $this->send_command($this->api_url, $methods); - return $this->search_response($res, array(0, 1, 'ids'), array()); + return $this->send_and_parse($methods, array(0, 1, 'ids'), array()); } public function get_message_headers($uid, $message_part=false, $raw=false) { @@ -308,39 +410,12 @@ public function get_message_headers($uid, $message_part=false, $raw=false) { 'properties' => $flds, 'bodyProperties' => array() ), - 'gmh' + 'gmh' )); - $res = $this->send_command($this->api_url, $methods); - $headers = $this->search_response($res, array(0, 1, 'list', 0), array()); + $headers = $this->send_and_parse($methods, array(0, 1, 'list', 0), array()); return $this->normalize_headers($headers); } - public function rename_mailbox($mailbox, $new_mailbox) { - $id = $this->folder_name_to_id($mailbox); - $parent = explode($this->delim, $new_mailbox); - $name = array_pop($parent); - $parent = implode($this->delim, $parent); - if (!$parent) { - $parent = NULL; - } - else { - $parent = $this->folder_name_to_id($parent); - } - $methods = array(array( - 'Mailbox/set', - array( - 'accountId' => $this->account_id, - 'update' => array($id => array('parentId' => $parent, 'name' => $name)) - ), - 'ma' - )); - $res = $this->send_command($this->api_url, $methods); - $updated = $this->search_response($res, array(0, 1, 'updated')); - /* TODO: Bust folder list cache */ - //$this->folder_list = $this->parse_imap_folders($this->get_folder_list()); - return count($updated) > 0; - } - public function message_action($action, $uids, $mailbox=false, $keyword=false) { $methods = array(); $key =false; @@ -359,8 +434,7 @@ public function message_action($action, $uids, $mailbox=false, $keyword=false) { if (!$key) { return false; } - $res = $this->send_command($this->api_url, $methods); - $changed_uids = array_keys($this->search_response($res, array(0, 1, $key))); + $changed_uids = array_keys($this->send_and_parse($methods, array(0, 1, $key), array())); return count($changed_uids) == count($uids); } @@ -376,7 +450,7 @@ public function search_bodystructure($struct, $search_flds, $all=true, $res=arra private function move_copy_methods($action, $uids, $mailbox) { /* TODO: this assumes a message can only be in ONE mailbox, other refs will be lost, - * we should switch to "patch to fix this */ + we should switch to "patch" syntax to fix this */ if ($action == 'MOVE') { $mailbox_ids = array('mailboxIds' => array($this->folder_name_to_id($mailbox) => true)); } @@ -395,7 +469,7 @@ private function move_copy_methods($action, $uids, $mailbox) { 'accountId' => $this->account_id, 'update' => $keywords ), - 'ma' + 'ma' )); } @@ -411,7 +485,7 @@ private function modify_msg_methods($action, $uids) { 'accountId' => $this->account_id, 'update' => $keywords ), - 'ma' + 'ma' )); } @@ -422,7 +496,7 @@ private function delete_msg_methods($uids) { 'accountId' => $this->account_id, 'destroy' => $uids ), - 'ma' + 'ma' )); } @@ -434,7 +508,7 @@ private function get_raw_bodystructure($uid) { 'ids' => array($uid), 'properties' => array('bodyStructure') ), - 'gbs' + 'gbs' )); $res = $this->send_command($this->api_url, $methods); return $this->search_response($res, array(0, 1, 'list', 0, 'bodyStructure'), array()); @@ -450,13 +524,19 @@ private function parse_bodystructure_response($data) { } return array($top); } - + private function parse_subs($data) { $res = array(); foreach ($data as $sub) { + if ($sub['partId']) { + $this->msg_part_id = $sub['partId']; + } + else { + $sub['partId'] = $this->msg_part_id + 1; + } $res[$sub['partId']] = $this->translate_struct_keys($sub); if (array_key_exists('subParts', $sub)) { - $res['subs'] = $this->parse_subs($sub['subParts']); + $res[$sub['partId']]['subs'] = $this->parse_subs($sub['subParts']); } } return $res; @@ -465,7 +545,9 @@ private function parse_subs($data) { private function translate_struct_keys($part) { return array( 'type' => explode('/', $part['type'])[0], + 'name' => $part['name'], 'subtype' => explode('/', $part['type'])[1], + 'blob_id' => array_key_exists('blobId', $part) ? $part['blobId'] : false, 'size' => $part['size'], 'attributes' => array('charset' => $part['charset']) ); @@ -501,8 +583,20 @@ private function init_session($data, $url, $port) { $port, $data['apiUrl'] ); + $this->download_url = sprintf( + '%s:%s%s', + preg_replace("/\/$/", '', $url), + $port, + $data['downloadUrl'] + ); + $this->upload_url = sprintf( + '%s:%s%s', + preg_replace("/\/$/", '', $url), + $port, + $data['uploadUrl'] + ); $this->account_id = array_keys($data['accounts'])[0]; - $this->folder_list = $this->parse_imap_folders($this->get_folder_list()); + $this->reset_folders(); } private function keywords_to_flags($keywords) { @@ -526,15 +620,41 @@ private function combine_addresses($addrs) { } return implode(', ', $res); } - private function send_command($url, $methods=array(), $method='POST', $post=array()) { + private function merge_headers($headers) { + $req_headers = $this->headers; + foreach ($headers as $index => $val) { + $req_headers[$index] = $val; + } + return $req_headers; + } + + private function send_command($url, $methods=array(), $method='POST', $post=array(), $headers=array()) { $body = ''; if (count($methods) > 0) { $body = $this->format_request($methods); } - $this->requests[] = array($url, $this->headers, $body, $method, $post); - $res = $this->api->command($url, $this->headers, $post, $body, $method); + $headers = $this->merge_headers($headers); + $this->requests[] = array($url, $headers, $body, $method, $post); + return $this->api->command($url, $headers, $post, $body, $method); + } + + private function search_response($data, $key_path, $default=false) { + array_unshift($key_path, 'methodResponses'); + foreach ($key_path as $key) { + if (is_array($data) && array_key_exists($key, $data)) { + $data = $data[$key]; + } + else { + return $default; + } + } + return $data; + } + + private function send_and_parse($methods, $key_path, $default=false, $method='POST', $post=array()) { + $res = $this->send_command($this->api_url, $methods, $method, $post); $this->responses[] = $res; - return $res; + return $this->search_response($res, $key_path, $default); } private function format_request($methods, $caps=array()) { @@ -561,19 +681,6 @@ private function prep_url($url, $port) { return sprintf('%s:%s/.well-known/jmap/', $url, $port); } - private function search_response($data, $key_path, $default=false) { - array_unshift($key_path, 'methodResponses'); - foreach ($key_path as $key) { - if (is_array($data) && array_key_exists($key, $data)) { - $data = $data[$key]; - } - else { - return $default; - } - } - return $data; - } - private function setup_selected_mailbox($mailbox, $total) { $this->selected_mailbox = array('detail' => array( 'selected' => 1, @@ -648,8 +755,8 @@ private function get_parent_recursive($vals, $lookup, $parents) { } private function folder_name_to_id($name) { - if (count($this->folder_list) == 0) { - $this->folder_list = $this->parse_imap_folders($this->get_folder_list()); + if (count($this->folder_list) == 0 || !array_key_exists($name, $this->folder_list)) { + $this->reset_folders(); } if (array_key_exists($name, $this->folder_list)) { return $this->folder_list[$name]['id']; @@ -657,6 +764,10 @@ private function folder_name_to_id($name) { return false; } + private function reset_folders() { + $this->folder_list = $this->parse_imap_folders($this->get_folder_list()); + } + private function process_imap_search_terms($terms) { $converted_terms = array(); $map = array( @@ -677,4 +788,14 @@ private function process_imap_search_terms($terms) { } return count($converted_terms) > 0 ? $converted_terms : false; } + + private function split_name_and_parent($mailbox) { + $parent = NULL; + $parts = explode($this->delim, $mailbox); + $name = array_pop($parts); + if (count($parts) > 0) { + $parent = $this->folder_name_to_id(implode($this->delim, $parts)); + } + return array($name, $parent); + } }