view src/lda/main.c @ 9159:6324a79d3ee1 HEAD

Initial commit for v2.0 master rewrite. Several features are still missing.
author Timo Sirainen <tss@iki.fi>
date Thu, 23 Apr 2009 19:53:44 -0400
parents bf448752f6c4
children 5d0a69504867
line wrap: on
line source

/* Copyright (c) 2005-2009 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "lib-signals.h"
#include "env-util.h"
#include "fd-set-nonblock.h"
#include "istream.h"
#include "istream-seekable.h"
#include "str.h"
#include "str-sanitize.h"
#include "strescape.h"
#include "rfc822-parser.h"
#include "message-address.h"
#include "imap-utf7.h"
#include "settings-parser.h"
#include "master-service.h"
#include "master-service-settings.h"
#include "mail-storage-service.h"
#include "mail-namespace.h"
#include "raw-storage.h"
#include "mail-deliver.h"
#include "mail-send.h"
#include "duplicate.h"
#include "mbox-from.h"
#include "lda-settings.h"

#include <stdio.h>
#include <stdlib.h>
#include <pwd.h>
#include <sysexits.h>

#ifndef EX_CONFIG
#  define EX_CONFIG 78 /* for HP-UX */
#endif

#define DEFAULT_ENVELOPE_SENDER "MAILER-DAEMON"

/* After buffer grows larger than this, create a temporary file to /tmp
   where to read the mail. */
#define MAIL_MAX_MEMORY_BUFFER (1024*128)

static const char *wanted_headers[] = {
	"From", "To", "Message-ID", "Subject", "Return-Path",
	NULL
};

static struct master_service *service;

static const char *escape_local_part(const char *local_part)
{
	const char *p;

	/* if there are non-atext chars, we need to return quoted-string */
	for (p = local_part; *p != '\0'; p++) {
		if (!IS_ATEXT(*p)) {
			return t_strdup_printf("\"%s\"",
					       str_escape(local_part));
		}
	}
	return local_part;
}

static const char *address_sanitize(const char *address)
{
	struct message_address *addr;
	const char *ret, *mailbox;
	pool_t pool;

	pool = pool_alloconly_create("address sanitizer", 256);
	addr = message_address_parse(pool, (const unsigned char *)address,
				     strlen(address), 1, FALSE);

	if (addr == NULL || addr->mailbox == NULL || addr->domain == NULL ||
	    *addr->mailbox == '\0')
		ret = DEFAULT_ENVELOPE_SENDER;
	else {
		mailbox = escape_local_part(addr->mailbox);
		if (*addr->domain == '\0')
			ret = t_strdup(mailbox);
		else
			ret = t_strdup_printf("%s@%s", mailbox, addr->domain);
	}
	pool_unref(&pool);
	return ret;
}


static struct istream *
create_raw_stream(struct mail_deliver_context *ctx,
		  const char *temp_path_prefix, int fd, time_t *mtime_r)
{
	struct istream *input, *input2, *input_list[2];
	const unsigned char *data;
	char *sender = NULL;
	size_t i, size;
	int ret, tz;

	*mtime_r = (time_t)-1;
	fd_set_nonblock(fd, FALSE);

	input = i_stream_create_fd(fd, 4096, FALSE);
	input->blocking = TRUE;
	/* If input begins with a From-line, drop it */
	ret = i_stream_read_data(input, &data, &size, 5);
	if (ret > 0 && size >= 5 && memcmp(data, "From ", 5) == 0) {
		/* skip until the first LF */
		i_stream_skip(input, 5);
		while ((ret = i_stream_read_data(input, &data, &size, 0)) > 0) {
			for (i = 0; i < size; i++) {
				if (data[i] == '\n')
					break;
			}
			if (i != size) {
				(void)mbox_from_parse(data, i, mtime_r, &tz,
						      &sender);
				i_stream_skip(input, i + 1);
				break;
			}
			i_stream_skip(input, size);
		}
	}

	if (sender != NULL && ctx->src_envelope_sender == NULL) {
		/* use the envelope sender from From_-line, but only if it
		   hasn't been specified with -f already. */
		ctx->src_envelope_sender = p_strdup(ctx->pool, sender);
	}
	i_free(sender);

	if (input->v_offset == 0) {
		input2 = input;
		i_stream_ref(input2);
	} else {
		input2 = i_stream_create_limit(input, (uoff_t)-1);
	}
	i_stream_unref(&input);

	input_list[0] = input2; input_list[1] = NULL;
	input = i_stream_create_seekable(input_list, MAIL_MAX_MEMORY_BUFFER,
					 temp_path_prefix);
	i_stream_unref(&input2);
	return input;
}

static void failure_exit_callback(int *status)
{
	/* we want all our exit codes to be sysexits.h compatible.
	   if we failed because of a logging related error, we most likely
	   aren't writing to stderr, so try writing there to give some kind of
	   a clue what's wrong. FATAL_LOGOPEN failure already wrote to
	   stderr, so don't duplicate it. */
	switch (*status) {
	case FATAL_LOGWRITE:
		fputs("Failed to write to log file", stderr);
		break;
	case FATAL_LOGERROR:
		fputs("Internal logging error", stderr);
		break;
	case FATAL_LOGOPEN:
	case FATAL_OUTOFMEM:
	case FATAL_EXEC:
	case FATAL_DEFAULT:
		break;
	default:
		return;
	}
	*status = EX_TEMPFAIL;
}

static void print_help(void)
{
	printf(
"Usage: dovecot-lda [-c <config file>] [-a <address>] [-d <username>] [-p <path>]\n"
"                   [-f <envelope sender>] [-m <mailbox>] [-e] [-k]\n");
}

int main(int argc, char *argv[])
{
	const struct setting_parser_info *set_roots[] = {
		&lda_setting_parser_info,
		NULL
	};
	struct mail_deliver_context ctx;
	enum mail_storage_service_flags service_flags = 0;
	const char *user, *errstr, *path, *getopt_str;
	struct mail_storage_service_input service_input;
	struct mail_user *raw_mail_user;
	struct mail_namespace *raw_ns;
	struct mail_namespace_settings raw_ns_set;
	struct mail_storage *storage;
	struct mailbox *box;
	struct raw_mailbox *raw_box;
	struct istream *input;
	struct mailbox_transaction_context *t;
	struct mailbox_header_lookup_ctx *headers_ctx;
	char cwd[PATH_MAX];
	void **sets;
	uid_t process_euid;
	bool stderr_rejection = FALSE;
	time_t mtime;
	int ret, c;
	string_t *str;
	enum mail_error error;

	if (getuid() != geteuid() && geteuid() == 0) {
		/* running setuid - don't allow this if the binary is
		   executable by anyone */
		struct stat st;

		if (stat(argv[0], &st) < 0) {
			fprintf(stderr, "stat(%s) failed: %s\n",
				argv[0], strerror(errno));
			return EX_CONFIG;
		} else if ((st.st_mode & 1) != 0) {
			fprintf(stderr, "%s must not be both world-executable "
				"and setuid-root. This allows root exploits. "
				"See http://wiki.dovecot.org/LDA#multipleuids\n",
				argv[0]);
			return EX_CONFIG;
		}
	}

	i_set_failure_exit_callback(failure_exit_callback);

	service = master_service_init("lda", MASTER_SERVICE_FLAG_STANDALONE,
				      argc, argv);

	memset(&ctx, 0, sizeof(ctx));
	ctx.pool = pool_alloconly_create("mail deliver context", 256);
	ctx.dest_mailbox_name = "INBOX";
	path = NULL;

	user = getenv("USER");
	getopt_str = t_strconcat("a:d:p:ekm:nsf:",
				 master_service_getopt_string(), NULL);
	while ((c = getopt(argc, argv, getopt_str)) > 0) {
		switch (c) {
		case 'a':
			/* destination address */
			ctx.dest_addr = optarg;
			break;
		case 'd':
			/* destination user */
			user = optarg;
			service_flags |= MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP;
			break;
		case 'p':
			/* input path */
			path = optarg;
			if (*path != '/') {
				/* expand relative paths before we chdir */
				if (getcwd(cwd, sizeof(cwd)) == NULL)
					i_fatal("getcwd() failed: %m");
				path = t_strconcat(cwd, "/", path, NULL);
			}
			break;
		case 'e':
			stderr_rejection = TRUE;
			break;
		case 'm':
			/* destination mailbox.
			   Ignore -m "". This allows doing -m ${extension}
			   in Postfix to handle user+mailbox */
			if (*optarg != '\0') T_BEGIN {
				str = t_str_new(256);
				if (imap_utf8_to_utf7(optarg, str) < 0) {
					i_fatal("Mailbox name not UTF-8: %s",
						optarg);
				}
				ctx.dest_mailbox_name =
					p_strdup(ctx.pool, str_c(str));
			} T_END;
			break;
		case 'f':
			/* envelope sender address */
			ctx.src_envelope_sender =
				p_strdup(ctx.pool, address_sanitize(optarg));
			break;
		default:
			if (!master_service_parse_option(service, c, optarg)) {
				print_help();
				exit(EX_USAGE);
			}
			break;
		}
	}
	if (optind != argc) {
		print_help();
		i_fatal_status(EX_USAGE, "Unknown argument: %s", argv[optind]);
	}

	process_euid = geteuid();
	if ((service_flags & MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP) != 0)
		;
	else if (process_euid != 0) {
		/* we're non-root. get our username and possibly our home. */
		struct passwd *pw;
		const char *home;

		home = getenv("HOME");
		if (user != NULL && home != NULL) {
			/* no need for a pw lookup */
		} else if ((pw = getpwuid(process_euid)) != NULL) {
			user = t_strdup(pw->pw_name);
			if (home == NULL)
				env_put(t_strconcat("HOME=", pw->pw_dir, NULL));
		} else if (user == NULL) {
			i_fatal_status(EX_USAGE,
				       "Couldn't lookup our username (uid=%s)",
				       dec2str(process_euid));
		}
	} else {
		i_fatal_status(EX_USAGE,
			"destination user parameter (-d user) not given");
	}

	memset(&service_input, 0, sizeof(service_input));
	service_input.username = user;

	service_flags |= MAIL_STORAGE_SERVICE_FLAG_DISALLOW_ROOT;
	ctx.dest_user = mail_storage_service_init_user(service, &service_input,
						       set_roots,
						       service_flags);
#ifdef SIGXFSZ
        lib_signals_ignore(SIGXFSZ, TRUE);
#endif
	ctx.set = mail_storage_service_get_settings(service);
        duplicate_init(mail_user_set_get_storage_set(ctx.dest_user->set));

	/* create a separate mail user for the internal namespace */
	if (master_service_set(service, "mail_full_filesystem_access=yes") < 0)
		i_unreached();
	sets = master_service_settings_get_others(service);
	raw_mail_user = mail_user_alloc(user, sets[0]);
	mail_user_set_home(raw_mail_user, "/");
	if (mail_user_init(raw_mail_user, &errstr) < 0)
		i_fatal("Raw user initialization failed: %s", errstr);

	memset(&raw_ns_set, 0, sizeof(raw_ns_set));
	raw_ns_set.location = "/tmp";

	raw_ns = mail_namespaces_init_empty(raw_mail_user);
	raw_ns->flags |= NAMESPACE_FLAG_INTERNAL;
	raw_ns->set = &raw_ns_set;
	if (mail_storage_create(raw_ns, "raw", 0, &errstr) < 0)
		i_fatal("Couldn't create internal raw storage: %s", errstr);
	if (path == NULL) {
		const char *prefix = mail_user_get_temp_prefix(ctx.dest_user);
		input = create_raw_stream(&ctx, prefix, 0, &mtime);
		box = mailbox_open(&raw_ns->storage, "Dovecot Delivery Mail",
				   input, MAILBOX_OPEN_NO_INDEX_FILES);
		i_stream_unref(&input);
	} else {
		mtime = (time_t)-1;
		box = mailbox_open(&raw_ns->storage, path, NULL,
				   MAILBOX_OPEN_NO_INDEX_FILES);
	}
	if (box == NULL) {
		i_fatal("Can't open delivery mail as raw: %s",
			mail_storage_get_last_error(raw_ns->storage, &error));
	}
	if (mailbox_sync(box, 0, 0, NULL) < 0) {
		i_fatal("Can't sync delivery mail: %s",
			mail_storage_get_last_error(raw_ns->storage, &error));
	}
	raw_box = (struct raw_mailbox *)box;
	raw_box->envelope_sender = ctx.src_envelope_sender != NULL ?
		ctx.src_envelope_sender : DEFAULT_ENVELOPE_SENDER;
	raw_box->mtime = mtime;

	t = mailbox_transaction_begin(box, 0);
	headers_ctx = mailbox_header_lookup_init(box, wanted_headers);
	ctx.src_mail = mail_alloc(t, 0, headers_ctx);
	mailbox_header_lookup_unref(&headers_ctx);
	mail_set_seq(ctx.src_mail, 1);

	if (ctx.dest_addr == NULL) {
		ctx.dest_addr = mail_deliver_get_address(&ctx, "Envelope-To");
		if (ctx.dest_addr == NULL) {
			ctx.dest_addr = strchr(user, '@') != NULL ? user :
				t_strconcat(user, "@", ctx.set->hostname, NULL);
		}
	}

	if (mail_deliver(&ctx, &storage) < 0) {
		if (storage == NULL) {
			/* This shouldn't happen */
			i_error("BUG: Saving failed to unknown storage");
			return EX_TEMPFAIL;
		}

		errstr = mail_storage_get_last_error(storage, &error);

		if (stderr_rejection) {
			/* write to stderr also for tempfails so that MTA
			   can log the reason if it wants to. */
			fprintf(stderr, "%s\n", errstr);
		}

		if (error != MAIL_ERROR_NOSPACE ||
		    ctx.set->quota_full_tempfail) {
			/* Saving to INBOX should always work unless
			   we're over quota. If it didn't, it's probably a
			   configuration problem. */
			return EX_TEMPFAIL;
		}

		/* we'll have to reply with permanent failure */
		mail_deliver_log(&ctx, "rejected: %s",
				 str_sanitize(errstr, 512));

		if (stderr_rejection)
			return EX_NOPERM;
		ret = mail_send_rejection(&ctx, user, errstr);
		if (ret != 0)
			return ret < 0 ? EX_TEMPFAIL : ret;
		/* ok, rejection sent */
	}

	mail_free(&ctx.src_mail);
	mailbox_transaction_rollback(&t);
	mailbox_close(&box);

	mail_user_unref(&ctx.dest_user);
	mail_user_unref(&raw_mail_user);
	duplicate_deinit();
	pool_unref(&ctx.pool);

	mail_storage_service_deinit_user();
	master_service_deinit(&service);
        return EX_OK;
}