/*
** aliasbr.c -- new aliasing mechanism
**
** This code is Copyright (c) 2002, by the authors of nmh.  See the
** COPYRIGHT file in the root directory of the nmh distribution for
** complete copyright information.
*/

#include <h/mh.h>
#include <h/aliasbr.h>
#include <h/addrsbr.h>
#include <h/utils.h>
#include <grp.h>
#include <pwd.h>
#include <ctype.h>

static int akvis;
static char *akerrst;

struct aka *akahead = NULL;
struct aka *akatail = NULL;

struct home *homehead = NULL;
struct home *hometail = NULL;

/*
** prototypes
*/
int alias(char *);
int akvisible(void);
void init_pw(void);
char *akresult(struct aka *);
char *akvalue(char *);
char *akerror(int);

static char *akval(struct aka *, char *);
static int aleq(char *, char *);
static char *scanp(unsigned char *);
static char *getp(char *);
static char *seekp(char *, char *, char **);
static int addfile(struct aka *, char *);
static int addgroup(struct aka *, char *);
static int addmember(struct aka *, char *);
static char *getalias(char *);
static void add_aka(struct aka *, char *);
static struct aka *akalloc(char *);
static struct home *hmalloc(struct passwd *);
struct home *seek_home(char *);


/* Do mh alias substitution on 's' and return the results. */
char *
akvalue(char *s)
{
	char *v;

	akvis = -1;
	v = akval(akahead, s);
	if (akvis == -1)
		akvis = 0;
	return v;
}


int
akvisible(void)
{
	return akvis;
}


char *
akresult(struct aka *ak)
{
	char *cp = NULL, *dp, *pp;
	struct adr *ad;

	for (ad = ak->ak_addr; ad; ad = ad->ad_next) {
		pp = ad->ad_local ? akval(ak->ak_next, ad->ad_text)
			: mh_xstrdup(ad->ad_text);

		if (cp) {
			dp = cp;
			cp = concat(cp, ",", pp, NULL);
			mh_free0(&dp);
			mh_free0(&pp);
		} else
			cp = pp;
	}

	if (akvis == -1)
		akvis = ak->ak_visible;
	return cp;
}


static char *
akval(struct aka *ak, char *s)
{
	if (!s)
		return s;  /* XXX */

	for (; ak; ak = ak->ak_next) {
		if (aleq (s, ak->ak_name)) {
			return akresult (ak);
		} else if (strchr (s, ':')) {
			/*
			** The first address in a blind list will contain the
			** alias name, so try to match, but just with just the
			** address (not including the list name).  If there's a
			** match, then replace the alias part with its
			** expansion.
			*/

			char *name = getname(s);
			char *cp = NULL;

			if (name) {
				/*
				** s is of the form "Blind list: address".  If address
				** is an alias, expand it.
				*/
				struct mailname *mp = getm(name, NULL, 0, AD_NAME, NULL);

				if (mp	&&  mp->m_ingrp) {
					char *gname = add (mp->m_gname, NULL);

					if (gname  &&  aleq(name, ak->ak_name)) {
						/* Will leak cp. */
						cp = concat (gname, akresult (ak), NULL);
						free(gname);
					}
				}
			mnfree(mp);
			}
			/* Need to flush getname after use. */
			while (getname("")) continue;

			if (cp) {
				return cp;
			}
		}
	}

	return mh_xstrdup(s);
}


static int
aleq(char *string, char *aliasent)
{
	char c;

	while ((c = *string++))
		if (*aliasent == '*')
			return 1;
		else if ((c | 040) != (*aliasent | 040))
			return 0;
		else
			aliasent++;

	return (*aliasent == 0 || *aliasent == '*');
}


/*
** file needs to be absolute or relative to cwd
*/
int
alias(char *file)
{
	int i;
	char *bp, *cp, *pp;
	char lc, *ap;
	struct aka *ak = NULL;
	FILE *fp;

	if ((fp = fopen(file, "r")) == NULL) {
		akerrst = file;
		return AK_NOFILE;
	}

	while (vfgets(fp, &ap) == OK) {
		bp = ap;
		switch (*(pp = scanp(bp))) {
		case '<':  /* recurse a level */
			if (!*(cp = getp(pp + 1))) {
				akerrst = "'<' without alias-file";
				fclose(fp);
				return AK_ERROR;
			}
			if ((i = alias(cp)) != AK_OK) {
				fclose(fp);
				return i;
			}

		case ':':  /* comment */
		case ';':
		case '#':
		case 0:
			continue;
		}

		akerrst = bp;
		if (!*(cp = seekp(pp, &lc, &ap))) {
			fclose(fp);
			return AK_ERROR;
		}
		if (!(ak = akalloc(cp))) {
			fclose(fp);
			return AK_LIMIT;
		}
		switch (lc) {
		case ':':
			ak->ak_visible = 0;
			break;

		case ';':
			ak->ak_visible = 1;
			break;

		default:
			fclose(fp);
			return AK_ERROR;
		}

		switch (*(pp = scanp(ap))) {
		case 0:  /* EOL */
			fclose(fp);
			return AK_ERROR;

		case '<':  /* read values from file */
			if (!*(cp = getp(pp + 1))) {
				fclose(fp);
				return AK_ERROR;
			}
			if (!addfile(ak, cp)) {
				fclose(fp);
				return AK_NOFILE;
			}
			break;

		case '=':  /* UNIX group */
			if (!*(cp = getp(pp + 1))) {
				fclose(fp);
				return AK_ERROR;
			}
			if (!addgroup(ak, cp)) {
				fclose(fp);
				return AK_NOGROUP;
			}
			break;

		case '+':  /* UNIX group members */
			if (!*(cp = getp(pp + 1))) {
				fclose(fp);
				return AK_ERROR;
			}
			if (!addmember(ak, cp)) {
				fclose(fp);
				return AK_NOGROUP;
			}
			break;

		default:  /* list */
			while ((cp = getalias(pp)))
				add_aka(ak, cp);
			break;
		}
	}

	fclose(fp);
	return AK_OK;
}


char *
akerror(int i)
{
	static char buffer[BUFSIZ];

	switch (i) {
	case AK_NOFILE:
		snprintf(buffer, sizeof(buffer), "unable to read '%s'",
				akerrst);
		break;

	case AK_ERROR:
		snprintf(buffer, sizeof(buffer), "error in line '%s'",
				akerrst);
		break;

	case AK_LIMIT:
		snprintf(buffer, sizeof(buffer), "out of memory while on '%s'",
				akerrst);
		break;

	case AK_NOGROUP:
		snprintf(buffer, sizeof(buffer), "no such group as '%s'",
				akerrst);
		break;

	default:
		snprintf(buffer, sizeof(buffer), "unknown error (%d)", i);
		break;
	}

	return buffer;
}


static char *
scanp(unsigned char *p)
{
	while (isspace(*p))
		p++;
	return p;
}


static char *
getp(char *p)
{
	unsigned char *cp = scanp(p);

	p = cp;
	while (!isspace(*cp) && *cp)
		cp++;
	*cp = 0;

	return p;
}


static char *
seekp(char *p, char *c, char **a)
{
	unsigned char *cp;

	p = cp = scanp(p);
	while (!isspace(*cp) && *cp && *cp != ':' && *cp != ';')
		cp++;
	*c = *cp;
	*cp++ = 0;
	*a = cp;

	return p;
}


static int
addfile(struct aka *ak, char *file)
{
	char *cp;
	char buffer[BUFSIZ];
	FILE *fp;

	if (!(fp = fopen(etcpath(file), "r"))) {
		akerrst = file;
		return 0;
	}

	while (fgets(buffer, sizeof buffer, fp))
		while ((cp = getalias(buffer)))
			add_aka(ak, cp);

	fclose(fp);
	return 1;
}


static int
addgroup(struct aka *ak, char *grp)
{
	char *gp;
	struct group *gr = getgrnam(grp);
	struct home *hm = NULL;

	if (!gr)
		gr = getgrgid(atoi(grp));
	if (!gr) {
		akerrst = grp;
		return 0;
	}

	while ((gp = *gr->gr_mem++))
	{
		struct passwd *pw;
		for (hm = homehead; hm; hm = hm->h_next)
			if (strcmp(hm->h_name, gp)==0) {
				add_aka(ak, hm->h_name);
				break;
			}
		if ((pw = getpwnam(gp))) {
			hmalloc(pw);
			add_aka(ak, gp);
		}
	}

	return 1;
}


static int
addmember(struct aka *ak, char *grp)
{
	gid_t gid;
	struct group *gr = getgrnam(grp);
	struct home *hm = NULL;

	if (gr)
		gid = gr->gr_gid;
	else {
		gid = atoi(grp);
		gr = getgrgid(gid);
	}
	if (!gr) {
		akerrst = grp;
		return 0;
	}

	init_pw();

	for (hm = homehead; hm; hm = hm->h_next)
		if (hm->h_gid == gid)
			add_aka(ak, hm->h_name);

	return 1;
}


static char *
getalias(char *addrs)
{
	unsigned char *pp, *qp;
	static char *cp = NULL;

	if (cp == NULL)
		cp = addrs;
	else
		if (*cp == 0)
			return (cp = NULL);

	for (pp = cp; isspace(*pp); pp++)
		continue;
	if (*pp == 0)
		return (cp = NULL);
	for (qp = pp; *qp != 0 && *qp != ','; qp++)
		continue;
	if (*qp == ',')
		*qp++ = 0;
	for (cp = qp, qp--; qp > pp; qp--)
		if (*qp != 0) {
			if (isspace(*qp))
				*qp = 0;
			else
				break;
		}

	return pp;
}


static void
add_aka(struct aka *ak, char *pp)
{
	struct adr *ad, *ld;

	for (ad = ak->ak_addr, ld = NULL; ad; ld = ad, ad = ad->ad_next)
		if (strcmp(pp, ad->ad_text)==0)
			return;

	ad = mh_xcalloc(1, sizeof(*ad));
	ad->ad_text = mh_xstrdup(pp);
	ad->ad_local = strchr(pp, '@') == NULL;
	ad->ad_next = NULL;
	if (ak->ak_addr)
		ld->ad_next = ad;
	else
		ak->ak_addr = ad;
}


void
init_pw(void)
{
	struct passwd *pw;
	static int init = 0;

	if (!init) {
		/* read the passwd database and build a list */
		setpwent();
		while ((pw = getpwent())) {
			if (!hmalloc(pw)) {
				break;
			}
		}
		endpwent();

		init++;  /* now we're initialized */
	}
}


static struct aka *
akalloc(char *id)
{
	struct aka *p;

	p = mh_xcalloc(1, sizeof(*p));

	p->ak_name = mh_xstrdup(id);
	p->ak_visible = 0;
	p->ak_addr = NULL;
	p->ak_next = NULL;
	if (akatail != NULL)
		akatail->ak_next = p;
	if (akahead == NULL)
		akahead = p;
	akatail = p;

	return p;
}


static struct home *
hmalloc(struct passwd *pw)
{
	struct home *p;

	p = mh_xcalloc(1, sizeof(*p));

	p->h_name = mh_xstrdup(pw->pw_name);
	p->h_uid = pw->pw_uid;
	p->h_gid = pw->pw_gid;
	p->h_home = mh_xstrdup(pw->pw_dir);
	p->h_shell = mh_xstrdup(pw->pw_shell);
	p->h_ngrps = 0;
	p->h_next = NULL;
	/* append to end */
	if (!homehead)
		homehead = p;
	if (hometail)
		hometail->h_next = p;
	hometail = p;

	return p;
}


struct home *
seek_home(char *name)
{
	struct home *hp;
	struct passwd *pw;
	char lname[32];
	unsigned char *c;
	char *c1;

	for (hp = homehead; hp; hp = hp->h_next)
		if (!mh_strcasecmp(name, hp->h_name))
			return hp;

	/*
	** The only place where there might be problems.
	** This assumes that ALL usernames are kept in lowercase.
	*/
	for (c = name, c1 = lname; *c && (c1 - lname < (int)sizeof(lname) - 1);
			c++, c1++) {
		if (isalpha(*c) && isupper(*c))
			*c1 = tolower(*c);
		else
			*c1 = *c;
	}
	*c1 = '\0';
	if ((pw = getpwnam(lname)))
		return(hmalloc(pw));

	return NULL;
}
