/*
 * Copyright (c) 1990 Regents of the University of Michigan.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms are permitted
 * provided that this notice is preserved and that due credit is given
 * to the University of Michigan at Ann Arbor. The name of the University
 * may not be used to endorse or promote products derived from this
 * software without specific prior written permission. This software
 * is provided ``as is'' without express or implied warranty.
 */

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <memory.h>
#include <sys/types.h>
#include <sys/time.h>
#include <syslog.h>
#include <sys/param.h>
#include <sys/resource.h>
#include <sys/wait.h>
#include <sysexits.h>
#include "portable.h"

#include "lber.h"
#include "ldap.h"
#include "ldapconfig.h"

#define USER		0x01
#define GROUP_ERRORS	0x02
#define GROUP_REQUEST	0x04
#define GROUP_MEMBERS	0x08
#define GROUP_OWNER	0x10

#define ERROR		"error"
#define ERRORS		"errors"
#define REQUEST		"request"
#define REQUESTS	"requests"
#define MEMBERS		"members"
#define OWNER		"owner"
#define OWNERS		"owners"

LDAP	*ld;
char	*vacationhost = NULL;
char	*errorsfrom = NULL;
char	*mailfrom = NULL;
char	*host = NULL;
char	*ldaphost = LDAPHOST;
int	hostlen = 0;
int	debug;

typedef struct errs {
	int		e_code;
#define E_USERUNKNOWN		1
#define E_AMBIGUOUS		2
#define E_NOEMAIL		3
#define E_NOREQUEST		4
#define E_NOERRORS		5
#define E_BADMEMBER		6
#define E_JOINMEMBERNOEMAIL	7
#define E_MEMBERNOEMAIL		8
#define E_LOOP			9
#define E_NOMEMBERS		10
#define	E_NOOWNER		11
#define E_GROUPUNKNOWN		12
	char		*e_addr;
	union {
		char		*e_u_loop;
		LDAPMessage	*e_u_msg;
	} e_union;
#define e_msg	e_union.e_u_msg
#define e_loop	e_union.e_u_loop
} Error;

typedef struct groupto {
	char	*g_dn;
	char	*g_errorsto;
	char	**g_members;
} Group;

typedef struct baseinfo {
	char	*b_dn;		/* dn to start searching at */
	char	b_rdnpref;	/* give rdn's preference when searching? */
	int	b_search;	/* ORed with the type of thing the address */
				/*  looks like (USER, GROUP_ERRORS, etc.)  */
				/*  to see if this should be searched	   */
	char	*b_filter[3];	/* filter to apply - name substituted for %s */
				/* (up to three of them) */
} Base;

Base	base[] = 
	{ "ou=People, o=University of Michigan, c=US",
		0, USER,
		"uid=%s", "cn=%s", NULL,
	  "ou=System Groups, ou=Groups, o=University of Michigan, c=US",
		1, 0xff,
		"(&(cn=%s)(associatedDomain=%h))", NULL, NULL,
	  "ou=User Groups, ou=Groups, o=University of Michigan, c=US",
		1, 0xff,
		"(&(cn=%s)(associatedDomain=%h))", NULL, NULL,
	  NULL
	};

char	*sendmailargs[] = { MAIL500_SENDMAIL, "-oMrX.500", "-odi", "-oi", "-f", NULL, NULL };

static char	*attrs[] = { "objectClass", "title", "postaladdress",
			"telephoneNumber", "mail", "description", "owner",
			"errorsTo", "rfc822ErrorsTo", "requestsTo",
			"rfc822RequestsTo", "joinable", "cn", "member",
			"moderator", "onVacation", "uid",
			"suppressNoEmailError", NULL };

static do_address();
static do_group();
static do_group_members();
static send_message();
static send_errors();
static do_noemail();
static do_ambiguous();
static add_to();
static isgroup();
static add_error();
static add_group();
static unbind_and_exit();
static group_loop();
static send_group();
static has_attributes();
static char **get_attributes_mail_dn();
static char *canonical();

main (argc, argv)
int	argc;
char	**argv;
{
	char		*myname;
	char		**tolist;
	Error		*errlist;
	Group		*togroups;
	int		numto, ngroups, numerr, nargs;
	int		i, j;
	FILE		*fp;
	extern int	optind, errno;
	extern char	*optarg;

	if ( (myname = strrchr( argv[0], '/' )) == NULL )
		myname = strdup( argv[0] );
	else
		myname = strdup( myname + 1 );

#ifdef LOG_MAIL
	openlog( myname, OPENLOG_OPTIONS, LOG_MAIL );
#else
	openlog( myname, OPENLOG_OPTIONS );
#endif

	while ( (i = getopt( argc, argv, "d:f:h:l:m:v:" )) != EOF ) {
		switch( i ) {
		case 'd':	/* turn on debugging */
			debug = atoi( optarg );
			break;

		case 'f':	/* who it's from & where errors should go */
			mailfrom = strdup( optarg );
			for ( j = 0; sendmailargs[j] != NULL; j++ ) {
				if ( strcmp( sendmailargs[j], "-f" ) == 0 ) {
					sendmailargs[j+1] = mailfrom;
					break;
				}
			}
			break;

		case 'h':	/* hostname */
			host = strdup( optarg );
			hostlen = strlen(host);
			break;

		case 'l':	/* ldap host */
			ldaphost = strdup( optarg );
			break;

				/* mailer-daemon address - who we should */
		case 'm':	/* say errors come from */
			errorsfrom = strdup( optarg );
			break;

		case 'v':	/* vacation host */
			vacationhost = strdup( optarg );
			break;

		default:
			syslog( LOG_ALERT, "unknown option" );
			break;
		}
	}

	if ( mailfrom == NULL ) {
		syslog( LOG_ALERT, "required argument -f not present" );
		exit( EX_TEMPFAIL );
	}
	if ( errorsfrom == NULL ) {
		syslog( LOG_ALERT, "required argument -m not present" );
		exit( EX_TEMPFAIL );
	}
	if ( host == NULL ) {
		syslog( LOG_ALERT, "required argument -h not present" );
		exit( EX_TEMPFAIL );
	}

	if ( connect_to_x500() != 0 )
		exit( EX_TEMPFAIL );

	setuid( geteuid() );

	if ( debug ) {
		char	buf[1024];
		int	i;

		syslog( LOG_ALERT, "running as %d", geteuid() );
		strcpy( buf, argv[0] );
		for ( i = 1; i < argc; i++ ) {
			strcat( buf, " " );
			strcat( buf, argv[i] );
		}

		syslog( LOG_ALERT, "args: (%s)", buf );
	}

	tolist = NULL;
	numto = 0;
	add_to( &tolist, &numto, sendmailargs );
	nargs = numto;
	ngroups = numerr = 0;
	togroups = NULL;
	errlist = NULL;
	for ( i = optind; i < argc; i++ ) {
		char	*s;
		int	type;

		for ( j = 0; argv[i][j] != '\0'; j++ ) {
			if ( argv[i][j] == '.' || argv[i][j] == '_' )
				argv[i][j] = ' ';
		}

		type = USER;
		if ( (s = strrchr( argv[i], '-' )) != NULL ) {
			s++;

			if ((strcasecmp(s, ERROR) == 0) ||
				(strcasecmp(s, ERRORS) == 0)) {
				type = GROUP_ERRORS;
				*(--s) = '\0';
			} else if ((strcasecmp(s, REQUEST) == 0) ||
				(strcasecmp(s, REQUESTS) == 0)) {
				type = GROUP_REQUEST;
				*(--s) = '\0';
			} else if ( strcasecmp( s, MEMBERS ) == 0 ) {
				type = GROUP_MEMBERS;
				*(--s) = '\0';
			} else if ((strcasecmp(s, OWNER) == 0) ||
				(strcasecmp(s, OWNERS) == 0)) {
				type = GROUP_OWNER;
				*(--s) = '\0';
			}
		}

		do_address( argv[i], &tolist, &numto, &togroups, &ngroups,
		    &errlist, &numerr, type );
	}

	/*
	 * If we have both errors and successful deliveries to make or if
	 * if there are any groups to deliver to, we basically need to read
	 * the message twice.  So, we have to put it in a tmp file.
	 */

	if ( numerr > 0 && numto > nargs || ngroups > 0 ) {
		int	fd;
		char	buf[BUFSIZ];

		umask( 077 );
		if ( (fp = tmpfile()) == NULL ) {
			syslog( LOG_ALERT, "could not open tmp file" );
			unbind_and_exit( EX_TEMPFAIL );
		}

		/* copy the message to a temp file */
		while ( fgets( buf, sizeof(buf), stdin ) != NULL ) {
			if ( fputs( buf, fp ) == EOF ) {
				syslog( LOG_ALERT, "error writing tmpfile" );
				unbind_and_exit( EX_TEMPFAIL );
			}
		}

		if ( dup2( fileno( fp ), 0 ) == -1 ) {
			syslog( LOG_ALERT, "could not dup2 tmpfile" );
			unbind_and_exit( EX_TEMPFAIL );
		}

		fclose( fp );
	}

	/* deal with errors */
	if ( numerr > 0 ) {
		if ( debug ) {
			syslog( LOG_ALERT, "sending errors" );
		}
		(void) rewind( stdin );
		send_errors( errlist, numerr );
	}

	(void) ldap_unbind( ld );

	/* send to groups with errorsTo */
	if ( ngroups > 0 ) {
		if ( debug ) {
			syslog( LOG_ALERT, "sending to groups with errorsto" );
		}
		(void) rewind( stdin );
		send_group( togroups, ngroups );
	}

	/* send to expanded aliases and groups w/o errorsTo */
	if ( numto > nargs ) {
		if ( debug ) {
			syslog( LOG_ALERT, "sending to aliases and groups" );
		}
		(void) rewind( stdin );
		send_message( tolist );
	}

	return( EX_OK );
}

connect_to_x500()
{
	if ( (ld = ldap_open( ldaphost, LDAP_PORT )) == NULL ) {
		syslog( LOG_ALERT, "ldap_open failed" );
		return( -1 );
	}
	ld->ld_sizelimit = MAIL500_MAXAMBIGUOUS;
	ld->ld_deref = LDAP_DEREF_ALWAYS;

	if ( ldap_simple_bind_s( ld, MAIL500_BINDDN, NULL ) != LDAP_SUCCESS ) {
		syslog( LOG_ALERT, "ldap_simple_bind_s failed" );
		return( -1 );
	}

	return( 0 );
}

static
mailcmp( a, b )
    char	*a;
    char	*b;
{
	int	i;

	for ( i = 0; a[i] != '\0'; i++ ) {
		if ( a[i] != b[i] ) {
			switch ( a[i] ) {
			case ' ':
			case '.':
			case '_':
				if ( b[i] == ' ' || b[i] == '.' || b[i] == '_' )
					break;
				return( 1 );

			default:
				return( 1 );
			}
		}
	}

	return( 0 );
}

static
do_address( name, to, nto, togroups, ngroups, err, nerr, type )
    char	*name;
    char	***to;
    int		*nto;
    Group	**togroups;
    int		*ngroups;
    Error	**err;
    int		*nerr;
    int		type;
{
	int		rc, b, f, match;
	LDAPMessage	*e, *res;
	struct timeval	timeout;
	char		*dn;
	char		filter[1024];
	char		realfilter[1024];
	char		**mail, **onvacation = NULL, **uid = NULL;

	/*
	 * Look up the name in X.500, add the appropriate addresses found
	 * to the to list, or to the err list in case of error.  Groups are
	 * handled by the do_group routine, individuals are handled here.
	 * When looking up name, we follow the bases hierarchy, looking
	 * in base[0] first, then base[1], etc.  For each base, there is
	 * a set of search filters to try, in order.  If something goes
	 * wrong here trying to contact X.500, we exit with EX_TEMPFAIL.
	 * If the b_rdnpref flag is set, then we give preference to entries
	 * that matched name because it's their rdn, otherwise not.
	 */

	timeout.tv_sec = MAIL500_TIMEOUT;
	timeout.tv_usec = 0;
	for ( b = 0, match = 0; !match && base[b].b_dn != NULL; b++ ) {
		if ( ! (base[b].b_search & type) ) {
			continue;
		}
		for ( f = 0; base[b].b_filter[f] != NULL; f++ ) {
			char	*format, *p, *s, *d;
			char	*argv[3];
			int	argc;

			for ( argc = 0; argc < 3; argc++ ) {
				argv[argc] = NULL;
			}

			format = strdup( base[b].b_filter[f] );
			for ( argc = 0, p = format; *p; p++ ) {
				if ( *p == '%' ) {
					switch ( *++p ) {
					case 's':	/* %s is the name */
						argv[argc] = name;
						break;

					case 'h':	/* %h is the host */
						*p = 's';
						argv[argc] = host;
						break;

					default:
						syslog( LOG_ALERT,
						    "unknown format %c", *p );
						break;
					}

					argc++;
				}
			}

			/* three names ought to do... */
			sprintf( filter, format, argv[0], argv[1], argv[2] );
			free( format );
			for ( s = filter, d = realfilter; *s; s++, d++ ) {
				if ( *s == '*' ) {
					*d++ = '\\';
				}
				*d = *s;
			}
			*d = '\0';

			res = NULL;
			rc = ldap_search_st( ld, base[b].b_dn,
			    LDAP_SCOPE_SUBTREE, realfilter, attrs, 0, &timeout,
			    &res );

			/* some other trouble - try again later */
			if ( rc != LDAP_SUCCESS &&
			    rc != LDAP_SIZELIMIT_EXCEEDED ) {
				syslog( LOG_ALERT, "return 0x%x from X.500",
				    rc );
				unbind_and_exit( EX_TEMPFAIL );
			}

			if ( (match = ldap_count_entries( ld, res )) != 0 )
				break;

			ldap_msgfree( res );
		}

		if ( match )
			break;
	}

	/* trouble - try again later */
	if ( match == -1 ) {
		syslog( LOG_ALERT, "error parsing result from X.500" );
		unbind_and_exit( EX_TEMPFAIL );
	}

	/* no matches - bounce with user unknown */
	if ( match == 0 ) {
		if ( type == USER ) {
			add_error( err, nerr, E_USERUNKNOWN, name, NULLMSG );
		} else {
			add_error( err, nerr, E_GROUPUNKNOWN, name, NULLMSG );
		}
		return;
	}

	/* more than one match - bounce with ambiguous user? */
	if ( match > 1 ) {
		LDAPMessage	*next, *tmpres = NULL;
		char		*dn;
		char		**xdn;

		/* not giving rdn preference - bounce with ambiguous user */
		if ( base[b].b_rdnpref == 0 ) {
			add_error( err, nerr, E_AMBIGUOUS, name, res );
			return;
		}

		/*
		 * giving rdn preference - see if any entries were matched
		 * because of their rdn.  If so, collect them to deal with
		 * later (== 1 we deliver, > 1 we bounce).
		 */

		for ( e = ldap_first_entry( ld, res ); e != NULL; e = next ) {
			next = ldap_next_entry( ld, e );
			dn = ldap_get_dn( ld, e );
			xdn = ldap_explode_dn( dn, 1 );

			/* XXX bad, but how else can we do it? XXX */
			if ( strcasecmp( xdn[0], name ) == 0 ) {
				ldap_delete_result_entry( &res, e );
				ldap_add_result_entry( &tmpres, e );
			}

			ldap_value_free( xdn );
			free( dn );
		}

		/* nothing matched by rdn - go ahead and bounce */
		if ( tmpres == NULL ) {
			add_error( err, nerr, E_AMBIGUOUS, name, res );
			return;

		/* more than one matched by rdn - bounce with rdn matches */
		} else if ( (match = ldap_count_entries( ld, tmpres )) > 1 ) {
			add_error( err, nerr, E_AMBIGUOUS, name, tmpres );
			return;

		/* trouble... */
		} else if ( match < 0 ) {
			syslog( LOG_ALERT, "error parsing result from X.500" );
			unbind_and_exit( EX_TEMPFAIL );
		}

		/* otherwise one matched by rdn - send to it */
		ldap_msgfree( res );
		res = tmpres;
	}

	/*
	 * if we get this far, it means that we found a single match for
	 * name.  for a user, we deliver to the mail attribute or bounce
	 * with address and phone if no mail attr.  for a group, we
	 * deliver to all members or bounce to rfc822ErrorsTo if no members.
	 */

	/* trouble */
	if ( (e = ldap_first_entry( ld, res )) == NULL ) {
		syslog( LOG_ALERT, "error parsing entry from X.500" );
		unbind_and_exit( EX_TEMPFAIL );
	}

	dn = ldap_get_dn( ld, e );

	if ( type == GROUP_ERRORS ) {
		/* sent to group-errors - resend to [rfc822]ErrorsTo attr */
		do_group_errors( e, dn, to, nto, err, nerr );

	} else if ( type == GROUP_REQUEST ) {
		/* sent to group-request - resend to [rfc822]RequestsTo attr */
		do_group_request( e, dn, to, nto, err, nerr );

	} else if ( type == GROUP_MEMBERS ) {
		/* sent to group-members - expand */
		do_group_members( e, dn, to, nto, togroups, ngroups, err,
		    nerr );

	} else if ( type == GROUP_OWNER ) {
		/* sent to group-owner - resend to owner attr */
		do_group_owner( e, dn, to, nto, err, nerr );

	} else if ( isgroup( e ) ) {
		/* 
		 * sent to group - resend from [rfc822]ErrorsTo if it's there,
		 * otherwise, expand the group
		 */

		do_group( e, dn, to, nto, togroups, ngroups, err, nerr );

		ldap_msgfree( res );

	} else {
		/*
		 * sent to user - mail attribute => add it to the to list,
		 * otherwise bounce
		 */
		if ( (mail = ldap_get_values( ld, e, "mail" )) != NULL ) {
			char	buf[1024];
			char	*h;
			int	i, j;

			/* try to detect simple mail loops */
			sprintf( buf, "%s@%s", name, host );
			for ( i = 0; mail[i] != NULL; i++ ) {
				/*
				 * address is the same as the one we're
				 * sending to - mail loop.  syslog the
				 * problem, bounce a message back to the
				 * sender (who else?), and delete the bogus
				 * addr from the list.
				 */

				if ( (h = strchr( mail[i], '@' )) != NULL ) {
					h++;
					if ( strcasecmp( h, host ) == 0 ) {
						syslog( LOG_ALERT,
					    "potential loop detected (%s)",
						    mail[i] );
					}
				}

				if ( mailcmp( buf, mail[i] ) == 0 ) {
					syslog( LOG_ALERT,
					    "loop detected (%s)", mail[i] );

					/* remove the bogus address */
					for ( j = i; mail[j] != NULL; j++ ) {
						mail[j] = mail[j+1];
					}
				}
			}
			if ( mail[0] != NULL ) {
				add_to( to, nto, mail );
			} else {
				add_error( err, nerr, E_NOEMAIL, name, res );
			}

			ldap_value_free( mail );
		} else {
			add_error( err, nerr, E_NOEMAIL, name, res );
		}

		/*
		 * If the user is on vacation, send a copy of the mail to
		 * the vacation server.  The address is constructed from
		 * the vacationhost (set in a command line argument) and
		 * the uid (XXX this should be more general XXX).
		 */

		if ( vacationhost != NULL && (onvacation = ldap_get_values( ld,
		    e, "onVacation" )) != NULL && strcasecmp( onvacation[0],
		    "TRUE" ) == 0 ) {
			char	buf[1024];
			char	*vaddr[2];

			if ( (uid = ldap_get_values( ld, e, "uid" )) != NULL ) {
				sprintf( buf, "%s@%s", uid[0], vacationhost );

				vaddr[0] = buf;
				vaddr[1] = NULL;

				add_to( to, nto, vaddr );
			} else {
				syslog( LOG_ALERT,
				    "user without a uid on vacation (%s)",
				    name );
			}
		}
	}

	if ( onvacation != NULL ) {
		ldap_value_free( onvacation );
	}
	if ( uid != NULL ) {
		ldap_value_free( uid );
	}
	free( dn );

	return;
}

static
do_group( e, dn, to, nto, togroups, ngroups, err, nerr )
    LDAPMessage	*e;
    char	*dn;
    char	***to;
    int		*nto;
    Group	**togroups;
    int		*ngroups;
    Error	**err;
    int		*nerr;
{
	int	i;
	char	**moderator;

	/*
	 * If this group has an rfc822ErrorsTo attribute, we need to
	 * arrange for errors involving this group to go there, not
	 * to the sender.  Since sendmail only has the concept of a
	 * single sender, we arrange for errors to go to groupname-errors,
	 * which we then handle specially when (if) it comes back to us
	 * by expanding to all the rfc822ErrorsTo addresses.  If it has no
	 * rfc822ErrorsTo attribute, we call do_group_members() to expand
	 * the group.
	 */

	if ( group_loop( dn ) ) {
		return( -1 );
	}

	/*
	 * check for moderated groups - if the group has a moderator
	 * attribute, we check to see if the from address is one of
	 * the moderator values.  if so, continue on.  if not, arrange
	 * to send the mail to the moderator(s).  need to do this before
	 * we change the from below.
	 */

	if ( (moderator = ldap_get_values( ld, e, "moderator" )) != NULL ) {
		/* check if it came from any of the group's moderators */
		for ( i = 0; moderator[i] != NULL; i++ ) {
			if ( strcasecmp( moderator[i], mailfrom ) == 0 )
				break;
		}

		/* not from the moderator? */
		if ( moderator[i] == NULL ) {
			add_to( to, nto, moderator );
			ldap_value_free( moderator );

			return( 0 );
		}
		/* else from the moderator - fall through and deliver it */
	}

	if ( has_attributes( e, "rfc822ErrorsTo", "errorsTo" ) ) {
		add_group( dn, togroups, ngroups );

		return( 0 );
	}

	do_group_members( e, dn, to, nto, togroups, ngroups, err, nerr );

	return( 0 );
}

/* ARGSUSED */
static
do_group_members( e, dn, to, nto, togroups, ngroups, err, nerr )
    LDAPMessage	*e;
    char	*dn;
    char	***to;
    int		*nto;
    Group	**togroups;
    int		*ngroups;
    Error	**err;
    int		*nerr;
{
	int		i, rc, anymembers;
	char		*ndn;
	char		**mail, **member, **joinable, **suppress;
	char		filter[1024];
	LDAPMessage	*ee, *res;
	struct timeval	timeout;

	/*
	 * if all has gone according to plan, we've already arranged for
	 * errors to go to the [rfc822]ErrorsTo attributes (if they exist),
	 * so all we have to do here is arrange to send to the
	 * rfc822Mailbox attribute, the member attribute, and anyone who
	 * has joined the group by setting memberOfGroup equal to the
	 * group dn.
	 */

	/* add members in the group itself - mail attribute */
	anymembers = 0;
	if ( (mail = ldap_get_values( ld, e, "mail" )) != NULL ) {
		anymembers = 1;
		add_to( to, nto, mail );

		ldap_value_free( mail );
	}

	/* add members in the group itself - member attribute */
	if ( (member = ldap_get_values( ld, e, "member" )) != NULL ) {
		suppress = ldap_get_values( ld, e, "suppressNoEmailError" );
		anymembers = 1;
		for ( i = 0; member[i] != NULL; i++ ) {
			if ( strcasecmp( dn, member[i] ) == 0 ) {
				syslog( LOG_ALERT, "group (%s) contains itself",
				    dn );
				continue;
			}
			add_member( dn, member[i], to, nto, togroups,
			    ngroups, err, nerr, suppress );
		}

		if ( suppress ) {
			ldap_value_free( suppress );
		}
		ldap_value_free( member );
	}

	/* add members who have joined by setting memberOfGroup */
	if ( (joinable = ldap_get_values( ld, e, "joinable" )) != NULL ) {
		if ( strcasecmp( joinable[0], "FALSE" ) == 0 ) {
			if ( ! anymembers ) {
				add_error( err, nerr, E_NOMEMBERS, dn,
				    NULLMSG );
			}

			ldap_value_free( joinable );
			return;
		}
		ldap_value_free( joinable );

		sprintf( filter, "(memberOfGroup=%s)", dn );

		timeout.tv_sec = MAIL500_TIMEOUT;
		timeout.tv_usec = 0;

		/* for each subtree to look in... */
		ld->ld_sizelimit = MAIL500_MAXGROUPMEMBERS;
		for ( i = 0; base[i].b_dn != NULL; i++ ) {
			/* find entries that have joined this group... */
			rc = ldap_search_st( ld, base[i].b_dn,
			    LDAP_SCOPE_SUBTREE, filter, attrs, 0, &timeout,
			    &res );

			if ( rc == LDAP_SIZELIMIT_EXCEEDED ||
			    rc == LDAP_TIMELIMIT_EXCEEDED ) {
				syslog( LOG_ALERT,
				    "group search limit exceeded %d", rc );
				unbind_and_exit( EX_TEMPFAIL );
			}

			if ( rc != LDAP_SUCCESS ) {
				syslog( LOG_ALERT, "group search return 0x%x",
				    rc );
				unbind_and_exit( EX_TEMPFAIL );
			}

			/* for each entry that has joined... */
			for ( ee = ldap_first_entry( ld, res ); ee != NULL;
			    ee = ldap_next_entry( ld, ee ) ) {
				anymembers = 1;
				if ( isgroup( ee ) ) {
					ndn = ldap_get_dn( ld, ee );

					if ( do_group( e, ndn, to, nto,
					    togroups, ngroups, err, nerr )
					    == -1 ) {
						syslog( LOG_ALERT,
						    "group loop (%s) (%s)",
						    dn, ndn );
					}

					free( ndn );

					continue;
				}

				/* add them to the to list */
				if ( (mail = ldap_get_values( ld, ee, "mail" ))
				    != NULL ) {
					add_to( to, nto, mail );

					ldap_value_free( mail );

				/* else generate a bounce */
				} else {
					ndn = ldap_get_dn( ld, ee );

					add_error( err, nerr,
					    E_JOINMEMBERNOEMAIL, ndn, NULLMSG );

					free( ndn );
				}
			}

			ldap_msgfree( res );
		}
		ld->ld_sizelimit = MAIL500_MAXAMBIGUOUS;
	}

	if ( ! anymembers ) {
		add_error( err, nerr, E_NOMEMBERS, dn, NULLMSG );
	}

	return;
}

add_member( gdn, dn, to, nto, togroups, ngroups, err, nerr, suppress )
    char	*gdn;
    char	*dn;
    char	***to;
    int		*nto;
    Group	**togroups;
    int		*ngroups;
    Error	**err;
    int		*nerr;
    char	**suppress;
{
	char		*ndn;
	char		**mail;
	int		i, rc;
	LDAPMessage	*res, *e;
	struct timeval	timeout;

	timeout.tv_sec = MAIL500_TIMEOUT;
	timeout.tv_usec = 0;
	if ( (rc = ldap_search_st( ld, dn, LDAP_SCOPE_BASE, "(objectclass=*)",
	    attrs, 0, &timeout, &res )) != LDAP_SUCCESS ) {
		if ( rc == LDAP_NO_SUCH_OBJECT ) {
			add_error( err, nerr, E_BADMEMBER, dn, NULLMSG );

			return;
		} else {
			syslog( LOG_ALERT, "member search return 0x%x", rc );

			unbind_and_exit( EX_TEMPFAIL );
		}
	}

	if ( (e = ldap_first_entry( ld, res )) == NULL ) {
		syslog( LOG_ALERT, "member search error parsing entry" );

		unbind_and_exit( EX_TEMPFAIL );
	}
	ndn = ldap_get_dn( ld, e );

	/* allow groups within groups */
	if ( isgroup( e ) ) {
		if ( do_group( e, ndn, to, nto, togroups, ngroups, err, nerr )
		    == -1 ) {
			syslog( LOG_ALERT, "group loop (%s) (%s)", gdn, ndn );
		}

		free( ndn );

		return;
	}

	/* send to the member's mail attribute */
	if ( (mail = ldap_get_values( ld, e, "mail" )) != NULL ) {
		add_to( to, nto, mail );

		ldap_value_free( mail );

	/* else generate a bounce */
	} else {
		if ( suppress == NULL || strcasecmp( suppress[0], "FALSE" )
		    == 0 ) {
			add_error( err, nerr, E_MEMBERNOEMAIL, ndn, NULLMSG );
		}
	}

	free( ndn );

	return;
}

do_group_request( e, dn, to, nto, err, nerr )
    LDAPMessage	*e;
    char	*dn;
    char	***to;
    int		*nto;
    Error	**err;
    int		*nerr;
{
	char		**requeststo;

	if ( (requeststo = get_attributes_mail_dn( e, "rfc822RequestsTo",
	    "requestsTo" )) != NULL ) {
		add_to( to, nto, requeststo );

		ldap_value_free( requeststo );
	} else {
		add_error( err, nerr, E_NOREQUEST, dn, NULLMSG );
	}

	return;
}

do_group_errors( e, dn, to, nto, err, nerr )
    LDAPMessage	*e;
    char	*dn;
    char	***to;
    int		*nto;
    Error	**err;
    int		*nerr;
{
	char		**errorsto;

	if ( (errorsto = get_attributes_mail_dn( e, "rfc822ErrorsTo",
	    "errorsTo" )) != NULL ) {
		add_to( to, nto, errorsto );

		ldap_value_free( errorsto );
	} else {
		add_error( err, nerr, E_NOERRORS, dn, NULLMSG );
	}

	return;
}

do_group_owner( e, dn, to, nto, err, nerr )
    LDAPMessage	*e;
    char	*dn;
    char	***to;
    int		*nto;
    Error	**err;
    int		*nerr;
{
	char		**owner;

	if ( (owner = get_attributes_mail_dn( e, "", "owner" )) != NULL ) {
		add_to( to, nto, owner );
		ldap_value_free( owner );
	} else {
		add_error( err, nerr, E_NOOWNER, dn, NULLMSG );
	}
	return;
}

static
send_message( to )
    char	**to;
{
	int	pid;
#ifndef USE_WAITPID
	WAITSTATUSTYPE	status;
#endif

	if ( debug ) {
		char	buf[1024];
		int	i;

		strcpy( buf, to[0] );
		for ( i = 1; to[i] != NULL; i++ ) {
			strcat( buf, " " );
			strcat( buf, to[i] );
		}

		syslog( LOG_ALERT, "send_message execing sendmail: (%s)", buf );
	}

	/* parent */
	if ( pid = fork() ) {
#ifdef USE_WAITPID
		waitpid( pid, (int *) NULL, 0 );
#else
		wait4( pid, &status, WAIT_FLAGS, 0 );
#endif
	/* child */
	} else {
		/* to includes sendmailargs */
		execv( MAIL500_SENDMAIL, to );

		syslog( LOG_ALERT, "execv failed" );

		exit( EX_TEMPFAIL );
	}
}

static
send_group( group, ngroup )
    Group	*group;
    int		ngroup;
{
	int	i, pid;
	char	**argv;
	int	argc;
	char	*iargv[7];
#ifndef USE_WAITPID
	WAITSTATUSTYPE	status;
#endif

	for ( i = 0; i < ngroup; i++ ) {
		(void) rewind( stdin );

		iargv[0] = MAIL500_SENDMAIL;
		iargv[1] = "-f";
		iargv[2] = group[i].g_errorsto;
		iargv[3] = "-oMrX.500";
		iargv[4] = "-odi";
		iargv[5] = "-oi";
		iargv[6] = NULL;

		argv = NULL;
		argc = 0;
		add_to( &argv, &argc, iargv );
		add_to( &argv, &argc, group[i].g_members );

		if ( debug ) {
			char	buf[1024];
			int	i;

			strcpy( buf, argv[0] );
			for ( i = 1; i < argc; i++ ) {
				strcat( buf, " " );
				strcat( buf, argv[i] );
			}

			syslog( LOG_ALERT, "execing sendmail: (%s)", buf );
		}

		/* parent */
		if ( pid = fork() ) {
#ifdef USE_WAITPID
			waitpid( pid, (int *) NULL, 0 );
#else
			wait4( pid, &status, WAIT_FLAGS, 0 );
#endif
		/* child */
		} else {
			execv( MAIL500_SENDMAIL, argv );

			syslog( LOG_ALERT, "execv failed" );

			exit( EX_TEMPFAIL );
		}
	}

	return;
}

static
send_errors( err, nerr )
    Error	*err;
    int		nerr;
{
	int	pid, i, namelen;
	FILE	*fp;
	int	fd[2];
	char	*argv[8];
	char	buf[1024];
#ifndef USE_WAITPID
	WAITSTATUSTYPE	status;
#endif

	argv[0] = MAIL500_SENDMAIL;
	argv[1] = "-oMrX.500";
	argv[2] = "-odi";
	argv[3] = "-oi";
	argv[4] = "-f";
	argv[5] = errorsfrom;
	argv[6] = mailfrom;
	argv[7] = NULL;

	if ( debug ) {
		int	i;

		strcpy( buf, argv[0] );
		for ( i = 1; argv[i] != NULL; i++ ) {
			strcat( buf, " " );
			strcat( buf, argv[i] );
		}

		syslog( LOG_ALERT, "execing sendmail: (%s)", buf );
	}

	if ( pipe( fd ) == -1 ) {
		syslog( LOG_ALERT, "cannot create pipe" );
		exit( EX_TEMPFAIL );
	}

	if ( pid = fork() ) {
		if ( (fp = fdopen( fd[1], "w" )) == NULL ) {
			syslog( LOG_ALERT, "cannot fdopen pipe" );
			exit( EX_TEMPFAIL );
		}

		fprintf( fp, "To: %s\n", mailfrom );
		fprintf( fp, "From: %s\n", errorsfrom );
		fprintf( fp, "Subject: undeliverable mail\n" );
		fprintf( fp, "\n" );
		fprintf( fp, "The following errors occurred when trying to deliver the attached mail:\n" );
		for ( i = 0; i < nerr; i++ ) {
			namelen = strlen( err[i].e_addr );
			fprintf( fp, "\n" );

			switch ( err[i].e_code ) {
			case E_USERUNKNOWN:
				fprintf( fp, "%s: User unknown\n", err[i].e_addr );
				break;

			case E_GROUPUNKNOWN:
				fprintf( fp, "%s: Group unknown\n", err[i].e_addr );
				break;

			case E_BADMEMBER:
				fprintf( fp, "%s: Group member does not exist\n",
				    err[i].e_addr );
				fprintf( fp, "This could be because the distinguished name of the person has changed\n" );
				fprintf( fp, "If this is the case, the problem can be solved by removing and\n" );
				fprintf( fp, "then re-adding the person to the group.\n" );
				break;

			case E_NOREQUEST:
				fprintf( fp, "%s: Group exists but has no request address\n",
				    err[i].e_addr );
				break;

			case E_NOERRORS:
				fprintf( fp, "%s: Group exists but has no errors-to address\n",
				    err[i].e_addr );
				break;

			case E_NOOWNER:
				fprintf( fp, "%s: Group exists but has no owner\n",
				    err[i].e_addr );
				break;

			case E_AMBIGUOUS:
				do_ambiguous( fp, &err[i], namelen );
				break;

			case E_NOEMAIL:
				do_noemail( fp, &err[i], namelen );
				break;

			case E_MEMBERNOEMAIL:
				fprintf( fp, "%s: Group member exists but does not have an email address\n",
				    err[i].e_addr );
				break;

			case E_JOINMEMBERNOEMAIL:
				fprintf( fp, "%s: User has joined group but does not have an email address\n",
				    err[i].e_addr );
				break;

			case E_LOOP:
				fprintf( fp, "%s: User has created a mail loop by adding address %s to their X.500 entry\n",
				    err[i].e_addr, err[i].e_loop );
				break;

			case E_NOMEMBERS:
				fprintf( fp, "%s: Group has no members\n",
				    err[i].e_addr );
				break;

			default:
				syslog( LOG_ALERT, "unknown error %d", err[i].e_code );
				unbind_and_exit( EX_TEMPFAIL );
				break;
			}
		}

		fprintf( fp, "\n------- The original message sent:\n\n" );

		while ( fgets( buf, sizeof(buf), stdin ) != NULL ) {
			fputs( buf, fp );
		}
		fclose( fp );

#ifdef USE_WAITPID
		waitpid( pid, (int *) NULL, 0 );
#else
		wait4( pid, &status, WAIT_FLAGS, 0 );
#endif
	} else {
		dup2( fd[0], 0 );

		execv( MAIL500_SENDMAIL, argv );

		syslog( LOG_ALERT, "execv failed" );

		exit( EX_TEMPFAIL );
	}

	return;
}

static
do_noemail( fp, err, namelen )
    FILE	*fp;
    Error	*err;
    int		namelen;
{
	int		i, last;
	char		*dn, *rdn;
	char		**ufn, **vals;

	fprintf(fp, "%s: User has no email address registered.\n",
	    err->e_addr );
	fprintf( fp, "%*s  Name, title, postal address and phone for '%s':\n\n",
	    namelen, " ", err->e_addr );

	/* name */
	dn = ldap_get_dn( ld, err->e_msg );
	ufn = ldap_explode_dn( dn, 1 );
	rdn = strdup( ufn[0] );
	if ( strcasecmp( rdn, err->e_addr ) == 0 ) {
		if ( (vals = ldap_get_values( ld, err->e_msg, "cn" ))
		    != NULL ) {
			for ( i = 0; vals[i]; i++ ) {
				last = strlen( vals[i] ) - 1;
				if ( isdigit( vals[i][last] ) ) {
					rdn = strdup( vals[i] );
					break;
				}
			}

			ldap_value_free( vals );
		}
	}
	fprintf( fp, "%*s  %s\n", namelen, " ", rdn );
	free( dn );
	free( rdn );
	ldap_value_free( ufn );

	/* titles or descriptions */
	if ( (vals = ldap_get_values( ld, err->e_msg, "title" )) == NULL &&
	    (vals = ldap_get_values( ld, err->e_msg, "description" ))
	    == NULL ) {
		fprintf( fp, "%*s  No title or description registered\n",
		    namelen, " " );
	} else {
		for ( i = 0; vals[i] != NULL; i++ ) {
			fprintf( fp, "%*s  %s\n", namelen, " ", vals[i] );
		}

		ldap_value_free( vals );
	}

	/* postal address */
	if ( (vals = ldap_get_values( ld, err->e_msg, "postalAddress" ))
	    == NULL ) {
		fprintf( fp, "%*s  No postal address registered\n", namelen,
		    " " );
	} else {
		fprintf( fp, "%*s  ", namelen, " " );
		for ( i = 0; vals[0][i] != '\0'; i++ ) {
			if ( vals[0][i] == '$' ) {
				fprintf( fp, "\n%*s  ", namelen, " " );
				while ( isspace( vals[0][i+1] ) )
					i++;
			} else {
				fprintf( fp, "%c", vals[0][i] );
			}
		}
		fprintf( fp, "\n" );

		ldap_value_free( vals );
	}

	/* telephone number */
	if ( (vals = ldap_get_values( ld, err->e_msg, "telephoneNumber" ))
	    == NULL ) {
		fprintf( fp, "%*s  No phone number registered\n", namelen,
		    " " );
	} else {
		for ( i = 0; vals[i] != NULL; i++ ) {
			fprintf( fp, "%*s  %s\n", namelen, " ", vals[i] );
		}

		ldap_value_free( vals );
	}
}

/* ARGSUSED */
static
do_ambiguous( fp, err, namelen )
    FILE	*fp;
    Error	*err;
    int		namelen;
{
	int		i, last;
	char		*dn, *rdn;
	char		**ufn, **vals;
	LDAPMessage	*e;

	i = ldap_result2error( ld, err->e_msg, 0 );

	fprintf( fp, "%s: Ambiguous user.  %s%d matches found:\n\n",
	    err->e_addr, i == LDAP_SIZELIMIT_EXCEEDED ? "First " : "",
	    ldap_count_entries( ld, err->e_msg ) );

	for ( e = ldap_first_entry( ld, err->e_msg ); e != NULL;
	    e = ldap_next_entry( ld, e ) ) {
		dn = ldap_get_dn( ld, e );
		ufn = ldap_explode_dn( dn, 1 );
		rdn = strdup( ufn[0] );
		if ( strcasecmp( rdn, err->e_addr ) == 0 ) {
			if ( (vals = ldap_get_values( ld, e, "cn" )) != NULL ) {
				for ( i = 0; vals[i]; i++ ) {
					last = strlen( vals[i] ) - 1;
					if ( isdigit( vals[i][last] ) ) {
						rdn = strdup( vals[i] );
						break;
					}
				}

				ldap_value_free( vals );
			}
		}

		if ( isgroup( e ) ) {
			vals = ldap_get_values( ld, e, "description" );
		} else {
			vals = ldap_get_values( ld, e, "title" );
		}

		fprintf( fp, "    %-20s %s\n", rdn, vals ? vals[0] : "" );
		for ( i = 1; vals && vals[i] != NULL; i++ ) {
			fprintf( fp, "                         %s\n", vals[i] );
		}

		free( dn );
		free( rdn );
		ldap_value_free( ufn );
		if ( vals != NULL )
			ldap_value_free( vals );
	}
}

static
count_values( list )
    char	**list;
{
	int	i;

	for ( i = 0; list && list[i] != NULL; i++ )
		;	/* NULL */

	return( i );
}

static
add_to( list, nlist, new )
    char	***list;
    int		*nlist;
    char	**new;
{
	int	i, nnew, oldnlist;

	nnew = count_values( new );

	oldnlist = *nlist;
	if ( *list == NULL || *nlist == 0 ) {
		*list = (char **) malloc( (nnew + 1) * sizeof(char *) );
		*nlist = nnew;
	} else {
		*list = (char **) realloc( *list, *nlist * sizeof(char *) +
		    nnew * sizeof(char *) + sizeof(char *) );
		*nlist += nnew;
	}

	for ( i = 0; i < nnew; i++ )
		(*list)[i + oldnlist] = strdup( new[i] );
	(*list)[*nlist] = NULL;

	return;
}

static
isgroup( e )
    LDAPMessage	*e;
{
	int	i;
	char	**oclist;

	oclist = ldap_get_values( ld, e, "objectClass" );

	for ( i = 0; oclist[i] != NULL; i++ ) {
		if ( strcasecmp( oclist[i], "rfc822MailGroup" ) == 0 ) {
			ldap_value_free( oclist );
			return( 1 );
		}
	}
	ldap_value_free( oclist );

	return( 0 );
}

static
add_error( err, nerr, code, addr, msg )
    Error	**err;
    int		*nerr;
    int		code;
    char	*addr;
    LDAPMessage	*msg;
{
	if ( *nerr == 0 ) {
		*err = (Error *) malloc( sizeof(Error) );
	} else {
		*err = (Error *) realloc( *err, (*nerr + 1) * sizeof(Error) );
	}

	(*err)[*nerr].e_code = code;
	(*err)[*nerr].e_addr = strdup( addr );
	(*err)[*nerr].e_msg = msg;
	(*nerr)++;

	return;
}

static
add_group( dn, list, nlist )
    char	*dn;
    Group	**list;
    int		*nlist;
{
	int	i, namelen;
	char	**ufn;

	for ( i = 0; i < *nlist; i++ ) {
		if ( strcmp( dn, (*list)[i].g_dn ) == 0 ) {
			syslog( LOG_ALERT, "group loop 2 detected (%s)", dn );
			return;
		}
	}

	ufn = ldap_explode_dn( dn, 1 );
	namelen = strlen( ufn[0] );

	if ( *nlist == 0 ) {
		*list = (Group *) malloc( sizeof(Group) );
	} else {
		*list = (Group *) realloc( *list, (*nlist + 1) *
		    sizeof(Group) );
	}

	/* send errors to groupname-errors@host */
	(*list)[*nlist].g_errorsto = (char *) malloc( namelen + sizeof(ERRORS)
	    + hostlen + 2 );
	sprintf( (*list)[*nlist].g_errorsto, "%s-%s@%s", ufn[0], ERRORS, host );
	(void) canonical( (*list)[*nlist].g_errorsto );

	/* send to groupname-members@host - make it a list for send_group */
	(*list)[*nlist].g_members = (char **) malloc( 2 * sizeof(char *) );
	(*list)[*nlist].g_members[0] = (char *) malloc( namelen +
	    sizeof(MEMBERS) + hostlen + 2 );
	sprintf( (*list)[*nlist].g_members[0], "%s-%s@%s", ufn[0], MEMBERS,
	    host );
	(void) canonical( (*list)[*nlist].g_members[0] );
	(*list)[*nlist].g_members[1] = NULL;

	/* save the group's dn so we can check for loops above */
	(*list)[*nlist].g_dn = strdup( dn );

	(*nlist)++;

	ldap_value_free( ufn );

	return;
}

static
unbind_and_exit( rc )
    int	rc;
{
	int	i;

	if ( (i = ldap_unbind( ld )) != LDAP_SUCCESS )
		syslog( LOG_ALERT, "ldap_unbind failed %d\n", i );

	exit( rc );
}

static char *
canonical( s )
    char	*s;
{
	char	*saves = s;

	for ( ; *s != '\0'; s++ ) {
		if ( *s == ' ' )
			*s = '.';
	}

	return( saves );
}

static
group_loop( dn )
    char	*dn;
{
	int		i;
	static char	**groups;
	static int	ngroups;

	for ( i = 0; i < ngroups; i++ ) {
		if ( strcmp( dn, groups[i] ) == 0 )
			return( 1 );
	}

	if ( ngroups == 0 )
		groups = (char **) malloc( sizeof(char *) );
	else
		groups = (char **) realloc( groups,
		    (ngroups + 1) * sizeof(char *) );

	groups[ngroups++] = strdup( dn );

	return( 0 );
}

static
has_attributes( e, attr1, attr2 )
    LDAPMessage	*e;
    char	*attr1;
    char	*attr2;
{
	char	**attr;

	if ( (attr = ldap_get_values( ld, e, attr1 )) != NULL ) {
		ldap_value_free( attr );
		return( 1 );
	}

	if ( (attr = ldap_get_values( ld, e, attr2 )) != NULL ) {
		ldap_value_free( attr );
		return( 1 );
	}

	return( 0 );
}

static char **
get_attributes_mail_dn( e, attr1, attr2 )
    LDAPMessage	*e;
    char	*attr1;
    char	*attr2;		/* this one is dn-valued */
{
	LDAPMessage	*ee, *res;
	char		**vals, **dnlist, **mail, **grname, **graddr;
        char            *dn;
	int		nto = 0, i, rc;
	struct timeval	timeout;

	dn = ldap_get_dn( ld, e );

	vals = ldap_get_values( ld, e, attr1 );
	for ( nto = 0; vals != NULL && vals[nto] != NULL; nto++ )
		;	/* NULL */

	if ( (dnlist = ldap_get_values( ld, e, attr2 )) != NULL ) {
		timeout.tv_sec = MAIL500_TIMEOUT;
		timeout.tv_usec = 0;

		for ( i = 0; dnlist[i] != NULL; i++ ) {
			if ( (rc = ldap_search_st( ld, dnlist[i],
			    LDAP_SCOPE_BASE, "(objectclass=*)", attrs, 0,
			    &timeout, &res )) != LDAP_SUCCESS ) {
				if ( rc != LDAP_NO_SUCH_OBJECT ) {
					unbind_and_exit( EX_TEMPFAIL );
				}

				syslog( LOG_ALERT, "bad (%s) dn (%s)", attr2,
				    dnlist[i] );

				continue;
			}

			if ( (ee = ldap_first_entry( ld, res )) == NULL ) {
				syslog( LOG_ALERT, "error parsing x500 entry" );
				continue;
			}

			if ( isgroup(ee) ) {
				char	*graddr[2];

				grname = ldap_explode_dn( dnlist[i], 1 );

				/* groupname + host + @ + null */
				graddr[0] = (char *) malloc( strlen( grname[0] )
				    + strlen( host ) + 2 );
				graddr[1] = NULL;
				sprintf( graddr[0], "%s@%s", grname[0], host);
				(void) canonical( graddr[0] );

				add_to( &vals, &nto, graddr );

				free( graddr[0] );
				ldap_value_free( grname );
			} else if ( (mail = ldap_get_values( ld, ee, "mail" ))
			    != NULL ) {
				add_to( &vals, &nto, mail );

				ldap_value_free( mail );
			}

			ldap_msgfree( res );
		}
	}

	return( vals );
}
