isync/src/drv_maildir.c
Oswald Buddenhagen 4e25fd59c1 fix possible attempts to set flags of M_DEAD messages
so far, we checked for M_DEAD only in loops over messages. but we should
have checked srec->msg uses as well. this would make the code a mess, so
instead call back from the drivers when messages are expunged, so we can
reset the pointers.

the only case where this really matters so far is the flag setting loop,
which may cause the concurrent expunge of not yet handled messages to be
detected by the maildir driver.
2022-06-19 16:03:44 +02:00

1951 lines
52 KiB
C

// SPDX-FileCopyrightText: 2000-2002 Michael R. Elkins <me@mutt.org>
// SPDX-FileCopyrightText: 2002-2022 Oswald Buddenhagen <ossi@users.sf.net>
// SPDX-FileCopyrightText: 2004 Theodore Y. Ts'o <tytso@mit.edu>
// SPDX-License-Identifier: GPL-2.0-or-later WITH LicenseRef-isync-GPL-exception
/*
* mbsync - mailbox synchronizer
*/
#define DEBUG_FLAG DEBUG_MAILDIR
#include "driver.h"
#include <ctype.h>
#include <dirent.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <errno.h>
#include <utime.h>
#if !defined(_POSIX_SYNCHRONIZED_IO) || _POSIX_SYNCHRONIZED_IO <= 0
# define fdatasync fsync
#endif
#ifdef USE_DB
#include <db.h>
#endif /* USE_DB */
#define SUB_UNSET 0
#define SUB_VERBATIM 1
#define SUB_MAILDIRPP 2
#define SUB_LEGACY 3
typedef union maildir_store_conf {
store_conf_t gen;
struct {
STORE_CONF
char *path;
char *inbox;
#ifdef USE_DB
int alt_map;
#endif /* USE_DB */
char info_delimiter;
char sub_style;
char failed;
char *info_prefix, *info_stop; /* precalculated from info_delimiter */
};
} maildir_store_conf_t;
typedef union maildir_message {
message_t gen;
struct {
MESSAGE(union maildir_message)
char *base;
};
} maildir_message_t;
typedef union maildir_store {
store_t gen;
struct {
STORE(union maildir_store)
int uvfd, uvok, is_inbox, fresh[3];
uint opts, minuid, maxuid, finduid, pairuid, newuid, uidvalidity, nuid;
uint_array_t excs;
char *path; // own
char *trash;
#ifdef USE_DB
DB *db;
char *usedb;
#endif /* USE_DB */
string_list_t *boxes; // _list results
char listed; // was _list already run with these flags?
// note that the message counts do _not_ reflect stats from msgs,
// but mailbox totals. also, don't trust them beyond the initial load.
int total_msgs, recent_msgs;
maildir_message_t *msgs;
wakeup_t lcktmr;
void (*expunge_callback)( message_t *msg, void *aux );
void (*bad_callback)( void *aux );
void *callback_aux;
};
} maildir_store_t;
#ifdef USE_DB
static DBT key, value; /* no need to be reentrant, and this saves lots of memset()s */
#endif /* USE_DB */
static struct flock lck;
static int MaildirCount;
static uchar
maildir_parse_flags( const char *info_prefix, const char *base )
{
const char *s;
uint i;
uchar flags;
flags = 0;
if ((s = strstr( base, info_prefix )))
for (s += 3, i = 0; i < as(MsgFlags); i++)
if (strchr( s, MsgFlags[i] ))
flags |= (1 << i);
return flags;
}
static int
maildir_ensure_path( maildir_store_conf_t *conf )
{
if (!conf->path) {
error( "Maildir error: store '%s' has no Path\n", conf->name );
conf->failed = FAIL_FINAL;
return -1;
}
return 0;
}
/* Subdirs of INBOX include a leading slash. */
/* Path includes a trailing slash, Inbox does not. */
static char *
maildir_join_path( maildir_store_conf_t *conf, int in_inbox, const char *box )
{
char *out, *p;
const char *prefix;
uint pl, bl, n;
char c;
if (in_inbox || conf->sub_style == SUB_MAILDIRPP) {
prefix = conf->inbox;
} else {
if (maildir_ensure_path( conf ) < 0)
return NULL;
prefix = conf->path;
}
pl = strlen( prefix );
for (bl = 0, n = 0; (c = box[bl]); bl++) {
if (c == '/') {
if (conf->sub_style == SUB_UNSET) {
error( "Maildir error: accessing subfolder '%s', but store '%s' does not specify SubFolders style\n",
box, conf->name );
return NULL;
}
n++;
} else if (c == '.' && conf->sub_style == SUB_MAILDIRPP) {
error( "Maildir error: store '%s', folder '%s': SubFolders style Maildir++ does not support dots in mailbox names\n",
conf->name, box );
return NULL;
}
}
switch (conf->sub_style) {
case SUB_VERBATIM:
n = 0;
break;
case SUB_MAILDIRPP:
n = 2;
break;
default: /* SUB_LEGACY and SUB_UNSET */
break;
}
out = nfmalloc( pl + bl + n + 1 );
memcpy( out, prefix, pl );
p = out + pl;
if (conf->sub_style == SUB_MAILDIRPP) {
*p++ = '/';
*p++ = '.';
}
while ((c = *box++)) {
if (c == '/') {
switch (conf->sub_style) {
case SUB_VERBATIM:
*p++ = c;
break;
case SUB_LEGACY:
*p++ = c;
FALLTHROUGH
default: /* SUB_MAILDIRPP */
*p++ = '.';
break;
}
} else {
*p++ = c;
}
}
*p = 0;
return out;
}
static int
maildir_validate_path( maildir_store_conf_t *conf )
{
struct stat st;
if (stat( conf->path, &st ) || !S_ISDIR(st.st_mode)) {
error( "Maildir error: cannot open store '%s'\n", conf->path );
conf->failed = FAIL_FINAL;
return -1;
}
return 0;
}
static void lcktmr_timeout( void *aux );
static store_t *
maildir_alloc_store( store_conf_t *gconf, const char *label ATTR_UNUSED )
{
maildir_store_t *ctx;
ctx = nfzalloc( sizeof(*ctx) );
ctx->driver = &maildir_driver;
ctx->gen.conf = gconf;
ctx->uvfd = -1;
init_wakeup( &ctx->lcktmr, lcktmr_timeout, ctx );
return &ctx->gen;
}
static void
maildir_connect_store( store_t *gctx,
void (*cb)( int sts, void *aux ), void *aux )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
maildir_store_conf_t *conf = ctx->conf;
if (conf->path && maildir_validate_path( conf ) < 0) {
cb( DRV_STORE_BAD, aux );
return;
}
if (conf->trash && !(ctx->trash = maildir_join_path( conf, 0, conf->trash ))) {
cb( DRV_STORE_BAD, aux );
return;
}
cb( DRV_OK, aux );
}
static void
free_maildir_messages( maildir_message_t *msg )
{
while (msg) {
maildir_message_t *tmsg = msg->next;
free( msg->base );
free( msg->msgid );
free( msg );
msg = tmsg;
}
}
static void
maildir_cleanup( store_t *gctx )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
free_maildir_messages( ctx->msgs );
#ifdef USE_DB
if (ctx->db)
ctx->db->close( ctx->db, 0 );
free( ctx->usedb );
#endif /* USE_DB */
free( ctx->path );
free( ctx->excs.data );
if (ctx->uvfd >= 0)
close( ctx->uvfd );
conf_wakeup( &ctx->lcktmr, -1 );
}
static void
maildir_free_store( store_t *gctx )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
maildir_cleanup( gctx );
wipe_wakeup( &ctx->lcktmr );
free( ctx->trash );
free_string_list( ctx->boxes );
free( gctx );
}
static void
maildir_cleanup_drv( void )
{
}
static void
maildir_set_callbacks( store_t *gctx, void (*exp_cb)( message_t *, void * ),
void (*bad_cb)( void * ), void *aux )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
ctx->expunge_callback = exp_cb;
ctx->bad_callback = bad_cb;
ctx->callback_aux = aux;
}
static void
maildir_invoke_bad_callback( maildir_store_t *ctx )
{
ctx->bad_callback( ctx->callback_aux );
}
static int
maildir_list_maildirpp( maildir_store_t *ctx, int flags, const char *inbox )
{
DIR *dir;
struct dirent *de;
int warned = 0;
struct stat st;
if (ctx->listed & LIST_PATH) // Implies LIST_INBOX
return 0;
if (!(ctx->listed & LIST_INBOX))
add_string_list( &ctx->boxes, "INBOX" );
char path[_POSIX_PATH_MAX];
int pathLen = nfsnprintf( path, _POSIX_PATH_MAX, "%s/", inbox );
if (!(dir = opendir( path ))) {
if (errno == ENOENT || errno == ENOTDIR)
return 0;
sys_error( "Maildir error: cannot list %s", path );
return -1;
}
while ((de = readdir( dir ))) {
const char *ent = de->d_name;
if (*ent++ != '.' || !*ent)
continue;
char name[_POSIX_PATH_MAX];
char *effName = name;
if (*ent == '.') {
if (ctx->listed & LIST_INBOX)
continue;
if (!*++ent)
continue;
// The Maildir++ Inbox is technically not under Path (as there is none), so
// "*" would never match INBOX*, which is rather unintuitive. Matching INBOX*
// implicitly instead makes it consistent with an IMAP Store with an empty Path.
} else {
if (!(flags & (LIST_PATH | LIST_PATH_MAYBE)))
continue;
// Explained in maildir_list_recurse().
if (starts_with( ent, -1, "INBOX", 5 ) && (!ent[5] || ent[5] == '.')) {
if (!warned) {
warned = 1;
path[pathLen] = 0;
warn( "Maildir warning: ignoring INBOX in %s\n", path );
}
continue;
}
effName += 6;
}
nfsnprintf( path + pathLen, _POSIX_PATH_MAX - pathLen, "%s/cur", de->d_name );
if (!stat( path, &st ) && S_ISDIR(st.st_mode)) {
int nl = nfsnprintf( name, _POSIX_PATH_MAX, "INBOX/%s", ent );
for (int i = 6; i < nl; i++) {
if (name[i] == '.')
name[i] = '/';
}
add_string_list( &ctx->boxes, effName );
}
}
closedir (dir);
if (flags & (LIST_PATH | LIST_PATH_MAYBE))
ctx->listed |= LIST_PATH;
ctx->listed |= LIST_INBOX;
return 0;
}
static int maildir_list_inbox( maildir_store_t *ctx, int flags, const char *basePath );
static int maildir_list_path( maildir_store_t *ctx, int flags, const char *inbox );
static int
maildir_list_recurse( maildir_store_t *ctx, int isBox, int flags, int depth,
const char *inbox, uint inboxLen, const char *basePath, uint basePathLen,
char *path, int pathLen, char *name, int nameLen )
{
DIR *dir;
int style = ctx->conf->sub_style;
int pl, nl;
struct dirent *de;
struct stat st;
if (!(dir = opendir( path ))) {
if (isBox && (errno == ENOENT || errno == ENOTDIR))
return 0;
sys_error( "Maildir error: cannot list %s", path );
return -1;
}
if (isBox > 1 && style == SUB_UNSET) {
error( "Maildir error: found subfolder '%.*s', but store '%s' does not specify SubFolders style\n",
nameLen - 1, name, ctx->conf->name );
closedir( dir );
return -1;
}
if (++depth > 10) {
// We do the other checks first to avoid confusing error messages for files.
error( "Maildir error: path %s is too deeply nested. Symlink loop?\n", path );
closedir( dir );
return -1;
}
while ((de = readdir( dir ))) {
const char *ent = de->d_name;
if (ent[0] == '.' && (!ent[1] || (ent[1] == '.' && !ent[2])))
continue;
pl = nfsnprintf( path + pathLen, _POSIX_PATH_MAX - pathLen, "%s", ent );
if (pl == 3 && (!memcmp( ent, "cur", 3 ) || !memcmp( ent, "new", 3 ) || !memcmp( ent, "tmp", 3 )))
continue;
pl += pathLen;
if (inbox && equals( path, pl, inbox, inboxLen )) {
// Inbox nested into Path.
if (maildir_list_inbox( ctx, flags, NULL ) < 0) {
closedir( dir );
return -1;
}
} else if (basePath && equals( path, pl, basePath, basePathLen )) {
// Path nested into Inbox.
if (maildir_list_path( ctx, flags, NULL ) < 0) {
closedir( dir );
return -1;
}
} else {
if (style == SUB_LEGACY) {
if (*ent == '.') {
if (!isBox)
continue;
ent++;
} else {
if (isBox)
continue;
}
}
// A folder named "INBOX" would be indistinguishable from the
// actual INBOX after prefix stripping, so drop it. This applies
// only to the fully uppercased spelling, as our canonical box
// names are case-sensitive (unlike IMAP's INBOX).
if (!nameLen && equals( ent, -1, "INBOX", 5 )) {
path[pathLen] = 0;
warn( "Maildir warning: ignoring INBOX in %s\n", path );
continue;
}
nl = nameLen + nfsnprintf( name + nameLen, _POSIX_PATH_MAX - nameLen, "%s", ent );
path[pl++] = '/';
nfsnprintf( path + pl, _POSIX_PATH_MAX - pl, "cur" );
if (!stat( path, &st ) && S_ISDIR(st.st_mode))
add_string_list( &ctx->boxes, name );
path[pl] = 0;
name[nl++] = '/';
if (maildir_list_recurse( ctx, isBox + 1, flags, depth, inbox, inboxLen, basePath, basePathLen, path, pl, name, nl ) < 0) {
closedir( dir );
return -1;
}
}
}
closedir (dir);
return 0;
}
static int
maildir_list_inbox( maildir_store_t *ctx, int flags, const char *basePath )
{
char path[_POSIX_PATH_MAX], name[_POSIX_PATH_MAX];
if (ctx->listed & LIST_INBOX)
return 0;
ctx->listed |= LIST_INBOX;
add_string_list( &ctx->boxes, "INBOX" );
return maildir_list_recurse(
ctx, 1, flags, 0, NULL, 0, basePath, basePath ? strlen( basePath ) - 1 : 0,
path, nfsnprintf( path, _POSIX_PATH_MAX, "%s/", ctx->conf->inbox ),
name, nfsnprintf( name, _POSIX_PATH_MAX, "INBOX/" ) );
}
static int
maildir_list_path( maildir_store_t *ctx, int flags, const char *inbox )
{
char path[_POSIX_PATH_MAX], name[_POSIX_PATH_MAX];
if (ctx->listed & LIST_PATH)
return 0;
ctx->listed |= LIST_PATH;
if (maildir_ensure_path( ctx->conf ) < 0)
return -1;
return maildir_list_recurse(
ctx, 0, flags, 0, inbox, inbox ? strlen( inbox ) : 0, NULL, 0,
path, nfsnprintf( path, _POSIX_PATH_MAX, "%s", ctx->conf->path ),
name, 0 );
}
static void
maildir_list_store( store_t *gctx, int flags,
void (*cb)( int sts, string_list_t *boxes, void *aux ), void *aux )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
maildir_store_conf_t *conf = ctx->conf;
if (conf->sub_style == SUB_MAILDIRPP
? maildir_list_maildirpp( ctx, flags, conf->inbox ) < 0
: ((((flags & LIST_PATH) || ((flags & LIST_PATH_MAYBE) && conf->path))
&& maildir_list_path( ctx, flags, conf->inbox ) < 0) ||
((flags & LIST_INBOX)
&& maildir_list_inbox( ctx, flags, conf->path ) < 0))) {
maildir_invoke_bad_callback( ctx );
cb( DRV_CANCELED, NULL, aux );
} else {
cb( DRV_OK, ctx->boxes, aux );
}
}
static const char *subdirs[] = { "cur", "new", "tmp" };
typedef struct {
char *base;
char *msgid;
uint size;
uint uid;
uchar recent;
char tuid[TUIDL];
} msg_t;
DEFINE_ARRAY_TYPE(msg_t)
static void
maildir_free_scan( msg_t_array_alloc_t *msglist )
{
uint i;
if (msglist->array.data) {
for (i = 0; i < msglist->array.size; i++) {
free( msglist->array.data[i].base );
free( msglist->array.data[i].msgid );
}
free( msglist->array.data );
}
}
#define _24_HOURS (3600 * 24)
static int
maildir_clear_tmp( char *buf, int bufsz, int bl )
{
DIR *dirp;
struct dirent *entry;
time_t now;
struct stat st;
memcpy( buf + bl, "tmp/", 5 );
bl += 4;
if (!(dirp = opendir( buf ))) {
sys_error( "Maildir error: cannot list %s", buf );
return DRV_BOX_BAD;
}
time( &now );
while ((entry = readdir( dirp ))) {
nfsnprintf( buf + bl, bufsz - bl, "%s", entry->d_name );
if (stat( buf, &st )) {
if (errno != ENOENT)
sys_error( "Maildir error: cannot access %s", buf );
} else if (S_ISREG(st.st_mode) && now - st.st_ctime >= _24_HOURS) {
/* This should happen infrequently enough that it won't be
* bothersome to the user to display when it occurs.
*/
notice( "Maildir notice: removing stale file %s\n", buf );
if (unlink( buf ) && errno != ENOENT)
sys_error( "Maildir error: cannot remove %s", buf );
}
}
closedir( dirp );
return DRV_OK;
}
static int
maildir_validate( const char *box, int create, maildir_store_t *ctx )
{
int i, bl, ret;
struct stat st;
char buf[_POSIX_PATH_MAX];
bl = nfsnprintf( buf, sizeof(buf) - 4, "%s/", box );
if (stat( buf, &st )) {
if (errno != ENOENT) {
sys_error( "Maildir error: cannot access mailbox '%s'", buf );
return DRV_BOX_BAD;
}
if (!create)
return DRV_BOX_BAD;
if (mkdir_p( buf, bl - 1 )) {
sys_error( "Maildir error: cannot create mailbox '%s'", buf );
ctx->conf->failed = FAIL_FINAL;
maildir_invoke_bad_callback( ctx );
return DRV_CANCELED;
}
} else if (!S_ISDIR(st.st_mode)) {
notdir:
error( "Maildir error: '%s' is no valid mailbox\n", buf );
return DRV_BOX_BAD;
}
for (i = 0; i < 3; i++) {
memcpy( buf + bl, subdirs[i], 4 );
if (stat( buf, &st )) {
/* We always create new/ and tmp/ if they are missing. cur/ is the presence indicator. */
if (!i && !create)
return DRV_BOX_BAD;
if (mkdir( buf, 0700 )) {
sys_error( "Maildir error: cannot create directory %s", buf );
return DRV_BOX_BAD;
}
ctx->fresh[i] = 1;
} else if (!S_ISDIR(st.st_mode)) {
goto notdir;
} else {
if (i == 2) {
if ((ret = maildir_clear_tmp( buf, sizeof(buf), bl )) != DRV_OK)
return ret;
}
}
}
return DRV_OK;
}
#ifdef USE_DB
static void
make_key( const char *info_stop, DBT *tkey, const char *name )
{
char *u = strpbrk( name, info_stop );
DIAG_PUSH
DIAG_DISABLE("-Wcast-qual") // C has no const_cast<> ...
tkey->data = (char *)name;
DIAG_POP
tkey->size = u ? (size_t)(u - name) : strlen( name );
}
#endif /* USE_DB */
static int
maildir_store_uidval( maildir_store_t *ctx )
{
int n;
#ifdef USE_DB
int ret;
uint uv[2];
#endif
char buf[128];
#ifdef USE_DB
if (ctx->db) {
key.data = (void *)"UIDVALIDITY";
key.size = 11;
uv[0] = ctx->uidvalidity;
uv[1] = ctx->nuid;
value.data = uv;
value.size = sizeof(uv);
if ((ret = ctx->db->put( ctx->db, NULL, &key, &value, 0 ))) {
ctx->db->err( ctx->db, ret, "Maildir error: db->put()" );
return DRV_BOX_BAD;
}
if ((ret = ctx->db->sync( ctx->db, 0 ))) {
ctx->db->err( ctx->db, ret, "Maildir error: db->sync()" );
return DRV_BOX_BAD;
}
} else
#endif /* USE_DB */
{
n = sprintf( buf, "%u\n%u\n", ctx->uidvalidity, ctx->nuid );
lseek( ctx->uvfd, 0, SEEK_SET );
if (write( ctx->uvfd, buf, (uint)n ) != n || ftruncate( ctx->uvfd, n ) || (UseFSync && fdatasync( ctx->uvfd ))) {
error( "Maildir error: cannot write UIDVALIDITY in %s\n", ctx->path );
return DRV_BOX_BAD;
}
}
conf_wakeup( &ctx->lcktmr, 2 );
return DRV_OK;
}
static int
maildir_init_uidval( maildir_store_t *ctx )
{
ctx->uidvalidity = (uint)time( NULL );
ctx->nuid = 0;
ctx->uvok = 0;
#ifdef USE_DB
if (ctx->db) {
u_int32_t count;
ctx->db->truncate( ctx->db, NULL, &count, 0 );
}
#endif /* USE_DB */
return maildir_store_uidval( ctx );
}
static int
maildir_init_uidval_new( maildir_store_t *ctx )
{
notice( "Maildir notice: no UIDVALIDITY in %s, creating new.\n", ctx->path );
return maildir_init_uidval( ctx );
}
static int
maildir_uidval_lock( maildir_store_t *ctx )
{
int n;
#ifdef USE_DB
int ret;
struct stat st;
#endif
char buf[128];
if (pending_wakeup( &ctx->lcktmr )) {
/* The unlock timer is active, so we are obviously already locked. */
return DRV_OK;
}
/* This (theoretically) works over NFS. Let's hope nobody else did
the same in the opposite order, as we'd deadlock then. */
#if SEEK_SET != 0
lck.l_whence = SEEK_SET;
#endif
lck.l_type = F_WRLCK;
if (fcntl( ctx->uvfd, F_SETLKW, &lck )) {
error( "Maildir error: cannot fcntl lock UIDVALIDITY in %s.\n", ctx->path );
return DRV_BOX_BAD;
}
#ifdef USE_DB
if (ctx->usedb) {
if (fstat( ctx->uvfd, &st )) {
sys_error( "Maildir error: cannot fstat UID database in %s", ctx->path );
return DRV_BOX_BAD;
}
if (db_create( &ctx->db, NULL, 0 )) {
fputs( "Maildir error: db_create() failed\n", stderr );
return DRV_BOX_BAD;
}
if ((ret = (ctx->db->open)( ctx->db, NULL, ctx->usedb, NULL, DB_HASH,
st.st_size ? 0 : DB_CREATE | DB_TRUNCATE, 0 ))) {
ctx->db->err( ctx->db, ret, "Maildir error: db->open(%s)", ctx->usedb );
return DRV_BOX_BAD;
}
key.data = (void *)"UIDVALIDITY";
key.size = 11;
if ((ret = ctx->db->get( ctx->db, NULL, &key, &value, 0 ))) {
if (ret != DB_NOTFOUND) {
ctx->db->err( ctx->db, ret, "Maildir error: db->get()" );
return DRV_BOX_BAD;
}
return maildir_init_uidval_new( ctx );
}
ctx->uidvalidity = ((uint *)value.data)[0];
ctx->nuid = ((uint *)value.data)[1];
} else
#endif
{
lseek( ctx->uvfd, 0, SEEK_SET );
if ((n = read( ctx->uvfd, buf, sizeof(buf) - 1 )) <= 0 ||
(buf[n] = 0, sscanf( buf, "%u\n%u", &ctx->uidvalidity, &ctx->nuid ) != 2)) {
#if 1
/* In a generic driver, resetting the UID validity would be the right thing.
* But this would mess up the sync state completely. So better bail out and
* give the user a chance to fix the mailbox. */
if (n) {
error( "Maildir error: cannot read UIDVALIDITY in %s.\n", ctx->path );
return DRV_BOX_BAD;
}
#endif
return maildir_init_uidval_new( ctx );
}
}
ctx->uvok = 1;
conf_wakeup( &ctx->lcktmr, 2 );
return DRV_OK;
}
static void
maildir_uidval_unlock( maildir_store_t *ctx )
{
#ifdef USE_DB
if (ctx->db) {
ctx->db->close( ctx->db, 0 );
ctx->db = NULL;
}
#endif /* USE_DB */
lck.l_type = F_UNLCK;
fcntl( ctx->uvfd, F_SETLK, &lck );
}
static void
lcktmr_timeout( void *aux )
{
maildir_uidval_unlock( (maildir_store_t *)aux );
}
static int
maildir_obtain_uid( maildir_store_t *ctx, uint *uid )
{
int ret;
if ((ret = maildir_uidval_lock( ctx )) != DRV_OK)
return ret;
*uid = ++ctx->nuid;
return maildir_store_uidval( ctx );
}
#ifdef USE_DB
static int
maildir_set_uid( maildir_store_t *ctx, const char *name, uint *uid )
{
int ret;
if ((ret = maildir_uidval_lock( ctx )) != DRV_OK)
return ret;
*uid = ++ctx->nuid;
make_key( ctx->conf->info_stop, &key, name );
value.data = uid;
value.size = sizeof(*uid);
if ((ret = ctx->db->put( ctx->db, NULL, &key, &value, 0 ))) {
ctx->db->err( ctx->db, ret, "Maildir error: db->put()" );
return DRV_BOX_BAD;
}
return maildir_store_uidval( ctx );
}
#endif
static int
maildir_compare( const void *l, const void *r )
{
const msg_t *lm = (const msg_t *)l, *rm = (const msg_t *)r;
char *ldot, *rdot, *ldot2, *rdot2, *lseq, *rseq;
uint llen, rlen;
int ret;
if (lm->uid != rm->uid) // Can't subtract, the result might not fit into signed int.
return lm->uid > rm->uid ? 1 : -1;
/* No UID, so sort by arrival date. We should not do this, but we rely
on the suggested unique file name scheme - we have no choice. */
/* The first field are always the seconds. Alphabetical sort should be
faster than numeric. */
if (!(ldot = strchr( lm->base, '.' )) || !(rdot = strchr( rm->base, '.' )))
goto stronly; /* Should never happen ... */
llen = (uint)(ldot - lm->base), rlen = (uint)(rdot - rm->base);
/* The shorter number is smaller. Really. This won't trigger with any
mail created after Sep 9 2001 anyway. */
if ((ret = (int)llen - (int)rlen))
return ret;
if ((ret = memcmp( lm->base, rm->base, llen )))
return ret;
ldot++, rdot++;
if ((llen = strtoul( ldot, &ldot2, 10 ))) {
if (!(rlen = strtoul( rdot, &rdot2, 10 )))
goto stronly; /* Comparing apples to oranges ... */
/* Classical PID specs */
if ((ret = (int)llen - (int)rlen)) {
retpid:
/* Handle PID wraparound. This works only on systems
where PIDs are not reused too fast */
if (ret > 20000 || ret < -20000)
ret = -ret;
return ret;
}
return (*ldot2 != '_' ? 0 : atoi( ldot2 + 1 )) -
(*rdot2 != '_' ? 0 : atoi( rdot2 + 1 ));
}
if (!(ldot2 = strchr( ldot, '.' )) || !(rdot2 = strchr( rdot, '.' )))
goto stronly; /* Should never happen ... */
llen = (uint)(ldot2 - ldot), rlen = (uint)(rdot2 - rdot);
if (((lseq = memchr( ldot, '#', llen )) && (rseq = memchr( rdot, '#', rlen ))) ||
((lseq = memchr( ldot, 'M', llen )) && (rseq = memchr( rdot, 'M', rlen ))))
return atoi( lseq + 1 ) - atoi( rseq + 1 );
if ((lseq = memchr( ldot, 'P', llen )) && (rseq = memchr( rdot, 'P', rlen ))) {
if ((ret = atoi( lseq + 1 ) - atoi( rseq + 1 )))
goto retpid;
if ((lseq = memchr( ldot, 'Q', llen )) && (rseq = memchr( rdot, 'Q', rlen )))
return atoi( lseq + 1 ) - atoi( rseq + 1 );
}
stronly:
/* Fall-back, so the sort order is defined at all */
return strcmp( lm->base, rm->base );
}
static int
maildir_scan( maildir_store_t *ctx, msg_t_array_alloc_t *msglist )
{
maildir_store_conf_t *conf = ctx->conf;
DIR *d;
FILE *f;
struct dirent *e;
const char *u, *ru;
#ifdef USE_DB
DB *tdb;
DBC *dbc;
#endif /* USE_DB */
msg_t *entry;
uint i;
int bl, fnl, ret;
uint uid;
time_t now, stamps[2];
struct stat st;
char buf[_POSIX_PATH_MAX], nbuf[_POSIX_PATH_MAX];
again:
ARRAY_INIT( msglist );
ctx->total_msgs = ctx->recent_msgs = 0;
if (ctx->uvok || ctx->maxuid == UINT_MAX) {
#ifdef USE_DB
if (ctx->usedb) {
if (db_create( &tdb, NULL, 0 )) {
fputs( "Maildir error: db_create() failed\n", stderr );
return DRV_BOX_BAD;
}
if ((tdb->open)( tdb, NULL, NULL, NULL, DB_HASH, DB_CREATE, 0 )) {
fputs( "Maildir error: tdb->open() failed\n", stderr );
bork:
tdb->close( tdb, 0 );
return DRV_BOX_BAD;
}
}
#endif /* USE_DB */
bl = nfsnprintf( buf, sizeof(buf) - 4, "%s/", ctx->path );
restat:
now = time( NULL );
for (i = 0; i < 2; i++) {
memcpy( buf + bl, subdirs[i], 4 );
if (stat( buf, &st )) {
sys_error( "Maildir error: cannot stat %s", buf );
goto dfail;
}
if (st.st_mtime == now && !(DFlags & ZERODELAY) && !ctx->fresh[i]) {
/* If the modification happened during this second, we wouldn't be able to
* tell if there were further modifications during this second. So wait.
* This has the nice side effect that we wait for "batches" of changes to
* complete. On the downside, it can potentially block indefinitely. */
notice( "Maildir notice: sleeping due to recent directory modification.\n" );
sleep( 1 ); /* FIXME: should make this async */
goto restat;
}
stamps[i] = st.st_mtime;
}
for (i = 0; i < 2; i++) {
memcpy( buf + bl, subdirs[i], 4 );
if (!(d = opendir( buf ))) {
sys_error( "Maildir error: cannot list %s", buf );
rfail:
maildir_free_scan( msglist );
dfail:
#ifdef USE_DB
if (ctx->usedb)
tdb->close( tdb, 0 );
#endif /* USE_DB */
return DRV_BOX_BAD;
}
while ((e = readdir( d ))) {
if (*e->d_name == '.')
continue;
ctx->total_msgs++;
ctx->recent_msgs += i;
#ifdef USE_DB
if (ctx->usedb) {
if (maildir_uidval_lock( ctx ) != DRV_OK)
goto mbork;
make_key( conf->info_stop, &key, e->d_name );
if ((ret = ctx->db->get( ctx->db, NULL, &key, &value, 0 ))) {
if (ret != DB_NOTFOUND) {
ctx->db->err( ctx->db, ret, "Maildir error: db->get()" );
mbork:
maildir_free_scan( msglist );
closedir( d );
goto bork;
}
uid = UINT_MAX;
} else {
value.size = 0;
if ((ret = tdb->put( tdb, NULL, &key, &value, 0 ))) {
tdb->err( tdb, ret, "Maildir error: tdb->put()" );
goto mbork;
}
uid = *(uint *)value.data;
}
} else
#endif /* USE_DB */
{
uid = (ctx->uvok && (u = strstr( e->d_name, ",U=" ))) ? strtoul( u + 3, NULL, 10 ) : 0;
if (!uid)
uid = UINT_MAX;
}
if (uid <= ctx->maxuid) {
if (uid < ctx->minuid && !find_uint_array( ctx->excs, uid ))
continue;
entry = msg_t_array_append( msglist );
entry->base = nfstrdup( e->d_name );
entry->msgid = NULL;
entry->uid = uid;
entry->recent = (uchar)i;
entry->size = 0;
entry->tuid[0] = 0;
}
}
closedir( d );
}
for (i = 0; i < 2; i++) {
memcpy( buf + bl, subdirs[i], 4 );
if (stat( buf, &st )) {
sys_error( "Maildir error: cannot re-stat %s", buf );
goto rfail;
}
if (st.st_mtime != stamps[i]) {
/* Somebody messed with the mailbox since we started listing it. */
#ifdef USE_DB
if (ctx->usedb)
tdb->close( tdb, 0 );
#endif /* USE_DB */
maildir_free_scan( msglist );
goto again;
}
}
#ifdef USE_DB
if (ctx->usedb) {
if (maildir_uidval_lock( ctx ) != DRV_OK) {
} else if ((ret = ctx->db->cursor( ctx->db, NULL, &dbc, 0 ))) {
ctx->db->err( ctx->db, ret, "Maildir error: db->cursor()" );
} else {
for (;;) {
if ((ret = dbc->c_get( dbc, &key, &value, DB_NEXT ))) {
if (ret != DB_NOTFOUND)
ctx->db->err( ctx->db, ret, "Maildir error: db->c_get()" );
break;
}
if (!equals( key.data, (int)key.size, "UIDVALIDITY", 11 ) &&
(ret = tdb->get( tdb, NULL, &key, &value, 0 ))) {
if (ret != DB_NOTFOUND) {
tdb->err( tdb, ret, "Maildir error: tdb->get()" );
break;
}
if ((ret = dbc->c_del( dbc, 0 ))) {
ctx->db->err( ctx->db, ret, "Maildir error: db->c_del()" );
break;
}
}
}
dbc->c_close( dbc );
}
tdb->close( tdb, 0 );
}
#endif /* USE_DB */
qsort( msglist->array.data, msglist->array.size, sizeof(msg_t), maildir_compare );
for (uid = i = 0; i < msglist->array.size; i++) {
entry = &msglist->array.data[i];
if (entry->uid != UINT_MAX) {
if (uid == entry->uid) {
#if 1
/* See comment in maildir_uidval_lock() why this is fatal. */
error( "Maildir error: duplicate UID %u in %s.\n", uid, ctx->path );
maildir_free_scan( msglist );
return DRV_BOX_BAD;
#else
notice( "Maildir notice: duplicate UID in %s; changing UIDVALIDITY.\n", ctx->path );
if ((ret = maildir_init_uid( ctx )) != DRV_OK) {
maildir_free_scan( msglist );
return ret;
}
maildir_free_scan( msglist );
goto again;
#endif
}
uid = entry->uid;
if (uid > ctx->nuid) {
/* In principle, we could just warn and top up nuid. However, getting into this
* situation might indicate some serious trouble, so let's not make it worse. */
error( "Maildir error: UID %u is beyond highest assigned UID %u in %s.\n",
uid, ctx->nuid, ctx->path );
maildir_free_scan( msglist );
return DRV_BOX_BAD;
}
fnl = 0;
#ifdef USE_DB
} else if (ctx->usedb) {
if ((ret = maildir_set_uid( ctx, entry->base, &uid )) != DRV_OK) {
maildir_free_scan( msglist );
return ret;
}
entry->uid = uid;
fnl = 0;
#endif /* USE_DB */
} else {
if ((ret = maildir_obtain_uid( ctx, &uid )) != DRV_OK) {
maildir_free_scan( msglist );
return ret;
}
entry->uid = uid;
if ((u = strstr( entry->base, ",U=" )))
for (ru = u + 3; isdigit( (uchar)*ru ); ru++);
else
u = ru = strchr( entry->base, conf->info_delimiter );
fnl = (u ?
nfsnprintf( buf + bl, _POSIX_PATH_MAX - bl, "%s/%.*s,U=%u%s", subdirs[entry->recent], (int)(u - entry->base), entry->base, uid, ru ) :
nfsnprintf( buf + bl, _POSIX_PATH_MAX - bl, "%s/%s,U=%u", subdirs[entry->recent], entry->base, uid ))
- 4;
memcpy( nbuf, buf, (size_t)(bl + 4) );
nfsnprintf( nbuf + bl + 4, _POSIX_PATH_MAX - bl - 4, "%s", entry->base );
if (rename( nbuf, buf )) {
if (errno != ENOENT) {
sys_error( "Maildir error: cannot rename %s to %s", nbuf, buf );
fail:
maildir_free_scan( msglist );
return DRV_BOX_BAD;
}
retry:
maildir_free_scan( msglist );
goto again;
}
free( entry->base );
entry->base = nfstrndup( buf + bl + 4, (size_t)fnl );
}
int want_size = ((ctx->opts & OPEN_NEW_SIZE) && uid > ctx->newuid);
int want_tuid = ((ctx->opts & OPEN_FIND) && uid >= ctx->finduid);
int want_msgid = ((ctx->opts & OPEN_OLD_IDS) && uid <= ctx->pairuid);
if (!want_size && !want_tuid && !want_msgid)
continue;
if (!fnl)
nfsnprintf( buf + bl, _POSIX_PATH_MAX - bl, "%s/%s", subdirs[entry->recent], entry->base );
if (want_size) {
if (stat( buf, &st )) {
if (errno != ENOENT) {
sys_error( "Maildir error: cannot stat %s", buf );
goto fail;
}
goto retry;
}
// The clipped value is good enough for MaxSize comparisons.
entry->size = st.st_size > UINT_MAX ? UINT_MAX : (uint)st.st_size;
}
if (want_tuid || want_msgid) {
if (!(f = fopen( buf, "r" ))) {
if (errno != ENOENT) {
sys_error( "Maildir error: cannot open %s", buf );
goto fail;
}
goto retry;
}
int off, in_msgid = 0;
char lnbuf[1000]; // Says RFC2822
while ((want_tuid || want_msgid) && fgets( lnbuf, sizeof(lnbuf), f )) {
int bufl = strlen( lnbuf );
if (bufl && lnbuf[bufl - 1] == '\n')
--bufl;
if (bufl && lnbuf[bufl - 1] == '\r')
--bufl;
if (!bufl)
break;
if (want_tuid && starts_with( lnbuf, bufl, "X-TUID: ", 8 )) {
if (bufl < 8 + TUIDL) {
error( "Maildir error: malformed X-TUID header in %s\n", buf );
continue;
}
memcpy( entry->tuid, lnbuf + 8, TUIDL );
want_tuid = 0;
in_msgid = 0;
continue;
}
if (want_msgid && starts_with_upper( lnbuf, bufl, "MESSAGE-ID:", 11 )) {
off = 11;
} else if (in_msgid) {
if (!isspace( lnbuf[0] )) {
in_msgid = 0;
continue;
}
off = 1;
} else {
continue;
}
while (off < bufl && isspace( lnbuf[off] ))
off++;
if (off == bufl) {
in_msgid = 1;
continue;
}
entry->msgid = nfstrndup( lnbuf + off, (size_t)(bufl - off) );
want_msgid = 0;
in_msgid = 0;
}
fclose( f );
}
}
ctx->uvok = 1;
}
return DRV_OK;
}
static void
maildir_init_msg( maildir_store_t *ctx, maildir_message_t *msg, msg_t *entry )
{
msg->base = entry->base;
entry->base = NULL; /* prevent deletion */
msg->msgid = entry->msgid;
entry->msgid = NULL; /* prevent deletion */
msg->size = entry->size;
memcpy( msg->tuid, entry->tuid, TUIDL );
if (entry->recent)
msg->status |= M_RECENT;
if (ctx->opts & OPEN_FLAGS) {
msg->status |= M_FLAGS;
msg->flags = maildir_parse_flags( ctx->conf->info_prefix, msg->base );
} else {
msg->flags = 0;
}
}
static void
maildir_app_msg( maildir_store_t *ctx, maildir_message_t ***msgapp, msg_t *entry )
{
maildir_message_t *msg = nfmalloc( sizeof(*msg) );
msg->next = **msgapp;
**msgapp = msg;
*msgapp = &msg->next;
msg->uid = entry->uid;
msg->status = 0;
msg->srec = NULL;
maildir_init_msg( ctx, msg, entry );
}
static int
maildir_select_box( store_t *gctx, const char *name )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
maildir_store_conf_t *conf = ctx->conf;
maildir_cleanup( gctx );
ctx->msgs = NULL;
ctx->excs.data = NULL;
ctx->uvfd = -1;
#ifdef USE_DB
ctx->db = NULL;
ctx->usedb = NULL;
#endif /* USE_DB */
ctx->fresh[0] = ctx->fresh[1] = 0;
if (starts_with( name, -1, "INBOX", 5 ) && (!name[5] || name[5] == '/')) {
if (!name[5]) {
ctx->path = nfstrdup( conf->inbox );
ctx->is_inbox = 1;
} else {
ctx->path = maildir_join_path( conf, 1, name + 5 );
ctx->is_inbox = 0;
}
} else {
if (!(ctx->path = maildir_join_path( conf, 0, name )))
return DRV_STORE_BAD;
ctx->is_inbox = 0;
}
return ctx->path ? DRV_OK : DRV_BOX_BAD;
}
static const char *
maildir_get_box_path( store_t *gctx )
{
return ((maildir_store_t *)gctx)->path;
}
static void
maildir_open_box( store_t *gctx,
void (*cb)( int sts, uint uidvalidity, void *aux ), void *aux )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
int ret;
char uvpath[_POSIX_PATH_MAX];
if ((ret = maildir_validate( ctx->path, ctx->is_inbox, ctx )) != DRV_OK)
goto bail;
nfsnprintf( uvpath, sizeof(uvpath), "%s/.uidvalidity", ctx->path );
#ifndef USE_DB
if ((ctx->uvfd = open( uvpath, O_RDWR|O_CREAT, 0600 )) < 0) {
sys_error( "Maildir error: cannot write %s", uvpath );
cb( DRV_BOX_BAD, UIDVAL_BAD, aux );
return;
}
#else
ctx->usedb = NULL;
if ((ctx->uvfd = open( uvpath, O_RDWR, 0600 )) < 0) {
nfsnprintf( uvpath, sizeof(uvpath), "%s/.isyncuidmap.db", ctx->path );
if ((ctx->uvfd = open( uvpath, O_RDWR, 0600 )) < 0) {
if (ctx->conf->alt_map) {
if ((ctx->uvfd = open( uvpath, O_RDWR|O_CREAT, 0600 )) >= 0)
goto dbok;
} else {
nfsnprintf( uvpath, sizeof(uvpath), "%s/.uidvalidity", ctx->path );
if ((ctx->uvfd = open( uvpath, O_RDWR|O_CREAT, 0600 )) >= 0)
goto fnok;
}
sys_error( "Maildir error: cannot write %s", uvpath );
cb( DRV_BOX_BAD, UIDVAL_BAD, aux );
return;
} else {
dbok:
ctx->usedb = nfstrdup( uvpath );
}
}
fnok:
#endif /* USE_DB */
ret = maildir_uidval_lock( ctx );
bail:
cb( ret, ctx->uidvalidity, aux );
}
static uint
maildir_get_uidnext( store_t *gctx )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
return ctx->nuid;
}
static xint
maildir_get_supported_flags( store_t *gctx ATTR_UNUSED )
{
return 255;
}
static void
maildir_create_box( store_t *gctx,
void (*cb)( int sts, void *aux ), void *aux )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
cb( maildir_validate( ctx->path, 1, ctx ), aux );
}
static int
maildir_confirm_box_empty( store_t *gctx )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
msg_t_array_alloc_t msglist;
ctx->excs.size = ctx->minuid = ctx->maxuid = ctx->finduid = 0;
if (maildir_scan( ctx, &msglist ) != DRV_OK)
return DRV_BOX_BAD;
maildir_free_scan( &msglist );
return ctx->total_msgs ? DRV_BOX_BAD : DRV_OK;
}
static void
maildir_delete_box( store_t *gctx,
void (*cb)( int sts, void *aux ), void *aux )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
int i, bl, ret = DRV_OK;
struct stat st;
char buf[_POSIX_PATH_MAX];
bl = nfsnprintf( buf, sizeof(buf) - 4, "%s/", ctx->path );
if (stat( buf, &st )) {
if (errno != ENOENT) {
sys_error( "Maildir error: cannot access mailbox '%s'", ctx->path );
ret = DRV_BOX_BAD;
}
} else if (!S_ISDIR(st.st_mode)) {
error( "Maildir error: '%s' is no valid mailbox\n", ctx->path );
ret = DRV_BOX_BAD;
} else if ((ret = maildir_clear_tmp( buf, sizeof(buf), bl )) == DRV_OK) {
nfsnprintf( buf + bl, _POSIX_PATH_MAX - bl, ".uidvalidity" );
if (unlink( buf ) && errno != ENOENT)
goto badrm;
#ifdef USE_DB
nfsnprintf( buf + bl, _POSIX_PATH_MAX - bl, ".isyncuidmap.db" );
if (unlink( buf ) && errno != ENOENT)
goto badrm;
#endif
/* We delete cur/ last, as it is the indicator for a present mailbox.
* That way an interrupted operation can be resumed. */
for (i = 3; --i >= 0; ) {
memcpy( buf + bl, subdirs[i], 4 );
if (rmdir( buf ) && errno != ENOENT) {
badrm:
sys_error( "Maildir error: cannot remove '%s'", buf );
ret = DRV_BOX_BAD;
break;
}
}
}
cb( ret, aux );
}
static int
maildir_finish_delete_box( store_t *gctx )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
/* Subfolders are not deleted; the deleted folder is only "stripped of its mailboxness".
* Consequently, the rmdir may legitimately fail. This behavior follows the IMAP spec. */
if (rmdir( ctx->path ) && errno != ENOENT && errno != ENOTEMPTY) {
sys_error( "Maildir warning: cannot remove '%s'", ctx->path );
return DRV_BOX_BAD;
}
return DRV_OK;
}
static uint
maildir_prepare_load_box( store_t *gctx, uint opts )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
if (opts & OPEN_SETFLAGS)
opts |= OPEN_OLD;
if (opts & OPEN_EXPUNGE)
opts |= OPEN_OLD|OPEN_NEW|OPEN_FLAGS;
ctx->opts = opts;
return opts;
}
static void
maildir_load_box( store_t *gctx, uint minuid, uint maxuid, uint finduid, uint pairuid, uint newuid, uint_array_t excs,
void (*cb)( int sts, message_t *msgs, int total_msgs, int recent_msgs, void *aux ), void *aux )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
maildir_message_t **msgapp;
msg_t_array_alloc_t msglist;
uint i;
ctx->minuid = minuid;
ctx->maxuid = maxuid;
ctx->finduid = finduid;
ctx->pairuid = pairuid;
ctx->newuid = newuid;
ARRAY_SQUEEZE( &excs );
ctx->excs = excs;
if (maildir_scan( ctx, &msglist ) != DRV_OK) {
cb( DRV_BOX_BAD, NULL, 0, 0, aux );
return;
}
msgapp = &ctx->msgs;
for (i = 0; i < msglist.array.size; i++)
maildir_app_msg( ctx, &msgapp, msglist.array.data + i );
maildir_free_scan( &msglist );
cb( DRV_OK, &ctx->msgs->gen, ctx->total_msgs, ctx->recent_msgs, aux );
}
static int
maildir_rescan( maildir_store_t *ctx )
{
maildir_message_t **msgapp, *msg;
msg_t_array_alloc_t msglist;
uint i;
ctx->fresh[0] = ctx->fresh[1] = 0;
if (maildir_scan( ctx, &msglist ) != DRV_OK)
return DRV_BOX_BAD;
debug( "Maildir processing rescan of %s:\n", ctx->path );
for (msgapp = &ctx->msgs, i = 0; (msg = *msgapp) || i < msglist.array.size; ) {
if (!msg) {
#if 0
debug( " adding new message %u\n", msglist.array.data[i].uid );
maildir_app_msg( ctx, &msgapp, msglist.array.data + i );
#else
debug( " ignoring new message %u\n", msglist.array.data[i].uid );
#endif
i++;
} else if (i >= msglist.array.size) {
debug( " purging deleted message %u\n", msg->uid );
msg->status = M_DEAD;
ctx->expunge_callback( &msg->gen, ctx->callback_aux );
msgapp = &msg->next;
} else if (msglist.array.data[i].uid < msg->uid) {
/* this should not happen, actually */
#if 0
debug( " adding new message %u\n", msglist.array.data[i].uid );
maildir_app_msg( ctx, &msgapp, msglist.array.data + i );
#else
debug( " ignoring new message %u\n", msglist.array.data[i].uid );
#endif
i++;
} else if (msglist.array.data[i].uid > msg->uid) {
debug( " purging deleted message %u\n", msg->uid );
msg->status = M_DEAD;
ctx->expunge_callback( &msg->gen, ctx->callback_aux );
msgapp = &msg->next;
} else {
debug( " updating message %u\n", msg->uid );
msg->status &= ~(M_FLAGS|M_RECENT);
free( msg->base );
free( msg->msgid );
maildir_init_msg( ctx, msg, msglist.array.data + i );
i++, msgapp = &msg->next;
}
}
maildir_free_scan( &msglist );
return DRV_OK;
}
static int ATTR_PRINTFLIKE(3, 0)
maildir_again( maildir_store_t *ctx, maildir_message_t *msg, const char *err, ... )
{
int ret;
if (errno != ENOENT) {
va_list va;
va_start( va, err );
vsys_error( err, va );
va_end( va );
return DRV_BOX_BAD;
}
if ((ret = maildir_rescan( ctx )) != DRV_OK)
return ret;
return (msg->status & M_DEAD) ? DRV_MSG_BAD : DRV_OK;
}
static void
maildir_fetch_msg( store_t *gctx, message_t *gmsg, msg_data_t *data, int minimal ATTR_UNUSED,
void (*cb)( int sts, void *aux ), void *aux )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
maildir_message_t *msg = (maildir_message_t *)gmsg;
int fd, ret;
struct stat st;
char buf[_POSIX_PATH_MAX];
for (;;) {
nfsnprintf( buf, sizeof(buf), "%s/%s/%s", ctx->path, subdirs[gmsg->status & M_RECENT], msg->base );
if ((fd = open( buf, O_RDONLY )) >= 0)
break;
if ((ret = maildir_again( ctx, msg, "Cannot open %s", buf )) != DRV_OK) {
cb( ret, aux );
return;
}
}
fstat( fd, &st );
if (st.st_size > INT_MAX) {
error( "Maildir error: %s is too big", buf );
goto mbad;
}
data->len = st.st_size;
if (data->date == -1)
data->date = st.st_mtime;
data->data = nfmalloc( data->len );
if (read( fd, data->data, data->len ) != data->len) {
sys_error( "Maildir error: cannot read %s", buf );
mbad:
close( fd );
cb( DRV_MSG_BAD, aux );
return;
}
close( fd );
if (!(gmsg->status & M_FLAGS))
data->flags = maildir_parse_flags( ctx->conf->info_prefix, msg->base );
cb( DRV_OK, aux );
}
static int
maildir_make_flags( char info_delimiter, uchar flags, char *buf )
{
int i, d;
buf[0] = info_delimiter;
buf[1] = '2';
buf[2] = ',';
for (d = 3, i = 0; i < (int)as(MsgFlags); i++)
if (flags & (1 << i))
buf[d++] = MsgFlags[i];
buf[d] = 0;
return d;
}
static void
maildir_store_msg( store_t *gctx, msg_data_t *data, int to_trash,
void (*cb)( int sts, uint uid, void *aux ), void *aux )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
const char *box;
int ret, fd, bl;
uint uid;
char buf[_POSIX_PATH_MAX], nbuf[_POSIX_PATH_MAX], fbuf[as(MsgFlags) + 3], base[128];
bl = nfsnprintf( base, sizeof(base), "%lld.%d_%d.%s", (long long)time( NULL ), Pid, ++MaildirCount, Hostname );
if (!to_trash) {
#ifdef USE_DB
if (ctx->usedb) {
if ((ret = maildir_set_uid( ctx, base, &uid )) != DRV_OK) {
free( data->data );
cb( ret, 0, aux );
return;
}
} else
#endif /* USE_DB */
{
if ((ret = maildir_obtain_uid( ctx, &uid )) != DRV_OK) {
free( data->data );
cb( ret, 0, aux );
return;
}
nfsnprintf( base + bl, (int)sizeof(base) - bl, ",U=%u", uid );
}
box = ctx->path;
} else {
uid = 0;
box = ctx->trash;
}
maildir_make_flags( ctx->conf->info_delimiter, data->flags, fbuf );
nfsnprintf( buf, sizeof(buf), "%s/tmp/%s%s", box, base, fbuf );
if ((fd = open( buf, O_WRONLY|O_CREAT|O_EXCL, 0600 )) < 0) {
if (errno != ENOENT || !to_trash) {
sys_error( "Maildir error: cannot create %s", buf );
free( data->data );
cb( DRV_BOX_BAD, 0, aux );
return;
}
if ((ret = maildir_validate( box, 1, ctx )) != DRV_OK) {
free( data->data );
cb( ret, 0, aux );
return;
}
if ((fd = open( buf, O_WRONLY|O_CREAT|O_EXCL, 0600 )) < 0) {
sys_error( "Maildir error: cannot create %s", buf );
free( data->data );
cb( DRV_BOX_BAD, 0, aux );
return;
}
}
ret = write( fd, data->data, data->len );
free( data->data );
if (ret != (int)data->len || (UseFSync && (ret = fsync( fd )))) {
if (ret < 0)
sys_error( "Maildir error: cannot write %s", buf );
else
error( "Maildir error: cannot write %s. Disk full?\n", buf );
close( fd );
cb( DRV_BOX_BAD, 0, aux );
return;
}
if (close( fd ) < 0) {
/* Quota exceeded may cause this. */
sys_error( "Maildir error: cannot write %s", buf );
cb( DRV_BOX_BAD, 0, aux );
return;
}
if (data->date) {
/* Set atime and mtime according to INTERNALDATE or mtime of source message */
struct utimbuf utimebuf;
utimebuf.actime = utimebuf.modtime = data->date;
if (utime( buf, &utimebuf ) < 0) {
sys_error( "Maildir error: cannot set times for %s", buf );
cb( DRV_BOX_BAD, 0, aux );
return;
}
}
/* Moving seen messages to cur/ is strictly speaking incorrect, but makes mutt happy. */
nfsnprintf( nbuf, sizeof(nbuf), "%s/%s/%s%s", box, subdirs[!(data->flags & F_SEEN)], base, fbuf );
if (rename( buf, nbuf )) {
sys_error( "Maildir error: cannot rename %s to %s", buf, nbuf );
cb( DRV_BOX_BAD, 0, aux );
return;
}
cb( DRV_OK, uid, aux );
}
static void
maildir_set_msg_flags( store_t *gctx, message_t *gmsg, uint uid ATTR_UNUSED, int add, int del,
void (*cb)( int sts, void *aux ), void *aux )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
maildir_store_conf_t *conf = ctx->conf;
maildir_message_t *msg = (maildir_message_t *)gmsg;
char *s, *p;
uint i;
int j, ret, ol, fl, bbl, bl, tl;
char buf[_POSIX_PATH_MAX], nbuf[_POSIX_PATH_MAX];
bbl = nfsnprintf( buf, sizeof(buf), "%s/", ctx->path );
memcpy( nbuf, ctx->path, (size_t)bbl - 1 );
memcpy( nbuf + bbl - 1, "/cur/", 5 );
for (;;) {
bl = bbl + nfsnprintf( buf + bbl, _POSIX_PATH_MAX - bbl, "%s/", subdirs[gmsg->status & M_RECENT] );
ol = strlen( msg->base );
if (_POSIX_PATH_MAX - bl < ol + 3 + (int)as(MsgFlags))
oob();
memcpy( buf + bl, msg->base, (size_t)ol + 1 );
memcpy( nbuf + bl, msg->base, (size_t)ol + 1 );
if ((s = strstr( nbuf + bl, conf->info_prefix ))) {
s += 3;
fl = ol - (s - (nbuf + bl));
for (i = 0; i < as(MsgFlags); i++) {
if ((p = strchr( s, MsgFlags[i] ))) {
if (del & (1 << i)) {
memmove( p, p + 1, (size_t)fl - (size_t)(p - s) );
fl--;
}
} else if (add & (1 << i)) {
for (j = 0; j < fl && MsgFlags[i] > s[j]; j++);
fl++;
memmove( s + j + 1, s + j, (size_t)(fl - j) );
s[j] = MsgFlags[i];
}
}
tl = ol + 3 + fl;
} else {
tl = ol + maildir_make_flags( conf->info_delimiter, (uchar)add, nbuf + bl + ol );
}
if (!rename( buf, nbuf ))
break;
if ((ret = maildir_again( ctx, msg, "Maildir error: cannot rename %s to %s", buf, nbuf )) != DRV_OK) {
cb( ret, aux );
return;
}
}
free( msg->base );
msg->base = nfstrndup( nbuf + bl, (size_t)tl );
msg->flags |= add;
msg->flags &= ~del;
gmsg->status &= ~M_RECENT;
cb( DRV_OK, aux );
}
#ifdef USE_DB
static int
maildir_purge_msg( maildir_store_t *ctx, const char *name )
{
int ret;
if ((ret = maildir_uidval_lock( ctx )) != DRV_OK)
return ret;
make_key( ctx->conf->info_stop, &key, name );
if ((ret = ctx->db->del( ctx->db, NULL, &key, 0 ))) {
ctx->db->err( ctx->db, ret, "Maildir error: db->del()" );
return DRV_BOX_BAD;
}
return DRV_OK;
}
#endif /* USE_DB */
static void
maildir_trash_msg( store_t *gctx, message_t *gmsg,
void (*cb)( int sts, void *aux ), void *aux )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
maildir_message_t *msg = (maildir_message_t *)gmsg;
char *s;
int ret;
struct stat st;
char buf[_POSIX_PATH_MAX], nbuf[_POSIX_PATH_MAX];
for (;;) {
nfsnprintf( buf, sizeof(buf), "%s/%s/%s", ctx->path, subdirs[gmsg->status & M_RECENT], msg->base );
s = strstr( msg->base, ctx->conf->info_prefix );
nfsnprintf( nbuf, sizeof(nbuf), "%s/%s/%lld.%d_%d.%s%s", ctx->trash,
subdirs[gmsg->status & M_RECENT], (long long)time( NULL ), Pid, ++MaildirCount, Hostname, s ? s : "" );
if (!rename( buf, nbuf ))
break;
if (!stat( buf, &st )) {
if ((ret = maildir_validate( ctx->trash, 1, ctx )) != DRV_OK) {
cb( ret, aux );
return;
}
if (!rename( buf, nbuf ))
break;
if (errno != ENOENT) {
sys_error( "Maildir error: cannot move %s to %s", buf, nbuf );
cb( DRV_BOX_BAD, aux );
return;
}
}
if ((ret = maildir_again( ctx, msg, "Maildir error: cannot move %s to %s", buf, nbuf )) != DRV_OK) {
cb( ret, aux );
return;
}
}
gmsg->status |= M_DEAD;
ctx->total_msgs--;
ctx->expunge_callback( gmsg, ctx->callback_aux );
#ifdef USE_DB
if (ctx->usedb) {
cb( maildir_purge_msg( ctx, msg->base ), aux );
return;
}
#endif /* USE_DB */
cb( DRV_OK, aux );
}
static void
maildir_close_box( store_t *gctx,
void (*cb)( int sts, void *aux ), void *aux )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
maildir_message_t *msg;
int basel, retry, ret;
char buf[_POSIX_PATH_MAX];
for (;;) {
retry = 0;
basel = nfsnprintf( buf, sizeof(buf), "%s/", ctx->path );
for (msg = ctx->msgs; msg; msg = msg->next) {
if (!(msg->status & M_DEAD) && (msg->flags & F_DELETED)) {
nfsnprintf( buf + basel, _POSIX_PATH_MAX - basel, "%s/%s", subdirs[msg->status & M_RECENT], msg->base );
if (unlink( buf )) {
if (errno == ENOENT)
retry = 1;
else
sys_error( "Maildir error: cannot remove %s", buf );
} else {
msg->status |= M_DEAD;
ctx->total_msgs--;
ctx->expunge_callback( &msg->gen, ctx->callback_aux );
#ifdef USE_DB
if (ctx->db && (ret = maildir_purge_msg( ctx, msg->base )) != DRV_OK) {
cb( ret, aux );
return;
}
#endif /* USE_DB */
}
}
}
if (!retry) {
cb( DRV_OK, aux );
return;
}
if ((ret = maildir_rescan( ctx )) != DRV_OK) {
cb( ret, aux );
return;
}
}
}
static void
maildir_cancel_cmds( store_t *gctx ATTR_UNUSED,
void (*cb)( void *aux ), void *aux )
{
cb( aux );
}
static void
maildir_commit_cmds( store_t *gctx )
{
(void) gctx;
}
static uint
maildir_get_memory_usage( store_t *gctx ATTR_UNUSED )
{
return 0;
}
static int
maildir_get_fail_state( store_conf_t *gconf )
{
return ((maildir_store_conf_t *)gconf)->failed;
}
static int
maildir_parse_store( conffile_t *cfg, store_conf_t **storep )
{
maildir_store_conf_t *store;
if (strcasecmp( "MaildirStore", cfg->cmd ))
return 0;
store = nfzalloc( sizeof(*store) );
store->info_delimiter = FieldDelimiter;
store->driver = &maildir_driver;
store->name = nfstrdup( cfg->val );
while (getcline( cfg ) && cfg->cmd) {
if (!strcasecmp( "Inbox", cfg->cmd )) {
store->inbox = expand_strdup( cfg->val, cfg );
} else if (!strcasecmp( "Path", cfg->cmd )) {
store->path = expand_strdup( cfg->val, cfg );
#ifdef USE_DB
} else if (!strcasecmp( "AltMap", cfg->cmd )) {
store->alt_map = parse_bool( cfg );
#endif /* USE_DB */
} else if (!strcasecmp( "InfoDelimiter", cfg->cmd )) {
if (strlen( cfg->val ) != 1) {
error( "%s:%d: Info delimiter must be exactly one character long\n", cfg->file, cfg->line );
cfg->err = 1;
continue;
}
store->info_delimiter = cfg->val[0];
if (!ispunct( store->info_delimiter )) {
error( "%s:%d: Info delimiter must be a punctuation character\n", cfg->file, cfg->line );
cfg->err = 1;
continue;
}
} else if (!strcasecmp( "SubFolders", cfg->cmd )) {
if (!strcasecmp( "Verbatim", cfg->val )) {
store->sub_style = SUB_VERBATIM;
} else if (!strcasecmp( "Maildir++", cfg->val )) {
store->sub_style = SUB_MAILDIRPP;
} else if (!strcasecmp( "Legacy", cfg->val )) {
store->sub_style = SUB_LEGACY;
} else {
error( "%s:%d: Unrecognized SubFolders style\n", cfg->file, cfg->line );
cfg->err = 1;
}
} else {
parse_generic_store( &store->gen, cfg, "MaildirStore" );
}
}
if (!store->inbox)
store->inbox = expand_strdup( "~/Maildir", NULL );
if (store->sub_style == SUB_MAILDIRPP && store->path) {
error( "Maildir store '%s': Setting Path is incompatible with 'SubFolders Maildir++'\n", store->name );
cfg->err = 1;
}
nfasprintf( &store->info_prefix, "%c2,", store->info_delimiter );
nfasprintf( &store->info_stop, "%c,", store->info_delimiter );
*storep = &store->gen;
return 1;
}
static uint
maildir_get_caps( store_t *gctx ATTR_UNUSED )
{
return 0; /* XXX DRV_CRLF? */
}
struct driver maildir_driver = {
maildir_get_caps,
maildir_parse_store,
maildir_cleanup_drv,
maildir_alloc_store,
maildir_set_callbacks,
maildir_connect_store,
maildir_free_store,
maildir_free_store, /* _cancel_, but it's the same */
maildir_list_store,
maildir_select_box,
maildir_get_box_path,
maildir_create_box,
maildir_open_box,
maildir_get_uidnext,
maildir_get_supported_flags,
maildir_confirm_box_empty,
maildir_delete_box,
maildir_finish_delete_box,
maildir_prepare_load_box,
maildir_load_box,
maildir_fetch_msg,
maildir_store_msg,
NULL, // find_new_msgs
maildir_set_msg_flags,
maildir_trash_msg,
maildir_close_box,
maildir_cancel_cmds,
maildir_commit_cmds,
maildir_get_memory_usage,
maildir_get_fail_state,
};