isync

mailbox synchronization program
git clone https://git.code.sf.net/p/isync/isync
Log | Files | Refs | README | LICENSE

commit 2585dd3324d3d324595305dbef3157303de1c88e
parent 4f94197e41b6441f36047405aa4b63a478f79273
Author: Oswald Buddenhagen <ossi@users.sf.net>
Date:   Sat, 11 Aug 2012 18:34:46 +0200

add support for hierarchical mailboxes

Diffstat:
Mconfigure.in | 2+-
Msrc/drv_imap.c | 142++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/drv_maildir.c | 139++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/isync.h | 13+++++++++++--
Msrc/main.c | 11+++++++++--
Msrc/mbsync.1 | 13+++++++++++++
Msrc/util.c | 36++++++++++++++++++++++++++++++++++++
7 files changed, 283 insertions(+), 73 deletions(-)

diff --git a/configure.in b/configure.in @@ -12,7 +12,7 @@ fi CPPFLAGS="$CPPFLAGS -D_GNU_SOURCE" AC_CHECK_HEADERS(sys/poll.h sys/select.h) -AC_CHECK_FUNCS(vasprintf) +AC_CHECK_FUNCS(vasprintf memrchr) AC_CHECK_LIB(socket, socket, [SOCK_LIBS="-lsocket"]) AC_CHECK_LIB(nsl, inet_ntoa, [SOCK_LIBS="$SOCK_LIBS -lnsl"]) diff --git a/src/drv_imap.c b/src/drv_imap.c @@ -50,6 +50,7 @@ typedef struct imap_store_conf { store_conf_t gen; imap_server_conf_t *server; unsigned use_namespace:1; + char delimiter; } imap_store_conf_t; typedef struct imap_message { @@ -82,6 +83,7 @@ typedef struct imap_store { /* trash folder's existence is not confirmed yet */ enum { TrashUnknown, TrashChecking, TrashKnown } trashnc; unsigned got_namespace:1; + char delimiter; /* hierarchy delimiter */ list_t *ns_personal, *ns_other, *ns_shared; /* NAMESPACE info */ message_t **msgapp; /* FETCH results */ unsigned caps; /* CAPABILITY results */ @@ -808,21 +810,63 @@ parse_list_rsp( imap_store_t *ctx, char *cmd ) return; } free_list( list ); - (void) next_arg( &cmd ); /* skip delimiter */ arg = next_arg( &cmd ); - l = strlen( ctx->gen.conf->path ); - if (memcmp( arg, ctx->gen.conf->path, l )) - return; - arg += l; - if (l && !strcmp( arg, "INBOX" )) { - warn( "IMAP warning: ignoring INBOX in %s\n", ctx->gen.conf->path ); - return; + if (!ctx->delimiter) + ctx->delimiter = *arg; + arg = next_arg( &cmd ); + if (memcmp( arg, "INBOX", 5 ) || (arg[5] && arg[5] != ctx->delimiter)) { + l = strlen( ctx->gen.conf->path ); + if (memcmp( arg, ctx->gen.conf->path, l )) + return; + arg += l; + if (!memcmp( arg, "INBOX", 5 ) && (!arg[5] || arg[5] == ctx->delimiter)) { + if (!arg[5]) + warn( "IMAP warning: ignoring INBOX in %s\n", ctx->gen.conf->path ); + return; + } } if (!memcmp( arg + strlen( arg ) - 5, ".lock", 5 )) /* workaround broken servers */ return; + if (map_name( arg, ctx->delimiter, '/') < 0) { + warn( "IMAP warning: ignoring mailbox %s (reserved character '/' in name)\n", arg ); + return; + } add_string_list( &ctx->gen.boxes, arg ); } +static int +prepare_name( char *buf, const imap_store_t *ctx, const char *prefix, const char *name ) +{ + int pl; + + nfsnprintf( buf, 1024, "%s%n%s", prefix, &pl, name ); + switch (map_name( buf + pl, '/', ctx->delimiter )) { + case -1: + error( "IMAP error: mailbox name %s contains server's hierarchy delimiter\n", buf + pl ); + return -1; + case -2: + error( "IMAP error: server's hierarchy delimiter not known\n" ); + return -1; + default: + return 0; + } +} + +static int +prepare_box( char *buf, const imap_store_t *ctx ) +{ + const char *name = ctx->gen.name; + + return prepare_name( buf, ctx, + (!memcmp( name, "INBOX", 5 ) && (!name[5] || name[5] == '/')) ? "" : ctx->prefix, name ); +} + +static int +prepare_trash( char *buf, const imap_store_t *ctx ) +{ + return prepare_name( buf, ctx, ctx->prefix, ctx->gen.conf->trash ); +} + struct imap_cmd_trycreate { struct imap_cmd gen; struct imap_cmd *orig_cmd; @@ -1157,6 +1201,7 @@ imap_open_store( store_conf_t *conf, ctx->gen.boxes = 0; ctx->gen.listed = 0; ctx->gen.conf = conf; + ctx->delimiter = 0; ctx->callbacks.imap_open = cb; ctx->callback_aux = aux; set_bad_callback( &ctx->gen, (void (*)(void *))imap_open_store_bail, ctx ); @@ -1367,10 +1412,9 @@ imap_open_store_namespace( imap_store_t *ctx ) { imap_store_conf_t *cfg = (imap_store_conf_t *)ctx->gen.conf; - ctx->prefix = ""; - if (*cfg->gen.path) - ctx->prefix = cfg->gen.path; - else if (cfg->use_namespace && CAP(NAMESPACE)) { + ctx->prefix = cfg->gen.path; + ctx->delimiter = cfg->delimiter; + if (((!*ctx->prefix && cfg->use_namespace) || !cfg->delimiter) && CAP(NAMESPACE)) { /* get NAMESPACE info */ if (!ctx->got_namespace) imap_exec( ctx, 0, imap_open_store_namespace_p2, "NAMESPACE" ); @@ -1395,11 +1439,20 @@ imap_open_store_namespace_p2( imap_store_t *ctx, struct imap_cmd *cmd ATTR_UNUSE static void imap_open_store_namespace2( imap_store_t *ctx ) { - /* XXX for now assume personal namespace */ - if (is_list( ctx->ns_personal ) && - is_list( ctx->ns_personal->child ) && - is_atom( ctx->ns_personal->child->child )) - ctx->prefix = ctx->ns_personal->child->child->val; + imap_store_conf_t *cfg = (imap_store_conf_t *)ctx->gen.conf; + list_t *nsp, *nsp_1st, *nsp_1st_ns, *nsp_1st_dl; + + /* XXX for now assume 1st personal namespace */ + if (is_list( (nsp = ctx->ns_personal) ) && + is_list( (nsp_1st = nsp->child) ) && + is_atom( (nsp_1st_ns = nsp_1st->child) ) && + is_atom( (nsp_1st_dl = nsp_1st_ns->next) )) + { + if (!*ctx->prefix && cfg->use_namespace) + ctx->prefix = nsp_1st_ns->val; + if (!ctx->delimiter) + ctx->delimiter = *nsp_1st_dl->val; + } imap_open_store_finalize( ctx ); } @@ -1446,15 +1499,14 @@ imap_select( store_t *gctx, int create, { imap_store_t *ctx = (imap_store_t *)gctx; struct imap_cmd_simple *cmd; - const char *prefix; + char buf[1024]; free_generic_messages( gctx->msgs ); gctx->msgs = 0; - if (!strcmp( gctx->name, "INBOX" )) { - prefix = ""; - } else { - prefix = ctx->prefix; + if (prepare_box( buf, ctx ) < 0) { + cb( DRV_BOX_BAD, aux ); + return; } ctx->gen.uidnext = 0; @@ -1463,7 +1515,7 @@ imap_select( store_t *gctx, int create, cmd->gen.param.create = create; cmd->gen.param.trycreate = 1; imap_exec( ctx, &cmd->gen, imap_done_simple_box, - "SELECT \"%s%s\"", prefix, gctx->name ); + "SELECT \"%s\"", buf ); } /******************* imap_load *******************/ @@ -1636,13 +1688,17 @@ imap_trash_msg( store_t *gctx, message_t *msg, { imap_store_t *ctx = (imap_store_t *)gctx; struct imap_cmd_simple *cmd; + char buf[1024]; INIT_IMAP_CMD(imap_cmd_simple, cmd, cb, aux) cmd->gen.param.create = 1; cmd->gen.param.to_trash = 1; + if (prepare_trash( buf, ctx ) < 0) { + cb( DRV_BOX_BAD, aux ); + return; + } imap_exec( ctx, &cmd->gen, imap_done_simple_msg, - "UID COPY %d \"%s%s\"", - msg->uid, ctx->prefix, gctx->conf->trash ); + "UID COPY %d \"%s\"", msg->uid, buf ); } /******************* imap_store_msg *******************/ @@ -1655,9 +1711,8 @@ imap_store_msg( store_t *gctx, msg_data_t *data, int to_trash, { imap_store_t *ctx = (imap_store_t *)gctx; struct imap_cmd_out_uid *cmd; - const char *prefix, *box; int d; - char flagstr[128]; + char flagstr[128], buf[1024]; d = 0; if (data->flags) { @@ -1672,16 +1727,20 @@ imap_store_msg( store_t *gctx, msg_data_t *data, int to_trash, cmd->out_uid = -2; if (to_trash) { - box = gctx->conf->trash; - prefix = ctx->prefix; cmd->gen.param.create = 1; cmd->gen.param.to_trash = 1; + if (prepare_trash( buf, ctx ) < 0) { + cb( DRV_BOX_BAD, -1, aux ); + return; + } } else { - box = gctx->name; - prefix = !strcmp( box, "INBOX" ) ? "" : ctx->prefix; + if (prepare_box( buf, ctx ) < 0) { + cb( DRV_BOX_BAD, -1, aux ); + return; + } } imap_exec( ctx, &cmd->gen, imap_store_msg_p2, - "APPEND \"%s%s\" %s", prefix, box, flagstr ); + "APPEND \"%s\" %s", buf, flagstr ); } static void @@ -1710,15 +1769,20 @@ imap_find_new_msgs( store_t *gctx, /******************* imap_list *******************/ static void -imap_list( store_t *gctx, +imap_list( store_t *gctx, int flags, void (*cb)( int sts, void *aux ), void *aux ) { imap_store_t *ctx = (imap_store_t *)gctx; - struct imap_cmd_simple *cmd; - - INIT_IMAP_CMD(imap_cmd_simple, cmd, cb, aux) - imap_exec( ctx, &cmd->gen, imap_done_simple_box, - "LIST \"\" \"%s%%\"", ctx->prefix ); + struct imap_cmd_refcounted_state *sts = imap_refcounted_new_state( cb, aux ); + + if (((flags & LIST_PATH) && + imap_exec( ctx, imap_refcounted_new_cmd( sts ), imap_refcounted_done_box, + "LIST \"\" \"%s*\"", ctx->prefix ) < 0) || + ((flags & LIST_INBOX) && (!(flags & LIST_PATH) || *ctx->prefix) && + imap_exec( ctx, imap_refcounted_new_cmd( sts ), imap_refcounted_done_box, + "LIST \"\" INBOX*" ) < 0)) + {} + imap_refcounted_done( sts ); } /******************* imap_cancel *******************/ @@ -1853,6 +1917,8 @@ imap_parse_store( conffile_t *cfg, store_conf_t **storep, int *err ) store->use_namespace = parse_bool( cfg ); else if (!strcasecmp( "Path", cfg->cmd )) store->gen.path = nfstrdup( cfg->val ); + else if (!strcasecmp( "PathDelimiter", cfg->cmd )) + store->delimiter = *cfg->val; else parse_generic_store( &store->gen, cfg, err ); continue; diff --git a/src/drv_maildir.c b/src/drv_maildir.c @@ -94,6 +94,29 @@ maildir_parse_flags( const char *base ) return flags; } +static char * +maildir_join_path( const char *prefix, const char *box ) +{ + char *out, *p; + int pl, bl, n; + char c; + + pl = strlen( prefix ); + for (bl = 0, n = 0; (c = box[bl]); bl++) + if (c == '/') + n++; + out = nfmalloc( pl + bl + n + 1 ); + memcpy( out, prefix, pl ); + p = out + pl; + while ((c = *box++)) { + *p++ = c; + if (c == '/') + *p++ = '.'; + } + *p = 0; + return out; +} + static void maildir_open_store( store_conf_t *conf, void (*cb)( store_t *ctx, void *aux ), void *aux ) @@ -109,7 +132,8 @@ maildir_open_store( store_conf_t *conf, ctx = nfcalloc( sizeof(*ctx) ); ctx->gen.conf = conf; ctx->uvfd = -1; - nfasprintf( &ctx->trash, "%s%s", conf->path, conf->trash ); + if (conf->trash) + ctx->trash = maildir_join_path( conf->path, conf->trash ); cb( &ctx->gen, aux ); } @@ -168,40 +192,87 @@ maildir_invoke_bad_callback( store_t *ctx ) ctx->bad_callback( ctx->bad_callback_aux ); } -static void -maildir_list( store_t *gctx, - void (*cb)( int sts, void *aux ), void *aux ) +static int maildir_list_part( store_t *gctx, int doInbox, int *flags ); + +static int +maildir_list_recurse( store_t *gctx, int isBox, int *flags, const char *inbox, + char *path, int pathLen, char *name, int nameLen ) { DIR *dir; + int pl, nl; struct dirent *de; + struct stat st; - if (!(dir = opendir( gctx->conf->path ))) { - sys_error( "Maildir error: cannot list %s", gctx->conf->path ); - maildir_invoke_bad_callback( gctx ); - cb( DRV_CANCELED, aux ); - return; + if (isBox) { + nfsnprintf( path + pathLen, _POSIX_PATH_MAX - pathLen, "/cur" ); + if (stat( path, &st ) || !S_ISDIR(st.st_mode)) + return 0; + path[pathLen] = 0; + add_string_list( &gctx->boxes, name ); + name[nameLen++] = '/'; + } + if (!(dir = opendir( path ))) { + sys_error( "Maildir error: cannot list %s", path ); + return -1; } while ((de = readdir( dir ))) { - const char *inbox = ((maildir_store_conf_t *)gctx->conf)->inbox; - int bl, isibx; - struct stat st; - char buf[PATH_MAX]; - - if (*de->d_name == '.') - continue; - bl = nfsnprintf( buf, sizeof(buf), "%s%s/cur", gctx->conf->path, de->d_name ); - if (stat( buf, &st ) || !S_ISDIR(st.st_mode)) - continue; - isibx = !memcmp( buf, inbox, bl - 4 ) && !inbox[bl - 4]; - if (!isibx && !strcmp( de->d_name, "INBOX" )) { - warn( "Maildir warning: ignoring INBOX in %s\n", gctx->conf->path ); - continue; + const char *ent = de->d_name; + pl = pathLen + nfsnprintf( path + pathLen, _POSIX_PATH_MAX - pathLen, "%s", ent ); + if (inbox && !memcmp( path, inbox, pl ) && !inbox[pl]) { + if (maildir_list_part( gctx, 1, flags ) < 0) + return -1; + } else { + if (!memcmp( ent, "INBOX", 6 )) { + path[pathLen] = 0; + warn( "Maildir warning: ignoring INBOX in %s\n", path ); + continue; + } + if (*ent == '.') { + if (!isBox) + continue; + ent++; + } else { + if (isBox) + continue; + } + nl = nameLen + nfsnprintf( name + nameLen, _POSIX_PATH_MAX - nameLen, "%s", ent ); + if (maildir_list_recurse( gctx, 1, flags, inbox, path, pl, name, nl ) < 0) + return -1; } - add_string_list( &gctx->boxes, isibx ? "INBOX" : de->d_name ); } closedir (dir); + return 0; +} - cb( DRV_OK, aux ); +static int +maildir_list_part( store_t *gctx, int doInbox, int *flags ) +{ + int pl, nl; + const char *inbox = ((maildir_store_conf_t *)gctx->conf)->inbox; + char path[_POSIX_PATH_MAX], name[_POSIX_PATH_MAX]; + + if (doInbox) { + *flags &= ~LIST_INBOX; + pl = nfsnprintf( path, _POSIX_PATH_MAX, "%s", inbox ); + nl = nfsnprintf( name, _POSIX_PATH_MAX, "INBOX" ); + return maildir_list_recurse( gctx, 1, flags, 0, path, pl, name, nl ); + } else { + pl = nfsnprintf( path, _POSIX_PATH_MAX, "%s", gctx->conf->path ); + return maildir_list_recurse( gctx, 0, flags, inbox, path, pl, name, 0 ); + } +} + +static void +maildir_list( store_t *gctx, int flags, + void (*cb)( int sts, void *aux ), void *aux ) +{ + if (((flags & LIST_PATH) && maildir_list_part( gctx, 0, &flags ) < 0) || + ((flags & LIST_INBOX) && maildir_list_part( gctx, 1, &flags ) < 0)) { + maildir_invoke_bad_callback( gctx ); + cb( DRV_CANCELED, aux ); + } else { + cb( DRV_OK, aux ); + } } static const char *subdirs[] = { "cur", "new", "tmp" }; @@ -237,8 +308,9 @@ maildir_validate( const char *box, int create, maildir_store_t *ctx ) { DIR *dirp; struct dirent *entry; + char *p; time_t now; - int i, bl; + int i, bl, ret; struct stat st; char buf[_POSIX_PATH_MAX]; @@ -246,6 +318,13 @@ maildir_validate( const char *box, int create, maildir_store_t *ctx ) if (stat( buf, &st )) { if (errno == ENOENT) { if (create) { + p = memrchr( buf, '/', bl - 1 ); + if (*(p + 1) == '.') { + *p = 0; + if ((ret = maildir_validate( buf, 1, ctx )) != DRV_OK) + return ret; + *p = '/'; + } if (mkdir( buf, 0700 )) { sys_error( "Maildir error: cannot create mailbox '%s'", buf ); maildir_invoke_bad_callback( &ctx->gen ); @@ -822,10 +901,10 @@ maildir_select( store_t *gctx, int create, #ifdef USE_DB ctx->db = 0; #endif /* USE_DB */ - if (!strcmp( gctx->name, "INBOX" )) - gctx->path = nfstrdup( ((maildir_store_conf_t *)gctx->conf)->inbox ); - else - nfasprintf( &gctx->path, "%s%s", gctx->conf->path, gctx->name ); + gctx->path = + (!memcmp( gctx->name, "INBOX", 5 ) && (!gctx->name[5] || gctx->name[5] == '/')) ? + maildir_join_path( ((maildir_store_conf_t *)gctx->conf)->inbox, gctx->name + 5 ) : + maildir_join_path( gctx->conf->path, gctx->name ); if ((ret = maildir_validate( gctx->path, create, ctx )) != DRV_OK) { cb( ret, aux ); diff --git a/src/isync.h b/src/isync.h @@ -259,6 +259,9 @@ typedef struct { */ #define DRV_CRLF 1 +#define LIST_PATH 1 +#define LIST_INBOX 2 + struct driver { int flags; @@ -283,8 +286,8 @@ struct driver { * Pending commands will have their callbacks synchronously invoked with DRV_CANCELED. */ void (*cancel_store)( store_t *ctx ); - /* List the mailboxes in this store. */ - void (*list)( store_t *ctx, + /* List the mailboxes in this store. Flags are ORed LIST_* values. */ + void (*list)( store_t *ctx, int flags, void (*cb)( int sts, void *aux ), void *aux ); /* Invoked before select(), this informs the driver which operations (OP_*) @@ -415,6 +418,10 @@ void free_string_list( string_list_t *list ); void free_generic_messages( message_t * ); +#ifndef HAVE_MEMRCHR +void *memrchr( const void *s, int c, size_t n ); +#endif + void *nfmalloc( size_t sz ); void *nfcalloc( size_t sz ); void *nfrealloc( void *mem, size_t sz ); @@ -426,6 +433,8 @@ void ATTR_NORETURN oob( void ); char *expand_strdup( const char *s ); +int map_name( char *arg, char in, char out ); + void sort_ints( int *arr, int len ); void arc4_init( void ); diff --git a/src/main.c b/src/main.c @@ -129,7 +129,7 @@ matches( const char *t, const char *p ) } else if (*p == '%') { p++; do { - if (*t == '.' || *t == '/') /* this is "somewhat" hacky ... */ + if (*t == '/') return 0; if (matches( t, p )) return 1; @@ -690,6 +690,8 @@ static void store_opened( store_t *ctx, void *aux ) { MVARS(aux) + string_list_t *cpat; + int flags; if (!ctx) { mvars->ret = mvars->skip = 1; @@ -699,8 +701,13 @@ store_opened( store_t *ctx, void *aux ) } mvars->ctx[t] = ctx; if (!mvars->skip && !mvars->boxlist && mvars->chan->patterns && !ctx->listed) { + for (flags = 0, cpat = mvars->chan->patterns; cpat; cpat = cpat->next) { + const char *pat = cpat->string; + if (*pat != '!') + flags |= (!memcmp( pat, "INBOX", 5 ) && (!pat[5] || pat[5] == '/')) ? LIST_INBOX : LIST_PATH; + } set_bad_callback( ctx, store_bad, AUX ); - mvars->drv[t]->list( ctx, store_listed, AUX ); + mvars->drv[t]->list( ctx, flags, store_listed, AUX ); } else { mvars->state[t] = ST_OPEN; sync_chans( mvars, E_OPEN ); diff --git a/src/mbsync.1 b/src/mbsync.1 @@ -105,6 +105,13 @@ There are two auxiliary object classes: Accounts and Groups. An Account describes the connection part of remote Stores, so a server connection can be shared between multiple Stores. A Group aggregates multiple Channels to save typing on the command line. +.P +File system locations (in particular, \fBPath\fR and \fBInbox\fR) use the +Store's internal path separators, which may be slashes, periods, etc., or +even combinations thereof. +.br +Mailbox names, OTOH, always use canonical path separators, which are +Unix-like forward slashes. .. .SS All Stores These options can be used in all supported Store types. @@ -140,6 +147,7 @@ If \fIsize\fR is 0, the maximum message size is \fBunlimited\fR. Create a virtual mailbox (relative to \fBPath\fR), which is backed by the \fBINBOX\fR. Makes sense in conjunction with \fBPatterns\fR in the Channels section. +This virtual mailbox does not support subfolders. .. .TP \fBTrash\fR \fImailbox\fR @@ -306,6 +314,11 @@ mailbox names. Disabling this makes sense for some broken IMAP servers. This option is meaningless if a \fBPath\fR was specified. (Default: \fIyes\fR) .. +.TP +\fBPathDelimiter\fR \fIdelim\fR +Specify the server's hierarchy delimiter character. +(Default: taken from the server's first "personal" NAMESPACE) +.. .SS Channels .TP \fBChannel\fR \fIname\fR diff --git a/src/util.c b/src/util.c @@ -229,6 +229,19 @@ vasprintf( char **strp, const char *fmt, va_list ap ) } #endif +#ifndef HAVE_MEMRCHR +void * +memrchr( const void *s, int c, size_t n ) +{ + u_char *b = (u_char *)s, *e = b + n; + + while (--e >= b) + if (*e == c) + return (void *)e; + return 0; +} +#endif + void oob( void ) { @@ -378,6 +391,29 @@ expand_strdup( const char *s ) return nfstrdup( s ); } +/* Return value: 0 = ok, -1 = out found in arg, -2 = in found in arg but no out specified */ +int +map_name( char *arg, char in, char out ) +{ + int l, k; + + if (!in || in == out) + return 0; + for (l = 0; arg[l]; l++) + if (arg[l] == in) { + if (!out) + return -2; + arg[l] = out; + } else if (arg[l] == out) { + /* restore original name for printing error message */ + for (k = 0; k < l; k++) + if (arg[k] == out) + arg[k] = in; + return -1; + } + return 0; +} + static int compare_ints( const void *l, const void *r ) {