Skip to content

Commit

Permalink
Start of IMAP PARTIAL support
Browse files Browse the repository at this point in the history
  • Loading branch information
ksmurchison committed Nov 8, 2023
1 parent c89f215 commit 7fd5c90
Show file tree
Hide file tree
Showing 9 changed files with 344 additions and 24 deletions.
68 changes: 68 additions & 0 deletions cassandane/Cassandane/Cyrus/Fetch.pm
Original file line number Diff line number Diff line change
Expand Up @@ -1066,4 +1066,72 @@ sub test_unknown_cte
$self->assert_matches(qr{UNKNOWN-CTE}, $imaptalk->get_last_error());
}

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);

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

# fetch all
my $res = $imaptalk->fetch('1:*', '(UID)');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_str_equals($res->{'1'}->{uid}, "2");
$self->assert_str_equals($res->{'2'}->{uid}, "3");
$self->assert_str_equals($res->{'3'}->{uid}, "4");
$self->assert_str_equals($res->{'4'}->{uid}, "5");
$self->assert_str_equals($res->{'5'}->{uid}, "7");
$self->assert_str_equals($res->{'6'}->{uid}, "8");
$self->assert_str_equals($res->{'7'}->{uid}, "9");
$self->assert_str_equals($res->{'8'}->{uid}, "10");

# fetch first 2
$res = $imaptalk->fetch('1:*', '(UID) (PARTIAL 1:2)');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_str_equals($res->{'1'}->{uid}, "2");
$self->assert_str_equals($res->{'2'}->{uid}, "3");

# fetch next 2
$res = $imaptalk->fetch('1:*', '(UID) (PARTIAL 3:4)');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_str_equals($res->{'3'}->{uid}, "4");
$self->assert_str_equals($res->{'4'}->{uid}, "5");

# fetch last 2
$res = $imaptalk->fetch('1:*', '(UID) (PARTIAL -1:-2)');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_str_equals($res->{'8'}->{uid}, "10");
$self->assert_str_equals($res->{'7'}->{uid}, "9");

# fetch the previous 2
$res = $imaptalk->fetch('1:*', '(UID) (PARTIAL -3:-4)');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_str_equals($res->{'6'}->{uid}, "8");
$self->assert_str_equals($res->{'5'}->{uid}, "7");

# enable UID mode...
$imaptalk->uid(1);

# fetch the middle 2 by UID
$res = $imaptalk->fetch('4:8', '(UID) (PARTIAL 2:3)');
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
$self->assert_str_equals($res->{'5'}->{uid}, "5");
$self->assert_str_equals($res->{'7'}->{uid}, "7");
}

1;
99 changes: 99 additions & 0 deletions cassandane/Cassandane/Cyrus/Search.pm
Original file line number Diff line number Diff line change
Expand Up @@ -757,4 +757,103 @@ 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]);

# 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]);
}

1;
64 changes: 49 additions & 15 deletions imap/imapd.c
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ static struct capa_struct base_capabilities[] = {
{ "NOTIFY", CAPA_POSTAUTH|CAPA_STATE, /* RFC 5465 */
{ .statep = &imapd_notify_enabled } },
{ "OBJECTID", CAPA_POSTAUTH, { 0 } }, /* RFC 8474 */
{ "PARTIAL", 0, /* not implemented */ { 0 } }, /* RFC 9394 */
{ "PARTIAL", CAPA_POSTAUTH, { 0 } }, /* RFC 9394 */
{ "PREVIEW", CAPA_POSTAUTH, { 0 } }, /* RFC 8970 */
{ "QRESYNC", CAPA_POSTAUTH, { 0 } }, /* RFC 7162 */
{ "QUOTA", CAPA_POSTAUTH, { 0 } }, /* RFC 9208 */
Expand Down Expand Up @@ -4983,6 +4983,8 @@ static int parse_fetch_args(const char *tag, const char *cmd,
struct octetinfo oi;
strarray_t *newfields = strarray_new();

fa->partial.high = ULONG_MAX;

c = getword(imapd_in, &fetchatt);
if (c == '(' && !fetchatt.s[0]) {
inlist = 1;
Expand Down Expand Up @@ -5469,6 +5471,19 @@ static int parse_fetch_args(const char *tag, const char *cmd,
!strcmp(fetchatt.s, "VANISHED")) {
fa->vanished = 1;
}
else if (!strcmp(fetchatt.s, "PARTIAL")) { /* RFC 9394 */
int r = -1;

if (c == ' ') {
c = getword(imapd_in, &fieldname);
r = imparse_range(fieldname.s, &fa->partial);
}
if (r) {
prot_printf(imapd_out, "%s BAD Invalid range in %s\r\n",
tag, cmd);
goto freeargs;
}
}
else {
prot_printf(imapd_out, "%s BAD Invalid %s modifier %s\r\n",
tag, cmd, fetchatt.s);
Expand Down Expand Up @@ -6487,7 +6502,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 @@ -6531,6 +6546,17 @@ static void cmd_search(char *tag, char *cmd)
goto done;
}

switch (searchargs->returnopts & ~(SEARCH_RETURN_SAVE|SEARCH_RETURN_RELEVANCY)) {
case SEARCH_RETURN_MAX:
searchargs->partial.is_last = 1;

GCC_FALLTHROUGH

case SEARCH_RETURN_MIN:
searchargs->partial.low = searchargs->partial.high = 1;
break;
}

// this refreshes the index, we may be looking at it in our search
imapd_check(NULL, 0);

Expand All @@ -6545,19 +6571,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
9 changes: 8 additions & 1 deletion imap/imapd.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
#include "annotate.h"
#include "bufarray.h"
#include "hash.h"
#include "imparse.h"
#include "mailbox.h"
#include "message.h"
#include "prot.h"
Expand Down Expand Up @@ -107,6 +108,8 @@ struct fetchargs {
struct auth_state *authstate;
hash_table *cidhash; /* for XCONVFETCH */
struct conversations_state *convstate; /* for FETCH_MAILBOXIDS */

range_t partial; /* For PARTIAL */
};

/* Bitmasks for fetchitems */
Expand Down Expand Up @@ -225,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 @@ -252,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: 30 additions & 1 deletion 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 @@ -757,6 +772,18 @@ EXPORTED int get_search_return_opts(struct protstream *pin,
*/
searchargs->returnopts |= SEARCH_RETURN_ALL;
}
else if (searchargs->partial.low &&
(searchargs->returnopts & SEARCH_RETURN_ALL)) {
/* 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 Expand Up @@ -1589,6 +1616,8 @@ EXPORTED int get_search_program(struct protstream *pin,
int c;

searchargs->root = search_expr_new(NULL, SEOP_AND);
searchargs->partial.high = ULONG_MAX;


do {
c = get_search_criterion(pin, pout, searchargs->root, searchargs);
Expand Down
Loading

0 comments on commit 7fd5c90

Please sign in to comment.