Skip to content

Commit 7fd5c90

Browse files
committed
Start of IMAP PARTIAL support
1 parent c89f215 commit 7fd5c90

File tree

9 files changed

+344
-24
lines changed

9 files changed

+344
-24
lines changed

Diff for: cassandane/Cassandane/Cyrus/Fetch.pm

+68
Original file line numberDiff line numberDiff line change
@@ -1066,4 +1066,72 @@ sub test_unknown_cte
10661066
$self->assert_matches(qr{UNKNOWN-CTE}, $imaptalk->get_last_error());
10671067
}
10681068

1069+
sub test_partial
1070+
{
1071+
my ($self) = @_;
1072+
1073+
my $imaptalk = $self->{store}->get_client();
1074+
1075+
xlog $self, "append some messages";
1076+
my %exp;
1077+
my $N = 10;
1078+
for (1..$N)
1079+
{
1080+
my $msg = $self->make_message("Message $_");
1081+
$exp{$_} = $msg;
1082+
}
1083+
xlog $self, "check the messages got there";
1084+
$self->check_messages(\%exp);
1085+
1086+
# expunge the 1st and 6th
1087+
$imaptalk->store('1,6', '+FLAGS', '(\\Deleted)');
1088+
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
1089+
$imaptalk->expunge();
1090+
1091+
# fetch all
1092+
my $res = $imaptalk->fetch('1:*', '(UID)');
1093+
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
1094+
$self->assert_str_equals($res->{'1'}->{uid}, "2");
1095+
$self->assert_str_equals($res->{'2'}->{uid}, "3");
1096+
$self->assert_str_equals($res->{'3'}->{uid}, "4");
1097+
$self->assert_str_equals($res->{'4'}->{uid}, "5");
1098+
$self->assert_str_equals($res->{'5'}->{uid}, "7");
1099+
$self->assert_str_equals($res->{'6'}->{uid}, "8");
1100+
$self->assert_str_equals($res->{'7'}->{uid}, "9");
1101+
$self->assert_str_equals($res->{'8'}->{uid}, "10");
1102+
1103+
# fetch first 2
1104+
$res = $imaptalk->fetch('1:*', '(UID) (PARTIAL 1:2)');
1105+
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
1106+
$self->assert_str_equals($res->{'1'}->{uid}, "2");
1107+
$self->assert_str_equals($res->{'2'}->{uid}, "3");
1108+
1109+
# fetch next 2
1110+
$res = $imaptalk->fetch('1:*', '(UID) (PARTIAL 3:4)');
1111+
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
1112+
$self->assert_str_equals($res->{'3'}->{uid}, "4");
1113+
$self->assert_str_equals($res->{'4'}->{uid}, "5");
1114+
1115+
# fetch last 2
1116+
$res = $imaptalk->fetch('1:*', '(UID) (PARTIAL -1:-2)');
1117+
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
1118+
$self->assert_str_equals($res->{'8'}->{uid}, "10");
1119+
$self->assert_str_equals($res->{'7'}->{uid}, "9");
1120+
1121+
# fetch the previous 2
1122+
$res = $imaptalk->fetch('1:*', '(UID) (PARTIAL -3:-4)');
1123+
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
1124+
$self->assert_str_equals($res->{'6'}->{uid}, "8");
1125+
$self->assert_str_equals($res->{'5'}->{uid}, "7");
1126+
1127+
# enable UID mode...
1128+
$imaptalk->uid(1);
1129+
1130+
# fetch the middle 2 by UID
1131+
$res = $imaptalk->fetch('4:8', '(UID) (PARTIAL 2:3)');
1132+
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
1133+
$self->assert_str_equals($res->{'5'}->{uid}, "5");
1134+
$self->assert_str_equals($res->{'7'}->{uid}, "7");
1135+
}
1136+
10691137
1;

Diff for: cassandane/Cassandane/Cyrus/Search.pm

+99
Original file line numberDiff line numberDiff line change
@@ -757,4 +757,103 @@ sub test_uidsearch_empty
757757
$self->assert_str_equals('0', $results[0][3]);
758758
}
759759

760+
sub test_partial
761+
{
762+
my ($self) = @_;
763+
764+
my $imaptalk = $self->{store}->get_client();
765+
766+
xlog $self, "append some messages";
767+
my %exp;
768+
my $N = 10;
769+
for (1..$N)
770+
{
771+
my $msg = $self->make_message("Message $_");
772+
$exp{$_} = $msg;
773+
}
774+
xlog $self, "check the messages got there";
775+
$self->check_messages(\%exp);
776+
777+
# delete the 1st and 6th
778+
$imaptalk->store('1,6', '+FLAGS', '(\\Deleted)');
779+
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
780+
781+
my @results;
782+
my %handlers =
783+
(
784+
esearch => sub
785+
{
786+
my (undef, $esearch) = @_;
787+
push(@results, $esearch);
788+
},
789+
);
790+
791+
# search and return all messages
792+
my $res = $imaptalk->_imap_cmd('SEARCH', 0, \%handlers,
793+
'RETURN', '()', 'UNDELETED');
794+
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
795+
$self->assert_str_equals('2:5,7:10', $results[0][2]);
796+
797+
# attempt search with all and partial
798+
@results = ();
799+
$res = $imaptalk->_imap_cmd('SEARCH', 0, \%handlers,
800+
'RETURN', '(ALL PARTIAL 1:2)', 'UNDELETED');
801+
$self->assert_str_equals('bad', $imaptalk->get_last_completion_response());
802+
803+
# search and return first 2 messages
804+
@results = ();
805+
$res = $imaptalk->_imap_cmd('SEARCH', 0, \%handlers,
806+
'RETURN', '(PARTIAL 1:2)', 'UNDELETED');
807+
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
808+
$self->assert_str_equals('PARTIAL', $results[0][1]);
809+
$self->assert_str_equals('1:2', $results[0][2][0]);
810+
$self->assert_str_equals('2:3', $results[0][2][1]);
811+
812+
# search and return next 2 messages
813+
@results = ();
814+
$res = $imaptalk->_imap_cmd('SEARCH', 0, \%handlers,
815+
'RETURN', '(PARTIAL 3:4)', 'UNDELETED');
816+
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
817+
$self->assert_str_equals('PARTIAL', $results[0][1]);
818+
$self->assert_str_equals('3:4', $results[0][2][0]);
819+
$self->assert_str_equals('4:5', $results[0][2][1]);
820+
821+
# search and return last 2 messages
822+
@results = ();
823+
$res = $imaptalk->_imap_cmd('SEARCH', 0, \%handlers,
824+
'RETURN', '(PARTIAL -1:-2)', 'UNDELETED');
825+
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
826+
$self->assert_str_equals('PARTIAL', $results[0][1]);
827+
$self->assert_str_equals('-1:-2', $results[0][2][0]);
828+
$self->assert_str_equals('9:10', $results[0][2][1]);
829+
830+
# search and return the previous 2 messages
831+
@results = ();
832+
$res = $imaptalk->_imap_cmd('SEARCH', 0, \%handlers,
833+
'RETURN', '(PARTIAL -3:-4)', 'UNDELETED');
834+
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
835+
$self->assert_str_equals('PARTIAL', $results[0][1]);
836+
$self->assert_str_equals('-3:-4', $results[0][2][0]);
837+
$self->assert_str_equals('7:8', $results[0][2][1]);
838+
839+
# search and return middle 2 messages by UID
840+
@results = ();
841+
$res = $imaptalk->_imap_cmd('SEARCH', 0, \%handlers,
842+
'RETURN', '(PARTIAL 2:3)',
843+
'UID', '4:8', 'UNDELETED');
844+
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
845+
$self->assert_str_equals('PARTIAL', $results[0][1]);
846+
$self->assert_str_equals('2:3', $results[0][2][0]);
847+
$self->assert_str_equals('5,7', $results[0][2][1]);
848+
849+
# search and return non-existent messages
850+
@results = ();
851+
$res = $imaptalk->_imap_cmd('SEARCH', 0, \%handlers,
852+
'RETURN', '(PARTIAL 9:10)', 'UNDELETED');
853+
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
854+
$self->assert_str_equals('PARTIAL', $results[0][1]);
855+
$self->assert_str_equals('9:10', $results[0][2][0]);
856+
$self->assert_null($results[0][2][1]);
857+
}
858+
760859
1;

Diff for: imap/imapd.c

+49-15
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ static struct capa_struct base_capabilities[] = {
436436
{ "NOTIFY", CAPA_POSTAUTH|CAPA_STATE, /* RFC 5465 */
437437
{ .statep = &imapd_notify_enabled } },
438438
{ "OBJECTID", CAPA_POSTAUTH, { 0 } }, /* RFC 8474 */
439-
{ "PARTIAL", 0, /* not implemented */ { 0 } }, /* RFC 9394 */
439+
{ "PARTIAL", CAPA_POSTAUTH, { 0 } }, /* RFC 9394 */
440440
{ "PREVIEW", CAPA_POSTAUTH, { 0 } }, /* RFC 8970 */
441441
{ "QRESYNC", CAPA_POSTAUTH, { 0 } }, /* RFC 7162 */
442442
{ "QUOTA", CAPA_POSTAUTH, { 0 } }, /* RFC 9208 */
@@ -4983,6 +4983,8 @@ static int parse_fetch_args(const char *tag, const char *cmd,
49834983
struct octetinfo oi;
49844984
strarray_t *newfields = strarray_new();
49854985

4986+
fa->partial.high = ULONG_MAX;
4987+
49864988
c = getword(imapd_in, &fetchatt);
49874989
if (c == '(' && !fetchatt.s[0]) {
49884990
inlist = 1;
@@ -5469,6 +5471,19 @@ static int parse_fetch_args(const char *tag, const char *cmd,
54695471
!strcmp(fetchatt.s, "VANISHED")) {
54705472
fa->vanished = 1;
54715473
}
5474+
else if (!strcmp(fetchatt.s, "PARTIAL")) { /* RFC 9394 */
5475+
int r = -1;
5476+
5477+
if (c == ' ') {
5478+
c = getword(imapd_in, &fieldname);
5479+
r = imparse_range(fieldname.s, &fa->partial);
5480+
}
5481+
if (r) {
5482+
prot_printf(imapd_out, "%s BAD Invalid range in %s\r\n",
5483+
tag, cmd);
5484+
goto freeargs;
5485+
}
5486+
}
54725487
else {
54735488
prot_printf(imapd_out, "%s BAD Invalid %s modifier %s\r\n",
54745489
tag, cmd, fetchatt.s);
@@ -6487,7 +6502,7 @@ static void cmd_search(char *tag, char *cmd)
64876502
imapd_userisadmin || imapd_userisproxyadmin);
64886503

64896504
if (searchargs->returnopts & SEARCH_RETURN_SAVE)
6490-
client_behavior.did_searchres = 1;
6505+
client_behavior.did_searchres = 1;
64916506

64926507
/* Set FUZZY search according to config and quirks */
64936508
static const char *annot = IMAP_ANNOT_NS "search-fuzzy-always";
@@ -6531,6 +6546,17 @@ static void cmd_search(char *tag, char *cmd)
65316546
goto done;
65326547
}
65336548

6549+
switch (searchargs->returnopts & ~(SEARCH_RETURN_SAVE|SEARCH_RETURN_RELEVANCY)) {
6550+
case SEARCH_RETURN_MAX:
6551+
searchargs->partial.is_last = 1;
6552+
6553+
GCC_FALLTHROUGH
6554+
6555+
case SEARCH_RETURN_MIN:
6556+
searchargs->partial.low = searchargs->partial.high = 1;
6557+
break;
6558+
}
6559+
65346560
// this refreshes the index, we may be looking at it in our search
65356561
imapd_check(NULL, 0);
65366562

@@ -6545,19 +6571,27 @@ static void cmd_search(char *tag, char *cmd)
65456571
"%s BAD Please select a mailbox first\r\n", tag);
65466572
goto done;
65476573
}
6548-
if ((searchargs->filter & ~SEARCH_SOURCE_SELECTED) &&
6549-
(searchargs->returnopts & SEARCH_RETURN_SAVE)) {
6550-
/* RFC 7377: 2.2
6551-
* If the server supports the SEARCHRES [RFC5182] extension, then the
6552-
* "SAVE" result option is valid only if "selected" is specified or
6553-
* defaulted to as the sole mailbox to be searched. If any source
6554-
* option other than "selected" is specified, the ESEARCH command MUST
6555-
* return a "BAD" result.
6556-
*/
6557-
prot_printf(imapd_out,
6558-
"%s BAD Search results requested for unselected mailbox(es)\r\n",
6559-
tag);
6560-
goto done;
6574+
if (searchargs->filter & ~SEARCH_SOURCE_SELECTED) {
6575+
if (searchargs->returnopts & SEARCH_RETURN_SAVE) {
6576+
/* RFC 7377: 2.2
6577+
* If the server supports the SEARCHRES [RFC5182] extension,
6578+
* then the "SAVE" result option is valid only if "selected"
6579+
* is specified or defaulted to as the sole mailbox to be
6580+
* searched.
6581+
* If any source option other than "selected" is specified,
6582+
* the ESEARCH command MUST return a "BAD" result.
6583+
*/
6584+
prot_printf(imapd_out,
6585+
"%s BAD Search results requested for unselected mailbox(es)\r\n",
6586+
tag);
6587+
goto done;
6588+
}
6589+
if (searchargs->returnopts & SEARCH_RETURN_PARTIAL) {
6590+
prot_printf(imapd_out,
6591+
"%s NO [CANNOT] Unsupported Search criteria\r\n",
6592+
tag);
6593+
goto done;
6594+
}
65616595
}
65626596

65636597
struct multisearch_rock mrock = {

Diff for: imap/imapd.h

+8-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
#include "annotate.h"
4747
#include "bufarray.h"
4848
#include "hash.h"
49+
#include "imparse.h"
4950
#include "mailbox.h"
5051
#include "message.h"
5152
#include "prot.h"
@@ -107,6 +108,8 @@ struct fetchargs {
107108
struct auth_state *authstate;
108109
hash_table *cidhash; /* for XCONVFETCH */
109110
struct conversations_state *convstate; /* for FETCH_MAILBOXIDS */
111+
112+
range_t partial; /* For PARTIAL */
110113
};
111114

112115
/* Bitmasks for fetchitems */
@@ -225,7 +228,8 @@ enum {
225228
SEARCH_RETURN_ALL = (1<<2),
226229
SEARCH_RETURN_COUNT = (1<<3),
227230
SEARCH_RETURN_SAVE = (1<<4), /* RFC 5182 */
228-
SEARCH_RETURN_RELEVANCY = (1<<5) /* RFC 6203 */
231+
SEARCH_RETURN_RELEVANCY = (1<<5), /* RFC 6203 */
232+
SEARCH_RETURN_PARTIAL = (1<<6), /* RFC 9394 */
229233
};
230234

231235
/* Things that may be searched for */
@@ -252,6 +256,9 @@ struct searchargs {
252256

253257
/* For SEARCHRES */
254258
ptrarray_t result_vars;
259+
260+
/* For PARTIAL */
261+
range_t partial;
255262
};
256263

257264
/* Windowing arguments for the XCONVSORT command */

Diff for: imap/imapparse.c

+30-1
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,7 @@ EXPORTED int get_search_return_opts(struct protstream *pin,
708708
struct searchargs *searchargs)
709709
{
710710
int c;
711-
static struct buf opt;
711+
static struct buf opt, arg;
712712

713713
c = prot_getc(pin);
714714
if (c != '(') {
@@ -740,6 +740,21 @@ EXPORTED int get_search_return_opts(struct protstream *pin,
740740
else if (!strcmp(opt.s, "relevancy")) { /* RFC 6203 */
741741
searchargs->returnopts |= SEARCH_RETURN_RELEVANCY;
742742
}
743+
else if (!strcmp(opt.s, "partial")) { /* RFC 9394 */
744+
int r = -1;
745+
746+
if (c == ' ') {
747+
c = getword(pin, &arg);
748+
r = imparse_range(arg.s, &searchargs->partial);
749+
}
750+
if (r) {
751+
prot_printf(pout, "%s BAD Invalid range in Search\r\n",
752+
searchargs->tag);
753+
goto bad;
754+
}
755+
756+
searchargs->returnopts |= SEARCH_RETURN_PARTIAL;
757+
}
743758
else {
744759
prot_printf(pout,
745760
"%s BAD Invalid Search return option %s\r\n",
@@ -757,6 +772,18 @@ EXPORTED int get_search_return_opts(struct protstream *pin,
757772
*/
758773
searchargs->returnopts |= SEARCH_RETURN_ALL;
759774
}
775+
else if (searchargs->partial.low &&
776+
(searchargs->returnopts & SEARCH_RETURN_ALL)) {
777+
/* RFC 9394, Section 3.1:
778+
* A single command MUST NOT contain more than one PARTIAL or ALL
779+
* search return option; that is, either one PARTIAL, one ALL,
780+
* or neither PARTIAL nor ALL is allowed.
781+
*/
782+
prot_printf(pout,
783+
"%s BAD Invalid return options in Search\r\n",
784+
searchargs->tag);
785+
goto bad;
786+
}
760787

761788
if (c != ')') {
762789
prot_printf(pout,
@@ -1589,6 +1616,8 @@ EXPORTED int get_search_program(struct protstream *pin,
15891616
int c;
15901617

15911618
searchargs->root = search_expr_new(NULL, SEOP_AND);
1619+
searchargs->partial.high = ULONG_MAX;
1620+
15921621

15931622
do {
15941623
c = get_search_criterion(pin, pout, searchargs->root, searchargs);

0 commit comments

Comments
 (0)