/*
 *   Copyright (c) 2003,2004 David Parsons. All rights reserved.
 *   
 *   Redistribution and use in source and binary forms, with or without
 *   modification, are permitted provided that the following conditions
 *   are met:
 *  1. Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in
 *     the documentation and/or other materials provided with the
 *     distribution.
 *  3. All advertising materials mentioning features or use of this
 *     software must display the following acknowledgement:
 *     
 *   This product includes software developed by David Parsons
 *   (orc@pell.chi.il.us)
 *
 *  4. My name may not be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *     
 *  THIS SOFTWARE IS PROVIDED BY DAVID PARSONS ``AS IS'' AND ANY
 *  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 *  THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 *  PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DAVID
 *  PARSONS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 *  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 *  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 *  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 *  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
 *  IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 *  THE POSSIBILITY OF SUCH DAMAGE.
 */

/*
 * set up passwords for virtual domains (vm-pop3d)
 *
 * usage: vpasswd -l
 *        valias  -l
 *        vpasswd [-f] [-adx] user [passwd]
 *        valias  [-f] [-adx] user1 user2
 */

#include <stdio.h>
#include <pwd.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <pwd.h>
#include <stdarg.h>
#include <string.h>
#ifdef __FreeBSD__
# include <libgen.h>		// basename() on freebsd
#endif
#ifdef __FreeBSD__
# include <stdlib.h>		// freebsd has copied malloc.h to stdlib.h
#else
# include <malloc.h>
#endif
#include <fcntl.h>
#include <ctype.h>
#include <signal.h>
#include <setjmp.h>

#include "vpw.h"
#include "transaction.h"
#include "pathnames.h"
#include "post.h"

void err(char *message, ...);
void usage();

char *vetcdir = NULL;
char *vdomain = NULL;
char *pgm = NULL;
int  is_valias = 0;	// vpasswd or valias ?
int  is_root = 0;	// called by uid 0

int  a_flag = 0;	// -n(ew account)
int  x_flag = 0;	// -x (disable account)
int  d_flag = 0;	// -d(elete account)
int  l_flag = 0;	// -l(ist account)
int  f_flag = 0;	// -f(orce action without confirmation)

char *please = "\n*PLEASE CONTACT THE SYSTEM MANAGER*";


/*
 * getvdomain() finds the domain this account is responsible for.
 */
char *
getvdomain(char *account)
{
    FILE *f;
    struct passwd *self;

    char *user, *domain, *active;
    char *dir;
    int rc;

    if (is_root) {
	if (account == 0)
	    usage();
	if ((self = getpwnam(account)) == NULL)
	    err("The %s account does not exist", account);
	if (self->pw_uid == 0)
	    err("%s is root, and that's a huge security hole", self->pw_name);
	if (setuid(self->pw_uid) != 0)
	    err("Cannot setuid to user %s (uid %d): %s",
		 account, self->pw_uid, strerror(errno));
    }
    else if ( (self = getpwuid(getuid())) == NULL)
	err("Cannot discover my real name.%s", please);
    else
        account = self->pw_name;

    if ( (f = fopen(_PATH_VDOMAINS, "r")) == NULL)
	if (errno == ENOENT)
	    err("Cannot find domain configuration file.%s", please);
	else
	    err("Cannot read domain configuration: %s%s", strerror(errno), please);

    while ( (rc=getrecord(f, &user, &domain, &active)) >= 0) {
	if (rc < 3) continue;
	if (strcmp(user, account) == 0) {
	    if (atoi(active)) {
		dir = concatenate(_PATH_VETC, domain, 0);
		if (access(dir, F_OK) != 0)
			if (errno == ENOENT)
			    err("No mailbox for %s.%s", domain, please);
			else
			    err("%s mailbox: %s.%s", domain, strerror(errno), please);
		free(dir);
		return concatenate(domain, 0);
	    }
	    else if (!is_root)	// root can edit disabled domains
		err("The domain %s has been disabled.%s", domain, please);
	}
    }
    err("Your account has no domain associated with it.");
} /* getvdomain */


/*
 * e_to_sys() is a dummy glue function needed by concatenate.
 */
void
e_to_sys(int errno)
{
    1;
} /* e_to_sys */


/*
 * complain, then die
 */
void
err(char *message, ...)
{
    va_list ap;

    va_start(ap, message);

    //fprintf(stderr, "%s: ", pgm);
    vfprintf(stderr, message, ap);
    fprintf(stderr, "\n");
    va_end(ap);
    exit(1);
} /* err */


void
usage()
{
    char *extrasw = is_root ? " account" : "";

    if (is_valias)
	fprintf(stderr, "usage: %s%s [-a] alias [user]\n", pgm, extrasw);
    else {
	fprintf(stderr, "usage: %s%s [-a] user [passwd]\n", pgm, extrasw);
	fprintf(stderr, "       %s%s -d user\n", pgm, extrasw);
    }
    fprintf(stderr, "       %s%s -l\n", pgm, extrasw);
    fprintf(stderr, "       %s%s [-f] -x user\n", pgm, extrasw);
    exit(1);
}


/*
 * list a password or alias file
 */
void
list()
{
    char *file = concatenate(vetcdir, is_valias ? "/alias" : "/passwd", 0);
    struct vpwent *vu;

    if (setvpw(file)) {
	while (vu = getvpwent()) {
	    if (is_valias)
		printf("%-16s\t%s\n", vu->v_name, vu->v_pwd);
	    else
		puts(vu->v_name);
	}
	endvpw();
    }
    exit(0);
} /* list */


/*
 * yes() tells us if the user typed YES or (anything else)
 */
int
yes(char *question, ...)
{
    char ans[80];
    char *p, *q;
    va_list ap;

    if (f_flag) return 1;

    va_start(ap, question);
    vprintf(question, ap);
    va_end(ap);
    printf(" (yes/no)? ");
    fflush(stdout);

    fgets(ans, sizeof ans, stdin);

    for (p = ans; *p && isspace(*p); ++p)
	;
    for (q = p; isalnum(*q); ++q)
	;
    *q = 0;

    return (strcasecmp(p, "y") == 0) || (strcasecmp(p, "yes") == 0);
} /* yes */


/*
 * delete() a user or alias
 */
void
delete(int disable, char *victim)
{
    struct vpwent *vu;
    char *file = is_valias ? "alias" : "passwd";
    char *title = is_valias ? "alias" : "user";
    char *mailbox = concatenate(_PATH_VMAIL, vdomain, "/", victim, 0);
    int hasmbox = (access(mailbox, F_OK) == 0);
    int deleted = 0;
    struct vlock *pass;


    if ( (pass = vpwlock(vetcdir, file)) == 0)
	err("Could not lock %s file: %s", file, strerror(errno));

    if (setvpw(pass->filename)) {
	while (vu= getvpwent()) {
	    if (strcasecmp(vu->v_name, victim) == 0) {
		if (disable && !is_valias) {
		    vu->v_pwd = "*";
		    putvpwent(pass->fd, vu);
		    printf("%s disabled\n", vu->v_name);
		}
		else if (yes("really delete %s %s", title, victim))
		    deleted = 1;
		else {
		    vpwrollback(pass);
		    exit(0);
		}
	    }
	    else
		putvpwent(pass->fd, vu);
	}
	endvpw();
	if (vpwcommit(pass) != 0)
	    err("could not save changes to %s: %s", title, strerror(errno));
	if ( deleted && (!is_valias) && (access(mailbox, F_OK) == 0) ) {
	    if (yes("%s has mail. Delete it", victim))
		unlink(mailbox);
	}
    }
    else
	err("Cannot open %s file for %s.%s",
	    title, vdomain, please);
} /* delete */


static jmp_buf abandon;

static void
intr(int sig)
{
    alarm(0);
    puts("\n*Timed Out*");
    longjmp(abandon,1);
}


/*
 * readalias() get an alias value
 */
char *
readalias(int argc, char **argv)
{
    static char ans[_PASSWORD_LEN];
    char *word;
    char *ret;

    if (argc > 1)
	word = argv[1];
    else if (isatty(0)) {

	alarm(60);
	if (setjmp(abandon) == 0) {
	    printf("New alias:");fflush(stdout);
	    ret = fgets(ans, sizeof ans, stdin);
	}
	alarm(0);

	if (ret == 0) return 0;

	word = strtok(ret, "\r\n");
    }
    else {
	fprintf(stderr, "Can't change alias without user input");
	return 0;
    }
    return (word[0]) ? word : 0;
} /* readalias */


/* From local_passwd.c (C) Regents of Univ. of California blah blah */
static unsigned char itoa64[] =         /* 0 ... 63 => ascii - 64 */
"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

static void
to64(register char *s, register long v, register int n)
{
    while (--n >= 0) {
	*s++ = itoa64[v&0x3f];
	v >>= 6;
    }
}


/*
 * readpass() get, validate, and encrypt a password
 */
char *
readpass(int argc, char **argv)
{
    static char ans[_PASSWORD_LEN+1];
    char *pw;
    char salt[3];
    int retry;
    char *vfy;

    if (argc > 1)
	pw = argv[1];
    else if (isatty(0)) {
	for (retry=0; retry < 6; ++retry) {

	    alarm(30);
	    if (setjmp(abandon) != 0)
		return 0;
	    pw = getpass("New password:");
	    alarm(0);
	    if (pw == 0 || *pw == 0)
		return 0;

	    strcpy(ans, pw);

	    alarm(30);
	    if (setjmp(abandon) != 0)
		return 0;
	    vfy = getpass("Retype new password");
	    alarm(0);

	    if (vfy == 0)	/* EOF */
		return 0;

	    if (strcasecmp(vfy, ans) != 0)
		printf("Mismatch; try again, EOF to quit.\n");
	    else {
		pw = ans;
		break;
	    }
	}
	if (retry >= 6) {
	    fprintf(stderr, "Too many failures -- giving up\n");
	    return 0;
	}
    }
    else {
	fprintf(stderr, "Can't change password without user input");
	return 0;
    }
    srandom( (int) time( (time_t*) 0 ) );
    to64( &salt[0], random(), 2 );
    return crypt( pw, salt );
} /* readpass */


/*
 * chpass() change a password or alias file
 *
 * TODO: clean up locking -- I shouldn't do user input inside a
 *       critical section.
 */
void
chpass(int argc, char **argv)
{
    struct vlock *pass;
    struct vpwent *vu;
    struct vpwent vnew;
    char *msg;
    char *file = is_valias ? "alias" : "passwd";
    int done = 0;

    if (argc < 1 || argc > 2)
	usage();

    signal(SIGALRM, intr);

    if ( (pass = vpwlock(vetcdir, file)) == 0)
	err("could not lock %s file: %s", file, strerror(errno));

    if (setvpw(pass->filename)) {
	while (vu = getvpwent()) {
	    if (strcasecmp(vu->v_name, argv[0]) == 0 && !done) {
		msg = is_valias ? "Changed alias for %s\n"
				: "Changed password for %s\n";
		vu->v_pwd = (is_valias ? readalias : readpass) (argc,argv);
		if (vu->v_pwd == 0) {
		    vpwrollback(pass);
		    exit(1);
		}
		done = 1;
	    }
	    putvpwent(pass->fd, vu);
	}
	endvpw();
    }

    if (!done)
	if (a_flag) {
	    msg = is_valias ? "Added alias %s\n"
			    : "Added user %s\n";
	    vnew.v_name = argv[0];
	    vnew.v_pwd  = (is_valias ? readalias : readpass) (argc,argv);
	    if (vnew.v_pwd == 0) {
		vpwrollback(pass);
		exit(1);
	    }
	    putvpwent(pass->fd, &vnew);
	}
	else {
	    vpwrollback(pass);
	    err("No %s %s", is_valias ? "alias" : "user", argv[0]);
	}
    printf(msg, argv[0]);
    if (vpwcommit(pass) != 0)
	err("could not save changes to %s file: %s",
		is_valias ? "alias" : "password", strerror(errno));
    exit(0);
}


main(int argc, char **argv)
{
    int opt, i;
    char *account = 0;

    pgm = basename(argv[0]);
    is_valias = (strcasecmp(pgm, "valias") == 0);
    if (getuid() == 0) {
	is_root = 1;
	please = "";
	if (argc > 1) {
	    /* if called as root, you MUST specify an account before
	     * the regular arguments and options
	     */
	    account = argv[1];
	    optind = 2;
	}
    }

    opterr = 0;
    while  ((opt = getopt(argc, argv, "adflx")) != EOF)
	switch (opt) {
	case 'a':   a_flag = 1;
		    break;
	case 'x':   if (is_valias) usage();
		    x_flag = 1;
		    break;
	case 'l':   l_flag = 1;
		    break;
	case 'f':   f_flag = 1;
		    break;
	case 'd':   d_flag = 1;
		    break;
	default:    usage();
	}

    if (d_flag + l_flag + a_flag + x_flag > 1)
	usage();

    argc -= optind;
    argv += optind;

    vdomain = getvdomain(account);
    vetcdir = concatenate(_PATH_VETC, vdomain, 0);

    if (l_flag) {
	list();
	exit(0);
    }
    else if (x_flag || d_flag) {
	if (argc == 1) {
	    delete(x_flag, argv[0]);
	    exit(0);
	}
	else
	    usage();
    }
    chpass(argc, argv);
}
