/*
#ident "@(#)smail/src:RELEASE-3_2_0_121:spool.c,v 1.77 2005/07/12 18:49:33 woods Exp"
*/
/*
* Copyright (C) 1987, 1988 Ronald S. Karr and Landon Curt Noll
* Copyright (C) 1992 Ronald S. Karr
*
* See the file COPYING, distributed with smail, for restriction
* and warranty information.
*/
/*
* spool.c:
* message spooling and retrieval. This source file implements a
* reliable spooling system which is resiliant against inaccessible
* directories, create errors and write errors. The algorithms here
* are set up such that alternate directories can be used in the case
* that smail is not able to complete spooling to a primary spool
* directory.
*
* NOTE: This section will probably require substantial
* modifications to work with a non-UN*X operating
* system.
*
* external functions: creat_spool, write_spool, open_spool,
* close_spool, unlink_spool, seek_spool,
* tell_spool, send_spool, read_spool,
* log_spool_errors, new_grade, freeze_message,
* message_date
*/
#include "defs.h"
#include <sys/types.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <limits.h>
#ifdef STDC_HEADERS
# include <stdlib.h>
# include <stddef.h>
#else
# ifdef HAVE_STDLIB_H
# include <stdlib.h>
# endif
#endif
#ifdef HAVE_STRING_H
# if !defined(STDC_HEADERS) && defined(HAVE_MEMORY_H)
# include <memory.h>
# endif
# include <string.h>
#endif
#ifdef HAVE_STRINGS_H
# include <strings.h>
#endif
#ifdef __STDC__
# include <stdarg.h>
#else
# include <varargs.h>
#endif
#if defined(UNIX_SYS5_4)
# include <sys/sysmacros.h> /* for MIN() & MAX() */
#endif
#ifdef HAVE_STATVFS
# include <sys/statvfs.h> /* SysVr4 & SUSv2 */
#else /* assume HAVE_STATFS */
# ifdef HAVE_SYS_STATFS_H
# include <sys/statfs.h> /* SysVr3 */
# else
# ifdef HAVE_SYS_VFS_H
# include <sys/vfs.h> /* ??? */
# else
# include <sys/mount.h> /* just 4.4BSD? */
# endif
# endif
#endif
#ifdef TIME_WITH_SYS_TIME
# include <sys/time.h>
# include <time.h>
#else
# ifdef HAVE_SYS_TIME_H
# include <sys/time.h>
# else
# include <time.h>
# endif
#endif
#if defined(UNIX_SYS5) || defined(POSIX_OS) || defined(USE_FCNTL)
# include <fcntl.h>
#else
# ifdef UNIX_BSD
# include <sys/file.h>
# endif
#endif
#ifdef __GLIBC__
# include <sys/file.h>
#endif
#ifdef HAVE_SYS_LOCKING_H
# include <sys/locking.h>
#endif
#if defined(HAVE_UNISTD_H)
# include <unistd.h>
#endif
#include <pcre.h>
#include "smail.h"
#include "smailarch.h"
#include "alloc.h"
#include "list.h"
#include "smailstring.h"
#include "dys.h"
#include "main.h"
#include "parse.h"
#include "addr.h"
#include "spool.h"
#include "direct.h"
#include "route.h"
#include "log.h"
#include "match.h"
#include "transport.h"
#include "exitcodes.h"
#include "debug.h"
#include "extern.h"
#include "smailport.h"
#ifdef STANDALONE
# define xmalloc malloc
# define xfree free
extern char *malloc();
int force_write_error = FALSE;
#endif /* STANDALONE */
/* variables exported from this file */
char *message_id = NULL; /* unique ID for this message */
char *spool_dir; /* directory used to spool message */
char *spool_fn = NULL; /* basename of open spool file */
char *input_spool_fn = NULL; /* name in input/ directory */
int spoolfile = -1; /* open spool file */
char *lock_fn; /* name of lock file for spool_fn */
char *msg_buf; /* i/o buffer for spool file */
char *msg_ptr; /* read placeholder in msg_buf */
char *msg_max; /* last valid char in msg_buf */
char *end_msg_buf; /* end of msg_buf */
off_t msg_foffset; /* file offset for msg_buf contents */
off_t msg_size; /* size of spool file */
time_t msg_mtime; /* last modified time of spool file */
/* types local to this file */
struct log_msgs { /* XXX this should be replaced by charplist_t */
struct log_msgs *succ;
char msg[1];
};
enum locker { l_creat, l_open, l_lock }; /* who is trying to lock */
/* variables local to this file */
static char *temp_fn = NULL; /* temp spool file name */
static char *funct_name; /* name of current function */
static char *old_spool_dir; /* saved value of spool_dir */
static char *old_spool_fn; /* saved value of spool_fn */
static char *old_input_spool_fn; /* saved value of input_spool_fn */
static int old_spoolfile; /* saved value of spoolfile */
static struct log_msgs *log_msgs; /* message lines to send to log */
static struct log_msgs **next_log_msg; /* where to put next log message */
static off_t start_msg_body = 0; /* possibly the start of the message body */
/* functions local to this file */
static int int_creat_spool __P((void));
static void build_spool_fn __P((int));
static void build_message_id __P((void));
static int set_alt_spool __P((char *));
static int lock_spoolfile __P((enum locker));
static void copy_old_names __P((void));
static void failed_write __P((void));
static int copy_old_spool __P((void));
static void log_message __P((char *));
/*
* creat_spool - create a spool file
*
* This is external entrypoint for spool file creation. It initializes some
* state and then calls int_creat_spool() to do the real work.
*
* If spoolfile creation succeeded, return SUCCEED. Also, the following
* external variables will be set:
*
* spool_dir - the directory in which the spoolfile was created
* spool_fn - the basename of the generated file
* input_spool_fn - the name of the spool file in the input directory
* spoolfile - the file descriptor (used by the PUTSPOOL macro)
* message_id - a unique message ID computed from the filename
* msg_buf,msg_ptr - points to a region in which the message can be
* written into memory
*
* In addition, the current directory will be set to spool_dir, and the file
* will be locked against premature attempts at delivery by background
* processes.
*
* If spoolfile creation failed, return FAIL. In this case, spool_fn,
* input_spool_fn, spool_dir and message_id will all be set to NULL.
*/
int
creat_spool()
{
DEBUG(DBG_SPOOL_HI, "creat_spool called\n");
#ifndef NDEBUG
funct_name = "creat_spool";
#endif
temp_fn = NULL;
spool_fn = NULL;
input_spool_fn = NULL;
message_id = NULL;
lock_fn = NULL;
spoolfile = -1;
log_msgs = NULL;
next_log_msg = &log_msgs;
msg_foffset = 0;
msg_size = 0;
msg_mtime = time((time_t *) NULL);
if (msg_buf == NULL) {
/* allocate the spool i/o buffer if it does not yet exist */
msg_buf = xmalloc(message_buf_size);
}
msg_max = msg_buf; /* start at beginning of buffer */
msg_ptr = msg_buf;
end_msg_buf = msg_buf + message_buf_size;
if (int_creat_spool() == FAIL) {
DEBUG(DBG_SPOOL_LO, "creat_spool failed!\n");
return FAIL;
}
DEBUG2(DBG_SPOOL_MID, "new spool file is %s/%s\n",
spool_dir, input_spool_fn);
return SUCCEED;
}
/*
* int_creat_spool - create a spool file (internal form)
*
* try to create a spool file in either the primary or alternate spool
* directory. The file will be locked and empty at the end of creat_spool.
*
* return SUCCEED or FAIL. If FAIL, then the spool file and lock file
* will not exist unless they could not be unlinked.
* log_spool_errors should be called once the log files are opened, as
* well.
*/
static int
int_creat_spool()
{
int attempts = 0; /* # open attempts in a directory */
char build_temp_fn[sizeof("input/msg.dddddddddd")];
/* first try the primary spool directory */
spool_dir = NULL;
/* get the first "alternate" directory, which is the primary directory */
if (set_alt_spool((char *) NULL) == FAIL) {
return FAIL;
}
/*
* create the temporary spool file in the first spool directory
* that the open succeeds in.
*/
(void) sprintf((temp_fn = build_temp_fn), "input/msg.%ld", (long int) getpid());
for (;;) {
/*
* unfortunately, the errno returned by creat(2) does not
* distinguish between directory and file access errors, so
* we use the open call, which does, for systems that
* support it
*
* for systems with O_SYNC but without fsync() use O_SYNC to
* ensure that the file is on disk before we return any responses
* to the user.
*/
#ifdef O_EXCL
# if defined(O_SYNC) && !defined(HAVE_FSYNC) && !defined(NO_SPOOL_O_SYNC)
spoolfile = open(temp_fn, O_RDWR|O_CREAT|O_EXCL|O_SYNC, spool_mode);
# else
spoolfile = open(temp_fn, O_RDWR|O_CREAT|O_EXCL, spool_mode);
# endif /* O_SYNC && !HAVE_FSYNC && !NO_SPOOL_O_SYNC */
#else /* O_EXCL */
/*
* if we must use creat, then the mode should not allow writing.
* As well, we will have to reopen for reading later on in
* read_spool, if we try to read on the file without closing
* and then opening again.
*/
spoolfile = creat(temp_fn, spool_mode & (~0222));
if (spoolfile < 0 && errno == EACCES) {
/* we assume that EACCES does not represent a directory error */
errno = EEXIST;
}
#endif /* O_EXCL */
if (spoolfile < 0 && errno == ENOENT) {
/* ENOENT means the directory did not exist, try to make it */
DEBUG1(DBG_SPOOL_LO, "make directory %s/input\n", spool_dir);
(void) mkdir("input", auto_mkdir_mode);
#ifdef O_EXCL
# if defined(O_SYNC) && !defined(HAVE_FSYNC) && !defined(NO_SPOOL_O_SYNC)
spoolfile = open(temp_fn, O_RDWR|O_CREAT|O_EXCL|O_SYNC, spool_mode);
# else
spoolfile = open(temp_fn, O_RDWR|O_CREAT|O_EXCL, spool_mode);
# endif /* O_SYNC && !HAVE_FSYNC && !NO_SPOOL_O_SYNC */
#else /* O_EXCL */
spoolfile = creat(temp_fn, spool_mode & (~0222));
if (spoolfile < 0 && errno == EACCES) {
errno = EEXIST;
}
#endif
}
if (spoolfile < 0) {
if (errno == EEXIST && attempts == 0) {
/* if the spool file existed, then try to unlink it */
DEBUG2(DBG_SPOOL_MID,
"int_creat_spool: %s/input/%s exists, unlinking\n",
spool_dir, temp_fn);
if (unlink(temp_fn) < 0) {
DEBUG3(DBG_SPOOL_MID,
"int_creat_spool: unlink(%s/input/%s): %s\n",
spool_dir, temp_fn, strerror(errno));
}
attempts++; /* only attempt unlink once */
continue;
}
/* otherwise we need to try an alternate spool directory */
if (set_alt_spool("cannot create spool file") == FAIL) {
return FAIL;
}
attempts = 0; /* new directory, reset try count */
continue;
}
/*
* we have an open temp file. Now create a lock file (to prevent other
* smail processes from trying to access this spool file while the
* message is still being spooled) and move the temp file to its more
* permanent name.
*/
build_spool_fn(spool_grade);
#ifndef lock_fd
if (lock_spoolfile(l_creat) == FAIL) {
/* we need to try an alternate directory */
(void) close(spoolfile);
(void) unlink(temp_fn);
if (set_alt_spool("cannot lock_spoolfile() message") == FAIL) {
return FAIL;
}
continue;
}
#else /* lock_fd */
if ((lock_by_name && lock_spoolfile(l_creat) == FAIL) ||
(! lock_by_name && lock_fd(spoolfile) == FAIL))
{
(void) close(spoolfile);
(void) unlink(temp_fn);
if (set_alt_spool(lock_by_name ?
"cannot lock_spoolfile() message" :
"cannot lock_fd() message"
) == FAIL) {
return FAIL;
}
continue;
}
#endif /* lock_fd */
if (rename(temp_fn, input_spool_fn) < 0) {
int oerrno = errno;
/*
* XXX this is actually quite bad! It would suggest the postmaster
* has copied the spool directory causing the old queue names to
* become invalid as they now no longer match their inode numbers!
*/
if (lock_by_name) {
(void) unlink(lock_fn);
}
(void) close(spoolfile);
if (oerrno != EEXIST) { /* otherwise keep temp_fn as a placeholder */
(void) unlink(temp_fn);
}
if (set_alt_spool("failed to rename temp file to spool file") == FAIL) {
return FAIL;
}
continue;
}
/*
* XXX we should probably check to be sure there's also no "error" file
* of the same name, and then if so then:
*
* panic("You evil postmaster! You seem to have copied your spool!")
*/
/* we have a valid, locked, spool file */
return SUCCEED;
}
}
/*
* build_spool_fn - form the spool filename
*
* as a side effect, generate the lock filename, if needed, and
* generate the message_id.
*
* the grade_char represents a priority which is appended to the
* filename.
*/
static void
build_spool_fn(grade_char)
int grade_char;
{
static char fn[SPOOL_FN_LEN + 1]; /* store the actual spool file basename here */
/* this is 14 chars which should work on all UN*X systems */
static char ifn[sizeof("input/") + SPOOL_FN_LEN]; /* name of spool file in input directory */
static char lfn[sizeof("lock/") + SPOOL_FN_LEN]; /* store the lock file name here */
time_t now = time((time_t *) NULL);
struct stat statbuf;
char a_inode[8]; /* ASCII base 62 inode number (+ spare byte) */
/* get the inode number of the temp_fn in base 62 notation */
(void) fstat(spoolfile, &statbuf);
(void) strcpy(a_inode, base62((unsigned long) statbuf.st_ino));
/* point global spool_fn basename pointer at static array above */
spool_fn = fn;
/* form the full basename using BASE-62 time and BASE-62 inode + msg grade */
(void) sprintf(spool_fn, "%s-%s%c", base62((unsigned long) now), a_inode, grade_char);
/* use this to build the path in the input directory */
input_spool_fn = ifn;
(void) sprintf(input_spool_fn, "input/%s", spool_fn);
/* have the message-ID computed from the computed spool file name */
build_message_id();
if (lock_by_name) {
lock_fn = lfn;
(void) sprintf(lock_fn, "lock/%s", spool_fn);
}
DEBUG2(DBG_SPOOL_HI, "build_spool_fn: try spool file %s/%s\n",
spool_dir, input_spool_fn);
}
/*
* build_message_id - build message_id from the value of spool_fn
*
* the only difference is that the message-Id begins with the letter `m', to
* make it a valid local-addr vis-a-vis RFC822.
*/
static void
build_message_id()
{
static char m_id[sizeof("m") + SPOOL_FN_LEN];
message_id = m_id;
(void) sprintf(message_id, "m%s", spool_fn);
}
/*
* lock_message - lock the current message
*
* When forking a process to deliver mail, locks are released by the
* parent process and then regained in the child process by calling
* lock_message(). If the lock cannot be regained then the child
* process should exit, assuming that some other process has decided
* to attempt delivery. This method is necessary when using lock
* files, as pids are used to determine whether the locking process
* still exists. It is also necessary when using the System V lockf
* call as this does not preserve locks in a child of the process that
* made the lock.
*
* Returns SUCCEED if the lock was obtained, FAIL otherwise.
*/
int
lock_message()
{
struct stat statbuf;
int success;
#ifdef lock_fd
off_t offset;
if (lock_by_name) {
success = lock_spoolfile(l_lock);
} else {
offset = lseek(spoolfile, (off_t) 0L, SEEK_CUR); /* XXX error check! */
(void) lseek(spoolfile, (off_t) 0L, SEEK_SET); /* XXX error check! */
success = lock_fd(spoolfile);
(void) lseek(spoolfile, offset, SEEK_SET); /* XXX error check! */
}
#else
success = lock_spoolfile(l_lock);
#endif
if (success == FAIL) {
return FAIL;
}
if (fstat(spoolfile, &statbuf) < 0) {
DEBUG3(DBG_SPOOL_MID,
"lock_message: %s/%s: fstat() failed: %s\n",
spool_dir, input_spool_fn, strerror(errno));
close_spool();
return FAIL;
}
/*
* If the file has been removed, then don't try to process it.
*/
if (statbuf.st_nlink == 0) {
DEBUG2(DBG_SPOOL_MID,
"lock_message: %s/%s: spool file was removed\n",
spool_dir, input_spool_fn);
close_spool();
return FAIL;
}
return SUCCEED;
}
/*
* unlock_message - unlock the current message
*
* The parent process calls this before forking a process to perform
* delivery. See lock_message() for an explanation.
*/
void
unlock_message()
{
off_t offset;
#ifdef lock_fd
if (lock_by_name) {
#endif
if (lock_fn) {
DEBUG2(DBG_SPOOL_HI, "close_spool: unlinking (lock) %s/%s\n",
spool_dir, lock_fn);
(void) unlink(lock_fn);
}
#ifdef lock_fd
} else {
offset = lseek(spoolfile, (off_t) 0L, SEEK_CUR); /* XXX error check! */
(void) lseek(spoolfile, (off_t) 0L, SEEK_SET);
unlock_fd(spoolfile);
(void) lseek(spoolfile, offset, SEEK_SET);
}
#endif
}
/*
* lock_spoolfile - create a lock file for the spool file
*
* algorithm:
* 1. create lock file exclusively. If this fails, goto step 3.
* 2. write ASCII process id, followed by newline. If this succeeds,
* then the lock succeeded, close the file and return.
* 3. stat the lock file
* 4. if st_size != 0, the proceed to step 10
* 5. The lock file is empty because the locking process has
* not yet written out its process id, or because the
* process or system crashed prior to the write.
* If the current time < Oct 28, 1986, then proceed to step 7.
* If st_ctime > the current time then proceed to step 8.
* If st_ctime < `now' - 2 hours proceed to step 9.
* 6. The empty lock file is too new to touch (the system could
* be very heavely loaded) and so we simply conclude that
* some process has it locked and go on to do something else.
* 7. Since this version of smail did not exist on Oct 28, 1986,
* the current time must be set wrong. At this point
* we give up all hope of stale lock file detection,
* consider the file locked and go on to do something else.
* 8. The empty lock file was created in the future so we don't
* know if the empty lock file is stale or not. We first
* bring the st_ctime of the file back into reality by
* doing a chmod (to the same permission). This forces
* the st_ctime to be set to `now'. Next we send the
* following error message to the system log:
*
* time warp on zero length lock file: lock/0571338010a72y
*
* We consider the lock file active for now.
* 9. The empty lock file is very likely stale. We will
* remove the lock file but we will still consider it
* locked, allowing some future process to operate
* 10. The lock file contains the process id of the locking
* process. We open the lock file and read it to find
* the associated pid.
* If pid == 0, then proceed to step 13.
* If pid is not a process, then proceed to step 12.
* 11. The process pid is a valid and currently
* running process, and thus the lock file is really
* valid. At this point we go on to do something else.
* 12. The lock file is very likely stale. We will remove the
* lock file but we will still consider it locked, allowing
* some future process to operate on the spool file. We
* now go on to do something else.
* 13. If pid == 0, then the lock file is a
* permanent lock file. (perhaps set by the system
* administrator to freeze a mail message) At this
* point we go on to do something else.
*
* return either SUCCEED or FAIL, and set errno to EEXIST if
* the spoolfile is already locked.
*/
static int lock_creat __P((void));
static int check_empty_lock __P((struct stat *));
static int verify_lock __P((void));
static int
lock_spoolfile(who)
enum locker who; /* who is locking, open or creat */
{
struct stat statbuf; /* temp buf for stats */
int attempts = 0;
int mkdir_tried = FALSE; /* TRUE if mkdir("lock") tried */
DEBUG1(DBG_SPOOL_HI, "lock_spoolfile called, lock_fn = %s\n", lock_fn);
/*
* loop until we have a lock or we fail to get a lock
*/
for (;;) {
/*
* try to create one
*/
if (lock_creat() == FAIL) {
if (errno == ENOENT && !mkdir_tried && auto_mkdir) {
mkdir_tried = TRUE;
DEBUG1(DBG_SPOOL_LO, "make directory %s/lock\n",
spool_dir);
(void) mkdir("lock", auto_mkdir_mode);
continue;
}
if (errno != EEXIST) {
/* it failed, but not because it exists, can't handle this */
DEBUG1(DBG_SPOOL_HI, "create failed: %s\n", strerror(errno));
return FAIL;
}
if (who == l_creat) {
/*
* for creat_spool, no lock file should exist because
* of inode uniqueness on a filesystem. Just unlink
* the lock file and try again (once).
*/
if (attempts == 0) {
(void) unlink(lock_fn);
attempts++;
continue;
}
DEBUG3(DBG_SPOOL_MID,
"lock_spoolfile: %s/%s: lock failed: %s\n",
spool_dir, lock_fn, strerror(errno));
return FAIL;
}
} else {
/* the create worked, great! */
return SUCCEED;
}
if (stat(lock_fn, &statbuf) < 0) {
/* failed to stat the file, hmmm */
DEBUG3(DBG_SPOOL_LO,
"lock_spoolfile: %s/%s: stat failed: %s\n",
spool_dir, lock_fn, strerror(errno));
return FAIL;
}
if (statbuf.st_size == 0) {
if (check_empty_lock(&statbuf) == SUCCEED) {
errno = EEXIST; /* lock was upheld, for now */
return FAIL;
}
} else {
if (verify_lock() == SUCCEED) {
errno = EEXIST; /* lock was upheld */
return FAIL;
}
}
/*
* we consider the lock to be stale, so unlink it,
* and let a future process try again.
*/
(void) unlink(lock_fn);
DEBUG2(DBG_SPOOL_LO,
"lock_spoolfile: %s/%s: stale lock file removed\n",
spool_dir, lock_fn);
errno = EEXIST;
return FAIL;
}
}
/*
* lock_creat - try to create a lock and write into it the current pid
*
* this is only called from lock_spoolfile and returns SUCCEED or FAIL.
*/
static int
lock_creat()
{
int lfd;
char apid[MAXLONG_B10_DIGITS + 1]; /* holds the pid as decimal# + \0 */
long ct;
#ifdef O_EXCL
lfd = open(lock_fn, O_RDWR|O_CREAT|O_EXCL, lock_mode);
#else /* O_EXCL */
/*
* it is time for the silly creat trick again
*/
lfd = creat(lock_fn, lock_mode&(~0222));
if (lfd < 0 && errno == EACCES) {
errno = EEXIST;
}
#endif /* O_EXCL */
if (lfd < 0) {
/* we failed to creat the lock the file */
return FAIL;
}
(void) sprintf(apid, "%ld\n", (long int) getpid());
ct = (long) strlen(apid);
if (write(lfd, apid, (size_t) ct) < ct) {
/* we failed to write the pid into the lock file, quit */
(void) unlink(lock_fn);
(void) close(lfd);
return FAIL;
}
(void) close(lfd);
return SUCCEED;
}
/*
* check_empty_lock - decide if zero-length lock file is stale
*
* return SUCCEED if not stale and should be kept, FAIL otherwise.
*/
static int
check_empty_lock(stp)
struct stat *stp; /* statbuf from lock_spoolfile */
{
struct tm *ctm;
time_t now = time((time_t *) NULL); /* verify based on age */
ctm = gmtime((time_t *) &now);
if (ctm->tm_year <= 86 && ctm->tm_mon <= 10 && ctm->tm_mday < 28) {
/*
* it is before chongo's birthday, but on a year that must be
* in the past. Since this version of smail has not been
* written yet, punt.
*/
write_log(WRITE_LOG_SYS|WRITE_LOG_CONS, "machine is in a time warp, check date");
return SUCCEED;
}
if (stp->st_ctime > now) {
/*
* file created in the future, bring it back to the present, but
* otherwise assume it is a valid lock.
*/
(void) chmod(lock_fn, stp->st_mode);
write_log(WRITE_LOG_SYS, "time warp on zero length lock file: %s", lock_fn);
return SUCCEED;
}
if (stp->st_ctime < now - 2*3600/*two hours in seconds*/) {
/* lock not upheld, file can be removed */
return FAIL;
}
/* a new lock file, leave it alone for now */
DEBUG(DBG_SPOOL_HI, "new zero-length lock file, let it stand\n");
return SUCCEED;
}
/*
* verify_lock - decide if non-zero lockfile is stale
*
* return SUCCEED if not stale and should be kept, FAIL otherwise.
*/
static int
verify_lock()
{
int lfd;
char rpid[BITS_PER_INT/3 + 2];
long int pid = 0; /* not pid_t -- we're reading it from a file */
int ct;
lfd = open(lock_fn, O_RDONLY);
if (lfd < 0) {
/* failed to open the lock file, don't consider it stale yet */
DEBUG3(DBG_SPOOL_HI, "verify_lock: %s/%s: %s\n",
spool_dir, lock_fn, strerror(errno));
return SUCCEED;
}
/* read in the pid */
ct = read(lfd, rpid, sizeof(rpid)-1);
if (ct <= 0) {
/* failed to read anything from the lock file, ignore it for now */
(void) close(lfd);
DEBUG3(DBG_SPOOL_HI, "verify_lock: %s/%s: read failed: %s\n",
spool_dir, lock_fn, strerror(errno));
return SUCCEED;
}
(void) close(lfd);
rpid[ct] = '\0'; /* firewall */
(void) sscanf(rpid, "%ld", &pid);
if (pid == 0) {
/* pid is 0 or not a valid number, consider the lock valid */
DEBUG(DBG_SPOOL_HI, "verify_lock: pid==0 lock considered valid\n");
return SUCCEED;
}
/*
* does the process exist? before you look at the man page,
* kill(pid,0) detects process existence on all versions of
* UN*X I know of including v6 and v7
*/
if (kill((pid_t) pid, 0) < 0 && errno == ESRCH) {
/* process does not exist, lock is stale */
return FAIL;
}
/* the lock is valid */
DEBUG2(DBG_SPOOL_HI, "verify_lock: %s/%s: valid lock\n",
spool_dir, lock_fn);
return SUCCEED;
}
/*
* write_spool - write completed block to the spool file
*
* write the contents of the spool i/o buffer out to the spool file,
* at the current i/o position. But otherwise don't change anything.
*
* If the write fails then try to create an alternate spool file and
* copy into it the contents previously written to the old spool file.
*
* NOTE: writing to the spool file should be done through the macro
* PUTSPOOL() defined in spool.h, except that the final write should be
* done with a call to write_spool() to flush the final buffer.
* PUTSPOOL() will manage the various associated pointers.
*/
int
write_spool(errorp)
char **errorp; /* return any error message here */
{
char *junkerr; /* NULL-deref protector */
DEBUG1(DBG_SPOOL_HI, "write_spool called%s\n", errorp ? "" : " from PUTSPOOL()");
if (!errorp) {
errorp = &junkerr;
}
old_spool_fn = NULL; /* set to old name if new created */
old_input_spool_fn = NULL;
old_spool_dir = NULL;
old_spoolfile = spoolfile;
#ifndef NDEBUG
funct_name = "write_spool";
#endif
/*
* loop until we have successfully written the block to something,
* or until we cannot recover from past errors
*/
for (;;) {
if (write(spoolfile, msg_buf, (size_t) (msg_max - msg_buf)) == msg_max - msg_buf
#ifdef STANDALONE
/* it is a bit tough to get random write errors while testing */
&& (!force_write_error || (force_write_error = FALSE))
#endif /* STANDALONE */
)
{
break; /* get outa the alt-spool loop! */
} else {
/*
* write failed, unlink the old spool file and copy contents
* to a new spool file in an alternate spool directory.
*/
copy_old_names();
failed_write();
for (;;) {
/* get an alternate spool directory */
if (set_alt_spool("write to spool failed") == FAIL) {
int oerrno = errno;
(void) close(old_spoolfile);
*errorp = xprintf("write_spool: no more spool dirs, write failed: %s", strerror(oerrno));
DEBUG1(DBG_SPOOL_LO, "%s\n", *errorp);
errno = oerrno;
return FAIL;
}
if (int_creat_spool() == FAIL) {
int oerrno = errno;
(void) close(old_spoolfile);
*errorp = xprintf("write_spool: can't create new spool file, write failed: %s", strerror(oerrno));
DEBUG1(DBG_SPOOL_LO, "%s\n", *errorp);
errno = oerrno;
return FAIL;
}
if (msg_size == 0) {
break;
} else {
int code;
int oerrno;
code = copy_old_spool();
oerrno = errno;
if (code == READ_FAIL) {
*errorp = xprintf("write_spool: failed to copy old spool file: read failed."); /* XXX */
DEBUG1(DBG_SPOOL_LO, "%s\n", *errorp);
errno = oerrno;
return FAIL;
} else if (code == SUCCEED) {
/* Phew! We got it safely copied to an alt spool. */
break;
}
*errorp = xprintf("write_spool: failed to copy old spool file: write failed: %s", strerror(oerrno));
DEBUG1(DBG_SPOOL_LO, "%s\n", *errorp);
/* go around to try for another alt spool dir */
}
}
}
}
if (old_spool_fn) {
/* we succeeded in copying to a new spool file, don't need these */
xfree(old_spool_fn);
xfree(old_input_spool_fn);
xfree(old_spool_dir);
}
msg_size += msg_max - msg_buf;
/* silly but effective, though perhaps unnecessary (see queue_message()) */
msg_mtime = time((time_t *) NULL);
if (accepted_msg_size >= 0 && msg_size >= accepted_msg_size) {
*errorp = xprintf("write_spool: message grew larger than allowed [%ld]\n", accepted_msg_size);
DEBUG1(DBG_SPOOL_LO, "%s\n", *errorp);
errno = 0; /* this is a forced reject */
return FAIL;
}
return SUCCEED;
}
/*
* search for matches of any pattern in pcre_list in the message headers
*
* NOTE: we have to be careful not to scan the queue envelope portion, or of
* course anything beyond the first blank line (i.e. the body portion).
*/
int
egrep_spool_headers(pcre_list, errorp)
voidplist_t *pcre_list; /* pre-compiled RE list */
char **errorp; /* error message return reference */
{
int c; /* input char */
int prev_c = EOF; /* previous input char */
static struct str str;
static int str_inited = FALSE;
register struct str *sp = &str;
DEBUG(DBG_SPOOL_HI, "egrep_spool_headers(): called...\n");
*errorp = NULL;
if (!str_inited) {
STR_INIT(sp);
str_inited = TRUE;
} else {
STR_CHECK(sp);
}
/*
* seek to the beginning of the message proper. queue_envelope() has
* helpfully recorded their position.
*/
if (start_msg_headers == 0) {
*errorp = "egrep_spool_headers(): internal calling sequence error, start_msg_headers not set!";
/*
* XXX we could find them ourselves -- they start on the line after the
* first blank line -- but for now this routine is only called after
* queue_envelope() has been called
*/
DEBUG1(DBG_SPOOL_LO, "%s\n", *errorp);
return MATCH_FAIL;
}
if (seek_spool(start_msg_headers) == FAIL) {
/*
* errors logged in seek_spool(), but we need to set this for the
* submission error
*/
*errorp = "egrep_spool_headers(): seek_spool(start_msg_headers) failed";
DEBUG1(DBG_SPOOL_LO, "%s\n", *errorp);
return MATCH_FAIL;
}
for (;;) {
int rc;
STR_CLEAR(sp);
/*
* Read the next header field, terminated by newline which is not
* followed by a SPACE or TAB char, or by EOF (or READ_FAIL).
*
* XXX Instead of actually copying the field we should just mark its
* beginning and length and pass direct pointers to match_pcre_list().
*/
if (prev_c != EOF) { /* start with any read-ahead char */
STR_NEXT(sp, prev_c);
}
while ((c = GETSPOOL()) != EOF && c != READ_FAIL) {
if (prev_c == '\n' && c != ' ' && c != '\t') {
break;
}
STR_NEXT(sp, c);
prev_c = c;
}
if (c == READ_FAIL) {
*errorp = "egrep_spool_headers(): error reading message body";
DEBUG1(DBG_SPOOL_LO, "%s\n", *errorp);
return MATCH_FAIL;
}
prev_c = c; /* preserve this for the next field */
STR_NEXT(sp, '\0');
if (STR_GET(sp, 0) == '\n') {
break; /* found a blank line, end-of-headers! */
}
/* note: don't count the trailing NUL */
rc = match_pcre_list(STR(sp), STR_LEN(sp) - 1, pcre_list, TRUE, errorp);
switch (rc) {
case MATCH_FAIL:
*errorp = xprintf("egrep_spool_headers(): %s", *errorp);
DEBUG1(DBG_SPOOL_LO, "%s\n", *errorp);
return MATCH_FAIL;
case MATCH_MATCHED:
DEBUG1(DBG_SPOOL_MID, "egrep_spool_headers(): matched: %s\n", *errorp);
return MATCH_MATCHED;
}
/* check this last in case message doesn't end with a newline */
if (c == EOF) {
break; /* shouldn't happen, but who cares? */
}
}
start_msg_body = tell_spool() - 1; /* note we read ahead one char... */
DEBUG1(DBG_QUEUE_HI, "egrep_spool_headers(): message body starts at %lu\n", (unsigned long int) start_msg_body);
return MATCH_NOMATCH;
}
/*
* search for matches of any pattern in pcre_list in the message body
*
* NOTE: we have to be careful not to scan the queue envelope or header portion.
*/
int
egrep_spool_body(pcre_list, errorp)
voidplist_t *pcre_list; /* pre-compiled RE list */
char **errorp; /* error message return reference */
{
int rc;
DEBUG(DBG_SPOOL_HI, "egrep_spool_body(): called...\n");
*errorp = NULL;
/*
* seek to the beginning of the message proper. queue_envelope() has
* helpfully recorded the position of the message headers.
*/
if (start_msg_body == 0) {
int c;
int prev_c;
if (start_msg_headers == 0) {
*errorp = "egrep_spool_body(): internal calling sequence error, start_msg_headers not set!";
/*
* XXX we could find them ourselves -- they start on the line after the
* first blank line -- but for now this routine is only called after
* queue_envelope() has been called
*/
DEBUG1(DBG_SPOOL_LO, "%s\n", *errorp);
return MATCH_FAIL;
}
if (seek_spool(start_msg_headers) == FAIL) {
/*
* errors logged in seek_spool(), but we need to set this for the
* submission error
*/
*errorp = "egrep_spool_body(): seek_spool(start_msg_headers) failed";
DEBUG1(DBG_SPOOL_LO, "%s\n", *errorp);
return MATCH_FAIL;
}
/*
* scan past the message headers...
*
* we don't have to unfold -- we're just looking for a blank line
* so this loop is much less forgiving than the real read_header().
*/
prev_c = '\0';
while ((c = GETSPOOL()) != EOF && c != READ_FAIL && !(c == '\n' && prev_c == '\n')) {
prev_c = c;
}
if (c == READ_FAIL) {
*errorp = "egrep_spool_body(): read error while scanning past message headers";
DEBUG1(DBG_SPOOL_LO, "%s\n", *errorp);
return MATCH_FAIL;
}
if (c == EOF) {
*errorp = "egrep_spool_body(): EOF while scanning past message headers";
DEBUG1(DBG_SPOOL_LO, "%s\n", *errorp);
return MATCH_NOMATCH; /* shouldn't happen, but who cares? */
}
start_msg_body = tell_spool();
} else {
if (seek_spool(start_msg_body) == FAIL) {
/*
* errors logged in seek_spool(), but we need to set this for the
* submission error
*/
*errorp = "egrep_spool_body(): seek_spool(start_msg_body) failed";
DEBUG1(DBG_SPOOL_LO, "%s\n", *errorp);
return MATCH_FAIL;
}
}
/*
* NOTE: this only matches each RE against the first message_buf_size (less
* start_msg_body) bytes as one blob. One should force whole-message
* matching by setting message_buf_size up to be equal to max_message_size,
* taking care of course to have enough VM available on the host machine.
*/
rc = match_pcre_list(msg_ptr, (size_t) (msg_max - msg_ptr), pcre_list, TRUE, errorp);
switch (rc) {
case MATCH_FAIL:
*errorp = xprintf("egrep_spool_body(): %s", *errorp);
DEBUG1(DBG_SPOOL_LO, "%s\n", *errorp);
break;
case MATCH_MATCHED:
DEBUG1(DBG_SPOOL_MID, "egrep_spool_body(): matched: %s\n", *errorp);
break;
}
return rc;
}
/*
* copy_old_names - make a copy of the old spool file names
*/
static void
copy_old_names()
{
if (old_spool_fn == NULL) {
/* save a copy of the old names if needed later */
old_spool_fn = COPY_STRING(spool_fn);
old_input_spool_fn = COPY_STRING(input_spool_fn);
old_spool_dir = COPY_STRING(spool_dir);
}
}
/*
* failed_write - remove old files after a write error
*
* NOTE: we aren't closing them, so the data in them can still be read.
*/
static void
failed_write()
{
if (spool_fn) {
(void) unlink(input_spool_fn);
spool_fn = NULL;
input_spool_fn = NULL;
}
if (lock_fn) {
(void) unlink(lock_fn);
}
}
/*
* copy_old_spool - copy old spool file to the new one
*
* return READ_FAIL on read error, SUCCEED on success, and
* WRITE_FAIL on write error
*/
static int
copy_old_spool()
{
char *tempbuf = xmalloc(message_buf_size);
size_t copied = 0; /* count of chars copied to new file */
/*
* previous contents exist, we will need to copy them
*/
(void) lseek(old_spoolfile, (off_t) 0L, SEEK_SET);
while ((off_t) copied < msg_size) {
register size_t ct;
/* fill up as much of the temp buffer as reasaonble */
if (msg_size - copied < message_buf_size) {
ct = msg_size - copied;
} else {
ct = message_buf_size;
}
ct = read(old_spoolfile, tempbuf, (size_t) ct);
if (ct <= 0) {
failed_write();
log_message("read failed in copying to new file");
(void) close(spoolfile);
(void) close(old_spoolfile);
xfree(tempbuf);
return READ_FAIL; /* read error - can't recover */
} else {
if (write(spoolfile, tempbuf, (size_t) ct) < (ssize_t) ct) {
failed_write();
xfree(tempbuf);
return WRITE_FAIL; /* write error - maybe can recover */
}
}
/* XXX we shouldn't have to check for overflow here... but... */
copied += ct;
}
xfree(tempbuf);
return SUCCEED; /* we have recovered */
}
/*
* open_spool - open and possibly lock the given spool file
*
* the filename is assumed to be absolute, so a chdir is done to
* the directory name, and spool_fn is set to the basename. This
* process is destructive of the spool file string passed.
*
* As a side effect, msg_buf will be loaded with the first part of the
* spool file.
*
* return SUCCEED or FAIL. If fail, set `exitvalue' with a suggested
* exit value. EX_TEMPFAIL means a temporary failure which might have
* occurred due to a race with another smail process (i.e. file already
* locked). EX_NOINPUT the input file didn't exitst (again because we
* may have raced with another smail process which processed the file).
* Other exitvalue settings represent more serious problems.
*
* No log message is generated for EX_TEMPFAIL and EX_NOINPUT.
*/
int
open_spool(fn, lock, logopts)
char *fn; /* basename of spool file */
int lock; /* if TRUE, lock the file */
int logopts; /* which log files to write */
{
static char lfn[sizeof("lock/") + SPOOL_FN_LEN];
struct stat statbuf;
DEBUG3(DBG_SPOOL_HI, "open_spool(%s, %d, 0x%x) called\n",
fn, lock, logopts);
#ifndef NDEBUG
funct_name = "open_spool";
#endif
message_id = NULL;
lock_fn = NULL;
spoolfile = -1;
log_msgs = NULL;
msg_foffset = 0;
msg_size = 0;
errno = 0;
if (msg_buf == NULL) {
/* allocate the spool i/o buffer if it does not yet exist */
msg_buf = xmalloc(message_buf_size);
}
msg_max = msg_buf; /* start at beginning of buffer */
end_msg_buf = msg_buf + message_buf_size;
if (fn[0] != '/') {
write_log(logopts, "open_spool: %s: filename not absolute", fn);
exitvalue = EX_SOFTWARE;
return FAIL;
}
spool_dir = fn; /* get directory from fn */
spool_fn = strrchr(fn, '/'); /* get basename from fn */
input_spool_fn = spool_fn - sizeof("/input") + 1;
spool_fn++;
if (input_spool_fn < spool_dir ||
strncmp(input_spool_fn, scan_frozen ? "/error" : "/input", sizeof("/input") - 1))
{
write_log(logopts, "open_spool failed: %s: invalid spool filename",
spool_dir);
exitvalue = EX_SOFTWARE;
return FAIL;
}
*input_spool_fn++ = '\0';
if (chdir(spool_dir) < 0) {
write_log(logopts, "open_spool failed: chdir(%s): %s",
spool_dir, strerror(errno));
exitvalue = EX_OSFILE;
return FAIL;
}
/*
* attempt to open the file. if the open fails because the file
* is missing, it is likely that we raced another smail process
* for the file.
*
* some of the OS locking algorithms require that the file be
* opened for writing. For such systems, the spool_mode must have
* at least ownership write permission.
*/
#ifdef LOCK_REQUIRES_WRITE
if (lock && ! lock_by_name) {
spoolfile = open(input_spool_fn, O_RDWR);
} else
#endif
{
spoolfile = open(input_spool_fn, O_RDONLY);
}
if (spoolfile < 0) {
exitvalue = EX_NOINPUT;
if (errno != ENOENT) {
exitvalue = EX_NOPERM;
write_log(logopts, "open_spool: %s/%s: open failed: %s",
spool_dir, input_spool_fn, strerror(errno));
}
return FAIL;
}
if (lock) {
#ifndef lock_fd
/* only lock_by_name is possible */
(void) sprintf(lfn, "lock/%s", spool_fn);
lock_fn = lfn;
if (lock_spoolfile(l_open) == FAIL) {
DEBUG2(DBG_SPOOL_LO, "open_spool: %s/%s: lock_spoolfile failed\n",
spool_dir, input_spool_fn);
(void) close(spoolfile);
exitvalue = EX_TEMPFAIL;
return FAIL;
}
#else /* lock_fd */
if (lock_by_name) {
(void) sprintf(lfn, "lock/%s", spool_fn);
lock_fn = lfn;
if (lock_spoolfile(l_open) == FAIL) {
DEBUG2(DBG_SPOOL_LO, "open_spool: %s/%s: lock_by_name failed\n",
spool_dir, input_spool_fn);
(void) close(spoolfile);
exitvalue = EX_TEMPFAIL;
return FAIL;
}
} else {
if (lock_fd(spoolfile) == FAIL) {
DEBUG2(DBG_SPOOL_LO, "open_spool: %s/%s: lock failed\n",
spool_dir, input_spool_fn);
(void) close(spoolfile);
exitvalue = EX_TEMPFAIL;
return FAIL;
}
}
#endif /* lock_fd */
}
/*
* Get inode info for the spool file.
*/
if (fstat(spoolfile, &statbuf) < 0) {
write_log(logopts, "open_spool: %s/%s: spool file fstat() error: %s",
spool_dir, input_spool_fn, strerror(errno));
close_spool();
exitvalue = EX_IOERR;
return FAIL;
}
/*
* so long as nobody's mucking around directly in the spool dir this should
* only be possible if we didn't ask for a lock and checking at this point
* seems kinda pointless since it's so soon after the open, but check
* anyway....
*/
if (statbuf.st_nlink == 0) {
write_log(logopts, "open_spool: %s/%s: spool file was removed!",
spool_dir, input_spool_fn, strerror(errno));
close_spool();
exitvalue = EX_NOINPUT;
return FAIL;
}
/*
* now that the file itself is locked, we need to setup the
* message_id and initialize the msg_buf pointers so that
* read_spool will be called the first time the GETSPOOL
* macro is called.
*/
build_message_id();
if (msg_buf == NULL) {
msg_buf = xmalloc(message_buf_size);
end_msg_buf = msg_buf + message_buf_size;
}
msg_max = msg_buf;
msg_ptr = msg_buf;
msg_size = statbuf.st_size;
msg_mtime = statbuf.st_mtime;
if (lock) {
DEBUG3(operation_mode == PRINT_QUEUE ? DBG_SPOOL_MID : DBG_SPOOL_LO, /* XXX not really necessary */
"opened and locked spool file %s/%s [%lu bytes]\n",
spool_dir, input_spool_fn, (unsigned long) msg_size);
} else {
DEBUG3(operation_mode == PRINT_QUEUE ? DBG_SPOOL_HI : DBG_SPOOL_LO,
"opened spool file %s/%s [%lu bytes]\n",
spool_dir, input_spool_fn, (unsigned long) msg_size);
}
return SUCCEED;
}
/*
* close_spool - unlock and close the spool file
*/
void
close_spool()
{
DEBUG(DBG_SPOOL_HI, "close_spool called\n");
if (lock_fn) {
DEBUG2(DBG_SPOOL_HI, "close_spool: unlinking %s/%s\n",
spool_dir, lock_fn);
(void) unlink(lock_fn);
}
if (spoolfile != -1) {
(void) close(spoolfile);
spoolfile = -1;
}
spool_fn = NULL;
input_spool_fn = NULL;
}
/*
* unlink_spool - unlock, close and unlink the spool file
*/
void
unlink_spool()
{
DEBUG(DBG_SPOOL_HI, "unlink_spool called\n");
if (input_spool_fn) {
if (message_id && message_date() == time((time_t *) NULL)) {
DEBUG(DBG_SPOOL_MID, "unlink_spool: pausing to prevent spool file clash\n");
sleep(2);
}
DEBUG2(DBG_SPOOL_HI, "unlink_spool: unlinking: %s/%s\n", spool_dir, input_spool_fn);
(void) unlink(input_spool_fn);
}
close_spool();
}
/*
* freeze_message - put the spool file in a defer directory
*
* some processing errors may need attention from the postmaster,
* and may be correctable through a change in configuration, or other
* such things. Rather than mail such problems to the sender or to
* the postmaster, put them in a special (error/) directory. When
* the problem that caused the error is resolved, the postmaster can
* then simply move the spool file back into the spool directory and
* delivery will be reattempted.
*/
void
freeze_message()
{
char error_fn[sizeof("error/") + SPOOL_FN_LEN];
int success;
(void) sprintf(error_fn, "error/%s", spool_fn);
success = rename(input_spool_fn, error_fn);
if (success < 0 && auto_mkdir && errno == ENOENT) {
DEBUG1(DBG_SPOOL_LO, "freeze_message(): make directory %s/error\n", spool_dir);
if (mkdir("error", auto_mkdir_mode) < 0) {
DEBUG1(DBG_SPOOL_LO, "freeze_message(): mkdir(error): %s\n", strerror(errno));
}
success = rename(input_spool_fn, error_fn);
}
if (success < 0) {
write_log(WRITE_LOG_PANIC|WRITE_LOG_SYS,
"freeze_message: error linking %s/%s to %s/%s: %s",
spool_dir, input_spool_fn, spool_dir, error_fn,
strerror(errno));
} else {
/* XXX for some reason this appears in the paniclog too! */
write_log(WRITE_LOG_TTY|WRITE_LOG_SYS, "mail moved to %s/%s",
spool_dir, error_fn);
unlink_spool(); /* just in case.... */
}
}
/*
* seek_spool - seek to the specified absolute spool file offset
*
* causes the aligned block around that point to be read into
* msg_buf.
*/
int
seek_spool(offset)
register off_t offset;
{
DEBUG1(DBG_SPOOL_HI, "seek_spool(%ld) called\n", (long) offset);
if (offset > msg_size) {
/* attempt to seek past the end of message */
msg_ptr = msg_max + 1; /* will cause EOF from GETSPOOL */
DEBUG(DBG_SPOOL_LO, "seek_spool failed, seek past end of file\n");
return FAIL;
}
/* is the offset in the current block? */
if (msg_foffset <= offset && offset < msg_foffset + (msg_max - msg_buf)) {
/* yes, just adjust the current loc pointer */
msg_ptr = msg_buf + (offset - msg_foffset);
DEBUG1(DBG_SPOOL_HI, "hurray! seek_spool already in the right block! [at msg_buf + %ld]\n", (long) (offset - msg_foffset));
return SUCCEED;
}
/* compute the beginning of the block containing the offset */
msg_foffset = offset - (offset % message_buf_size);
/* seek to the start of the block containing the offset */
if (lseek(spoolfile, msg_foffset, SEEK_SET) < 0) {
DEBUG3(DBG_SPOOL_LO, "seek_spool failed: lseek(%d, %lu, SEEK_SET): %s\n",
spoolfile, (unsigned long int) msg_foffset, strerror(errno));
/* XXX log a complaint! */
return FAIL;
}
/* set the buffer to zero-length so read_spool won't advance foffset */
msg_max = msg_buf;
/* actually read in the message */
if (read_spool() == FAIL) {
return FAIL;
}
/* set up the ptr to point to the byte for the offset */
msg_ptr = msg_buf + (offset - msg_foffset);
if (msg_ptr > msg_max) {
DEBUG(DBG_SPOOL_LO, "seek_spool failed, read came up short\n");
return FAIL;
}
return SUCCEED;
}
/*
* tell_spool - return the current offset in the spool file.
*/
off_t
tell_spool()
{
return msg_foffset + msg_ptr - msg_buf;
}
/*
* send_spool - write the spool file to a stdio FILE pointer
*
* send the spool file to an open file, starting at the current
* offset and ending at the end of the spool file. If PUT_DOTS is set
* then use the hidden dot algorithm, prepending a dot to each
* line that begins with a dot. If PUT_CRLF is set than put a carriage
* return before each newline. If UNIX_FROM_HACK then put > in front of
* any line beginning with From.
*
* return SUCCEED, READ_FAIL or WRITE_FAIL.
*/
int
send_spool(f, flags)
register FILE *f; /* file to write on */
unsigned long flags; /* transport flags */
{
register char *p;
int eof = FALSE; /* TRUE if found end of file */
register int last_c = '\n';
int dot = ((flags & PUT_DOTS) != 0);
int crlf = ((flags & PUT_CRLF) != 0);
int uucp_hack = ((flags & UNIX_FROM_HACK) != 0);
DEBUG(DBG_SPOOL_HI, "send_spool called\n");
while (!eof) {
for (p = msg_ptr; p < msg_max; p++) {
if (last_c == '\n') {
if (dot && *p == '.') {
putc('.', f); /* hidden dot algorithm */
}
if (uucp_hack && *p == 'F') {
size_t ct = msg_max - p;
/* look ahead to see if this line begins with From */
if (ct > sizeof("From ") - 1) {
if (strncmp("From ", p, sizeof("From ") - 1) == 0) {
putc('>', f);
}
} else if (strncmp("From ", p, ct) == 0) {
/*
* we have to look ahead to the next block to
* determine if this is a From, but make sure
* there is another block, first.
*/
if (msg_foffset + (msg_max - msg_buf) < msg_size) {
unsigned int i;
msg_ptr = p;
if (read_spool() == FAIL) {
return READ_FAIL;
}
p = msg_ptr;
if (strncmp("From " + ct, p,
sizeof("From ") - ct - 1) == 0)
{
putc('>', f);
}
for (i = 0; i < ct; i++) {
putc("From "[i], f);
}
}
}
}
}
if (*p == '\n' && crlf) {
putc('\r', f); /* ARPAnet CR/LF */
}
if (putc(last_c = *p, f) == EOF) {
DEBUG(DBG_SPOOL_LO, "send_spool: write failed\n");
return WRITE_FAIL;
}
}
msg_ptr = p;
if (msg_foffset + (msg_max - msg_buf) < msg_size) {
if (read_spool() == FAIL) {
return READ_FAIL;
}
DEBUG(DBG_DRIVER_LO, "."); /* progress indicator, esp. for tcpsmtp */
} else {
eof = TRUE;
}
}
if (last_c != '\n') {
if (crlf) {
putc('\r', f);
}
if (putc('\n', f) == EOF) {
DEBUG(DBG_SPOOL_LO, "send_spool: write failed\n");
return WRITE_FAIL;
}
}
return SUCCEED;
}
/*
* read_spool - read in a block from the spool file
*
* read a block starting at the current file position into msg_buf
* and update the various associated variables.
*
* return SUCCEED or FAIL.
*
* XXX should return an error message in an errorp parameter!
*/
int
read_spool()
{
register long ct;
/* advance the current file offset by the amount previously read */
msg_foffset += msg_max - msg_buf;
if ((off_t) message_buf_size > msg_size - msg_foffset) {
/* the remaining size is less then a whole buffer */
ct = (long) (msg_size - msg_foffset);
if (ct < 0) {
errno = ENXIO; /* reading beyond end of the file */
DEBUG1(DBG_SPOOL_LO, "read_spool: read failed, msg_foffset of '%lu' is past end of file",
(unsigned long int) msg_foffset);
return FAIL;
}
if (ct == 0) {
/* end of file */
msg_max = msg_ptr = msg_buf;
DEBUG(DBG_SPOOL_MID, "end of file on spool file\n");
return FAIL;
}
} else {
/* there is enough data to fit in a complete buffer */
ct = (long) message_buf_size;
}
/* now, where were we? */
if (lseek(spoolfile, msg_foffset, SEEK_SET) < 0) {
DEBUG2(DBG_SPOOL_LO, "read_spool: lseek to %lu of spool file failed: %s\n",
(unsigned long int) msg_foffset, strerror(errno));
return FAIL;
}
/* read in a buffer */
ct = read(spoolfile, msg_buf, (size_t) ct);
if (ct < 0) {
DEBUG2(DBG_SPOOL_LO, "read_spool: read of %ld bytes from spool file failed: %s\n",
ct, strerror(errno));
return FAIL;
}
/* point to the end and beginning of the used region */
msg_max = msg_buf + ct;
msg_ptr = msg_buf;
return SUCCEED;
}
/*
* log_spool_errors - write spooling errors to system log
*
* while spooling, errors are not written out to the spool files. This
* avoid having to deal with the fact that those writes may fail. Thus,
* when finished spooling the message, we must write out any errors to the
* panic file.
*/
void
log_spool_errors()
{
struct log_msgs *next;
DEBUG(DBG_SPOOL_HI, "log_spool_errors called, saving any errors from spool\n");
while (log_msgs) {
next = log_msgs->succ;
write_log(WRITE_LOG_PANIC, "%s", log_msgs->msg);
xfree((char *)log_msgs);
log_msgs = next;
}
}
/*
* log_message - save a message to be written to the log file
*
* the message is not in printf format, but filename and directory
* information will be added to it.
*
* XXX the guts of this silly little routine should be replaced by a call to
* xprintf() and a call to add_charplist().
*/
static void
log_message(m)
char *m; /* message */
{
char *p, *q; /* temp */
register size_t a; /* how much to alloc */
char *save;
a = sizeof(*log_msgs) + strlen(m);
if (spool_dir) {
a += sizeof(", dir=") + strlen(spool_dir);
}
if (input_spool_fn) {
a += sizeof(", spoolfile=") + strlen(input_spool_fn);
}
if (lock_fn) {
a += sizeof(", lockfile=") + strlen(lock_fn);
}
*next_log_msg = (struct log_msgs *) xmalloc(a);
(*next_log_msg)->succ = NULL;
save = p = (*next_log_msg)->msg;
next_log_msg = &(*next_log_msg)->succ;
/* copy message */
for (q = m; *q; *p++ = *q++) {
;
}
if (spool_dir) {
/* put spool directory in message */
for (q = ", dir="; *q; *p++ = *q++) ;
for (q = spool_dir; *q; *p++ = *q++) ;
}
if (input_spool_fn) {
/* put spoolfile name in message */
for (q = ", spoolfile="; *q; *p++ = *q++) ;
for (q = input_spool_fn; *q; *p++ = *q++) ;
}
if (lock_fn) {
/* put lockfile name in message */
for (q = ", lockfile="; *q; *p++ = *q++) ;
for (q = lock_fn; *q; *p++ = *q++) ;
}
*p++ = '\0';
DEBUG2(DBG_SPOOL_MID, "%s: %s\n", funct_name, save);
}
/*
* set_alt_spool - set a primary or alternate spool directory
*
* step through the list of spool directories, advancing once per
* call to set_alt_spool and resetting when the current spool directory
* was not set by set_alt_spool. Do a chdir(2) to the directory
* set spool_dir and return SUCCEED on the first one for chdir
* succeeds. If none remain, return FAIL.
*/
static int
set_alt_spool(m)
char *m; /* message to put in log */
{
static char altname[PATH_MAX];
static char *altlist = NULL;
char *p;
DEBUG1(DBG_SPOOL_HI, "set_alt_spool called\n", m);
/* log the message using the old spool dir then set the new one */
if (m) {
log_message(m);
}
if (spool_dir != altname) {
/* previous spool dir not one of ours, reset to start */
altlist = spool_dirs;
}
spool_dir = altname;
/* loop until we have a valid alternate spool directory, or none left */
while (altlist && *altlist) {
/* copy a directory name into altname, may end in ':' or nul */
for (p = altname; *altlist && *altlist != ':'; *p++ = *altlist++) {
if ((size_t) (p - altname) >= (sizeof(altname) - 1)) {
log_message("config error: alternate spool directory name is too long to be a valid pathname");
break;
}
}
if (*altlist == ':') {
/* next try gets the next directory */
altlist++;
} else if (*altlist) {
continue; /* altname over-run above... */
}
*p++ = '\0'; /* terminate directory name */
if (*altname != '/') {
char *emsg = xprintf("config error: alternate spool directory '%s' doesn't begin with /", altname);
/* directory must begin with '/', ignore it */
log_message(emsg);
xfree(emsg);
continue;
}
if (chdir(altname) < 0) {
/* Okay, try to make the directory and then chdir again */
if (auto_mkdir && errno == ENOENT) {
DEBUG1(DBG_SPOOL_LO, "trying to make alt spool directory %s\n", altname);
(void) mkdir(altname, auto_mkdir_mode);
if (chdir(altname) < 0) {
char *emsg = xprintf("chdir(%s) failed: %s", altname, strerror(errno));
log_message(emsg);
xfree(emsg);
continue;
}
} else {
char *emsg = xprintf("chdir(%s) failed: %s", altname, strerror(errno));
log_message(emsg);
xfree(emsg);
continue;
}
}
/* we found a valid alternate directory */
return SUCCEED;
}
/* we failed, *sigh* */
return FAIL;
}
/*
* new_grade - assign a new grade to the spool file
*
* assign a new grade, or precedence, code to the spool file. This
* involves linking to a new name with a different precedence character.
* We go through build_spool_fn to compute new names and keep trying
* new names until we have one that doesn't already exist. If links
* fail for some reason other than a name not existing then don't bother
* trying to change the grade.
*
* return SUCCEED or FAIL, though a FAIL should be ignored as there is
* no good way to recover and it is not generally that important.
*/
int
new_grade(grade)
int grade; /* grade character */
{
/*
* NOTE: the above must be the same size as the names
* built by build_spool_fn
*/
char save_spool_fn[SPOOL_FN_LEN + 1];
char save_input_spool_fn[sizeof("input/") + SPOOL_FN_LEN + 1];
char save_message_id[SPOOL_FN_LEN + 2];
char save_lock_fn[sizeof("lock/") + SPOOL_FN_LEN + 1];
DEBUG1(DBG_SPOOL_HI, "new_grade(%c) called\n", grade);
/*
* make a copy of the old names to use as args to link, and so that
* the old names can be restored
*/
(void) memcpy(save_spool_fn, spool_fn, sizeof(save_spool_fn));
(void) memcpy(save_input_spool_fn, input_spool_fn,
sizeof(save_input_spool_fn));
(void) memcpy(save_message_id, message_id, sizeof(save_message_id));
if (lock_by_name) {
(void) memcpy(save_lock_fn, lock_fn, sizeof(save_lock_fn));
}
/*
* loop until we link both lock file and spool file to new names
* and unlink the old ones.
*/
for (;;) {
build_spool_fn(grade);
/* try to link the lock file first so that it is initially locked */
if (lock_by_name && link(save_lock_fn, lock_fn) < 0) {
if (errno != EEXIST) {
DEBUG1(DBG_SPOOL_LO, "new_grade: failed to link lock: %s\n",
strerror(errno));
break;
}
DEBUG1(DBG_SPOOL_MID,
"new_grade: failed to link lock:%s, try another\n",
strerror(errno));
continue;
}
/* now try to link the actual spool file */
if (link(save_input_spool_fn, input_spool_fn) < 0) {
if (errno != EEXIST) {
DEBUG1(DBG_SPOOL_LO, "new_grade: failed to link spool: %s\n",
strerror(errno));
if (lock_by_name) {
(void) unlink(lock_fn);
}
break;
}
DEBUG1(DBG_SPOOL_MID,
"new_grade: failed to link spool file: %s, try another\n",
strerror(errno));
continue;
}
/* success, now unlink the old names */
(void) unlink(save_input_spool_fn);
if (lock_by_name) {
(void) unlink(save_lock_fn);
}
return SUCCEED;
}
/* we failed, copy the old names back */
(void) memcpy(spool_fn, save_spool_fn, sizeof(save_spool_fn));
(void) memcpy(input_spool_fn, save_input_spool_fn,
sizeof(save_input_spool_fn));
(void) memcpy(message_id, save_message_id, sizeof(save_message_id));
if (lock_by_name) {
(void) memcpy(lock_fn, save_lock_fn, sizeof(save_lock_fn));
}
return FAIL;
}
/*
* message_date - return the date that the message was spooled
*
* The message spool date is encoded in the actual Message Id as a
* base 62 number. Return the date as the number of seconds since
* the epoch.
*/
time_t
message_date()
{
time_t clck = 0;
register int digit;
register char *p;
if (!message_id) {
DEBUG(DBG_SPOOL_LO, "message_date(): called with no message_id set!\n");
return clck;
}
for (p = message_id + 1; *p; p++) {
if (isdigit((int) *p)) {
digit = *p - '0';
} else if (isupper((int) *p)) {
digit = *p - ('A' - 10);
} else if (islower((int) *p)) {
digit = *p - ('a' - 36);
} else {
break;
}
clck = (clck * 62) + digit;
}
return clck;
}
#if defined(HAVE_STATFS) || defined(HAVE_STATVFS)
/*
* Return the maximum space free on any of spool_dirs, in Kbytes (but never
* return more than (LONG_MAX / 1024) Kbytes, because it will be multiplied out
* to bytes eventually and stored in a long).
*
* If we can't figure it out for any reason just return (-1).
*
* Note we don't really care about the total space available on all spool dirs
* because we're only going to use this to calculate the available space for a
* single message.
*/
long
spool_max_free_space()
{
char *dirs = spool_dirs;
char dirname[PATH_MAX];
char *p;
long max_free_kbytes = -1; /* max free space in any spoo_dir */
long free_kbytes; /* free space in a given spool_dir */
long reserved_kbytes; /* free space to reserve (kb) */
fsblkcnt_t blocks_free; /* blocks free on the filesystem */
unsigned long block_size; /* filesystem block size */
int result; /* stat*fs() return code */
# ifdef HAVE_STATVFS
struct statvfs buf;
# else
struct statfs buf;
# endif
if (max_message_size > 0 && smtp_accept_max > 0) {
/* leave 2MB for logs, etc., plus max incoming messages */
reserved_kbytes = (2 * 1024) + (smtp_accept_max * max_message_size / 1024);
DEBUG3(DBG_SPOOL_MID,
"spool_max_free_space: reserving %ld kbytes for [2MB plus %ld max message bytes * %d accepts]...\n",
reserved_kbytes, max_message_size, smtp_accept_max);
} else if (smtp_accept_max > 0) {
/*
* just round out to a conservative 2MB plus 1MB per potential incoming
* message.... (it would be better to leave a certain percentage free,
* but to do that we'd have to add another function to first calculate
* the total size of all the spool_dirs filesystems)
*/
reserved_kbytes = (2 + smtp_accept_max) * 1024;
DEBUG2(DBG_SPOOL_LO,
"spool_max_free_space: max_message_size == 0, defaulting to reserve of %ld kbytes for [2MB plus 1MB * %d accepts]\n",
reserved_kbytes, smtp_accept_max);
} else {
/*
* just go for 10MB -- this is really lame (maybe we should warn when
* smtp_accept_max isn't set to something > 0?)
*/
reserved_kbytes = 10 * 1024;
DEBUG1(DBG_SPOOL_LO,
"spool_max_free_space: smtp_accept_max == 0, defaulting to reserve of %ld kbytes\n",
reserved_kbytes);
}
while (dirs && *dirs) {
for (p = dirname; *dirs && *dirs != ':'; *p++ = *dirs++) {
if ((size_t) (p - dirname) >= (sizeof(dirname) - 1)) {
log_message("config error: directory name too long!");
break;
}
}
if (*dirs == ':') {
/* next try gets the next directory */
dirs++;
} else if (*dirs) {
continue; /* dirname over-run above... */
}
*p++ = '\0'; /* terminate directory name */
# ifdef HAVE_STATVFS
# define STATFS_FN "statvfs"
result = statvfs(dirname, &buf);
# else
# ifdef BSD /* 4.4 only? */
# define STATFS_FN "bsd statfs"
result = statfs(dirname, &buf);
# else
# define STATFS_FN "sysv statfs"
/* AT&T UNIX SysVr3 and clones */
result = statfs(dirname, &buf, sizeof(buf), 0);
# endif
# endif
if (result == -1) {
/*
* XXX should this be write_log(SYS &| PANIC)?
*
* NOTE: The first time ever this is called on a new system which
* doesn't yet have the firt of spool_dirs created this will always
* fail with ENOENT....
*/
DEBUG3(DBG_SPOOL_LO, "spool_max_free_space: %s: %s() failed: %s\n",
dirname, STATFS_FN, strerror(errno));
continue;
}
# ifdef HAVE_STATVFS
blocks_free = buf.f_bfree;
block_size = buf.f_frsize;
# else
# ifdef BSD
blocks_free = buf.f_bavail;
block_size = buf.f_bsize;
# else
blocks_free = buf.f_bfree;
block_size= buf.f_bsize;
# endif
# endif
DEBUG3(DBG_SPOOL_MID,
"spool_max_free_space: %s: [bfree=%lu, bsize=%lu]\n",
dirname, (unsigned long) blocks_free, block_size);
/*
* try desparately to avoid overflowing ulong, and never return more
* than (LONG_MAX / 1024) Kbytes (since it will be multiplied out to
* bytes eventually)
*
* [Do I ever wish C had the option of integer overflow detection!]
*/
if (block_size == 0 || blocks_free == 0) {
continue; /* avoid divide by zero */
}
if (block_size >= 1024 || blocks_free >= 1024) {
unsigned long little1 = MIN(block_size, (unsigned long) blocks_free);
unsigned long big1 = MAX(block_size, (unsigned long) blocks_free);
/*
* here we've pedantically found the smaller value of block_size
* and blocks_free and converted them to unsigned long ints so that
* we can calculate the number of free bytes on the filesystem with
* less risk of overflowing, and so that we can try to detect any
* overflow.
*/
if ((little1 * (big1 / 1024)) / little1 != (big1 / 1024)) {
free_kbytes = LONG_MAX / 1024; /* overflowed, just go for LONG_MAX */
} else {
free_kbytes = MIN((long) (little1 * (big1 / 1024)), (LONG_MAX / 1024));
}
} else {
free_kbytes = MIN((long) ((block_size * blocks_free) / 1024), (LONG_MAX / 1024));
}
if (free_kbytes > max_free_kbytes) {
max_free_kbytes = free_kbytes;
}
DEBUG3(DBG_SPOOL_MID,
"spool_max_free_space: %s has %lu free kbytes [max is %ld]\n",
dirname, free_kbytes, max_free_kbytes);
}
if (max_free_kbytes >= reserved_kbytes) {
max_free_kbytes -= reserved_kbytes;
} else if (max_free_kbytes >= 0) {
max_free_kbytes = 0;
}
DEBUG1(DBG_SPOOL_HI, "spool_max_free_space: returning %ld kbytes\n", max_free_kbytes);
return max_free_kbytes;
}
#else /* not HAVE_STAT*FS */
/*
* XXX we don't (yet?) support ancient systems with ustat(2) only...
*/
long
spool_max_free_space()
{
return -1;
}
#endif /* HAVE_STAT*FS */
#ifdef STANDALONE
char *program = "spool";
FILE *errfile = stderr;
int return_to_sender = FALSE;
int debug = 0;
int exitvalue = 0;
/*
* standalone main for spooling subsystem.
* usage:
* spool [-ddebug] [-msize] [-sspool_dirs] [-n] o|c[w][u] [filename]
*
* -d sets the debug level, -m sets message_buf_size, -s sets the spool
* directory(s), and -n causes the lock_by_name code to be used.
*
* An arg of `o' causes a spool file in the given filename to be locked,
* written to standard out and closed. An arg of `c' causes a spool
* file to be read from standard input and put in a new spool file. If
* `w' follows `o' or `c' then write the spool file to standard out
* before closing. If `u' then unspool the message.
*
* If an arg of `c' is used and stdin contains a `@' character than
* a write error is simulated to exercise the alternate spooling
* directory code.
*/
void
main(argc, argv)
int argc;
char *argv[];
{
char *p;
int mode = 0;
int dowrite = FALSE;
int dounspool = FALSE;
program = *argv++;
--argc;
while (*argv && (*argv)[0] == '-') {
switch ((*argv)[1]) {
case 'd':
debug = atoi(*argv + 2);
break;
case 'm':
message_buf_size = atoi(*argv + 2);
break;
case 's':
spool_dirs = *argv + 2;
break;
case 'n':
lock_by_name = 1;
break;
default:
stand_spool_usage();
/*NOTREACHED*/
}
argv++;
--argc;
}
if (argc <= 0) {
stand_spool_usage();
/*NOTREACHED*/
}
for (p = *argv++, --argc; *p; p++) {
switch (*p) {
case 'o':
if (mode) {
stand_spool_usage();
/*NOTREACHED*/
}
mode = 'o';
break;
case 'c':
if (mode) {
stand_spool_usage();
/*NOTREACHED*/
}
mode = 'c';
break;
case 'w':
dowrite = TRUE;
break;
case 'u':
dounspool = TRUE;
break;
default:
stand_spool_usage();
/*NOTREACHED*/
}
}
if (!mode) {
stand_spool_usage();
/*NOTREACHED*/
}
if (mode == 'o' && argc <= 0) {
(void) fprintf(stderr, "%s: open mode requires filename\n", program);
exit(EX_USAGE);
}
if (mode == 'o') {
if (open_spool(*argv, TRUE, 0) < 0) {
(void) fprintf(stderr, "%s: open failed\n", program);
log_spool_errors();
exit(exitvalue);
}
} else {
register int c;
if (creat_spool(*argv) < 0) {
(void) fprintf(stderr, "%s: creat failed\n", program);
log_spool_errors();
exit(EX_OSFILE);
}
while ((c = getchar()) != EOF) {
char **errorp = NULL;
if (c == '@') {
force_write_error = TRUE;
}
if (PUTSPOOL(c, errorp) == FAIL) {
(void) fprintf(stderr, "%s: write failed: %s\n", program, *errorp);
log_spool_errors();
exit(EX_IOERR);
}
}
if (write_spool() == FAIL) {
(void) fprintf(stderr, "%s: write failed\n", program);
}
log_spool_errors();
}
if (dowrite) {
if (seek_spool((off_t) 0L) != SUCCEED) {
(void) fprintf(stderr, "seek_spool failed\n");
}
if (send_spool(stdout, (PUT_DOTS | UNIX_FROM_HACK)) != SUCCEED) {
(void) fprintf(stderr, "send_spool failed\n");
}
}
if (dounspool) {
unlink_spool();
} else {
close_spool();
}
exit(EX_OK);
}
stand_spool_usage()
{
(void) fprintf(stderr,
"usage: %s [-ddebug] [-msize] [-sspool_dirs] [-n] o|c[w][u] [file]\n",
program);
exit(EX_USAGE);
}
#endif /* STANDALONE */
/*
* Local Variables:
* c-file-style: "smail"
* End:
*/
syntax highlighted by Code2HTML, v. 0.9.1