add support for hierarchical mailboxes

This commit is contained in:
Oswald Buddenhagen 2012-08-11 18:34:46 +02:00
parent 4f94197e41
commit 2585dd3324
7 changed files with 282 additions and 72 deletions

View File

@ -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"])

View File

@ -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;
struct imap_cmd_refcounted_state *sts = imap_refcounted_new_state( cb, aux );
INIT_IMAP_CMD(imap_cmd_simple, cmd, cb, aux)
imap_exec( ctx, &cmd->gen, imap_done_simple_box,
"LIST \"\" \"%s%%\"", ctx->prefix );
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;

View File

@ -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 );

View File

@ -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 );

View File

@ -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 );

View File

@ -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

View File

@ -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 )
{