Skip to content

Commit

Permalink
IMAP SEARCH RETURN PARTIAL support
Browse files Browse the repository at this point in the history
  • Loading branch information
ksmurchison committed Dec 11, 2023
1 parent 0596702 commit 164baea
Show file tree
Hide file tree
Showing 10 changed files with 516 additions and 246 deletions.
128 changes: 128 additions & 0 deletions cassandane/Cassandane/Cyrus/Search.pm
Original file line number Diff line number Diff line change
Expand Up @@ -757,4 +757,132 @@ sub test_uidsearch_empty
$self->assert_str_equals('0', $results[0][3]);
}

sub test_partial
{
my ($self) = @_;

my $imaptalk = $self->{store}->get_client();

xlog $self, "append some messages";
my %exp;
my $N = 10;
for (1..$N)
{
my $msg = $self->make_message("Message $_");
$exp{$_} = $msg;
}
xlog $self, "check the messages got there";
$self->check_messages(\%exp);

# delete the 1st and 6th
$imaptalk->store('1,6', '+FLAGS', '(\\Deleted)');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());

my @results;
my %handlers =
(
esearch => sub
{
my (undef, $esearch) = @_;
push(@results, $esearch);
},
);

# search and return all messages
my $res = $imaptalk->_imap_cmd('SEARCH', 0, \%handlers,
'RETURN', '()', 'UNDELETED');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_str_equals('2:5,7:10', $results[0][2]);

# attempt search with all and partial
@results = ();
$res = $imaptalk->_imap_cmd('SEARCH', 0, \%handlers,
'RETURN', '(ALL PARTIAL 1:2)', 'UNDELETED');
$self->assert_str_equals('bad', $imaptalk->get_last_completion_response());

# search and return first 2 messages
@results = ();
$res = $imaptalk->_imap_cmd('SEARCH', 0, \%handlers,
'RETURN', '(PARTIAL 1:2)', 'UNDELETED');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_str_equals('PARTIAL', $results[0][1]);
$self->assert_str_equals('1:2', $results[0][2][0]);
$self->assert_str_equals('2:3', $results[0][2][1]);

# search and return next 2 messages
@results = ();
$res = $imaptalk->_imap_cmd('SEARCH', 0, \%handlers,
'RETURN', '(PARTIAL 3:4)', 'UNDELETED');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_str_equals('PARTIAL', $results[0][1]);
$self->assert_str_equals('3:4', $results[0][2][0]);
$self->assert_str_equals('4:5', $results[0][2][1]);

# flag the last message
$imaptalk->store('10', '+FLAGS', '(\\flagged)');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());

# search and return next 2 messages
@results = ();
$res = $imaptalk->_imap_cmd('SEARCH', 0, \%handlers,
'RETURN', '(PARTIAL 5:6)', 'UNDELETED');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_str_equals('PARTIAL', $results[0][1]);
$self->assert_str_equals('5:6', $results[0][2][0]);
$self->assert_str_equals('7:8', $results[0][2][1]);

# search and return last 2 messages
@results = ();
$res = $imaptalk->_imap_cmd('SEARCH', 0, \%handlers,
'RETURN', '(PARTIAL -1:-2)', 'UNDELETED');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_str_equals('PARTIAL', $results[0][1]);
$self->assert_str_equals('-1:-2', $results[0][2][0]);
$self->assert_str_equals('9:10', $results[0][2][1]);

# search and return the previous 2 messages
@results = ();
$res = $imaptalk->_imap_cmd('SEARCH', 0, \%handlers,
'RETURN', '(PARTIAL -3:-4)', 'UNDELETED');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_str_equals('PARTIAL', $results[0][1]);
$self->assert_str_equals('-3:-4', $results[0][2][0]);
$self->assert_str_equals('7:8', $results[0][2][1]);

# search and return middle 2 messages by UID
@results = ();
$res = $imaptalk->_imap_cmd('SEARCH', 0, \%handlers,
'RETURN', '(PARTIAL 2:3)',
'UID', '4:8', 'UNDELETED');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_str_equals('PARTIAL', $results[0][1]);
$self->assert_str_equals('2:3', $results[0][2][0]);
$self->assert_str_equals('5,7', $results[0][2][1]);

# search and return non-existent messages
@results = ();
$res = $imaptalk->_imap_cmd('SEARCH', 0, \%handlers,
'RETURN', '(PARTIAL 9:10)', 'UNDELETED');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_str_equals('PARTIAL', $results[0][1]);
$self->assert_str_equals('9:10', $results[0][2][0]);
$self->assert_null($results[0][2][1]);

# search and return count, min, max, and partial
@results = ();
$res = $imaptalk->_imap_cmd('SEARCH', 0, \%handlers,
'RETURN', '(MIN MAX COUNT PARTIAL 3:4)',
'UNDELETED');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_str_equals('COUNT', $results[0][1]);
$self->assert_str_equals('8', $results[0][2]);
$self->assert_str_equals('MIN', $results[0][3]);
$self->assert_str_equals('2', $results[0][4]);
$self->assert_str_equals('MAX', $results[0][5]);
$self->assert_str_equals('10', $results[0][6]);
$self->assert_str_equals('PARTIAL', $results[0][7]);
$self->assert_str_equals('3:4', $results[0][8][0]);
$self->assert_str_equals('4:5', $results[0][8][1]);
}

1;
14 changes: 14 additions & 0 deletions changes/next/imap-partial
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

Description:

Adds support for IMAP PARTIAL extension (RFC 9394)


Config changes:

None


Upgrade instructions:

None
4 changes: 4 additions & 0 deletions docsrc/imap/rfc-support.rst
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,10 @@ The following is an inventory of RFCs supported by Cyrus IMAP.

IMAP QUOTA Extension

:rfc:`9394`

IMAP PARTIAL Extension for Paged SEARCH and FETCH

IETF RFC Drafts
===============

Expand Down
36 changes: 22 additions & 14 deletions imap/imapd.c
Original file line number Diff line number Diff line change
Expand Up @@ -6526,7 +6526,7 @@ static void cmd_search(char *tag, char *cmd)
imapd_userisadmin || imapd_userisproxyadmin);

if (searchargs->returnopts & SEARCH_RETURN_SAVE)
client_behavior.did_searchres = 1;
client_behavior.did_searchres = 1;

/* Set FUZZY search according to config and quirks */
static const char *annot = IMAP_ANNOT_NS "search-fuzzy-always";
Expand Down Expand Up @@ -6584,19 +6584,27 @@ static void cmd_search(char *tag, char *cmd)
"%s BAD Please select a mailbox first\r\n", tag);
goto done;
}
if ((searchargs->filter & ~SEARCH_SOURCE_SELECTED) &&
(searchargs->returnopts & SEARCH_RETURN_SAVE)) {
/* RFC 7377: 2.2
* If the server supports the SEARCHRES [RFC5182] extension, then the
* "SAVE" result option is valid only if "selected" is specified or
* defaulted to as the sole mailbox to be searched. If any source
* option other than "selected" is specified, the ESEARCH command MUST
* return a "BAD" result.
*/
prot_printf(imapd_out,
"%s BAD Search results requested for unselected mailbox(es)\r\n",
tag);
goto done;
if (searchargs->filter & ~SEARCH_SOURCE_SELECTED) {
if (searchargs->returnopts & SEARCH_RETURN_SAVE) {
/* RFC 7377: 2.2
* If the server supports the SEARCHRES [RFC5182] extension,
* then the "SAVE" result option is valid only if "selected"
* is specified or defaulted to as the sole mailbox to be
* searched.
* If any source option other than "selected" is specified,
* the ESEARCH command MUST return a "BAD" result.
*/
prot_printf(imapd_out,
"%s BAD Search results requested for unselected mailbox(es)\r\n",
tag);
goto done;
}
if (searchargs->returnopts & SEARCH_RETURN_PARTIAL) {
prot_printf(imapd_out,
"%s NO [CANNOT] Unsupported Search criteria\r\n",
tag);
goto done;
}
}

struct multisearch_rock mrock = {
Expand Down
6 changes: 5 additions & 1 deletion imap/imapd.h
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,8 @@ enum {
SEARCH_RETURN_ALL = (1<<2),
SEARCH_RETURN_COUNT = (1<<3),
SEARCH_RETURN_SAVE = (1<<4), /* RFC 5182 */
SEARCH_RETURN_RELEVANCY = (1<<5) /* RFC 6203 */
SEARCH_RETURN_RELEVANCY = (1<<5), /* RFC 6203 */
SEARCH_RETURN_PARTIAL = (1<<6), /* RFC 9394 */
};

/* Things that may be searched for */
Expand All @@ -255,6 +256,9 @@ struct searchargs {

/* For SEARCHRES */
ptrarray_t result_vars;

/* For PARTIAL */
range_t partial;
};

/* Windowing arguments for the XCONVSORT command */
Expand Down
31 changes: 29 additions & 2 deletions imap/imapparse.c
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,7 @@ EXPORTED int get_search_return_opts(struct protstream *pin,
struct searchargs *searchargs)
{
int c;
static struct buf opt;
static struct buf opt, arg;

c = prot_getc(pin);
if (c != '(') {
Expand Down Expand Up @@ -740,6 +740,21 @@ EXPORTED int get_search_return_opts(struct protstream *pin,
else if (!strcmp(opt.s, "relevancy")) { /* RFC 6203 */
searchargs->returnopts |= SEARCH_RETURN_RELEVANCY;
}
else if (!strcmp(opt.s, "partial")) { /* RFC 9394 */
int r = -1;

if (c == ' ') {
c = getword(pin, &arg);
r = imparse_range(arg.s, &searchargs->partial);
}
if (r) {
prot_printf(pout, "%s BAD Invalid range in Search\r\n",
searchargs->tag);
goto bad;
}

searchargs->returnopts |= SEARCH_RETURN_PARTIAL;
}
else {
prot_printf(pout,
"%s BAD Invalid Search return option %s\r\n",
Expand All @@ -749,14 +764,26 @@ EXPORTED int get_search_return_opts(struct protstream *pin,

} while (c == ' ');

if (!(searchargs->returnopts & ~(SEARCH_RETURN_SAVE|SEARCH_RETURN_RELEVANCY))) {
if (!(searchargs->returnopts & ~(SEARCH_RETURN_SAVE | SEARCH_RETURN_RELEVANCY))) {
/* RFC 4731:
* If the list of result options is empty, that requests the server to
* return an ESEARCH response instead of the SEARCH response. This is
* equivalent to "(ALL)".
*/
searchargs->returnopts |= SEARCH_RETURN_ALL;
}
else if ((searchargs->returnopts & SEARCH_RETURN_ALL) &&
(searchargs->returnopts & SEARCH_RETURN_PARTIAL)) {
/* RFC 9394, Section 3.1:
* A single command MUST NOT contain more than one PARTIAL or ALL
* search return option; that is, either one PARTIAL, one ALL,
* or neither PARTIAL nor ALL is allowed.
*/
prot_printf(pout,
"%s BAD Invalid return options in Search\r\n",
searchargs->tag);
goto bad;
}

if (c != ')') {
prot_printf(pout,
Expand Down
Loading

0 comments on commit 164baea

Please sign in to comment.