commit 3aead3300865b33b086f12961621d655a16bd6c2
parent f748bd45dfafc8c8882017168f86b27da85ebc72
Author: Oswald Buddenhagen <ossi@users.sf.net>
Date: Fri, 10 Jun 2022 18:29:55 +0200
rework IMAP list parsing
replace the "DOM-like" model with a "streaming" model, i.e., the tokens
are now immediately processed by stateful callbacks as they are found.
this avoids plenty of allocations and copies (though not of the message
contents, yet).
Diffstat:
M | src/drv_imap.c | | | 839 | +++++++++++++++++++++++++++++++++++++++++++++++++------------------------------ |
1 file changed, 524 insertions(+), 315 deletions(-)
diff --git a/src/drv_imap.c b/src/drv_imap.c
@@ -58,26 +58,61 @@ typedef union imap_store_conf {
};
} imap_store_conf_t;
-#define NIL (void*)0x1
-#define LIST (void*)0x2
-
-typedef struct _list {
- struct _list *next, *child;
- char *val;
- uint len;
-} list_t;
+typedef union imap_store imap_store_t;
-#define MAX_LIST_DEPTH 5
+enum { AtomNil, AtomString, AtomLiteral, AtomChunkedLiteral };
-typedef union imap_store imap_store_t;
+typedef struct {
+ int (*enter)( imap_store_t *ctx );
+ int (*leave)( imap_store_t *ctx );
+ int (*atom)( imap_store_t *ctx, char *val, uint len, int type );
+ int (*done)( imap_store_t *ctx, int resp );
+} parse_list_cb_t;
typedef struct {
- list_t *head, **stack[MAX_LIST_DEPTH];
- int (*callback)( imap_store_t *ctx, list_t *list );
- int level, need_bytes;
+ parse_list_cb_t *callback;
+ uint need_bytes;
+ int level, chunked, big_literal;
+ enum { NotLiteral, AtomicLiteral, ChunkedLiteral } in_literal;
const char *err;
} parse_list_state_t;
+typedef enum {
+ NsInit,
+ Ns1st,
+ Ns1stNs,
+ Ns1stDelim,
+ NsDone
+} ParseNsState;
+
+typedef enum {
+ FetchInit,
+ FetchAttrib,
+ FetchUid,
+ FetchFlags,
+ FetchFlagVal,
+ FetchDate,
+ FetchSize,
+ FetchBody,
+ FetchBodyChunk,
+ FetchHeaders,
+ FetchHeaderFields,
+ FetchHeaderBracket,
+ FetchHeaderCont
+} ParseFetchState;
+
+typedef enum {
+ ListInit,
+ ListAttrib,
+ ListSkip
+} ParseListState;
+
+typedef enum {
+ PermflagsInit,
+ PermflagsFlags,
+ PermflagsHasFwd
+} ParsePermflagsState;
+
typedef struct imap_cmd imap_cmd_t;
union imap_store {
@@ -106,12 +141,25 @@ union imap_store {
int total_msgs, recent_msgs;
uint uidvalidity, uidnext;
imap_messages_t msgs;
- uint fetch_seq; // FETCH results
+ struct { // pending FETCH result
+ char *body, *msgid;
+ time_t date;
+ uint seq, uid, size, body_len;
+ uchar flags, status;
+ char tuid[TUIDL];
+ } fetch;
uint caps; // CAPABILITY results
string_list_t *auth_mechs;
parse_list_state_t parse_list_sts;
const char *parse_list_what;
char *parse_list_cmd;
+ union {
+ ParseNsState ns_state;
+ ParseFetchState fetch_state;
+ ParseListState list_state;
+ ParsePermflagsState permflags_state;
+ int any_state; // We always init this one to zero
+ };
// Command queue
imap_cmd_t *pending, **pending_append;
imap_cmd_t *in_progress, **in_progress_append;
@@ -682,39 +730,6 @@ next_arg( char **ps, uint *len )
return ret;
}
-static int
-is_opt_atom( list_t *list )
-{
- return list && list->val && list->val != LIST;
-}
-
-static int
-is_atom( list_t *list )
-{
- return list && list->val && list->val != NIL && list->val != LIST;
-}
-
-static int
-is_list( list_t *list )
-{
- return list && list->val == LIST;
-}
-
-static void
-free_list( list_t *list )
-{
- list_t *tmp;
-
- for (; list; list = tmp) {
- tmp = list->next;
- if (is_list( list ))
- free_list( list->child );
- else if (is_atom( list ))
- free( list->val );
- free( list );
- }
-}
-
enum {
LIST_OK,
LIST_PARTIAL,
@@ -724,23 +739,20 @@ enum {
static int
parse_imap_list( imap_store_t *ctx, char **sp, parse_list_state_t *sts )
{
- list_t *cur, **curp;
char *s = *sp, *d, *p;
- int bytes;
+ uint bytes;
uint n;
char c;
+ assert( ctx );
assert( sts );
- assert( sts->level > 0 );
- curp = sts->stack[--sts->level];
- bytes = sts->need_bytes;
- if (bytes >= 0) {
- sts->need_bytes = -1;
+ if (sts->in_literal != NotLiteral) {
+ bytes = sts->need_bytes;
if (!bytes)
goto getline;
- cur = (list_t *)((char *)curp - offsetof(list_t, next));
- s = cur->val + cur->len - bytes;
- goto getbytes;
+ if (sts->in_literal == ChunkedLiteral)
+ goto get_chunked;
+ goto get_atomic;
}
if (!s) {
@@ -752,40 +764,50 @@ parse_imap_list( imap_store_t *ctx, char **sp, parse_list_state_t *sts )
s++;
if (sts->level && *s == ')') {
s++;
- curp = sts->stack[--sts->level];
+ if (sts->callback->leave && sts->callback->leave( ctx ) != LIST_OK)
+ goto bail;
+ sts->level--;
goto next;
}
- *curp = cur = nfmalloc( sizeof(*cur) );
- cur->val = NULL; /* for clean bail */
- curp = &cur->next;
- *curp = NULL; /* ditto */
if (*s == '(') {
/* sublist */
- if (sts->level == MAX_LIST_DEPTH)
- goto toodeep;
s++;
- cur->val = LIST;
- sts->stack[sts->level++] = curp;
- curp = &cur->child;
- *curp = NULL; /* for clean bail */
+ sts->level++;
+ if (sts->callback->enter && sts->callback->enter( ctx ) != LIST_OK)
+ goto bail;
goto next2;
- } else if (ctx && *s == '{') {
+ } else if (*s == '{') {
/* literal */
- bytes = (int)(cur->len = strtoul( s + 1, &s, 10 ));
+ bytes = strtoul( s + 1, &s, 10 );
if (*s != '}' || *++s) {
sts->err = "malformed literal";
goto bail;
}
- if ((uint)bytes >= INT_MAX) {
+ if (bytes >= INT_MAX) {
sts->err = "excessively large literal - THIS MIGHT BE AN ATTEMPT TO HACK YOU!";
goto bail;
}
- s = cur->val = nfmalloc( cur->len + 1 );
- s[cur->len] = 0;
-
- getbytes:
- if (!(p = socket_read( &ctx->conn, 1, (uint)bytes, &n )))
+ if (sts->chunked) {
+ sts->chunked = 0;
+ assert( sts->callback->atom );
+ if (sts->callback->atom( ctx, NULL, bytes, AtomChunkedLiteral ) != LIST_OK)
+ goto bail;
+ sts->in_literal = ChunkedLiteral;
+ sts->big_literal = 1;
+ get_chunked:
+ n = 1;
+ } else {
+ if (bytes > sizeof(ctx->conn.buf)) {
+ sts->err = "unexpectedly large literal";
+ goto bail;
+ }
+ sts->in_literal = AtomicLiteral;
+ sts->big_literal = bytes > 64;
+ get_atomic:
+ n = bytes;
+ }
+ if (!(p = socket_read( &ctx->conn, n, bytes, &n )))
goto postpone;
if (p == (void *)~0) {
badeof:
@@ -793,7 +815,7 @@ parse_imap_list( imap_store_t *ctx, char **sp, parse_list_state_t *sts )
goto bail;
}
if (DFlags & DEBUG_NET) {
- if (cur->len < 64) {
+ if (!sts->big_literal) {
xprintf( "%s%.*!s\n", ctx->label, (int)n, p );
} else if (DFlags & DEBUG_NET_ALL) {
printf( "%s=========\n", ctx->label );
@@ -806,10 +828,13 @@ parse_imap_list( imap_store_t *ctx, char **sp, parse_list_state_t *sts )
}
fflush( stdout );
}
- memcpy( s, p, n );
+ if (sts->callback->atom && sts->callback->atom( ctx, p, n, AtomLiteral ) != LIST_OK)
+ goto bail;
bytes -= n;
if (bytes > 0)
goto postpone;
+ if (sts->in_literal == ChunkedLiteral && sts->callback->atom( ctx, NULL, 0, AtomLiteral ) != LIST_OK)
+ goto bail;
getline:
if (!(s = socket_read_line( &ctx->conn )))
goto postpone;
@@ -819,6 +844,7 @@ parse_imap_list( imap_store_t *ctx, char **sp, parse_list_state_t *sts )
printf( "%s%s\n", ctx->label, s );
fflush( stdout );
}
+ sts->in_literal = NotLiteral;
} else if (*s == '"') {
/* quoted string */
s++;
@@ -832,19 +858,22 @@ parse_imap_list( imap_store_t *ctx, char **sp, parse_list_state_t *sts )
}
*d++ = c;
}
- cur->len = (uint)(d - p);
- cur->val = nfstrndup( p, cur->len );
+ if (sts->callback->atom && sts->callback->atom( ctx, p, (uint)(d - p), AtomString ) != LIST_OK)
+ goto bail;
} else {
/* atom */
p = s;
for (; *s && !isspace( (uchar)*s ); s++)
if (sts->level && *s == ')')
break;
- cur->len = (uint)(s - p);
- if (equals_upper( p, (int)cur->len, "NIL", 3 ))
- cur->val = NIL;
- else
- cur->val = nfstrndup( p, cur->len );
+ if (sts->callback->atom) {
+ int t = AtomString;
+ n = (uint)(s - p);
+ if (equals_upper( p, (int)n, "NIL", 3 ))
+ p = NULL, n = 0, t = AtomNil;
+ if (sts->callback->atom( ctx, p, n, t ) != LIST_OK)
+ goto bail;
+ }
}
next:
@@ -860,121 +889,135 @@ parse_imap_list( imap_store_t *ctx, char **sp, parse_list_state_t *sts )
return LIST_OK;
postpone:
- if (sts->level < MAX_LIST_DEPTH) {
- sts->stack[sts->level++] = curp;
- sts->need_bytes = bytes;
- return LIST_PARTIAL;
- }
- toodeep:
- sts->err = "too deeply nested lists";
+ sts->need_bytes = bytes;
+ return LIST_PARTIAL;
bail:
- free_list( sts->head );
sts->level = 0;
return LIST_BAD;
}
static void
-parse_list_init( parse_list_state_t *sts )
+parse_list_init( imap_store_t *ctx, parse_list_cb_t *cb )
{
- sts->need_bytes = -1;
- sts->level = 1;
+ parse_list_state_t *sts = &ctx->parse_list_sts;
+ sts->in_literal = NotLiteral;
+ sts->level = 0;
+ sts->chunked = 0;
+ sts->callback = cb;
sts->err = NULL;
- sts->head = NULL;
- sts->stack[0] = &sts->head;
+ ctx->any_state = 0;
}
static int
parse_list_continue( imap_store_t *ctx )
{
- list_t *list;
int resp;
if ((resp = parse_imap_list( ctx, &ctx->parse_list_cmd, &ctx->parse_list_sts )) != LIST_PARTIAL) {
- list = (resp == LIST_BAD) ? NULL : ctx->parse_list_sts.head;
- ctx->parse_list_sts.head = NULL;
- resp = ctx->parse_list_sts.callback( ctx, list );
- free_list( list );
+ if (ctx->parse_list_sts.callback->done)
+ resp = ctx->parse_list_sts.callback->done( ctx, resp );
+ if (resp == LIST_BAD) {
+ if (!ctx->parse_list_sts.err)
+ ctx->parse_list_sts.err = "unexpected value";
+ error( "IMAP error: malformed %s response from %s: %s\n",
+ ctx->parse_list_what, ctx->conn.name, ctx->parse_list_sts.err );
+ }
}
return resp;
}
static int
-parse_next_list( imap_store_t *ctx, int (*cb)( imap_store_t *ctx, list_t *list ) )
+parse_next_list( imap_store_t *ctx, parse_list_cb_t *cb )
{
- parse_list_init( &ctx->parse_list_sts );
- ctx->parse_list_sts.callback = cb;
+ parse_list_init( ctx, cb );
return parse_list_continue( ctx );
}
static int
-parse_list( imap_store_t *ctx, char *s, int (*cb)( imap_store_t *ctx, list_t *list ), const char *what )
+parse_list( imap_store_t *ctx, char *s, parse_list_cb_t *cb, const char *what )
{
ctx->parse_list_cmd = s;
ctx->parse_list_what = what;
return parse_next_list( ctx, cb );
}
+static parse_list_cb_t ignore_one_list_cb = {
+ .done = NULL
+};
+
static int
-parse_list_perror( imap_store_t *ctx )
+ignore_two_done( imap_store_t *ctx, int sts )
{
- if (!ctx->parse_list_sts.err)
- ctx->parse_list_sts.err = "unexpected value";
- error( "IMAP error: malformed %s response from %s: %s\n",
- ctx->parse_list_what, ctx->conn.name, ctx->parse_list_sts.err );
- return LIST_BAD;
+ if (sts != LIST_OK)
+ return sts;
+ return parse_next_list( ctx, &ignore_one_list_cb );
}
-static int parse_namespace_rsp_p2( imap_store_t *, list_t * );
-static int parse_namespace_rsp_p3( imap_store_t *, list_t * );
+static parse_list_cb_t ignore_two_lists_cb = {
+ .done = ignore_two_done,
+};
static int
-parse_namespace_rsp( imap_store_t *ctx, list_t *list )
+namespace_rsp_enter( imap_store_t *ctx )
{
// We use only the 1st personal namespace. Making this configurable
// would not add value over just specifying Path.
-
- if (!list) {
- bad:
- return parse_list_perror( ctx );
- }
- if (list->val != NIL) {
- if (list->val != LIST)
- goto bad;
- list_t *nsp_1st = list->child;
- if (nsp_1st->val != LIST)
- goto bad;
- list_t *nsp_1st_ns = nsp_1st->child;
- if (!is_atom( nsp_1st_ns ))
- goto bad;
- ctx->ns_prefix = nsp_1st_ns->val;
- nsp_1st_ns->val = NULL;
- list_t *nsp_1st_dl = nsp_1st_ns->next;
- if (!is_opt_atom( nsp_1st_dl ))
- goto bad;
- if (is_atom( nsp_1st_dl ))
- ctx->ns_delimiter = nsp_1st_dl->val[0];
- // Namespace response extensions may follow here; we don't care.
+ switch (ctx->ns_state) {
+ case NsInit:
+ ctx->ns_state = Ns1st;
+ return LIST_OK;
+ case Ns1st:
+ ctx->ns_state = Ns1stNs;
+ return LIST_OK;
+ case NsDone:
+ return LIST_OK;
+ default:
+ return LIST_BAD;
}
-
- return parse_next_list( ctx, parse_namespace_rsp_p2 );
}
-// Note that parse_list_rsp() below refers to these as well.
static int
-parse_namespace_rsp_p2( imap_store_t *ctx, list_t *list )
+namespace_rsp_leave( imap_store_t *ctx )
{
- if (!list)
- return parse_list_perror( ctx );
- return parse_next_list( ctx, parse_namespace_rsp_p3 );
+ if (ctx->ns_state != NsDone)
+ return LIST_BAD;
+ return LIST_OK;
}
static int
-parse_namespace_rsp_p3( imap_store_t *ctx, list_t *list )
+namespace_rsp_atom( imap_store_t *ctx, char *val, uint len, int type ATTR_UNUSED )
{
- if (!list)
- return parse_list_perror( ctx );
+ if (ctx->ns_state == Ns1stNs) {
+ if (!val)
+ return LIST_BAD;
+ ctx->ns_prefix = nfstrndup( val, len );
+ ctx->ns_state = Ns1stDelim;
+ } else if (ctx->ns_state == Ns1stDelim) {
+ if (val) {
+ if (!len)
+ return LIST_BAD;
+ ctx->ns_delimiter = val[0];
+ }
+ // Namespace response extensions may follow here; we don't care.
+ ctx->ns_state = NsDone;
+ }
return LIST_OK;
}
+static int
+namespace_rsp_done( imap_store_t *ctx, int sts )
+{
+ if (sts != LIST_OK)
+ return sts;
+ return parse_next_list( ctx, &ignore_two_lists_cb );
+}
+
+static parse_list_cb_t namespace_rsp_cb = {
+ .enter = namespace_rsp_enter,
+ .leave = namespace_rsp_leave,
+ .atom = namespace_rsp_atom,
+ .done = namespace_rsp_done,
+};
+
static time_t
parse_date( const char *str )
{
@@ -993,54 +1036,47 @@ parse_date( const char *str )
return date - (hours * 60 + mins) * 60;
}
-static int
-parse_fetched_flags( list_t *list, uchar *flags, uchar *status )
+static void
+parse_fetched_flag( char *val, uint len, uchar *flags, uchar *status )
{
- for (; list; list = list->next) {
- if (!is_atom( list )) {
- error( "IMAP error: unable to parse FLAGS list\n" );
- return 0;
- }
- if (list->val[0] != '\\' && list->val[0] != '$')
- continue;
- to_upper( list->val, list->len );
- if (equals( list->val, list->len, "\\RECENT", 7 )) {
- *status |= M_RECENT;
- goto flagok;
- }
- for (uint i = 0; i < as(Flags); i++) {
- if (equals( list->val, list->len, Flags[i].ustr, Flags[i].len )) {
- *flags |= 1 << i;
- goto flagok;
- }
+ if (val[0] != '\\' && val[0] != '$')
+ return;
+ to_upper( val, len );
+ if (equals( val, len, "\\RECENT", 7 )) {
+ *status |= M_RECENT;
+ return;
+ }
+ for (uint i = 0; i < as(Flags); i++) {
+ if (equals( val, len, Flags[i].ustr, Flags[i].len )) {
+ *flags |= 1 << i;
+ return;
}
- if (list->val[0] == '$')
- goto flagok; // Ignore unknown user-defined flags (keywords)
- if (list->val[1] == 'X' && list->val[2] == '-')
- goto flagok; // Ignore system flag extensions
- warn( "IMAP warning: unknown system flag %s\n", list->val );
- flagok: ;
}
- return 1;
+ if (val[0] == '$')
+ return; // Ignore unknown user-defined flags (keywords)
+ if (len > 2 && val[1] == 'X' && val[2] == '-')
+ return; // Ignore system flag extensions
+ warn( "IMAP warning: unknown system flag %.*s\n", (int)len, val );
}
static void
-parse_fetched_header( char *val, uint uid, char **tuid, char **msgid, uint *msgid_len )
+parse_fetched_header( char *val, uint vlen, uint uid, char *tuid, char **msgid )
{
char *end;
int off, in_msgid = 0;
- for (; (end = strchr( val, '\n' )); val = end + 1) {
+ for (; (end = memchr( val, '\n', vlen )); val = end + 1) {
int len = (int)(end - val);
+ vlen -= len + 1;
if (len && end[-1] == '\r')
len--;
if (!len)
break;
if (starts_with_upper( val, len, "X-TUID: ", 8 )) {
- if (len < 8 + TUIDL) {
+ if (len != 8 + TUIDL) {
warn( "IMAP warning: malformed X-TUID header (UID %u)\n", uid );
continue;
}
- *tuid = val + 8;
+ memcpy( tuid, val + 8, TUIDL );
in_msgid = 0;
continue;
}
@@ -1061,122 +1097,224 @@ parse_fetched_header( char *val, uint uid, char **tuid, char **msgid, uint *msgi
in_msgid = 1;
continue;
}
- *msgid = val + off;
- *msgid_len = (uint)(len - off);
+ *msgid = nfstrndup( val + off, (uint)(len - off) );
in_msgid = 0;
}
}
static int
-parse_fetch_rsp( imap_store_t *ctx, list_t *list )
+str_to_num( const char *val, uint len, uint *outp )
{
- list_t *body = NULL, *tmp;
- char *tuid = NULL, *msgid = NULL, *ep;
- imap_message_t *cur;
- msg_data_t *msgdata;
- imap_cmd_t *cmdp;
- uchar mask = 0, status = 0;
- uint uid = 0, size = 0, msgid_len = 0;
- time_t date = 0;
+ uint i = 0;
+ uint out = 0;
+
+ if (!len)
+ return 0;
+ for (;;) {
+ uchar c = val[i] - '0';
+ if (c > 9) // Covers underflow as well
+ return 0;
+ out += c;
+ if (++i == len)
+ break;
+ if (out >= UINT_MAX / 10)
+ return 0;
+ out *= 10;
+ }
+ *outp = out;
+ return 1;
+}
+
+static int
+fetch_rsp_enter( imap_store_t *ctx )
+{
+ switch (ctx->fetch_state) {
+ case FetchInit:
+ ctx->fetch_state = FetchAttrib;
+ return LIST_OK;
+ case FetchFlags:
+ ctx->fetch_state = FetchFlagVal;
+ return LIST_OK;
+ case FetchHeaders:
+ ctx->fetch_state = FetchHeaderFields;
+ return LIST_OK;
+ default:
+ return LIST_BAD;
+ }
+}
- if (!is_list( list ))
- return parse_list_perror( ctx );
+static int
+fetch_rsp_leave( imap_store_t *ctx )
+{
+ switch (ctx->fetch_state) {
+ case FetchAttrib:
+ return LIST_OK;
+ case FetchFlagVal:
+ ctx->fetch_state = FetchAttrib;
+ return LIST_OK;
+ case FetchHeaderFields:
+ ctx->fetch_state = FetchHeaderBracket;
+ return LIST_OK;
+ default:
+ return LIST_BAD;
+ }
+}
- for (tmp = list->child; tmp; tmp = tmp->next) {
- if (!is_atom( tmp )) {
- error( "IMAP error: bogus item name in FETCH response\n" );
+static int
+fetch_rsp_atom( imap_store_t *ctx, char *val, uint len, int type )
+{
+ switch (ctx->fetch_state) {
+ case FetchInit:
+ case FetchFlags:
+ case FetchHeaders:
+ return LIST_BAD;
+ case FetchAttrib:
+ if (!len) {
+ ctx->parse_list_sts.err = "bogus attribute name";
return LIST_BAD;
}
- char *name = tmp->val;
- uint namel = tmp->len;
- tmp = tmp->next;
- to_upper( name, namel );
- if (equals( name, namel, "UID", 3 )) {
- if (!is_atom( tmp ) || (uid = strtoul( tmp->val, &ep, 10 ), *ep)) {
- error( "IMAP error: unable to parse UID\n" );
- return LIST_BAD;
- }
- } else if (equals( name, namel, "FLAGS", 5 )) {
- if (!is_list( tmp )) {
- error( "IMAP error: unable to parse FLAGS\n" );
- return LIST_BAD;
- }
- if (!parse_fetched_flags( tmp->child, &mask, &status ))
- return LIST_BAD;
- status |= M_FLAGS;
- } else if (equals( name, namel, "INTERNALDATE", 12 )) {
- if (!is_atom( tmp )) {
- error( "IMAP error: unable to parse INTERNALDATE\n" );
- return LIST_BAD;
- }
- if ((date = parse_date( tmp->val )) == -1) {
- error( "IMAP error: unable to parse INTERNALDATE format\n" );
- return LIST_BAD;
- }
- status |= M_DATE;
- } else if (equals( name, namel, "RFC822.SIZE", 11 )) {
- if (!is_atom( tmp ) || (size = strtoul( tmp->val, &ep, 10 ), *ep)) {
- error( "IMAP error: unable to parse RFC822.SIZE\n" );
- return LIST_BAD;
- }
- status |= M_SIZE;
- } else if (equals( name, namel, "BODY[]", 6 ) || equals( name, namel, "BODY[HEADER]", 12 )) {
- if (!is_atom( tmp )) {
- error( "IMAP error: unable to parse BODY[]\n" );
- return LIST_BAD;
- }
- body = tmp;
- status |= M_BODY;
- } else if (equals( name, namel, "BODY[HEADER.FIELDS", 18 )) {
- if (!is_list( tmp )) {
- bfail:
- error( "IMAP error: unable to parse BODY[HEADER.FIELDS ...]\n" );
- return LIST_BAD;
- }
- tmp = tmp->next;
- if (!is_atom( tmp ) || !equals( tmp->val, tmp->len, "]", 1 ))
- goto bfail;
- tmp = tmp->next;
- if (!is_atom( tmp ))
- goto bfail;
- parse_fetched_header( tmp->val, uid, &tuid, &msgid, &msgid_len );
- status |= M_HEADER;
+ uchar field;
+ to_upper( val, len );
+ if (equals( val, len, "UID", 3 )) {
+ ctx->fetch_state = FetchUid;
+ return LIST_OK;
+ } else if (equals( val, len, "FLAGS", 5 )) {
+ ctx->fetch_state = FetchFlags;
+ field = M_FLAGS;
+ } else if (equals( val, len, "INTERNALDATE", 12 )) {
+ ctx->fetch_state = FetchDate;
+ field = M_DATE;
+ } else if (equals( val, len, "RFC822.SIZE", 11 )) {
+ ctx->fetch_state = FetchSize;
+ field = M_SIZE;
+ } else if (equals( val, len, "BODY[]", 6 ) || equals( val, len, "BODY[HEADER]", 12 )) {
+ ctx->parse_list_sts.chunked = 1;
+ ctx->fetch_state = FetchBody;
+ field = M_BODY;
+ } else if (equals( val, len, "BODY[HEADER.FIELDS", 18 )) {
+ ctx->fetch_state = FetchHeaders;
+ field = M_HEADER;
+ } else {
+ ctx->parse_list_sts.err = "unexpected attribute";
+ return LIST_BAD;
+ }
+ if (ctx->fetch.status & field) {
+ ctx->parse_list_sts.err = "duplicated attribute";
+ return LIST_BAD;
+ }
+ ctx->fetch.status |= field;
+ return LIST_OK;
+ case FetchUid:
+ if (!str_to_num( val, len, &ctx->fetch.uid )) {
+ ctx->parse_list_sts.err = "unable to parse UID";
+ return LIST_BAD;
+ }
+ break;
+ case FetchFlagVal:
+ if (!len) {
+ ctx->parse_list_sts.err = "unable to parse FLAGS";
+ return LIST_BAD;
+ }
+ parse_fetched_flag( val, len, &ctx->fetch.flags, &ctx->fetch.status );
+ return LIST_OK;
+ case FetchDate:
+ if (type != AtomString) {
+ dfail:
+ ctx->parse_list_sts.err = "unable to parse INTERNALDATE";
+ return LIST_BAD;
}
+ val[len] = 0;
+ if ((ctx->fetch.date = parse_date( val )) == -1)
+ goto dfail;
+ break;
+ case FetchSize:
+ if (!str_to_num( val, len, &ctx->fetch.size )) {
+ ctx->parse_list_sts.err = "unable to parse RFC822.SIZE";
+ return LIST_BAD;
+ }
+ break;
+ case FetchBody:
+ if (type != AtomChunkedLiteral) {
+ ctx->parse_list_sts.err = "BODY is no literal";
+ return LIST_BAD;
+ }
+ ctx->fetch.body = nfmalloc( len );
+ ctx->fetch.body_len = 0;
+ ctx->fetch_state = FetchBodyChunk;
+ return LIST_OK;
+ case FetchBodyChunk:
+ if (!len)
+ break;
+ memcpy( ctx->fetch.body + ctx->fetch.body_len, val, len );
+ ctx->fetch.body_len += len;
+ return LIST_OK;
+ case FetchHeaderFields:
+ // This looks like BODY[HEADER.FIELDS (X-TUID Message-Id)] {content}.
+ // To avoid parsing the bracketed list, we just treat these as separate tokens.
+ return LIST_OK;
+ case FetchHeaderBracket:
+ if (!equals( val, len, "]", 1 )) {
+ bfail:
+ ctx->parse_list_sts.err = "unable to parse BODY[HEADER.FIELDS ...]";
+ return LIST_BAD;
+ }
+ ctx->fetch_state = FetchHeaderCont;
+ return LIST_OK;
+ case FetchHeaderCont:
+ if (!val)
+ goto bfail;
+ parse_fetched_header( val, len, ctx->fetch.uid, ctx->fetch.tuid, &ctx->fetch.msgid );
+ break;
}
+ ctx->fetch_state = FetchAttrib;
+ return LIST_OK;
+}
+
+static int
+fetch_rsp_done( imap_store_t *ctx, int sts )
+{
+ imap_message_t *cur;
+ msg_data_t *msgdata;
+ imap_cmd_t *cmdp;
- if (!uid) {
+ if (sts != LIST_OK)
+ goto bail;
+
+ uchar status = ctx->fetch.status;
+ if (!ctx->fetch.uid) {
// Ignore async flag updates for now.
status &= ~(M_FLAGS | M_RECENT);
} else if (status & M_BODY) {
for (cmdp = ctx->in_progress; cmdp; cmdp = cmdp->next)
- if (cmdp->param.uid == uid)
+ if (cmdp->param.uid == ctx->fetch.uid)
goto gotuid;
- error( "IMAP error: unexpected FETCH response with BODY (UID %u)\n", uid );
- return LIST_BAD;
+ ctx->parse_list_sts.err = "unexpected BODY";
+ sts = LIST_BAD;
+ goto bail;
gotuid:
msgdata = ((imap_cmd_fetch_msg_t *)cmdp)->msg_data;
- msgdata->data = body->val;
- body->val = NULL; // Don't free together with list.
- msgdata->len = body->len;
- msgdata->date = date;
+ msgdata->data = ctx->fetch.body;
+ ctx->fetch.body = NULL;
+ msgdata->len = ctx->fetch.body_len;
+ msgdata->date = ctx->fetch.date;
if (status & M_FLAGS)
- msgdata->flags = mask;
+ msgdata->flags = ctx->fetch.flags;
status &= ~(M_FLAGS | M_RECENT | M_BODY | M_DATE);
} else if (ctx->fetch_sts == FetchUidNext) {
// Workaround for server not sending UIDNEXT and/or APPENDUID.
- ctx->uidnext = uid + 1;
+ ctx->uidnext = ctx->fetch.uid + 1;
} else if (ctx->fetch_sts == FetchMsgs) {
imap_ensure_absolute( &ctx->msgs ); // In case of interleaved EXPUNGE
cur = imap_new_msg( & ctx->msgs );
- cur->seq = ctx->fetch_seq;
- cur->uid = uid;
- cur->flags = mask;
+ cur->seq = ctx->fetch.seq;
+ cur->uid = ctx->fetch.uid;
+ cur->flags = ctx->fetch.flags;
cur->status = status;
- cur->size = size;
- if (msgid)
- cur->msgid = nfstrndup( msgid, msgid_len );
- if (tuid)
- memcpy( cur->tuid, tuid, TUIDL );
+ cur->size = ctx->fetch.size;
+ cur->msgid = ctx->fetch.msgid;
+ ctx->fetch.msgid = NULL;
+ if (ctx->fetch.tuid[0])
+ memcpy( cur->tuid, ctx->fetch.tuid, TUIDL );
status &= ~(M_FLAGS | M_RECENT | M_SIZE | M_HEADER);
} else {
// These may come in as a result of STORE FLAGS despite .SILENT.
@@ -1184,13 +1322,25 @@ parse_fetch_rsp( imap_store_t *ctx, list_t *list )
}
if (status) {
- error( "IMAP error: received extraneous data in FETCH response\n" );
- return LIST_BAD;
+ ctx->parse_list_sts.err = "extraneous data";
+ sts = LIST_BAD;
}
- return LIST_OK;
+ bail:
+ free( ctx->fetch.body );
+ ctx->fetch.body = NULL;
+ free( ctx->fetch.msgid );
+ ctx->fetch.msgid = NULL;
+ return sts;
}
+static parse_list_cb_t fetch_rsp_cb = {
+ .enter = fetch_rsp_enter,
+ .leave = fetch_rsp_leave,
+ .atom = fetch_rsp_atom,
+ .done = fetch_rsp_done,
+};
+
static void
parse_capability( imap_store_t *ctx, char *cmd )
{
@@ -1216,6 +1366,32 @@ parse_capability( imap_store_t *ctx, char *cmd )
}
static int
+permflags_enter( imap_store_t *ctx )
+{
+ if (ctx->permflags_state != PermflagsInit)
+ return LIST_BAD;
+ ctx->permflags_state = PermflagsFlags;
+ return LIST_OK;
+}
+
+static int
+permflags_atom( imap_store_t *ctx, char *val, uint len, int type ATTR_UNUSED )
+{
+ if (ctx->permflags_state == PermflagsHasFwd)
+ return LIST_OK;
+ if (ctx->permflags_state != PermflagsFlags || !val)
+ return LIST_BAD;
+ if (equals( val, len, "\\*", 2 ) || equals_upper( val, len, "$FORWARDED", 10 ))
+ ctx->permflags_state = PermflagsHasFwd;
+ return LIST_OK;
+}
+
+static parse_list_cb_t permflags_cb = {
+ .enter = permflags_enter,
+ .atom = permflags_atom,
+};
+
+static int
parse_response_code( imap_store_t *ctx, imap_cmd_t *cmd, char *s )
{
char *arg, *earg, *p;
@@ -1283,56 +1459,85 @@ parse_response_code( imap_store_t *ctx, imap_cmd_t *cmd, char *s )
return RESP_CANCEL;
}
} else if (equals( arg, argl, "PERMANENTFLAGS", 14 )) {
- parse_list_init( &ctx->parse_list_sts );
- if (parse_imap_list( NULL, &s, &ctx->parse_list_sts ) != LIST_OK || *s != ']') {
+ parse_list_init( ctx, &permflags_cb );
+ // Note: we croak on LIST_PARTIAL, as literals are not expected anyway.
+ if (parse_imap_list( ctx, &s, &ctx->parse_list_sts ) != LIST_OK || *s != ']') {
error( "IMAP error: malformed PERMANENTFLAGS status\n" );
return RESP_CANCEL;
}
- int ret = RESP_OK;
- for (list_t *tmp = ctx->parse_list_sts.head->child; tmp; tmp = tmp->next) {
- if (!is_atom( tmp )) {
- error( "IMAP error: malformed PERMANENTFLAGS status item\n" );
- ret = RESP_CANCEL;
- break;
- }
- if (equals( tmp->val, tmp->len, "\\*", 2 ) || equals_upper( tmp->val, tmp->len, "$FORWARDED", 10 )) {
- ctx->has_forwarded = 1;
- break;
- }
- }
- free_list( ctx->parse_list_sts.head );
- ctx->parse_list_sts.head = NULL;
- return ret;
+ ctx->has_forwarded = (ctx->permflags_state == PermflagsHasFwd);
}
return RESP_OK;
}
-static int parse_list_rsp_p1( imap_store_t *, list_t * );
-static int parse_list_rsp_p2( imap_store_t *, list_t * );
+static int
+list_rsp_enter( imap_store_t *ctx )
+{
+ if (ctx->list_state != ListInit)
+ return LIST_BAD;
+ ctx->list_state = ListAttrib;
+ return LIST_OK;
+}
+
+static int
+list_rsp_atom( imap_store_t *ctx, char *val, uint len, int type ATTR_UNUSED )
+{
+ if (ctx->list_state == ListSkip)
+ return LIST_OK;
+ if (ctx->list_state != ListAttrib || !val)
+ return LIST_BAD;
+ if (equals_upper( val, len, "\\NOSELECT", 9 ))
+ ctx->list_state = ListSkip;
+ return LIST_OK;
+}
+
+static parse_list_cb_t list2_rsp_cb;
+static parse_list_cb_t list3_rsp_cb;
+
+static int
+list_rsp_done( imap_store_t *ctx, int sts )
+{
+ if (sts != LIST_OK)
+ return sts;
+ if (ctx->list_state == ListSkip)
+ return parse_next_list( ctx, &ignore_two_lists_cb );
+ return parse_next_list( ctx, &list2_rsp_cb );
+}
+
+static parse_list_cb_t list_rsp_cb = {
+ .enter = list_rsp_enter,
+ .atom = list_rsp_atom,
+ .done = list_rsp_done,
+};
static int
-parse_list_rsp( imap_store_t *ctx, list_t *list )
+list2_rsp_enter( imap_store_t *ctx ATTR_UNUSED )
{
- list_t *lp;
+ return LIST_BAD;
+}
- if (!is_list( list ))
- return parse_list_perror( ctx );
- for (lp = list->child; lp; lp = lp->next)
- if (is_atom( lp ) && equals_upper( lp->val, lp->len, "\\NOSELECT", 9 ))
- return parse_next_list( ctx, parse_namespace_rsp_p2 ); // (sic!)
- return parse_next_list( ctx, parse_list_rsp_p1 );
+static int
+list2_rsp_atom( imap_store_t *ctx, char *val, uint len, int type ATTR_UNUSED )
+{
+ if (!ctx->delimiter[0] && val)
+ ctx->delimiter[0] = len > 0 ? val[0] : 0;
+ return LIST_OK;
}
static int
-parse_list_rsp_p1( imap_store_t *ctx, list_t *list )
+list2_rsp_done( imap_store_t *ctx, int sts )
{
- if (!is_opt_atom( list ))
- return parse_list_perror( ctx );
- if (!ctx->delimiter[0] && is_atom( list ))
- ctx->delimiter[0] = list->val[0];
- return parse_next_list( ctx, parse_list_rsp_p2 );
+ if (sts != LIST_OK)
+ return sts;
+ return parse_next_list( ctx, &list3_rsp_cb );
}
+static parse_list_cb_t list2_rsp_cb = {
+ .enter = list2_rsp_enter,
+ .atom = list2_rsp_atom,
+ .done = list2_rsp_done,
+};
+
// Use this to check whether a full path refers to the actual IMAP INBOX.
static int
is_inbox( imap_store_t *ctx, const char *arg, int argl )
@@ -1350,7 +1555,7 @@ is_INBOX( imap_store_t *ctx, const char *arg, int argl )
{
if (!starts_with( arg, argl, "INBOX", 5 ))
return 0;
- if (arg[5] && arg[5] != ctx->delimiter[0])
+ if (argl > 5 && arg[5] != ctx->delimiter[0])
return 0;
return 1;
}
@@ -1363,17 +1568,14 @@ normalize_INBOX( imap_store_t *ctx, char *arg, int argl )
}
static int
-parse_list_rsp_p2( imap_store_t *ctx, list_t *list )
+list3_rsp_atom( imap_store_t *ctx, char *arg, uint len, int type ATTR_UNUSED )
{
string_list_t *narg;
- char *arg, c;
- int argl;
+ int argl = (int)len;
uint l;
- if (!is_atom( list ))
- return parse_list_perror( ctx );
- arg = list->val;
- argl = (int)list->len;
+ if (!arg)
+ return LIST_BAD;
if (argl > 1000) {
warn( "IMAP warning: ignoring unreasonably long mailbox name '%.100s[...]'\n", arg );
return LIST_OK;
@@ -1394,7 +1596,7 @@ parse_list_rsp_p2( imap_store_t *ctx, list_t *list )
// only to the fully uppercased spelling, as our canonical box
// names are case-sensitive (unlike IMAP's INBOX).
if (is_INBOX( ctx, arg, argl )) {
- if (!arg[5]) // No need to complain about subfolders as well.
+ if (argl > 5) // No need to complain about subfolders as well.
warn( "IMAP warning: ignoring INBOX in %s\n", ctx->prefix );
return LIST_OK;
}
@@ -1410,6 +1612,7 @@ parse_list_rsp_p2( imap_store_t *ctx, list_t *list )
// '//' and '/./', and '/../' being forbidden is a limitation of the Maildir
// driver, but there isn't really a legitimate reason for these being present.
for (const char *p = narg->string, *sp = p;;) {
+ char c;
if (!(c = *p) || c == '/') {
uint pcl = (uint)(p - sp);
if (!pcl) {
@@ -1439,6 +1642,11 @@ parse_list_rsp_p2( imap_store_t *ctx, list_t *list )
return LIST_OK;
}
+static parse_list_cb_t list3_rsp_cb = {
+ .enter = list2_rsp_enter, // (sic!)
+ .atom = list3_rsp_atom,
+};
+
static int
prepare_name( char **buf, const imap_store_t *ctx, const char *prefix, const char *name )
{
@@ -1581,10 +1789,10 @@ imap_socket_read( void *aux )
} else if (equals( arg, argl, "CAPABILITY", 10 )) {
parse_capability( ctx, cmd );
} else if (equals( arg, argl, "LIST", 4 ) || equals( arg, argl, "LSUB", 4 )) {
- resp = parse_list( ctx, cmd, parse_list_rsp, "LIST" );
+ resp = parse_list( ctx, cmd, &list_rsp_cb, "LIST" );
goto listret;
} else if (equals( arg, argl, "NAMESPACE", 9 )) {
- resp = parse_list( ctx, cmd, parse_namespace_rsp, "NAMESPACE" );
+ resp = parse_list( ctx, cmd, &namespace_rsp_cb, "NAMESPACE" );
goto listret;
} else if ((arg1 = next_arg( &cmd, &argl1 ))) {
to_upper( arg1, argl1 );
@@ -1601,10 +1809,11 @@ imap_socket_read( void *aux )
} else if (equals( arg1, argl1, "RECENT", 6 )) {
ctx->recent_msgs = atoi( arg );
} else if (equals( arg1, argl1, "FETCH", 5 )) {
+ memset( &ctx->fetch, 0, sizeof(ctx->fetch) );
if (!(seq = strtoul( arg, &arg1, 10 )) || *arg1)
goto badseq;
- ctx->fetch_seq = seq;
- resp = parse_list( ctx, cmd, parse_fetch_rsp, "FETCH" );
+ ctx->fetch.seq = seq;
+ resp = parse_list( ctx, cmd, &fetch_rsp_cb, "FETCH" );
goto listret;
}
} else {