isync/src/drv_maildir.c
Michael J Gruber e71f0ccc2a mark MAILBOX_DRIVER_FLAG locations in code
Mailbox driver flags are defined in several places. It is essential that
they are kept in sync, so mark them with the same string for easy
grepping with an alerting boiler plate.

Signed-off-by: Michael J Gruber <github@grubix.eu>
2018-07-01 12:30:59 +02:00

1953 lines
52 KiB
C

/*
* mbsync - mailbox synchronizer
* Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org>
* Copyright (C) 2002-2006,2010-2013 Oswald Buddenhagen <ossi@users.sf.net>
* Copyright (C) 2004 Theodore Y. Ts'o <tytso@mit.edu>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* As a special exception, mbsync may be linked with the OpenSSL library,
* despite that library's more restrictive license.
*/
#include "driver.h"
#include <assert.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <dirent.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
#include <time.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 struct {
store_conf_t gen;
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 struct {
message_t gen;
char *base;
} maildir_message_t;
typedef struct {
store_t gen;
uint opts;
int uvfd, uvok, is_inbox, fresh[3];
uint minuid, maxuid, newuid, seenuid, 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;
message_t *msgs;
wakeup_t lcktmr;
void (*bad_callback)( void *aux );
void *bad_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 void ATTR_PRINTFLIKE(1, 2)
debug( const char *msg, ... )
{
va_list va;
va_start( va, msg );
vdebug( DEBUG_SYNC, msg, va );
va_end( va );
}
/* Keep the mailbox driver flag definitions in sync: */
/* grep for MAILBOX_DRIVER_FLAG */
/* The order is according to alphabetical maildir flag sort */
static const char Flags[] = { 'D', 'F', 'R', 'S', 'T' };
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(Flags); i++)
if (strchr( s, Flags[i] ))
flags |= (1 << i);
return flags;
}
static int
maildir_ensure_path( maildir_store_conf_t *conf )
{
if (!conf->gen.path) {
error( "Maildir error: store '%s' has no Path\n", conf->gen.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;
int pl, bl, n;
char c;
if (in_inbox || conf->sub_style == SUB_MAILDIRPP) {
prefix = conf->inbox;
} else {
if (maildir_ensure_path( conf ) < 0)
return 0;
prefix = conf->gen.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->gen.name );
return 0;
}
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->gen.name, box );
return 0;
}
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->gen.path, &st ) || !S_ISDIR(st.st_mode)) {
error( "Maildir error: cannot open store '%s'\n", conf->gen.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 = nfcalloc( sizeof(*ctx) );
ctx->gen.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 = (maildir_store_conf_t *)ctx->gen.conf;
if (conf->gen.path && maildir_validate_path( conf ) < 0) {
cb( DRV_STORE_BAD, aux );
return;
}
if (conf->gen.trash && !(ctx->trash = maildir_join_path( conf, 0, conf->gen.trash ))) {
cb( DRV_STORE_BAD, aux );
return;
}
cb( DRV_OK, aux );
}
static void
free_maildir_messages( message_t *msg )
{
message_t *tmsg;
for (; (tmsg = msg); msg = tmsg) {
tmsg = msg->next;
free( ((maildir_message_t *)msg)->base );
free( msg );
}
}
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_bad_callback( store_t *gctx, void (*cb)( void *aux ), void *aux )
{
maildir_store_t *ctx = (maildir_store_t *)gctx;
ctx->bad_callback = cb;
ctx->bad_callback_aux = aux;
}
static void
maildir_invoke_bad_callback( maildir_store_t *ctx )
{
ctx->bad_callback( ctx->bad_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;
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,
const char *inbox, int inboxLen, const char *basePath, int basePathLen,
char *path, int pathLen, char *name, int nameLen )
{
DIR *dir;
int style = ((maildir_store_conf_t *)ctx->gen.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->gen.conf->name );
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, 0 ) < 0) {
closedir( dir );
return -1;
}
} else if (basePath && equals( path, pl, basePath, basePathLen )) {
// Path nested into Inbox.
if (maildir_list_path( ctx, flags, 0 ) < 0) {
closedir( dir );
return -1;
}
} else {
if (style == SUB_LEGACY) {
if (*ent == '.') {
if (!isBox)
continue;
ent++;
} else {
if (isBox)
continue;
}
}
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, 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, 0, basePath, basePath ? strlen( basePath ) - 1 : 0,
path, nfsnprintf( path, _POSIX_PATH_MAX, "%s/", ((maildir_store_conf_t *)ctx->gen.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( (maildir_store_conf_t *)ctx->gen.conf ) < 0)
return -1;
return maildir_list_recurse(
ctx, 0, flags, inbox, inbox ? strlen( inbox ) : 0, 0, 0,
path, nfsnprintf( path, _POSIX_PATH_MAX, "%s", ctx->gen.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 = (maildir_store_conf_t *)gctx->conf;
if (conf->sub_style == SUB_MAILDIRPP
? maildir_list_maildirpp( ctx, flags, conf->inbox ) < 0
: ((((flags & LIST_PATH) || ((flags & LIST_PATH_MAYBE) && gctx->conf->path))
&& maildir_list_path( ctx, flags, conf->inbox ) < 0) ||
((flags & LIST_INBOX)
&& maildir_list_inbox( ctx, flags, gctx->conf->path ) < 0))) {
maildir_invoke_bad_callback( ctx );
cb( DRV_CANCELED, 0, aux );
} else {
cb( DRV_OK, ctx->boxes, aux );
}
}
static const char *subdirs[] = { "cur", "new", "tmp" };
typedef struct {
char *base;
char *msgid;
int 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 )
{
int i;
if (msglist->array.data) {
for (i = 0; i < msglist->array.size; i++)
free( msglist->array.data[i].base );
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
make_box_dir( char *buf, int bl )
{
char *p;
if (!mkdir( buf, 0700 ) || errno == EEXIST)
return 0;
p = memrchr( buf, '/', bl - 1 );
*p = 0;
if (make_box_dir( buf, (int)(p - buf) ))
return -1;
*p = '/';
return mkdir( buf, 0700 );
}
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'", box );
return DRV_BOX_BAD;
}
if (!create)
return DRV_BOX_BAD;
if (make_box_dir( buf, bl )) {
sys_error( "Maildir error: cannot create mailbox '%s'", box );
((maildir_store_conf_t *)ctx->gen.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", box );
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, char *name )
{
char *u = strpbrk( name, info_stop );
tkey->data = name;
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, 0, &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, n ) != n || ftruncate( ctx->uvfd, n ) || (UseFSync && fdatasync( ctx->uvfd ))) {
error( "Maildir error: cannot write UIDVALIDITY.\n" );
return DRV_BOX_BAD;
}
}
conf_wakeup( &ctx->lcktmr, 2 );
return DRV_OK;
}
static int
maildir_init_uidval( maildir_store_t *ctx )
{
ctx->uidvalidity = time( 0 );
ctx->nuid = 0;
ctx->uvok = 0;
#ifdef USE_DB
if (ctx->db) {
u_int32_t count;
ctx->db->truncate( ctx->db, 0, &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, creating new.\n" );
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.\n" );
return DRV_BOX_BAD;
}
#ifdef USE_DB
if (ctx->usedb) {
if (fstat( ctx->uvfd, &st )) {
sys_error( "Maildir error: cannot fstat UID database" );
return DRV_BOX_BAD;
}
if (db_create( &ctx->db, 0, 0 )) {
fputs( "Maildir error: db_create() failed\n", stderr );
return DRV_BOX_BAD;
}
if ((ret = (ctx->db->open)( ctx->db, 0, ctx->usedb, 0, 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, 0, &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.\n" );
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 = 0;
}
#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( ((maildir_store_conf_t *)ctx->gen.conf)->info_stop, &key, (char *)name );
value.data = uid;
value.size = sizeof(*uid);
if ((ret = ctx->db->put( ctx->db, 0, &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 )
{
msg_t *lm = (msg_t *)l, *rm = (msg_t *)r;
char *ldot, *rdot, *ldot2, *rdot2, *lseq, *rseq;
int ret, llen, rlen;
if ((ret = lm->uid - rm->uid))
return ret;
/* 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 = ldot - lm->base, rlen = rdot - rm->base;
/* The shorter number is smaller. Really. This won't trigger with any
mail created after Sep 9 2001 anyway. */
if ((ret = llen - rlen))
return ret;
if ((ret = memcmp( lm->base, rm->base, llen )))
return ret;
ldot++, rdot++;
if ((llen = strtol( ldot, &ldot2, 10 ))) {
if (!(rlen = strtol( rdot, &rdot2, 10 )))
goto stronly; /* Comparing apples to oranges ... */
/* Classical PID specs */
if ((ret = llen - 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 = ldot2 - ldot, rlen = 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 = (maildir_store_conf_t *)ctx->gen.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;
int i, 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, 0, 0 )) {
fputs( "Maildir error: db_create() failed\n", stderr );
return DRV_BOX_BAD;
}
if ((tdb->open)( tdb, 0, 0, 0, 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( 0 );
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, 0, &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, 0, &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 = 0;
entry->uid = uid;
entry->recent = 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, 0, &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, key.size, "UIDVALIDITY", 11 ) &&
(ret = tdb->get( tdb, 0, &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.\n", uid );
maildir_free_scan( msglist );
return DRV_BOX_BAD;
#else
notice( "Maildir notice: duplicate UID; changing UIDVALIDITY.\n");
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.\n", uid, ctx->nuid );
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, sizeof(buf) - bl, "%s/%.*s,U=%u%s", subdirs[entry->recent], (int)(u - entry->base), entry->base, uid, ru ) :
nfsnprintf( buf + bl, sizeof(buf) - bl, "%s/%s,U=%u", subdirs[entry->recent], entry->base, uid ))
- 4;
memcpy( nbuf, buf, bl + 4 );
nfsnprintf( nbuf + bl + 4, sizeof(nbuf) - 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, fnl );
}
int want_size = (uid > ctx->seenuid) ? (ctx->opts & OPEN_NEW_SIZE) : (ctx->opts & OPEN_OLD_SIZE);
int want_tuid = ((ctx->opts & OPEN_FIND) && uid >= ctx->newuid);
int want_msgid = ((ctx->opts & OPEN_OLD_IDS) && uid <= ctx->seenuid);
if (!want_size && !want_tuid && !want_msgid)
continue;
if (!fnl)
nfsnprintf( buf + bl, sizeof(buf) - 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;
}
entry->size = 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 (UID %u)\n", uid );
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, 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 = 0; /* prevent deletion */
msg->gen.msgid = entry->msgid;
entry->msgid = 0; /* prevent deletion */
msg->gen.size = entry->size;
msg->gen.srec = 0;
memcpy( msg->gen.tuid, entry->tuid, TUIDL );
if (entry->recent)
msg->gen.status |= M_RECENT;
if (ctx->opts & OPEN_FLAGS) {
msg->gen.status |= M_FLAGS;
msg->gen.flags = maildir_parse_flags( ((maildir_store_conf_t *)ctx->gen.conf)->info_prefix, msg->base );
} else
msg->gen.flags = 0;
}
static void
maildir_app_msg( maildir_store_t *ctx, message_t ***msgapp, msg_t *entry )
{
maildir_message_t *msg = nfmalloc( sizeof(*msg) );
msg->gen.next = **msgapp;
**msgapp = &msg->gen;
*msgapp = &msg->gen.next;
msg->gen.uid = entry->uid;
msg->gen.status = 0;
maildir_init_msg( ctx, msg, entry );
}
static int
maildir_select_box( store_t *gctx, const char *name )
{
maildir_store_conf_t *conf = (maildir_store_conf_t *)gctx->conf;
maildir_store_t *ctx = (maildir_store_t *)gctx;
maildir_cleanup( gctx );
ctx->msgs = 0;
ctx->excs.data = 0;
ctx->uvfd = -1;
#ifdef USE_DB
ctx->db = 0;
ctx->usedb = 0;
#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_CANCELED;
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, int 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 = 0;
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 (((maildir_store_conf_t *)gctx->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 int
maildir_get_uidnext( store_t *gctx ATTR_UNUSED )
{
return 0;
}
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->newuid = 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, sizeof(buf) - bl, ".uidvalidity" );
if (unlink( buf ) && errno != ENOENT)
goto badrm;
#ifdef USE_DB
nfsnprintf( buf + bl, sizeof(buf) - 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 int
maildir_prepare_load_box( store_t *gctx, int 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 newuid, uint seenuid, 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;
message_t **msgapp;
msg_t_array_alloc_t msglist;
int i;
ctx->minuid = minuid;
ctx->maxuid = maxuid;
ctx->newuid = newuid;
ctx->seenuid = seenuid;
ARRAY_SQUEEZE( &excs );
ctx->excs = excs;
if (maildir_scan( ctx, &msglist ) != DRV_OK) {
cb( DRV_BOX_BAD, 0, 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, ctx->total_msgs, ctx->recent_msgs, aux );
}
static int
maildir_rescan( maildir_store_t *ctx )
{
message_t **msgapp;
maildir_message_t *msg;
msg_t_array_alloc_t msglist;
int i;
ctx->fresh[0] = ctx->fresh[1] = 0;
if (maildir_scan( ctx, &msglist ) != DRV_OK)
return DRV_BOX_BAD;
for (msgapp = &ctx->msgs, i = 0;
(msg = (maildir_message_t *)*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->gen.uid );
msg->gen.status = M_DEAD;
msgapp = &msg->gen.next;
} else if (msglist.array.data[i].uid < msg->gen.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->gen.uid) {
debug( "purging deleted message %u\n", msg->gen.uid );
msg->gen.status = M_DEAD;
msgapp = &msg->gen.next;
} else {
debug( "updating message %u\n", msg->gen.uid );
msg->gen.status &= ~(M_FLAGS|M_RECENT);
free( msg->base );
free( msg->gen.msgid );
maildir_init_msg( ctx, msg, msglist.array.data + i );
i++, msgapp = &msg->gen.next;
}
}
maildir_free_scan( &msglist );
return DRV_OK;
}
static int
maildir_again( maildir_store_t *ctx, maildir_message_t *msg,
const char *err, const char *fn, const char *fn2 )
{
int ret;
if (errno != ENOENT) {
sys_error( err, fn, fn2 );
return DRV_BOX_BAD;
}
if ((ret = maildir_rescan( ctx )) != DRV_OK)
return ret;
return (msg->gen.status & M_DEAD) ? DRV_MSG_BAD : DRV_OK;
}
static void
maildir_fetch_msg( store_t *gctx, message_t *gmsg, msg_data_t *data,
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, 0 )) != DRV_OK) {
cb( ret, aux );
return;
}
}
fstat( fd, &st );
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 );
close( fd );
cb( DRV_MSG_BAD, aux );
return;
}
close( fd );
if (!(gmsg->status & M_FLAGS))
data->flags = maildir_parse_flags( ((maildir_store_conf_t *)gctx->conf)->info_prefix, msg->base );
cb( DRV_OK, aux );
}
static int
maildir_make_flags( char info_delimiter, int flags, char *buf )
{
uint i, d;
buf[0] = info_delimiter;
buf[1] = '2';
buf[2] = ',';
for (d = 3, i = 0; i < as(Flags); i++)
if (flags & (1 << i))
buf[d++] = Flags[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[NUM_FLAGS + 3], base[128];
bl = nfsnprintf( base, sizeof(base), "%ld.%d_%d.%s", (long)time( 0 ), 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, sizeof(base) - bl, ",U=%u", uid );
}
box = ctx->path;
} else {
uid = 0;
box = ctx->trash;
}
maildir_make_flags( ((maildir_store_conf_t *)gctx->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 != 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_find_new_msgs( store_t *gctx ATTR_UNUSED, uint newuid ATTR_UNUSED,
void (*cb)( int sts, message_t *msgs, void *aux ) ATTR_UNUSED, void *aux ATTR_UNUSED )
{
assert( !"maildir_find_new_msgs is not supposed to be called" );
}
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_conf_t *conf = (maildir_store_conf_t *)gctx->conf;
maildir_store_t *ctx = (maildir_store_t *)gctx;
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, bbl - 1 );
memcpy( nbuf + bbl - 1, "/cur/", 5 );
for (;;) {
bl = bbl + nfsnprintf( buf + bbl, sizeof(buf) - bbl, "%s/", subdirs[gmsg->status & M_RECENT] );
ol = strlen( msg->base );
if ((int)sizeof(buf) - bl < ol + 3 + NUM_FLAGS)
oob();
memcpy( buf + bl, msg->base, ol + 1 );
memcpy( nbuf + bl, msg->base, ol + 1 );
if ((s = strstr( nbuf + bl, conf->info_prefix ))) {
s += 3;
fl = ol - (s - (nbuf + bl));
for (i = 0; i < as(Flags); i++) {
if ((p = strchr( s, Flags[i] ))) {
if (del & (1 << i)) {
memmove( p, p + 1, fl - (p - s) );
fl--;
}
} else if (add & (1 << i)) {
for (j = 0; j < fl && Flags[i] > s[j]; j++);
fl++;
memmove( s + j + 1, s + j, fl - j );
s[j] = Flags[i];
}
}
tl = ol + 3 + fl;
} else {
tl = ol + maildir_make_flags( conf->info_delimiter, msg->gen.flags, 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, tl );
msg->gen.flags |= add;
msg->gen.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( ((maildir_store_conf_t *)ctx->gen.conf)->info_stop, &key, (char *)name );
if ((ret = ctx->db->del( ctx->db, 0, &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, ((maildir_store_conf_t *)gctx->conf)->info_prefix );
nfsnprintf( nbuf, sizeof(nbuf), "%s/%s/%ld.%d_%d.%s%s", ctx->trash,
subdirs[gmsg->status & M_RECENT], (long)time( 0 ), 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--;
#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;
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, sizeof(buf) - basel, "%s/%s", subdirs[msg->status & M_RECENT], ((maildir_message_t *)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--;
#ifdef USE_DB
if (ctx->db && (ret = maildir_purge_msg( ctx, ((maildir_message_t *)msg)->base )) != DRV_OK) {
cb( ret, aux );
return;
}
#endif /* USE_DB */
}
}
if (!retry) {
cb( DRV_OK, aux );
return;
}
if ((ret = maildir_rescan( (maildir_store_t *)gctx )) != 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 int
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 = nfcalloc( sizeof(*store) );
store->info_delimiter = FieldDelimiter;
store->gen.driver = &maildir_driver;
store->gen.name = nfstrdup( cfg->val );
while (getcline( cfg ) && cfg->cmd)
if (!strcasecmp( "Inbox", cfg->cmd ))
store->inbox = expand_strdup( cfg->val );
else if (!strcasecmp( "Path", cfg->cmd ))
store->gen.path = expand_strdup( cfg->val );
#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 );
if (!store->inbox)
store->inbox = expand_strdup( "~/Maildir" );
if (store->sub_style == SUB_MAILDIRPP && store->gen.path) {
error( "Maildir store '%s': Setting Path is incompatible with 'SubFolders Maildir++'\n", store->gen.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 int
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_bad_callback,
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_confirm_box_empty,
maildir_delete_box,
maildir_finish_delete_box,
maildir_prepare_load_box,
maildir_load_box,
maildir_fetch_msg,
maildir_store_msg,
maildir_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,
};