Skip to content

Commit 9bd2257

Browse files
committed
IMAP PARTIAL support
1 parent 9bcdad0 commit 9bd2257

File tree

12 files changed

+636
-254
lines changed

12 files changed

+636
-254
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

+115
Original file line numberDiff line numberDiff line change
@@ -757,4 +757,119 @@ 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+
# search and return count, min, max, and partial
859+
@results = ();
860+
$res = $imaptalk->_imap_cmd('SEARCH', 0, \%handlers,
861+
'RETURN', '(MIN MAX COUNT PARTIAL 3:4)',
862+
'UNDELETED');
863+
$self->assert_str_equals('ok', $imaptalk->get_last_completion_response());
864+
$self->assert_str_equals('COUNT', $results[0][1]);
865+
$self->assert_str_equals('8', $results[0][2]);
866+
$self->assert_str_equals('MIN', $results[0][3]);
867+
$self->assert_str_equals('2', $results[0][4]);
868+
$self->assert_str_equals('MAX', $results[0][5]);
869+
$self->assert_str_equals('10', $results[0][6]);
870+
$self->assert_str_equals('PARTIAL', $results[0][7]);
871+
$self->assert_str_equals('3:4', $results[0][8][0]);
872+
$self->assert_str_equals('4:5', $results[0][8][1]);
873+
}
874+
760875
1;

Diff for: changes/next/imap-partial

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
Description:
3+
4+
Adds support for IMAP PARTIAL extension (RFC 9394)
5+
6+
7+
Config changes:
8+
9+
None
10+
11+
12+
Upgrade instructions:
13+
14+
None

Diff for: docsrc/imap/rfc-support.rst

+4
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,10 @@ The following is an inventory of RFCs supported by Cyrus IMAP.
898898

899899
IMAP QUOTA Extension
900900

901+
:rfc:`9394`
902+
903+
IMAP PARTIAL Extension for Paged SEARCH and FETCH
904+
901905
IETF RFC Drafts
902906
===============
903907

Diff for: imap/imapd.c

+45-18
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ static struct capa_struct base_capabilities[] = {
437437
{ "NOTIFY", CAPA_POSTAUTH|CAPA_STATE, /* RFC 5465 */
438438
{ .statep = &imapd_notify_enabled } },
439439
{ "OBJECTID", CAPA_POSTAUTH, { 0 } }, /* RFC 8474 */
440-
{ "PARTIAL", 0, /* not implemented */ { 0 } }, /* RFC 9394 */
440+
{ "PARTIAL", CAPA_POSTAUTH, { 0 } }, /* RFC 9394 */
441441
{ "PREVIEW", CAPA_POSTAUTH, { 0 } }, /* RFC 8970 */
442442
{ "QRESYNC", CAPA_POSTAUTH, { 0 } }, /* RFC 7162 */
443443
{ "QUOTA", CAPA_POSTAUTH, { 0 } }, /* RFC 9208 */
@@ -889,7 +889,7 @@ static void imapd_log_client_behavior(void)
889889
"%s%s%s%s"
890890
"%s%s%s%s"
891891
"%s%s%s%s"
892-
"%s",
892+
"%s%s",
893893

894894
session_id(),
895895
imapd_userid ? imapd_userid : "",
@@ -908,13 +908,14 @@ static void imapd_log_client_behavior(void)
908908
client_behavior.did_move ? " move=<1>" : "",
909909
client_behavior.did_multisearch ? " multisearch=<1>" : "",
910910
client_behavior.did_notify ? " notify=<1>" : "",
911-
client_behavior.did_preview ? " preview=<1>" : "",
911+
client_behavior.did_partial ? " partial=<1>" : "",
912912

913+
client_behavior.did_preview ? " preview=<1>" : "",
913914
client_behavior.did_qresync ? " qresync=<1>" : "",
914915
client_behavior.did_replace ? " replace=<1>" : "",
915916
client_behavior.did_savedate ? " savedate=<1>" : "",
916-
client_behavior.did_searchres ? " searchres=<1>" : "",
917917

918+
client_behavior.did_searchres ? " searchres=<1>" : "",
918919
client_behavior.did_uidonly ? " uidonly=<1>" : "");
919920
}
920921

@@ -5001,6 +5002,8 @@ static int parse_fetch_args(const char *tag, const char *cmd,
50015002
struct octetinfo oi;
50025003
strarray_t *newfields = strarray_new();
50035004

5005+
fa->partial.high = ULONG_MAX;
5006+
50045007
c = getword(imapd_in, &fetchatt);
50055008
if (c == '(' && !fetchatt.s[0]) {
50065009
inlist = 1;
@@ -5487,6 +5490,19 @@ static int parse_fetch_args(const char *tag, const char *cmd,
54875490
!strcmp(fetchatt.s, "VANISHED")) {
54885491
fa->vanished = 1;
54895492
}
5493+
else if (!strcmp(fetchatt.s, "PARTIAL")) { /* RFC 9394 */
5494+
int r = -1;
5495+
5496+
if (c == ' ') {
5497+
c = getword(imapd_in, &fieldname);
5498+
r = imparse_range(fieldname.s, &fa->partial);
5499+
}
5500+
if (r) {
5501+
prot_printf(imapd_out, "%s BAD Invalid range in %s\r\n",
5502+
tag, cmd);
5503+
goto freeargs;
5504+
}
5505+
}
54905506
else {
54915507
prot_printf(imapd_out, "%s BAD Invalid %s modifier %s\r\n",
54925508
tag, cmd, fetchatt.s);
@@ -5619,6 +5635,9 @@ static void cmd_fetch(char *tag, char *sequence, int usinguid)
56195635
if (fetchargs.binsections || fetchargs.sizesections)
56205636
client_behavior.did_binary = 1;
56215637

5638+
if (fetchargs.partial.low)
5639+
client_behavior.did_partial = 1;
5640+
56225641
r = index_fetch(imapd_index, sequence, usinguid, &fetchargs,
56235642
&fetchedsomething);
56245643

@@ -6507,7 +6526,7 @@ static void cmd_search(char *tag, char *cmd)
65076526
imapd_userisadmin || imapd_userisproxyadmin);
65086527

65096528
if (searchargs->returnopts & SEARCH_RETURN_SAVE)
6510-
client_behavior.did_searchres = 1;
6529+
client_behavior.did_searchres = 1;
65116530

65126531
/* Set FUZZY search according to config and quirks */
65136532
static const char *annot = IMAP_ANNOT_NS "search-fuzzy-always";
@@ -6565,19 +6584,27 @@ static void cmd_search(char *tag, char *cmd)
65656584
"%s BAD Please select a mailbox first\r\n", tag);
65666585
goto done;
65676586
}
6568-
if ((searchargs->filter & ~SEARCH_SOURCE_SELECTED) &&
6569-
(searchargs->returnopts & SEARCH_RETURN_SAVE)) {
6570-
/* RFC 7377: 2.2
6571-
* If the server supports the SEARCHRES [RFC5182] extension, then the
6572-
* "SAVE" result option is valid only if "selected" is specified or
6573-
* defaulted to as the sole mailbox to be searched. If any source
6574-
* option other than "selected" is specified, the ESEARCH command MUST
6575-
* return a "BAD" result.
6576-
*/
6577-
prot_printf(imapd_out,
6578-
"%s BAD Search results requested for unselected mailbox(es)\r\n",
6579-
tag);
6580-
goto done;
6587+
if (searchargs->filter & ~SEARCH_SOURCE_SELECTED) {
6588+
if (searchargs->returnopts & SEARCH_RETURN_SAVE) {
6589+
/* RFC 7377: 2.2
6590+
* If the server supports the SEARCHRES [RFC5182] extension,
6591+
* then the "SAVE" result option is valid only if "selected"
6592+
* is specified or defaulted to as the sole mailbox to be
6593+
* searched.
6594+
* If any source option other than "selected" is specified,
6595+
* the ESEARCH command MUST return a "BAD" result.
6596+
*/
6597+
prot_printf(imapd_out,
6598+
"%s BAD Search results requested for unselected mailbox(es)\r\n",
6599+
tag);
6600+
goto done;
6601+
}
6602+
if (searchargs->returnopts & SEARCH_RETURN_PARTIAL) {
6603+
prot_printf(imapd_out,
6604+
"%s NO [CANNOT] Unsupported Search criteria\r\n",
6605+
tag);
6606+
goto done;
6607+
}
65816608
}
65826609

65836610
struct multisearch_rock mrock = {

0 commit comments

Comments
 (0)