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


#include	"config.h"
#include	"maildircache.h"
#include	"numlib/numlib.h"
#include	<stdio.h>
#include	<string.h>
#include	<stdlib.h>
#include	<ctype.h>
#include	<signal.h>
#include	<pwd.h>
#if	HAVE_UNISTD_H
#include	<unistd.h>
#endif
#include	<sys/types.h>
#if	HAVE_SYS_STAT_H
#include	<sys/stat.h>
#endif
#if	HAVE_SYS_WAIT_H
#include	<sys/wait.h>
#endif
#if	HAVE_FCNTL_H
#include	<fcntl.h>
#endif
#if HAVE_DIRENT_H
#include <dirent.h>
#define NAMLEN(dirent) strlen((dirent)->d_name)
#else
#define dirent direct
#define NAMLEN(dirent) (dirent)->d_namlen
#if HAVE_SYS_NDIR_H
#include <sys/ndir.h>
#endif
#if HAVE_SYS_DIR_H
#include <sys/dir.h>
#endif
#if HAVE_NDIR_H
#include <ndir.h>
#endif
#endif
#if	HAVE_SYSLOG_H
#include	<syslog.h>
#else
#define	syslog(a,b,c)
#endif

#define exit(_a_) _exit(_a_)

static const char rcsid[]="$Id: maildircache.c,v 1.5 2003/01/05 04:01:16 mrsam Exp $";

static const char * const *authvars;
static char **authvals;
static time_t expinterval;
static time_t lastclean=0;
static const char *cachedir;
static const char *cacheowner;

int maildir_cache_init(time_t n, const char *d, const char *o,
			const char * const *a)
{
	unsigned x;

	expinterval=n;
	cachedir=d;
	cacheowner=o;
	authvars=a;

	for (x=0; a[x]; x++)
		;

	if ((authvals=malloc(sizeof(char *)*(x+1))) == NULL)
		return (-1);

	for (x=0; a[x]; x++)
		authvals[x]=0;
	return (0);
}

static char *create_cache_name(const char *userid, time_t login_time)
{
int	l;
char	buf[NUMBUFSIZE];
const char	*p;
char	*q;
char	*f, *g;

	login_time /= expinterval;
	l=1;
	for (p=userid; *p; p++)
	{
		++l;
		if (*p < ' ' || *p == ';' || *p == '\'' || *p == ';')
		{
			syslog(LOG_DAEMON | LOG_CRIT,
			       "maildircache: invalid chars in userid: %s",
			       p);
			return (NULL);
		}
		if (*p == '/' || *p == '+' || (int)(unsigned char)*p >= 127)
			l += 2;
	}
	g=malloc(l);
	if (!g)
	{
		syslog(LOG_DAEMON | LOG_CRIT,
		       "%s: malloc failed",
		       "maildircache");
		return (NULL);
	}
	q=g;
	while (*userid)
	{
		if (*userid == '/' || *userid == '+'
			|| (int)(unsigned char)*userid >= 127)
		{
		static char xdigit[]="0123456789ABCDEF";

			*q++ = '+';
			*q++ = xdigit[ (*userid >> 4) & 15 ];
			*q++ = xdigit[ (*userid) & 15 ];
		}
		else
			*q++ = *userid;

		++userid;
	}
	*q=0;

	l=sizeof("//xx/xxxxxxx") + strlen(cachedir);
	l += strlen(libmail_str_time_t( login_time, buf)) + strlen(g);
	f=malloc(l);
	if (!f)
	{
		free(g);
		syslog(LOG_DAEMON | LOG_CRIT,
		       "%s: malloc failed",
		       "maildircache");
		return (NULL);
	}
	strcat(strcat(strcat(strcpy(f, cachedir), "/"), buf), "/");
	strncpy(buf, g, 2);
	buf[2]=0;
	while (strlen(buf) < 2)	strcat(buf, "+");
	strcat(strcat(strcat(f, buf), "/"), g);
	free(g);
	return (f);
}

static pid_t childproc= -1;
static int childpipe;

void maildir_cache_start()
{
int	pipefd[2];
char	buf[2048];
int	i, j;
char	*userid, *login_time, *data;
time_t	login_time_n;
char	*f;
FILE	*fp;

	if (pipe(pipefd) < 0)
	{
		syslog(LOG_DAEMON | LOG_CRIT,
		       "%s: pipe() failed",
		       "maildircache");
		perror("pipe");
		return;
	}
	while ((childproc=fork()) < 0)
	{
		sleep(5);
	}

	if (childproc)
	{
		close(pipefd[0]);
		childpipe=pipefd[1];
		return;
	}
	close(pipefd[1]);
	i=0;

	for (;;)
	{
		if (i >= sizeof(buf)-1)
		{
			close(pipefd[0]);

			/* Problems */

			syslog(LOG_DAEMON | LOG_CRIT,
			       "%s: Max cache buffer overflow.",
			       "maildircache");
			exit(1);
		}

		j=read(pipefd[0], buf+i, sizeof(buf)-1-i);
		if (j < 0)
		{
			syslog(LOG_DAEMON | LOG_CRIT,
			       "%s: Cache create failure.",
			       "maildircache");
			exit(1);
		}
		if (j == 0)	break;
		i += j;
	}
	close(pipefd[0]);
	buf[i]=0;

	{
	struct passwd *pwd=getpwnam(cacheowner);

		if (!pwd || setgid(pwd->pw_gid) || setuid(pwd->pw_uid))
		{
			syslog(LOG_DAEMON | LOG_CRIT,
				"maildircache: Cache create failure - cannot change to %s",
			       cacheowner);
			exit(1);
		}
	}

	if (strncmp(buf, "CANCELLED\n", 10) == 0)
		exit (0);

	userid=buf;
	if ((login_time=strchr(userid, ' ')) == 0)
	{
		syslog(LOG_DAEMON | LOG_CRIT,
			"%s: Cache create failure - authentication process crashed.", "maildircache");
		exit(1);
	}
	*login_time++=0;
	if ((data=strchr(login_time, ' ')) == 0)
	{
		syslog(LOG_DAEMON | LOG_CRIT,
			"%s: Cache create failure - authentication process crashed.", "maildircache");
		exit(1);
	}
	*data++=0;

	login_time_n=0;
	while (*login_time >= '0' && *login_time <= '9')
		login_time_n = login_time_n * 10 + (*login_time++ -'0');

	f=create_cache_name(userid, login_time_n);

	if (!f)
		exit(0);

	if ((fp=fopen(f, "w")) == 0)	/* Try creating subdirs */
	{
		char	*p=f+strlen(cachedir);

		while (p && *p == '/')
		{
			*p=0;
			mkdir(f, 0700);
			*p='/';
			p=strchr(p+1, '/');
		}

		if ((fp=fopen(f, "w")) == 0)
		{
			syslog(LOG_DAEMON | LOG_CRIT,
				"maildircache: Cache create failure - unable to create %s.", f);
			exit(1);
		}
	}
	free(f);

	if ( fwrite(data, strlen(data), 1, fp) != 1 || fflush(fp)
	     || ferror(fp))
	{
		fclose(fp);
		unlink(f);	/* Problems */
		syslog(LOG_DAEMON | LOG_CRIT,
		       "%s: Cache create failure - write error.",
		       "maildircache");
		exit(1);
	}
	else	fclose(fp);
	exit(0);
}

static int savebuf(char *p, int l)
{
	while (l)
	{
	int	n=write(childpipe, p, l);

		if (n <= 0)	return (-1);
		p += n;
		l -= n;
	}
	return (0);
}

void maildir_cache_save(const char *a, time_t b, const char *homedir,
		      uid_t u, gid_t g)
{
char	buf[2048];
char	buf2[NUMBUFSIZE];
pid_t	p;
int	waitstat;

	strcat(strcpy(buf, a), " ");
	strcat(strcat(buf, libmail_str_time_t(b, buf2)), " ");
	strcat(strcat(buf, libmail_str_uid_t(u, buf2)), " ");
	strcat(strcat(buf, libmail_str_gid_t(g, buf2)), " ");
	strncat(buf, homedir, sizeof(buf)-2-strlen(homedir));
	strcat(buf, "\n");

	if (savebuf(buf, strlen(buf)) == 0)
	{
	int	i;

		for (i=0; authvars[i]; i++)
		{
		const char *p;

			strcat(strcpy(buf, authvars[i]), "=");
			p=getenv(authvars[i]);
			if (!p || strlen(p)+strlen(buf) >= sizeof(buf)-2 ||
				strchr(p, '\n'))
				continue;
			strcat(strcat(buf, p), "\n");
			if (savebuf(buf, strlen(buf)))	break;
		}
	}
	close(childpipe);
	while ((p=wait(&waitstat)) != -1 && p != childproc)
		;
	childproc= -1;
}

void maildir_cache_cancel()
{
	if (childproc > 0)
	{
		write(childpipe, "CANCELLED\n", 10); 
		close(childpipe);
	}
}

int maildir_cache_search(const char *a, time_t b,
			 int (*callback_func)(uid_t, gid_t, const char *,
					      void *), void *callback_arg)
{
	char *f=create_cache_name(a, b);
	FILE *fp;
	uid_t	u;
	gid_t	g;
	char dir[1024];
	int	n;
	int	c;

	if (!f)
		return (-1);
	fp=fopen(f, "r");
	free(f);
	if (!fp)
		return (-1);

	u=0;
	while ((c=getc(fp)) != ' ')
	{
		if (c < '0' || c > '9')
		{
			fclose(fp);
			return (-1);
		}
		u=u*10 + (c-'0');
	}

	g=0;
	while ((c=getc(fp)) != ' ')
	{
		if (c < '0' || c > '9')
		{
			fclose(fp);
			return (-1);
		}
		g=g*10 + (c-'0');
	}

	for (n=0; (c=getc(fp)) != EOF; n++)
	{
		if (c == '\n')	break;
		if (n >= sizeof(dir)-1)
		{
			fclose(fp);
			syslog(LOG_DAEMON | LOG_CRIT,
				"%s: Cache record overflow.", "maildircache");
			return (-1);
		}
		dir[n]=(char)c;
	}
	dir[n]=0;

	if ((n=(*callback_func)(u, g, dir, callback_arg)) != 0)
	{
		fclose(fp);
		return (n);
	}

	if (c != EOF)
	{
		while (fgets(dir, sizeof(dir), fp))
		{
		char	*q;

			if ( (q=strchr(dir, '\n')) == 0)
			{
				fclose(fp);
				syslog(LOG_DAEMON | LOG_CRIT,
				       "%s: Cache record overflow.", "maildircache");
				return (-1);
			}
			*q=0;

			for (n=0; authvars[n]; n++)
			{
				int l=strlen(authvars[n]);

				if (strncmp(dir, authvars[n], l) == 0 &&
				    dir[l] == '=')
				{
					char *s=strdup(dir);

					if (!s)
					{
						fclose(fp);
						syslog(LOG_DAEMON | LOG_CRIT,
						       "%s: malloc failed",
						       "maildircache");
						return (-1);
					}

					putenv(s);

					if (authvals[n])
						free(authvals[n]);
					authvals[n]=s;
					break;
				}
			}
		}
		fclose(fp);
	}
	return (0);
}

struct purge_list {
	struct purge_list *next;
	char *n;
} ;

static void add_purge_list(struct purge_list **p, const char *a)
{
	char *c=malloc(strlen(a) + 1);
	struct purge_list *pp;

	if (!c)
		return;

	pp=malloc(sizeof(struct purge_list));
	if (!pp)
	{
		free(c);
		return;
	}

	pp->next=*p;

	*p=pp;
	pp->n=c;
	strcpy(c, a);
}

static void rmrf(const char *);

void maildir_cache_purge()
{
	time_t now;
	pid_t p;
	int waitstat;
	struct passwd *pw;
	struct purge_list *pl;
	DIR *dirp;
	struct dirent *de;

	time(&now);

	if (lastclean && lastclean >= now - expinterval)
		return;

	lastclean=now;

	p=fork();

	if (p < 0)
		return;

	if (p)
	{
		pid_t p2;

		while ((p2=wait(&waitstat)) >= 0 && p2 != p)
			;
		return;
	}

	p=fork();

	if (p)
		exit(0);

	pw=getpwnam(cacheowner);

	if (!pw)
	{
		syslog(LOG_CRIT, "no such user %s - cannot purge login cache dir",
		       cacheowner);
		exit(0);
	}

	if (setgid(pw->pw_gid) < 0 || setuid(pw->pw_uid) < 0)
	{
		syslog(LOG_CRIT,
		       "cannot change to uid/gid for %s - cannot purge login cache dir",
		       cacheowner);
		exit(0);
	}

	if (chdir(cachedir))
	{
		syslog(LOG_CRIT,
		       "cannot change dir to %s", cachedir);
		exit(0);
	}

	pl=NULL;

	dirp=opendir(".");

	now /= expinterval;
	--now;

	while (dirp && (de=readdir(dirp)) != NULL)
	{
		if (!isdigit((int)(unsigned char)de->d_name[0]))
			continue;

		if (atol(de->d_name) >= now)
			continue;

		add_purge_list(&pl, de->d_name);
	}
	if (dirp)
		closedir(dirp);

	while (pl)
	{
		struct purge_list *p=pl;

		pl=pl->next;

		rmrf(p->n);
		free(p->n);
		free(p);
	}
	exit(0);
}

static void rmrf(const char *d)
{
	DIR *dirp;
	struct dirent *de;
	struct purge_list *pl=NULL, *p;

	if (chdir(d))
		return;

	dirp=opendir(".");

	while (dirp && (de=readdir(dirp)) != NULL)
	{
		if (strcmp(de->d_name, ".") == 0 ||
		    strcmp(de->d_name, "..") == 0)
			continue;

		add_purge_list(&pl, de->d_name);
	}
	if (dirp)
		closedir(dirp);

	while (pl)
	{
		p=pl;

		pl=pl->next;

		if (unlink(p->n))
			rmrf(p->n);
		free(p->n);
		free(p);
	}

	if (chdir("..") < 0 || rmdir(d) < 0)
	{
		syslog(LOG_CRIT,
		       "cannot chdir to .. while purging login cache");
		exit(1);
	}
}
