Skip to content

Commit 0596702

Browse files
committed
IMAP FETCH PARTIAL support
1 parent 9fa0491 commit 0596702

File tree

7 files changed

+221
-8
lines changed

7 files changed

+221
-8
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: cunit/imparse.testc

+59
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,62 @@ static void test_isatom(void)
6262
/* XXX test imparse_issequence() */
6363

6464
/* XXX test imparse_isnumber() */
65+
66+
static void test_parse_range(void)
67+
{
68+
range_t range;
69+
70+
/*
71+
* https://tools.ietf.org/html/rfc9051#name-formal-syntax
72+
*
73+
* nz-number = digit-nz *DIGIT
74+
* ; Non-zero unsigned 32-bit integer
75+
* ; (0 < n < 4,294,967,296)
76+
*
77+
*
78+
* https://tools.ietf.org/html/rfc9394#name-formal-syntax
79+
*
80+
* MINUS = "-"
81+
*
82+
* partial-range-first = nz-number ":" nz-number
83+
* ;; Request to search from oldest (lowest UIDs) to
84+
* ;; more recent messages.
85+
* ;; A range 500:400 is the same as 400:500.
86+
* ;; This is similar to <seq-range> from [RFC3501]
87+
* ;; but cannot contain "*".
88+
*
89+
* partial-range-last = MINUS nz-number ":" MINUS nz-number
90+
* ;; Request to search from newest (highest UIDs) to
91+
* ;; oldest messages.
92+
* ;; A range -500:-400 is the same as -400:-500.
93+
*
94+
* partial-range = partial-range-first / partial-range-last
95+
*/
96+
97+
CU_ASSERT_EQUAL(imparse_range("1:1", &range), 0);
98+
CU_ASSERT_EQUAL(imparse_range("1:2", &range), 0);
99+
CU_ASSERT_EQUAL(imparse_range("2:1", &range), 0);
100+
CU_ASSERT_EQUAL(imparse_range("-1:-2", &range), 0);
101+
CU_ASSERT_EQUAL(imparse_range("-2:-1", &range), 0);
102+
103+
CU_ASSERT_EQUAL(imparse_range("1:-2", &range), 0);
104+
CU_ASSERT_EQUAL(imparse_range("-1:2", &range), 0);
105+
106+
CU_ASSERT_NOT_EQUAL(imparse_range("0:1", &range), 0);
107+
CU_ASSERT_NOT_EQUAL(imparse_range("--1:-2", &range), 0);
108+
CU_ASSERT_NOT_EQUAL(imparse_range("+1:-2", &range), 0);
109+
110+
CU_ASSERT_NOT_EQUAL(imparse_range("1", &range), 0);
111+
CU_ASSERT_NOT_EQUAL(imparse_range("1:", &range), 0);
112+
CU_ASSERT_NOT_EQUAL(imparse_range(":1", &range), 0);
113+
114+
CU_ASSERT_NOT_EQUAL(imparse_range("1:a", &range), 0);
115+
CU_ASSERT_NOT_EQUAL(imparse_range("-1:-a", &range), 0);
116+
CU_ASSERT_NOT_EQUAL(imparse_range("1a:2", &range), 0);
117+
CU_ASSERT_NOT_EQUAL(imparse_range("1:2a", &range), 0);
118+
119+
CU_ASSERT_NOT_EQUAL(imparse_range("1:4294967296", &range), 0);
120+
CU_ASSERT_NOT_EQUAL(imparse_range("-1:-4294967296", &range), 0);
121+
CU_ASSERT_NOT_EQUAL(imparse_range("1:18446744073709551616", &range), 0);
122+
CU_ASSERT_NOT_EQUAL(imparse_range("-1:-18446744073709551616", &range), 0);
123+
}

Diff for: imap/imapd.c

+23-4
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 = UINT32_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

Diff for: imap/imapd.h

+7-3
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 */
@@ -434,21 +437,22 @@ extern struct protstream *imapd_out, *imapd_in;
434437

435438
struct client_behavior_registry {
436439
unsigned int did_annotate : 1; /* used SETANNOTATION or FETCH-ed ANNOTATION */
440+
unsigned int did_binary : 1; /* fetched BINARY or APPEND literal8 */
441+
unsigned int did_catenate : 1; /* used CATENATE on APPEND */
437442
unsigned int did_condstore : 1; /* gave CONDSTORE on SELECT */
438443
unsigned int did_compress : 1; /* started COMPRESS */
439444
unsigned int did_idle : 1; /* used IDLE */
445+
unsigned int did_imap4rev2 : 1; /* used ENABLE IMAP4rev2 */
440446
unsigned int did_metadata : 1; /* called GETMETADATA or SETMETADATA */
441447
unsigned int did_multisearch : 1; /* called ESEARCH */
442448
unsigned int did_move : 1; /* used MOVE */
443449
unsigned int did_notify : 1; /* used NOTIFY */
450+
unsigned int did_partial : 1; /* used SEARCH/FETCH PARTIAL */
444451
unsigned int did_preview : 1; /* fetched PREVIEW */
445452
unsigned int did_qresync : 1; /* gave QRESYNC on SELECT */
446453
unsigned int did_savedate : 1; /* fetched SAVEDATE */
447454
unsigned int did_searchres : 1; /* used SAVE on SEARCH */
448455
unsigned int did_replace : 1; /* used REPLACE */
449-
unsigned int did_imap4rev2 : 1; /* used ENABLE IMAP4rev2 */
450-
unsigned int did_binary : 1; /* fetched BINARY or APPEND literal8 */
451-
unsigned int did_catenate : 1; /* used CATENATE on APPEND */
452456
unsigned int did_uidonly : 1; /* used ENABLE UIDONLY */
453457
};
454458

Diff for: imap/index.c

+14-1
Original file line numberDiff line numberDiff line change
@@ -1107,6 +1107,8 @@ EXPORTED int index_fetchresponses(struct index_state *state,
11071107
struct index_map *im;
11081108
int fetched = 0;
11091109
int r = 0;
1110+
int inc;
1111+
unsigned long count = 0;
11101112
annotate_db_t *annot_db = NULL;
11111113

11121114
/* Keep an open reference on the per-mailbox db to avoid
@@ -1152,14 +1154,25 @@ EXPORTED int index_fetchresponses(struct index_state *state,
11521154
if (start < 1) start = 1;
11531155
if (end > state->exists) end = state->exists;
11541156

1155-
for (msgno = start; msgno <= end; msgno++) {
1157+
if (fetchargs->partial.is_last) {
1158+
msgno = end;
1159+
inc = -1;
1160+
}
1161+
else {
1162+
msgno = start;
1163+
inc = 1;
1164+
}
1165+
for (; msgno >= start && msgno <= end &&
1166+
count < fetchargs->partial.high; msgno += inc) {
11561167
im = &state->map[msgno-1];
11571168
if (seq && !seqset_ismember(seq, usinguid ? im->uid : msgno)) {
11581169
if (im->told_modseq !=0 && im->modseq > im->told_modseq)
11591170
index_printflags(state, msgno, (usinguid ? TELL_UID : 0));
11601171
continue;
11611172
}
11621173

1174+
if (++count < fetchargs->partial.low) continue;
1175+
11631176
if ((r = index_fetchreply(state, msgno, fetchargs)))
11641177
break;
11651178
fetched = 1;

Diff for: lib/imparse.c

+43
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
*/
4242

4343
#include <config.h>
44+
#include <errno.h>
4445
#include <stdio.h>
4546

4647
#include "imparse.h"
@@ -214,3 +215,45 @@ EXPORTED int imparse_isnumber(const char *s)
214215
}
215216
return 1;
216217
}
218+
219+
/*
220+
* Parse a range from the string starting at the pointer pointed to by 's'.
221+
* and populate the structure in the pointer at 'range'.
222+
* Returns 0 on success, and non-zero on failure.
223+
*/
224+
EXPORTED int imparse_range(char *s, range_t *range)
225+
{
226+
if (*s == '-') {
227+
range->is_last = 1;
228+
s++;
229+
}
230+
if (!Uisdigit(*s)) return -1;
231+
232+
errno = 0;
233+
range->low = strtoul(s, &s, 10);
234+
if (!range->low || range->low > UINT32_MAX || errno || *s != ':') {
235+
errno = 0;
236+
return -1;
237+
}
238+
239+
if (*++s == '-') {
240+
if (!range->is_last) return -1;
241+
s++;
242+
}
243+
if (!Uisdigit(*s)) return -1;
244+
245+
range->high = strtoul(s, &s, 10);
246+
if (!range->high || range->high > UINT32_MAX || errno || *s) {
247+
errno = 0;
248+
return -1;
249+
}
250+
251+
if (range->low > range->high) {
252+
unsigned long n = range->high;
253+
254+
range->high = range->low;
255+
range->low = n;
256+
}
257+
258+
return 0;
259+
}

Diff for: lib/imparse.h

+7
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,18 @@
4343
#ifndef INCLUDED_IMPARSE_H
4444
#define INCLUDED_IMPARSE_H
4545

46+
typedef struct {
47+
uint32_t low;
48+
uint32_t high;
49+
u_char is_last : 1;
50+
} range_t;
51+
4652
extern int imparse_word (char **s, char **retval);
4753
extern int imparse_astring (char **s, char **retval);
4854
extern int imparse_isnatom (const char *s, int len);
4955
extern int imparse_isatom (const char *s);
5056
extern int imparse_issequence (const char *s);
5157
extern int imparse_isnumber (const char *s);
58+
extern int imparse_range (char *s, range_t *range);
5259

5360
#endif /* INCLUDED_IMPARSE_H */

0 commit comments

Comments
 (0)