diff --git a/CHANGELOG.md b/CHANGELOG.md index 71103ab2423..ff05a354007 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ - Advanced mail search syntax with more possibilities (without UI) (#8502) - Added an option for a default mail search scope (#9077, #7556) - Added option to define font list and font-size list for HTML editor - available_fonts/available_font_sizes (#5700) -- Support for HAproxy protocol header in IMAP connections (#8625) +- IMAP: Support for HAproxy protocol header in IMAP connections (#8625) +- IMAP: Partial support for ANNOTATE-EXPERIMENT-1 extension (RFC 5257) - Change 'smtp_log' option default value to False - Add 'php' log_driver value (#6138) - Delete messages directly from Junk on folder purge if delete_junk is enabled (#8766) diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php index 7d27c3b5c33..dda979cfc03 100644 --- a/program/lib/Roundcube/rcube_imap.php +++ b/program/lib/Roundcube/rcube_imap.php @@ -1289,7 +1289,7 @@ public function fetch_headers($folder, $msgs, $sort = true, $force = false) } else { // fetch requested headers from server $headers = $this->conn->fetchHeaders( - $folder, $msgs, true, false, $this->get_fetch_headers()); + $folder, $msgs, true, false, $this->get_fetch_headers(), $this->get_fetch_items()); } if (empty($headers)) { @@ -1877,7 +1877,7 @@ public function get_message_headers($uid, $folder = null, $force = false) $headers = false; } else { $headers = $this->conn->fetchHeader( - $folder, $uid, true, true, $this->get_fetch_headers()); + $folder, $uid, true, true, $this->get_fetch_headers(), $this->get_fetch_items()); if (is_object($headers)) { $headers->folder = $folder; @@ -2788,6 +2788,31 @@ public function expunge_message($uids, $folder = null, $clear_cache = true) return $result; } + /** + * Annotate a message. + * + * @param array $annotation Message annotation key-value array + * @param mixed $uids Message UIDs as array or comma-separated string, or '*' + * @param string $folder Folder name + * + * @return bool True on success, False on failure + */ + #[Override] + public function annotate_message($annotation, $uids, $folder = null) + { + [$uids] = $this->parse_uids($uids); + + if (!is_string($folder) || !strlen($folder)) { + $folder = $this->folder; + } + + if (!$this->check_connection()) { + return false; + } + + return $this->conn->storeMessageAnnotation($folder, $uids, $annotation); + } + /* -------------------------------- * folder management * --------------------------------*/ @@ -3833,6 +3858,16 @@ protected function get_fetch_headers() return $headers; } + /** + * Get additional FETCH items for rcube_imap_generic::fetchHeader(s) + * + * @return array List of items + */ + protected function get_fetch_items() + { + return $this->options['fetch_items'] ?? []; + } + /* ----------------------------------------- * ACL and METADATA/ANNOTATEMORE methods * ----------------------------------------*/ diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php index ecb1f2f12e7..6af4878bbd2 100644 --- a/program/lib/Roundcube/rcube_imap_generic.php +++ b/program/lib/Roundcube/rcube_imap_generic.php @@ -2493,10 +2493,10 @@ public function fetch($mailbox, $message_set, $is_uid = false, $query_items = [] $result[$id]->id = $id; $result[$id]->subject = ''; $result[$id]->messageID = 'mid:' . $id; + $result[$id]->folder = $mailbox; $headers = null; $line = substr($line, strlen($m[0]) + 2); - // Tokenize response and assign to object properties while (($tokens = $this->tokenizeResponse($line, 2)) && count($tokens) == 2) { [$name, $value] = $tokens; @@ -2519,6 +2519,20 @@ public function fetch($mailbox, $message_set, $is_uid = false, $query_items = [] $result[$id]->flags[$flag] = true; } } + } elseif ($name == 'ANNOTATION') { + $result[$id]->annotations = []; + if (!empty($value) && is_array($value)) { + $n = 0; + while (!empty($value[$n]) && is_string($value[$n])) { + $name = $value[$n++]; + $list = $value[$n++]; + $result[$id]->annotations[$name] = []; + $c = 0; + while (!empty($list[$c]) && is_string($list[$c])) { + $result[$id]->annotations[$name][$list[$c++]] = $list[$c++]; + } + } + } } elseif ($name == 'MODSEQ') { $result[$id]->modseq = $value[0]; } elseif ($name == 'ENVELOPE') { @@ -2652,13 +2666,14 @@ public function fetch($mailbox, $message_set, $is_uid = false, $query_items = [] * @param mixed $message_set Message(s) sequence identifier(s) or UID(s) * @param bool $is_uid True if $message_set contains UIDs * @param bool $bodystr Enable to add BODYSTRUCTURE data to the result - * @param array $add_headers List of additional headers + * @param array $add_headers List of additional headers to fetch + * @param array $query_items List of additional items to fetch * * @return bool|array List of rcube_message_header elements, False on error */ - public function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add_headers = []) + public function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add_headers = [], $query_items = []) { - $query_items = ['UID', 'RFC822.SIZE', 'FLAGS', 'INTERNALDATE']; + $query_items = array_unique(array_merge($query_items, ['UID', 'RFC822.SIZE', 'FLAGS', 'INTERNALDATE'])); $headers = ['DATE', 'FROM', 'TO', 'SUBJECT', 'CONTENT-TYPE', 'CC', 'REPLY-TO', 'LIST-POST', 'DISPOSITION-NOTIFICATION-TO', 'X-PRIORITY']; @@ -2684,12 +2699,13 @@ public function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = * @param bool $is_uid True if $id is an UID * @param bool $bodystr Enable to add BODYSTRUCTURE data to the result * @param array $add_headers List of additional headers + * @param array $query_items List of additional items to fetch * * @return bool|rcube_message_header Message data, False on error */ - public function fetchHeader($mailbox, $id, $is_uid = false, $bodystr = false, $add_headers = []) + public function fetchHeader($mailbox, $id, $is_uid = false, $bodystr = false, $add_headers = [], $query_items = []) { - $a = $this->fetchHeaders($mailbox, $id, $is_uid, $bodystr, $add_headers); + $a = $this->fetchHeaders($mailbox, $id, $is_uid, $bodystr, $add_headers, $query_items); if (is_array($a)) { return array_first($a); @@ -3750,6 +3766,60 @@ public function getAnnotation($mailbox, $entries, $attribs) return null; } + /** + * Send the STORE X ANNOTATION command (RFC5257) + * + * @param string $mailbox Mailbox name + * @param array $entries + * + * @return bool True on success, False on failure + * + * @since 1.6.10 + */ + public function storeMessageAnnotation($mailbox, $uids, $entries) + { + if (!$this->hasCapability('ANNOTATE-EXPERIMENT-1')) { + return false; + } + + if (empty($entries) || empty($uids)) { + $this->setError(self::ERROR_COMMAND, 'Wrong argument for STORE ANNOTATION command'); + return false; + } + + if (!$this->select($mailbox)) { + return false; + } + + /* Example input compatible with rcube_message_header::$annotations: + $entries = [ + '/comment' => [ + 'value.priv' => 'test1', + 'value.shared' => null, + ], + ]; + */ + + $request = []; + foreach ($entries as $name => $annotation) { + if (!empty($annotation)) { + foreach ($annotation as $key => $value) { + $annotation[$key] = $this->escape($key) . ' ' . $this->escape($value, true); + } + $request[] = $this->escape($name); + $request[] = $annotation; + } + } + + $result = $this->execute( + 'UID STORE', + [$this->compressMessageSet($uids), 'ANNOTATION', $request], + self::COMMAND_NORESPONSE + ); + + return $result == self::ERROR_OK; + } + /** * Returns BODYSTRUCTURE for the specified message. * diff --git a/program/lib/Roundcube/rcube_message_header.php b/program/lib/Roundcube/rcube_message_header.php index f79918b0ba2..01045df53bf 100644 --- a/program/lib/Roundcube/rcube_message_header.php +++ b/program/lib/Roundcube/rcube_message_header.php @@ -205,6 +205,13 @@ class rcube_message_header */ public $flags = []; + /** + * Message annotations (RFC 5257) + * + * @var ?array + */ + public $annotations; + /** * Extra flags (for the messages list) *