/*
* maildir.c:
* Qmail-style maildir support for tpop3d.
*
* Copyright (c) 2001 Paul Makepeace (realprogrammers.com).
* All rights reserved.
*
*/
#ifdef HAVE_CONFIG_H
#include "configuration.h"
#endif /* HAVE_CONFIG_H */
#ifdef MBOX_MAILDIR
static const char rcsid[] = "$Id: maildir.c,v 1.19 2003/11/24 20:23:20 chris Exp $";
#include <sys/types.h>
#include <dirent.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <syslog.h>
#include <unistd.h>
#include <utime.h>
#include <time.h>
#include <sys/fcntl.h>
#include <sys/stat.h>
#include <sys/time.h>
#include "config.h"
#include "connection.h"
#include "mailbox.h"
#include "util.h"
#include "vector.h"
/*
* Although maildir is a locking-free mailstore, we optionally support the
* exclusive locking of maildirs so that we can implement the RFC1939
* semantics where POP3 sessions are exclusive. To do this we create a lock
* directory called .poplock in the root of the maildir. This is convenient
* because mkdir(2) is atomic, even on NFS.
*/
/* MAILDIR_LOCK_LIFETIME
* How long a maildir lock lasts if it is never unlocked. */
#define MAILDIR_LOCK_LIFETIME 1800
/* maildir_lock DIRECTORY
* Attempt to atomically create a .poplock lock directory in DIRECTORY. Returns
* 1 on success or 0 on failure. If such a directory exists and is older than
* MAILDIR_LOCK_LIFETIME, we will update the time in it, claiming the lock
* ourselves. */
static int maildir_lock(const char *dirname) {
char *lockdirname = NULL;
int ret = 0;
lockdirname = xmalloc(strlen(dirname) + sizeof("/.poplock"));
sprintf(lockdirname, "%s/.poplock", dirname);
if (mkdir(lockdirname, 0777) == -1) {
if (errno == EEXIST) {
/*
* Already locked. Now we have a problem, because we can't
* atomically discover the creation time of the directory and
* update it. For the moment, just do this the nonatomic way and
* hope for the best. It's not too serious since we now react
* properly in the case that the message has been deleted by
* another user.
*/
struct stat st;
if (stat(lockdirname, &st) == -1)
log_print(LOG_ERR, _("maildir_lock: %s: could not stat .poplock directory: %m"), dirname);
else if (st.st_atime < time(NULL) - MAILDIR_LOCK_LIFETIME) {
/* XXX Race condition here. */
if (utime(lockdirname, NULL) == -1)
log_print(LOG_ERR, _("maildir_lock: %s: could not update access time on .poplock directory: %m"), dirname);
else {
log_print(LOG_WARNING, _("maildir_lock: %s: grabbed stale (age %d:%02d) lock"), dirname, (int)(time(NULL) - st.st_atime) / 60, (int)(time(NULL) - st.st_atime) % 60);
ret = 1;
}
}
} else
log_print(LOG_ERR, _("maildir_lock: %s: could not create .poplock directory: %m"), dirname);
} else
ret = 1;
if (lockdirname)
xfree(lockdirname);
return ret;
}
/* maildir_update_lock DIRECTORY
* Update the access time on DIRECTORY. */
static void maildir_update_lock(const char *dirname) {
static time_t last_updated_lock;
if (last_updated_lock < time(NULL) - 60) {
char *lockdirname = NULL;
lockdirname = xmalloc(strlen(dirname) + sizeof("/.poplock"));
sprintf(lockdirname, "%s/.poplock", dirname);
utime(lockdirname, NULL);
xfree(lockdirname);
last_updated_lock = time(NULL);
}
}
/* maildir_unlock DIRECTORY
* Remove any .poplock lock directory in DIRECTORY. */
static void maildir_unlock(const char *dirname) {
char *lockdirname;
lockdirname = xmalloc(strlen(dirname) + sizeof("/.poplock"));
sprintf(lockdirname, "%s/.poplock", dirname);
rmdir(lockdirname); /* Nothing we can do if this fails. */
xfree(lockdirname);
}
/* maildir_make_indexpoint:
* Make an indexpoint to put in a maildir. */
static void maildir_make_indexpoint(struct indexpoint *m, const char *filename, off_t size, time_t mtime) {
memset(m, 0, sizeof(struct indexpoint));
m->filename = xstrdup(filename);
if (!m->filename) {
return;
}
m->offset = 0; /* not used */
m->length = 0; /* "\n\nFrom " delimiter not used */
m->deleted = 0;
m->msglength = size;
/* In previous versions of tpop3d, the first 16 characters of the file name
* of a maildir message were used to form a unique ID. Unfortunately, this
* is not a good strategy, especially now that time_t's are 10 characters
* long. So now we form an MD5 hash of the file name; obviously, these
* unique IDs are not compatible with the old ones, so optionally you can
* retain the old scheme by replacing the following line with
* strncpy(m->hash, filename+4, sizeof(m->hash));
*/
md5_digest(filename + 4, strcspn(filename + 4, ":"), m->hash); /* +4: skip cur/ or new/ subdir; ignore flags at end. */
m->mtime = mtime;
}
/* maildir_build_index MAILDIR SUBDIR TIME
* Build an index of the MAILDIR; SUBDIRis one of cur, tmp or new; TIME is the
* time at which the operation started, used to ignore messages delivered
* during processes. Returns 0 on success, -1 otherwise. */
int maildir_build_index(mailbox M, const char *subdir, time_t time) {
DIR *dir;
struct dirent *d;
if (!M) return -1;
dir = opendir(subdir);
if (!dir) {
log_print(LOG_ERR, "maildir_build_index: opendir(%s): %m", subdir);
return -1;
}
while ((d = readdir(dir))) {
struct stat st;
char *filename;
if (d->d_name[0] == '.') continue;
filename = xmalloc(strlen(subdir) + strlen(d->d_name) + 2);
sprintf(filename, "%s/%s", subdir, d->d_name);
if (!filename) return -1;
if (stat(filename, &st) == 0 && st.st_mtime < time) {
/* These get sorted by mtime later. */
struct indexpoint pt;
maildir_make_indexpoint(&pt, filename, st.st_size, st.st_mtime);
mailbox_add_indexpoint(M, &pt);
/* Accumulate size of messages. */
M->totalsize += st.st_size;
}
xfree(filename);
}
closedir(dir);
if (d) {
log_print(LOG_ERR, "maildir_build_index: readdir(%s): %m", subdir);
return -1;
}
#ifdef IGNORE_CCLIENT_METADATA
#warning IGNORE_CCLIENT_METADATA not supported with maildir.
#endif /* IGNORE_CCLIENT_METADATA */
return 0;
}
/* maildir_sort_callback A B
* qsort(3) callback for ordering messages in a maildir. */
int maildir_sort_callback(const void *a, const void *b) {
const struct indexpoint *A = a, *B = b;
return A->mtime - B->mtime;
}
/* maildir_new DIRECTORY
* Create a mailbox object from the named DIRECTORY. */
mailbox maildir_new(const char *dirname) {
mailbox M, failM = NULL;
struct timeval tv1, tv2;
float f;
int locked = 0;
alloc_struct(_mailbox, M);
M->delete = maildir_delete; /* generic destructor */
M->apply_changes = maildir_apply_changes;
M->sendmessage = maildir_sendmessage;
/* Allocate space for the index. */
M->index = (struct indexpoint*)xcalloc(32, sizeof(struct indexpoint));
M->size = 32;
if (chdir(dirname) == -1) {
if (errno == ENOENT) failM = MBOX_NOENT;
else log_print(LOG_ERR, "maildir_new: chdir(%s): %m", dirname);
goto fail;
} else
M->name = xstrdup(dirname);
/* Optionally, try to lock the maildir. */
if (config_get_bool("maildir-exclusive-lock") && !(locked = maildir_lock(M->name))) {
log_print(LOG_INFO, _("maildir_new: %s: couldn't lock maildir"), dirname);
goto fail;
}
gettimeofday(&tv1, NULL);
/* Build index of maildir. */
if (maildir_build_index(M, "new", tv1.tv_sec) != 0) goto fail;
if (maildir_build_index(M, "cur", tv1.tv_sec) != 0) goto fail;
/* Now sort the messages. */
qsort(M->index, M->num, sizeof(struct indexpoint), maildir_sort_callback);
gettimeofday(&tv2, NULL);
f = (float)(tv2.tv_sec - tv1.tv_sec) + 1e-6 * (float)(tv2.tv_usec - tv1.tv_usec);
log_print(LOG_DEBUG, "maildir_new: scanned maildir %s (%d messages) in %0.3fs", dirname, (int)M->num, f);
return M;
fail:
if (M) {
if (locked) maildir_unlock(M->name);
if (M->name) xfree(M->name);
if (M->index) xfree(M->index);
xfree(M);
}
return failM;
}
/* maildir_delete MAILDIR
* Destructor for MAILDIR; this does nothing maildir-specific unless maildir
* locking is enabled, in which case we must unlock it. */
void maildir_delete(mailbox M) {
if (config_get_bool("maildir-exclusive-lock"))
maildir_unlock(M->name);
mailbox_delete(M);
}
/* maildir_open_message_file MESSAGE
* Return a file descriptor on the file associated with MESSAGE. If it has
* changed name since then, we try to find the file and update MESSAGE. If we
* can't find the MESSAGE, return -1. */
static int open_message_file(struct indexpoint *m) {
int fd;
DIR *d;
struct dirent *de;
size_t msgnamelen;
fd = open(m->filename, O_RDONLY);
if (fd != -1)
return fd;
/*
* Where's that message?
*/
/* Possibility 1: message was in new/, and is now in cur/ with a :2,S
* suffix. */
if (strncmp(m->filename, "new/", 4)) {
char *name;
name = xmalloc(strlen(m->filename) + sizeof(":2,S"));
sprintf(name, "cur/%s:2,S", m->filename + 4);
if ((fd = open(name, O_RDONLY)) != -1) {
/* We win! */
xfree(m->filename);
m->filename = name;
return fd;
} else if (errno != ENOENT) {
/* Bad news. */
log_print(LOG_ERR, "maildir_open_message_file: %s: %m", name);
xfree(name);
return -1;
}
}
/* Possibility 2: message is now in cur with some random suffix. This is
* really bad, because we need to rescan the whole maildir. But this
* shouldn't happen very often. */
/* Figure out the name of the message. */
msgnamelen = strcspn(m->filename + 4, ":");
if (!(d = opendir("cur"))) {
log_print(LOG_ERR, "maildir_open_message_file: cur: %m");
return -1;
}
while ((de = readdir(d))) {
/* Compare base name of this message against the new message. */
if (strncmp(de->d_name, m->filename + 4, msgnamelen) == 0
&& (de->d_name[msgnamelen] == ':' || de->d_name[msgnamelen] == 0)) {
char *name;
name = xmalloc(strlen(de->d_name) + sizeof("cur/"));
sprintf(name, "cur/%s", de->d_name);
if ((fd = open(name, O_RDONLY)) != -1) {
closedir(d);
xfree(m->filename);
m->filename = name;
return fd;
} else {
/* Either something's gone wrong or the message has just been
* moved or deleted. Just give up at this point. */
closedir(d);
xfree(name);
return -1;
}
}
}
log_print(LOG_ERR, _("maildir_open_message_file: %s: can't find message"), m->filename);
/* Message must have been deleted. */
return -1;
}
/* maildir_sendmessage MAILDIR CONNECTION MSGNUM LINES
* Send a +OK response and the header and the given number of LINES of the body
* of message number MSGNUM from MAILDIR escaping lines which begin . as
* required by RFC1939, or, if it cannot, a -ERR error response. Returns 1 on
* is -1. Sends a +OK / -ERR message in front of the message itself. Note that
* the maildir specification says that messages use only `\n' to indicate EOL,
* though some extended formats don't. We assume that the specification is
* obeyed. It's possible that the message will have moved or been deleted under
* us, in which case we make some effort to find the new version. */
int maildir_sendmessage(const mailbox M, connection c, const int i, int n) {
struct indexpoint *m;
int fd, status;
if (!M || i < 0 || i >= M->num) {
/* Shouldn't happen. */
connection_sendresponse(c, 0, _("Unable to send that message"));
return -1;
}
if (config_get_bool("maildir-exclusive-lock"))
maildir_update_lock(M->name);
m = M->index +i;
if ((fd = open_message_file(m)) == -1) {
connection_sendresponse(c, 0, _("Can't send that message; it may have been deleted by a concurrent session"));
log_print(LOG_ERR, "maildir_sendmessage: unable to send message %d", i + 1);
return -1;
}
status = connection_sendmessage(c, fd, 0 /* offset */, 0 /* skip */, m->msglength, n);
close(fd);
return status;
}
/* maildir_apply_changes MAILDIR
* Apply deletions to a maildir. */
int maildir_apply_changes(mailbox M) {
struct indexpoint *m;
if (!M) return 1;
for (m = M->index; m < M->index + M->num; ++m) {
if (m->deleted) {
if (unlink(m->filename) == -1)
log_print(LOG_ERR, "maildir_apply_changes: unlink(%s): %m", m->filename);
/* Warn but proceed anyway. */
} else {
/* Mark message read. */
if (strncmp(m->filename, "new/", 4) == 0) {
char *cur;
cur = xmalloc(strlen(m->filename) + 5);
sprintf(cur, "cur/%s:2,S", m->filename + 4); /* Set seen flag */
rename(m->filename, cur); /* doesn't matter if it can't */
xfree(cur);
}
}
}
return 1;
}
#endif /* MBOX_MAILDIR */
syntax highlighted by Code2HTML, v. 0.9.1