/*
 * pam_mail module
 *
 * Written by Andrew Morgan <morgan@linux.kernel.org> 1996/3/11
 * $HOME additions by David Kinchlea <kinch@kinch.ark.com> 1997/1/7
 * mailhash additions by Chris Adams <cadams@ro.com> 1998/7/11
 */

#include "config.h"

#include <ctype.h>
#include <pwd.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <dirent.h>
#include <errno.h>

#ifdef HAVE_PATHS_H
#include <paths.h>
#endif

#define DEFAULT_MAIL_DIRECTORY    PAM_PATH_MAILDIR
#define MAIL_FILE_FORMAT          "%s%s/%s"
#define MAIL_ENV_NAME             "MAIL"
#define MAIL_ENV_FORMAT           MAIL_ENV_NAME "=%s"

#include <security/pam_modules.h>
#include <security/_pam_macros.h>
#include <security/pam_modutil.h>
#include <security/pam_ext.h>
#include "pam_inline.h"
#include "pam_i18n.h"

/* argument parsing */

#define PAM_DEBUG_ARG		0x0001
#define PAM_NO_LOGIN		0x0002
#define PAM_LOGOUT_TOO		0x0004
#define PAM_NEW_MAIL_DIR	0x0010
#define PAM_MAIL_SILENT		0x0020
#define PAM_NO_ENV		0x0040
#define PAM_HOME_MAIL		0x0100
#define PAM_EMPTY_TOO		0x0200
#define PAM_STANDARD_MAIL	0x0400
#define PAM_QUIET_MAIL		0x1000

#define HAVE_NEW_MAIL           0x1
#define HAVE_OLD_MAIL           0x2
#define HAVE_NO_MAIL            0x3
#define HAVE_MAIL               0x4

static int
_pam_parse (const pam_handle_t *pamh, int flags, int argc,
	    const char **argv, const char **maildir, size_t *hashcount)
{
    int ctrl=0;

    if (flags & PAM_SILENT) {
	ctrl |= PAM_MAIL_SILENT;
    }

    *hashcount = 0;

    /* step through arguments */
    for (; argc-- > 0; ++argv) {
	const char *str;

	/* generic options */

	if (!strcmp(*argv,"debug"))
	    ctrl |= PAM_DEBUG_ARG;
	else if (!strcmp(*argv,"quiet"))
	    ctrl |= PAM_QUIET_MAIL;
	else if (!strcmp(*argv,"standard"))
	    ctrl |= PAM_STANDARD_MAIL | PAM_EMPTY_TOO;
	else if ((str = pam_str_skip_prefix(*argv, "dir=")) != NULL) {
	    *maildir = str;
	    if (**maildir != '\0') {
		D(("new mail directory: %s", *maildir));
		ctrl |= PAM_NEW_MAIL_DIR;
	    } else {
		pam_syslog(pamh, LOG_ERR,
			   "dir= specification missing argument - ignored");
	    }
	} else if ((str = pam_str_skip_prefix(*argv, "hash=")) != NULL) {
	    char *ep = NULL;
	    *hashcount = strtoul(str,&ep,10);
	    if (!ep) {
		*hashcount = 0;
	    }
	} else if (!strcmp(*argv,"close")) {
	    ctrl |= PAM_LOGOUT_TOO;
	} else if (!strcmp(*argv,"nopen")) {
	    ctrl |= PAM_NO_LOGIN;
	} else if (!strcmp(*argv,"noenv")) {
	    ctrl |= PAM_NO_ENV;
	} else if (!strcmp(*argv,"empty")) {
	    ctrl |= PAM_EMPTY_TOO;
	} else {
	    pam_syslog(pamh, LOG_ERR, "unknown option: %s", *argv);
	}
    }

    if ((*hashcount != 0) && !(ctrl & PAM_NEW_MAIL_DIR)) {
	*maildir = DEFAULT_MAIL_DIRECTORY;
	ctrl |= PAM_NEW_MAIL_DIR;
    }

    return ctrl;
}

static int
get_folder(pam_handle_t *pamh, int ctrl,
	   const char *path_mail, char **folder_p, size_t hashcount,
	   const struct passwd *pwd)
{
    int retval;
    const char *path;
    char *folder = NULL;

    if (ctrl & PAM_NEW_MAIL_DIR) {
	path = path_mail;
	if (*path == '~') {	/* support for $HOME delivery */
	    /*
	     * "~/xxx" and "~xxx" are treated as same
	     */
	    if (!*++path || (*path == '/' && !*++path)) {
		pam_syslog(pamh, LOG_ERR,
			   "badly formed mail path [%s]", path_mail);
		retval = PAM_SERVICE_ERR;
		goto get_folder_cleanup;
	    }
	    ctrl |= PAM_HOME_MAIL;
	    if (hashcount != 0) {
		pam_syslog(pamh, LOG_ERR,
			   "cannot do hash= and home directory mail");
	    }
	}
    } else {
	path = DEFAULT_MAIL_DIRECTORY;
    }

    /* put folder together */

    hashcount = hashcount < strlen(pwd->pw_name) ?
      hashcount : strlen(pwd->pw_name);

    retval = PAM_BUF_ERR;
    if (ctrl & PAM_HOME_MAIL) {
	if (asprintf(&folder, MAIL_FILE_FORMAT, pwd->pw_dir, "", path) < 0)
	    goto get_folder_cleanup;
    } else {
	int rc;
	size_t i;
	char *hash;

	if ((hash = malloc(2 * hashcount + 1)) == NULL)
	    goto get_folder_cleanup;

	for (i = 0; i < hashcount; i++) {
	    hash[2 * i] = '/';
	    hash[2 * i + 1] = pwd->pw_name[i];
	}
	hash[2 * i] = '\0';

	rc = asprintf(&folder, MAIL_FILE_FORMAT, path, hash, pwd->pw_name);
	pam_overwrite_string(hash);
	_pam_drop(hash);
	if (rc < 0)
	    goto get_folder_cleanup;
    }
    D(("folder=[%s]", folder));
    retval = PAM_SUCCESS;

    /* tidy up */

  get_folder_cleanup:
    path = NULL;

    *folder_p = folder;
    folder = NULL;

    if (retval == PAM_BUF_ERR)
	pam_syslog(pamh, LOG_CRIT, "out of memory for mail folder");

    return retval;
}

static int
get_mail_status(pam_handle_t *pamh, int ctrl, const char *folder)
{
    int type = 0;
    struct stat mail_st;

    if (stat(folder, &mail_st) < 0)
	return 0;

    if (S_ISDIR(mail_st.st_mode)) {	/* Assume Maildir format */
	int i, save_errno;
	char *dir;
	struct dirent **namelist;

	if (asprintf(&dir, "%s/new", folder) < 0) {
	    pam_syslog(pamh, LOG_CRIT, "out of memory");
	    goto get_mail_status_cleanup;
	}
	i = scandir(dir, &namelist, 0, alphasort);
	save_errno = errno;
	pam_overwrite_string(dir);
	_pam_drop(dir);
	if (i < 0) {
	    type = 0;
	    namelist = NULL;
	    if (save_errno == ENOMEM) {
		pam_syslog(pamh, LOG_CRIT, "out of memory");
		goto get_mail_status_cleanup;
	    }
	}
	type = (i > 2) ? HAVE_NEW_MAIL : 0;
	while (--i >= 0)
	    _pam_drop(namelist[i]);
	_pam_drop(namelist);
	if (type == 0) {
	    if (asprintf(&dir, "%s/cur", folder) < 0) {
		pam_syslog(pamh, LOG_CRIT, "out of memory");
		goto get_mail_status_cleanup;
	    }
	    i = scandir(dir, &namelist, 0, alphasort);
	    save_errno = errno;
	    pam_overwrite_string(dir);
	    _pam_drop(dir);
	    if (i < 0) {
		type = 0;
		namelist = NULL;
		if (save_errno == ENOMEM) {
		    pam_syslog(pamh, LOG_CRIT, "out of memory");
		    goto get_mail_status_cleanup;
		}
	    }
	    if (i > 2)
	        type = HAVE_OLD_MAIL;
	    else
	        type = (ctrl & PAM_EMPTY_TOO) ? HAVE_NO_MAIL : 0;
	    while (--i >= 0)
		_pam_drop(namelist[i]);
	    _pam_drop(namelist);
	}
    } else {
	if (mail_st.st_size > 0) {
	    if (mail_st.st_atime < mail_st.st_mtime)	/* new */
	        type = HAVE_NEW_MAIL;
	    else		/* old */
	        type = (ctrl & PAM_STANDARD_MAIL) ? HAVE_MAIL : HAVE_OLD_MAIL;
	} else if (ctrl & PAM_EMPTY_TOO) {
	    type = HAVE_NO_MAIL;
	} else {
	    type = 0;
	}
    }

  get_mail_status_cleanup:
    pam_overwrite_object(&mail_st);
    D(("user has %d mail in %s folder", type, folder));
    return type;
}

static int
report_mail(pam_handle_t *pamh, int ctrl, int type, const char *folder)
{
    int retval;

    if ((ctrl & PAM_MAIL_SILENT) ||
	((ctrl & PAM_QUIET_MAIL) && type != HAVE_NEW_MAIL))
      {
	D(("keeping quiet"));
	retval = PAM_SUCCESS;
      }
    else
      {
	if (ctrl & PAM_STANDARD_MAIL)
	  switch (type)
	    {
	    case HAVE_NO_MAIL:
	      retval = pam_info (pamh, "%s", _("You do not have any new mail."));
	      break;
	    case HAVE_NEW_MAIL:
	      retval = pam_info (pamh, "%s", _("You have new mail."));
	      break;
	    case HAVE_OLD_MAIL:
	      retval = pam_info (pamh, "%s", _("You have old mail."));
	      break;
	    case HAVE_MAIL:
	    default:
	      retval = pam_info (pamh, "%s", _("You have mail."));
	      break;
	    }
	else
	  switch (type)
	    {
	    case HAVE_NO_MAIL:
	      retval = pam_info (pamh, _("You have no mail in folder %s."),
				 folder);
	      break;
	    case HAVE_NEW_MAIL:
	      retval = pam_info (pamh, _("You have new mail in folder %s."),
				 folder);
	      break;
	    case HAVE_OLD_MAIL:
	      retval = pam_info (pamh, _("You have old mail in folder %s."),
				 folder);
	      break;
	    case HAVE_MAIL:
	    default:
	      retval = pam_info (pamh, _("You have mail in folder %s."),
				 folder);
	      break;
	    }
      }

    D(("returning %s", pam_strerror(pamh, retval)));
    return retval;
}

static int _do_mail(pam_handle_t *, int, int, const char **, int);

/* --- authentication functions --- */

int
pam_sm_authenticate (pam_handle_t *pamh UNUSED, int flags UNUSED,
		     int argc UNUSED, const char **argv UNUSED)
{
    return PAM_IGNORE;
}

/* Checking mail as part of authentication */
int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc,
    const char **argv)
{
    if (!(flags & (PAM_ESTABLISH_CRED|PAM_DELETE_CRED)))
      return PAM_IGNORE;
    return _do_mail(pamh,flags,argc,argv,(flags & PAM_ESTABLISH_CRED));
}

/* --- session management functions --- */

int pam_sm_close_session(pam_handle_t *pamh,int flags,int argc
			 ,const char **argv)
{
    return _do_mail(pamh,flags,argc,argv,0);
}

/* Checking mail as part of the session management */
int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc,
    const char **argv)
{
    return _do_mail(pamh,flags,argc,argv,1);
}


/* --- The Beaf (Tm) --- */

static int _do_mail(pam_handle_t *pamh, int flags, int argc,
    const char **argv, int est)
{
    int retval, ctrl, type;
    size_t hashcount;
    char *folder = NULL;
    const char *user;
    const char *path_mail = NULL;
    const struct passwd *pwd = NULL;

    /*
     * this module (un)sets the MAIL environment variable, and checks if
     * the user has any new mail.
     */

    ctrl = _pam_parse(pamh, flags, argc, argv, &path_mail, &hashcount);

    retval = pam_get_user(pamh, &user, NULL);
    if (retval != PAM_SUCCESS) {
	pam_syslog(pamh, LOG_NOTICE, "cannot determine user name: %s",
		   pam_strerror(pamh, retval));
	return PAM_USER_UNKNOWN;
    }

    pwd = pam_modutil_getpwnam (pamh, user);
    if (pwd == NULL) {
        pam_syslog(pamh, LOG_NOTICE, "user unknown");
        return PAM_USER_UNKNOWN;
    }

    /* which folder? */

    retval = get_folder(pamh, ctrl, path_mail, &folder, hashcount, pwd);
    if (retval != PAM_SUCCESS) {
	D(("failed to find folder"));
	return retval;
    }

    /* set the MAIL variable? */

    if (!(ctrl & PAM_NO_ENV) && est) {
	char *tmp;

	if (asprintf(&tmp, MAIL_ENV_FORMAT, folder) < 0) {
	    pam_syslog(pamh, LOG_CRIT,
		       "no memory for " MAIL_ENV_NAME " variable");
	    retval = PAM_BUF_ERR;
	    goto do_mail_cleanup;
	}
	D(("setting env: %s", tmp));
	retval = pam_putenv(pamh, tmp);
	pam_overwrite_string(tmp);
	_pam_drop(tmp);
	if (retval != PAM_SUCCESS) {
	    pam_syslog(pamh, LOG_CRIT,
		       "unable to set " MAIL_ENV_NAME " variable");
	    retval = PAM_BUF_ERR;
	    goto do_mail_cleanup;
	}
    } else {
	D(("not setting " MAIL_ENV_NAME " variable"));
    }

    /*
     * OK. we've got the mail folder... what about its status?
     */

    if ((est && !(ctrl & PAM_NO_LOGIN))
	|| (!est && (ctrl & PAM_LOGOUT_TOO))) {
	PAM_MODUTIL_DEF_PRIVS(privs);

	if (pam_modutil_drop_priv(pamh, &privs, pwd)) {
	  retval = PAM_SESSION_ERR;
	  goto do_mail_cleanup;
	} else {
	  type = get_mail_status(pamh, ctrl, folder);
	  if (pam_modutil_regain_priv(pamh, &privs)) {
	    retval = PAM_SESSION_ERR;
	    goto do_mail_cleanup;
	  }
	}

	if (type != 0) {
	    retval = report_mail(pamh, ctrl, type, folder);
	    type = 0;
	}
    }

    /* Delete environment variable? */
    if ( ! est && ! (ctrl & PAM_NO_ENV) )
	(void) pam_putenv(pamh, MAIL_ENV_NAME);

  do_mail_cleanup:
    pam_overwrite_string(folder);
    _pam_drop(folder);

    /* indicate success or failure */

    return retval;
}

/* end of module definition */
