/*
** Copyright 1998 - 2009 Double Precision, Inc.
** See COPYING for distribution information.
*/

#include	"lexer.h"
#include	"recipe.h"
#include	"varlist.h"
#include	"funcs.h"
#include	"tempfile.h"
#include	"message.h"
#include	"messageinfo.h"
#include	"xconfig.h"
#include	"exittrap.h"
#include	"maildrop.h"
#include	"config.h"
#include	"setgroupid.h"
#include	<sys/types.h>

#if	HAVE_LOCALE_H
#include	<locale.h>
#endif

#if HAVE_SYS_STAT_H
#include	<sys/stat.h>
#endif
#include	<sysexits.h>
#include	<string.h>
#include	<stdio.h>
#include	<stdlib.h>
#include	<ctype.h>
#include	<pwd.h>
#include	<grp.h>
#include	"../dbobj.h"
#if AUTHLIB
#include	<courierauth.h>
#endif

#if	HAS_GETHOSTNAME
#else
extern "C" int gethostname(const char *, size_t);
#endif


extern void setprocgroup();

static Message m1, m2;
extern char **environ;
static int errexit=EX_TEMPFAIL;
int quota_warn_percent = -1;
const char *quota_warn_message=0;

static const char *defaults_vars[]={"LOCKEXT","LOCKSLEEP","LOCKTIMEOUT",
					"LOCKREFRESH", "PATH", "SENDMAIL",
					"MAILDIRQUOTA"};
static const char *defaults_vals[]={LOCKEXT_DEF,LOCKSLEEP_DEF,LOCKTIMEOUT_DEF,
					LOCKREFRESH_DEF, DEFAULT_PATH,
					SENDMAIL_DEF, ""};

Maildrop maildrop;

Maildrop::Maildrop()
{
	verbose_level=0;
	isdelivery=0;
	sigfpe=0;
	includelevel=0;
	embedded_mode=0;
	msgptr= &m1;
	savemsgptr= &m2;
#if AUTHLIB_TEMPREJECT
	authlib_essential=1;
#else
	authlib_essential=0;
#endif
}

static void help()
{
	mout << "Usage: maildrop [options] [-d user] [arg] [arg] ...\n";
	mout << "       maildrop [options] [filterfile [arg] [arg] ...\n";
}

static void bad()
{
	errexit=EX_TEMPFAIL;
	throw "Bad command line arguments, -h for help.";
}

static void nouser()
{
	merr << "Invalid user specified.\n";
	exit(EX_NOUSER);
}

static void nochangeuidgid()
{
	errexit=EX_TEMPFAIL;
	throw "Cannot set my user or group id.";
}

static int trusted_user(uid_t uid)
{
static char trusted_users[]=TRUSTED_USERS;
static char buf[ sizeof(trusted_users) ];
char	*p;

	strcpy(buf, trusted_users);
	for (p=buf; (p=strtok(p, " ")) != 0; p=0)
	{
	struct	passwd *q=getpwnam(p);

		if (q && q->pw_uid == uid)
			return (1);
	}
	return (0);
}

static int trusted_group(gid_t gid)
{
static char trusted_groups[]=TRUSTED_GROUPS;
static char buf[ sizeof(trusted_groups) ];
char	*p;

	strcpy(buf, trusted_groups);
	for (p=buf; (p=strtok(p, " ")) != 0; p=0)
	{
	struct	group *q=getgrnam(p);

		if (q && (gid_t)q->gr_gid == gid)
			return (1);
	}
	return (0);
}

static int trusted_uidgid(uid_t uid, gid_t gid, gid_t gid2)
{
	if (trusted_user(uid) || trusted_group(gid) ||
		trusted_group(gid2))
		return (1);
	return (0);
}

static void sethostname(Buffer &buf)
{
char    hostname[256];

        hostname[0]=0;
        gethostname(hostname, 256);
        hostname[sizeof(hostname)-1]=0;

	buf=hostname;
}


static void copyright()
{
static const char msg[]="maildrop " VERSION " Copyright 1998-2005 Double Precision, Inc."

#if CRLF_TERM
	"\r\n"
#else
	"\n"
#endif
#ifdef DbObj
	"GDBM/DB extensions enabled."
#if CRLF_TERM
	"\r\n"
#else
	"\n"
#endif
#endif
#if AUTHLIB
	"Courier Authentication Library extension enabled."
#if CRLF_TERM
	"\r\n"
#else
	"\n"
#endif
#endif
	"Maildir quota extension are now always enabled."
#if CRLF_TERM
	"\r\n"
#else
	"\n"
#endif
	"This program is distributed under the terms of the GNU General Public"
#if CRLF_TERM
	"\r\n"
#else
	"\n"
#endif
        "License. See COPYING for additional information."
#if CRLF_TERM
	"\r\n"
#else
	"\n"
#endif
 
		;

	mout << msg;
	mout.flush();
}

void Maildrop::reset_vars()
{
int	i;
Buffer	name, value;

	for (i=0; i<(int)(sizeof(defaults_vars)/sizeof(defaults_vars[0])); i++)
	{
		name=defaults_vars[i];
		value=defaults_vals[i];
		SetVar(name, value);
	}
	name="HOME";
	SetVar(name, maildrop.init_home);
	name="LOGNAME";
	SetVar(name, maildrop.init_logname);
	name="SHELL";
	SetVar(name, maildrop.init_shell);
	name="DEFAULT";
	SetVar(name, maildrop.init_default);

	name="UMASK";
	value="077";
	SetVar(name, value);

	if (maildrop.init_quota.Length() > 0)
	{
		name="MAILDIRQUOTA";
		SetVar(name, maildrop.init_quota);
	}
}

#if AUTHLIB
// Authlib lookup

static int callback_authlib(struct authinfo *auth,
			    void *void_arg)
{
	Maildrop &maildrop=*(Maildrop *)void_arg;

	if (VerboseLevel() > 1)
	{
		Buffer b;

		b.set(auth->sysgroupid);
		b.push(0);

		merr << "maildrop: authlib: groupid="
		     << b << "\n";
	}

	setgroupid(auth->sysgroupid);

	uid_t u;
	if (auth->sysusername)
	{
		struct	passwd *q=getpwnam(auth->sysusername);

		if (q == NULL)
		{
			merr << "Cannot find system user "
			     << auth->sysusername
			     << "\n";

			nochangeuidgid();
		}

		u=q->pw_uid;
	}
	else
		u=*auth->sysuserid;

	if (VerboseLevel() > 1)
	{
		Buffer b;

		b.set(u);
		b.push(0);

		merr << "maildrop: authlib: userid="
		     << b << "\n";
	}

	setuid(u);

	if ( getuid() != u)
		nochangeuidgid();

	if (VerboseLevel() > 1)
	{
		merr << "maildrop: authlib: logname="
		     << auth->address
		     << ", home="
		     << auth->homedir
		     << ", mail="
		     << (auth->maildir ? auth->maildir:"(default)")
		     << "\n";
	}

	maildrop.init_home=auth->homedir;
	maildrop.init_logname=auth->address;
	maildrop.init_shell="/bin/sh";
	maildrop.init_default=auth->maildir ? auth->maildir:
		GetDefaultMailbox(auth->address);

	if ( auth->quota )
		maildrop.init_quota=auth->quota;

	return 0;
}

int find_in_authlib(Maildrop *maildrop, const char* user)
{
	int rc=auth_getuserinfo("login",
				user, callback_authlib, maildrop);

	if (rc == 0)
		return 1;

	if ((rc > 0) && (maildrop->authlib_essential == 1))
	{
		errexit=EX_TEMPFAIL;
		throw "Temporary authentication failure.";
	}

	return 0;
}
#else
int find_in_authlib(Maildrop *maildrop, const char* user)
{
	return 0;
}
#endif

static void tempfail(const char *msg)
{
	errexit = EX_TEMPFAIL;
	throw msg;
}

static int run(int argc, char **argv)
{
int	argn;
const	char *deliverymode=0;
char *embedded_filter=0;
const	char *from=0;
int     explicit_from=0;
Buffer	recipe;
uid_t	orig_uid;
gid_t	orig_gid, orig_gid2;
Buffer	extra_headers;
struct passwd *my_pw;
int	found;
#if	HAVE_COURIER
#if	RESTRICT_TRUSTED
const	char *numuidgid=0;
#endif
#endif


	umask( 0007 );
	for (argn=1; argn < argc; )
	{
		if (argv[argn][0] != '-')	break;
		if (strcmp(argv[argn], "--") == 0)	{ ++argn; break; }

	char	optc=argv[argn][1];
	const char *optarg=argv[argn]+2;

		++argn;
		switch (optc)	{

#if	HAVE_COURIER
#if	RESTRICT_TRUSTED
		case 'D':
			if (!*optarg && argn < argc)	optarg=argv[argn++];
			numuidgid=optarg;
			break;
#endif
#endif
		case 'd':
			if (!*optarg && argn < argc)	optarg=argv[argn++];
			deliverymode=optarg;
			break;
		case 'V':
			if (!*optarg && argn < argc)	optarg=argv[argn++];
			maildrop.verbose_level=atoi(optarg);
			break;
		case 'v':
			copyright();
			return (0);
		case 'm':
			maildrop.embedded_mode=1;
			break;
		case 'M':
			if (!*optarg && argn < argc)	optarg=argv[argn++];
			maildrop.embedded_mode=1;
			if (!deliverymode)	deliverymode="";
			if (!*optarg)
			{
				help();
				return (EX_TEMPFAIL);
			}
			embedded_filter=(char *)malloc(strlen(optarg)+1);
			if (!embedded_filter)	outofmem();
			strcpy(embedded_filter, optarg);
			{
			char *p;

				for (p=embedded_filter; *p; p++)
					if (*p == SLASH_CHAR || *p == '.')
						*p=':';
			}
			break;
		case 'A':
			if (!*optarg && argn < argc)	optarg=argv[argn++];
			if (*optarg)
			{
				extra_headers += optarg;
				extra_headers += '\n';
			}
			break;
		case 'f':
			if (!*optarg && argn < argc)	optarg=argv[argn++];
			if (*optarg)
			{
				from=optarg;
				explicit_from=1;
			}
			break;
		case 'w':
			if (!*optarg && argn < argc)	optarg=argv[argn++];
			if (*optarg)
				quota_warn_percent=atoi(optarg);
			break;
		case 'W':
			if (!*optarg && argn < argc)	optarg=argv[argn++];
			if (*optarg)
				quota_warn_message=optarg;
			break;
		case 'a':
			maildrop.authlib_essential=1;
			break;
		case 'h':
			help();
			return (EX_TEMPFAIL);
		default:
			bad();
		}
	}

	my_pw=0;
	found=0;
	orig_uid=getuid();
	orig_gid=getgid();
	orig_gid2=getegid();
	if (!deliverymode && argn < argc)
		recipe=argv[argn++];
	else
	{
		if (!deliverymode)	deliverymode="";

		if (*deliverymode)
		{
			found = find_in_authlib(&maildrop, deliverymode);

			if ( !found )
			{
				my_pw=getpwnam(deliverymode);
				if (!my_pw)
					nouser();
#if	RESET_GID
				setgroupid(my_pw->pw_gid);
#else
				setgroupid(getegid());
#endif
				setuid(my_pw->pw_uid);
				if (getuid() != my_pw->pw_uid)
					nochangeuidgid(); // Security violation.

				maildrop.init_home=my_pw->pw_dir;
				maildrop.init_logname=my_pw->pw_name;
				maildrop.init_shell=
					my_pw->pw_shell && *my_pw->pw_shell
						? my_pw->pw_shell:"/bin/sh";
				maildrop.init_default=
					GetDefaultMailbox(my_pw->pw_name);
				found=1;
			}
		}
		maildrop.isdelivery=1;
#if	RESTRICT_TRUSTED
		if ( getuid() != orig_uid && !trusted_uidgid(orig_uid,
			orig_gid, orig_gid2))
		{
			errexit=EX_TEMPFAIL;
			throw "You are not a trusted user.";
					// Security violation
		}
#endif
	}

#if	HAVE_COURIER
#if	RESTRICT_TRUSTED
	if (numuidgid)
	{
	uid_t	un=0;
	gid_t	gn=getgid();

		if (deliverymode && *deliverymode)
		{
			errexit=EX_TEMPFAIL;
			throw "Cannot use both -d and -D options.";
		}

		if ( !trusted_uidgid(orig_uid, orig_gid, orig_gid2))
		{
			errexit=EX_TEMPFAIL;
			throw "You are not authorized to use the -D option.";
		}

		if (!isdigit( (int)(unsigned char)*numuidgid))
		{
			errexit=EX_TEMPFAIL;
			throw "Invalid -D option.";
		}

		do
		{
			un=un * 10 + (*numuidgid++ - '0');
		} while (isdigit( (int)(unsigned char)*numuidgid));

		if ( *numuidgid )
		{
			if ( *numuidgid++ != '/' ||
				!isdigit( (int)(unsigned char)*numuidgid))
			{
				errexit=EX_TEMPFAIL;
				throw "Invalid -D option.";
			}
			gn=0;
			do
			{
				gn=gn * 10 + (*numuidgid++ - '0');
			} while (isdigit( (int)(unsigned char)*numuidgid));

			if ( *numuidgid )
			{
				errexit=EX_TEMPFAIL;
				throw "Invalid -D option.";
			}
		}
		setgroupid(gn);
		setuid(un);
		deliverymode="";
		orig_uid=un;	/* See below for another Courier hook */
	}
#endif
#endif


#if	RESET_GID
	setgroupid(getgid());
#endif

uid_t	my_u=getuid();

	setuid(my_u);	// Drop any setuid privileges.

	if (!found)
	{
#if HAVE_COURIER
		if (!deliverymode)
#endif
		{
			my_pw=getpwuid(my_u);
			if (!my_pw)
			{
				errexit=EX_TEMPFAIL;
				throw "Cannot determine my username.";
			}

			maildrop.init_home=my_pw->pw_dir;
			maildrop.init_logname=my_pw->pw_name;
			maildrop.init_shell=
				my_pw->pw_shell && *my_pw->pw_shell
					? my_pw->pw_shell:"/bin/sh";
			maildrop.init_default=
				GetDefaultMailbox(my_pw->pw_name);
		}
	}

int	i;
Buffer	name;
Buffer	value;

	for (i=0; environ[i]; i++)
	{
		name=environ[i];

		char	*p=strchr(environ[i], '=');

		value= p ? (name.Length(p - environ[i]), p+1):"";

		if (maildrop.isdelivery)
		{
			if (name == "LANG" ||
			    name == "LANGUAGE" ||
			    strncmp(name, "LC_", 3) == 0)
				;
			else
				continue;
		}

		SetVar(name, value);
	}

	i=1;
	while (argn < argc)
	{
		name="";
		name.append( (unsigned long)i);
		value=argv[argn++];
		SetVar(name, value);
		++i;
	}

#if	HAVE_COURIER
	if (deliverymode && orig_uid == getuid())
	{
	const char *p;

		if ((p=getenv("HOME")) && *p)
			maildrop.init_home=p;

		if ((p=getenv("LOGNAME")) && *p)
			maildrop.init_logname=p;

		if ((p=getenv("SHELL")) && *p)
			maildrop.init_shell=p;

		p=getenv("MAILDROPDEFAULT");

		if (!p || !*p)
		{
			p=getenv("LOCAL");

			if (p && *p)
				p=GetDefaultMailbox(p);
			else
				p="./Maildir";
		}
		maildrop.init_default=p;

		if ((p=getenv("MAILDIRQUOTA")) && *p)
			maildrop.init_quota=p;
	}
#endif

	if (deliverymode)
	{
	struct	stat	buf;
	Buffer	b;

		b=maildrop.init_home;
		b += '\0';

	const char *h=b;

		if (VerboseLevel() > 1)
			merr << "maildrop: Changing to " << h << "\n";

		if (chdir(h) < 0)
		{
			errexit=EX_TEMPFAIL;
			throw "Unable to change to home directory.";
		}
		recipe=".mailfilter";

		if ( stat(".", &buf) < 0)
			tempfail("Cannot stat() home directory.");
		if ( !S_ISDIR(buf.st_mode))
			tempfail("Home directory is not a directory.");
		if ( buf.st_mode & S_IWOTH)
			tempfail("Invalid home directory permissions - world writable.");
		if ( buf.st_uid != getuid())
			tempfail("Home directory owned by wrong user.");

		// Quietly terminate if the sticky bit is set on the homedir

		if ( buf.st_mode & S_ISVTX)
			return (EX_TEMPFAIL);

		if (embedded_filter)
		{
			i=stat(".mailfilters", &buf);

			if ( i < 0 && errno != ENOENT)
				tempfail("Unable to read $HOME/.mailfilters.");
			else if ( i >= 0)
			{
				if ( !S_ISDIR(buf.st_mode))
					tempfail("$HOME/.mailfilters should be a directory.");
				if ( buf.st_mode & (S_IRWXO|S_IRWXG))
					tempfail("Invalid permissions on $HOME/.mailfilters - remove world and group perms.");
				if ( buf.st_uid != getuid())
					tempfail("Invalid user ownership of $HOME/.mailfilters.");
			}
			recipe = embedded_filter;
		}
	}
#if	SHARED_TEMPDIR

#else
	maildrop.tempdir=maildrop.init_home;
	maildrop.tempdir += "/" TEMPDIR;
	maildrop.tempdir += '\0';
	mkdir( (const char *)maildrop.tempdir, 0700 );
#endif
	maildrop.reset_vars();

Buffer	msg;

	maildrop.global_timer.Set(GLOBAL_TIMEOUT);
	maildrop.msgptr->Init(0);	// Read message from standard input.
	maildrop.msginfo.info( *maildrop.msgptr );
	maildrop.msgptr->ExtraHeaders(extra_headers);
	maildrop.msgptr->setmsgsize();


	if (!from)	from="";
	if (*from)	maildrop.msginfo.fromname=from;

// If invoking user is trusted, trust the From line, else set it to invoking
// user.

	if (
#if KEEP_FROMLINE

// The original From_ line is kept, if necessary

#else

// If invoking user is trusted, trust the From line, else set it to invoking
// user.
		!trusted_uidgid(orig_uid, orig_gid, orig_gid2) ||
#endif

		maildrop.msginfo.fromname.Length() == 0)
	{
		maildrop.msginfo.fromname=maildrop.init_logname;
	}

	if (explicit_from)
		maildrop.msginfo.fromname=from;

	name="FROM";
	value=maildrop.msginfo.fromname;
	SetVar(name, value);

	if (VerboseLevel() > 1)
	{
		msg.reset();
		msg.append("Message start at ");
		msg.append((unsigned long)maildrop.msginfo.msgoffset);
		msg.append(" bytes, envelope sender=");
		if (maildrop.msginfo.fromname.Length() > 0)
			msg += maildrop.msginfo.fromname;
		msg.append("\n");
		msg += '\0';
		merr.write(msg);
	}

	name="HOSTNAME";
	sethostname(value);
	SetVar(name, value);

int	fd;

	//
	//

	if (!embedded_filter && deliverymode)
	{
	Recipe r;
	Lexer in;

		fd=in.Open(ETCDIR "/maildroprc");
		if (fd < 0)
		{
			if (errno != ENOENT)
			{
				errexit=EX_TEMPFAIL;
				throw "Error opening " ETCDIR "/maildroprc.";
			}
		}
		else
		{
			if (r.ParseRecipe(in) < 0)
				return (EX_TEMPFAIL);
			r.ExecuteRecipe();
		}
	}

Recipe	r;
Lexer	in;

#ifdef	DEFAULTEXT
int	firstdefault=1;
#endif

	name="MAILFILTER";
	value=recipe;
	SetVar(name, value);

	for (;;)
	{
		if (embedded_filter)
		{
			msg=".mailfilters/";
			msg += recipe;
			if (VerboseLevel() > 1)
				merr << "maildrop: Attempting " << msg << "\n";
			msg += '\0';
			fd=in.Open((const char *)msg);
		}
		else
		{
			msg=recipe;
			if (VerboseLevel() > 1)
				merr << "maildrop: Attempting " << msg << "\n";
			msg += '\0';
			fd=in.Open((const char *)msg);
			break;
		}
#ifndef	DEFAULTEXT
		break;
#else
		if (fd >= 0)	break;
		if (errno != ENOENT)	break;

		if (firstdefault)
		{
			recipe += DEFAULTEXT;
			firstdefault=0;
			continue;
		}

		// Pop DEFAULTEXT bytes from end of recipe name

		for (fd=sizeof(DEFAULTEXT)-1; fd; --fd)
			recipe.pop();

		while (recipe.Length())
		{
			if (recipe.pop() == '-')
			{
				recipe += DEFAULTEXT;
				break;
			}
		}
		if (recipe.Length() == 0)
		{
			msg=".mailfilters/";
			msg += DEFAULTEXT+1;
			if (VerboseLevel() > 1)
				merr << "maildrop: Attempting " << msg << "\n";
			msg += '\0';
			fd=in.Open((const char *)msg);
			break;
		}
#endif
	}

	if (fd < 0)
	{
		//
		//  If we are operating in delivery mode, it's ok if
		//  .mailfilter does not exist.
		//
		if (!deliverymode || errno != ENOENT)
		{
		static char buf[80];

			sprintf(buf, "Unable to open filter file, errno=%d.",
				errno);
			errexit=EX_TEMPFAIL;
			throw buf;
		}
	}
	else
	{
	struct	stat	stat_buf;

		setprocgroup();

		if (fstat( fd, &stat_buf) != 0)
			tempfail("stat() failed.");

		if (!S_ISREG(stat_buf.st_mode))
			tempfail("mailfilter file isn't a regular file.");
		if (stat_buf.st_mode & (S_IRWXO | S_IRWXG))
			tempfail("Cannot have world/group permissions on the filter file - for your own good.");
		if (stat_buf.st_uid != getuid())
			tempfail("mailfilter file is owned by the wrong user.");

		if (r.ParseRecipe(in) < 0)
			return (EX_TEMPFAIL);

//		if (maildrop.msgptr->MessageSize() > 0)
			r.ExecuteRecipe();
	}
//
// If a message is successfully delivered, an exception is thrown.
// If we're here, we should deliver to the default mailbox.
//

	if (!maildrop.embedded_mode)
	{
		name="DEFAULT";

	const char *v=GetVarStr(name);

		if (!v)
		{
			errexit=EX_TEMPFAIL;
			throw "DEFAULT mailbox not defined.";
		}

		value=v;
		value += '\0';
		if (delivery((const char *)value) < 0)
			return (EX_TEMPFAIL);
	}

	value="EXITCODE";
	return ( GetVar(value)->Int("0") );
}

int main(int argc, char **argv)
{

#if HAVE_SETLOCALE
	setlocale(LC_ALL, "C");
#endif

	_exit(Maildrop::trap(run, argc, argv));
}

const char *GetDefaultMailbox(const char *username)
{
static Buffer buf;
int	isfile=0;

	buf="";

const	char *p=DEFAULT_DEF;

	if (*p != SLASH_CHAR)	// Relative to home directory
	{
		buf=maildrop.init_home;
		buf.push(SLASH_CHAR);
		isfile=1;
	}

	while (*p)
	{
		if (*p != '=')
		{
			buf.push(*p);
			++p;
		}

	const char *q=username;

		while (*p == '=')
		{
			buf.push (*q ? *q:'.');
			if (*q)	q++;
			p++;
		}
	}

	if (!isfile)
	{
		buf.push(SLASH_CHAR);
		buf += username;
	}
	buf += '\0';
	return (buf);
}

#if	SHARED_TEMPDIR

#else
const char *TempName()
{
Buffer	t;

	t=(const char *)maildrop.tempdir;
	t += "/tmp.";
	t += '\0';
	return (TempName((const char *)t, 0));
}
#endif
