/*-
 * $Id: expn.c,v 1.12 1995/08/11 17:42:50 dupuy Exp $
 *
 *	To compile with DNS support, cc -O -o expn expn.c -lresolv
 *	To compile without DNS support, cc -O -o expn -DNO_DOMAINS expn.c
 * 	To use, expn user@host
 *		exit codes: 0 = valid address
 *			    1 = system error (address may be valid)
 *			    2 = SMTP server error (address may be valid)
 *			    3 = invalid user
 *			    4 = invalid host
 *
 * This started out as mverify, by Jeff Beadles <jeff@quark.WV.TEK.COM>
 * with a couple of lines of autobounce.c by Pete Shipley <shipley@berkley.edu>
 *
 * I decided to enhance it for use in verifying mailing lists, requiring better
 * support for all the varieties of RFC-821 mailers out there in the swamps.
 * So I added support for MX records and real handling of RFC-821 result codes.
 *
 * If you think there's anything left to add, please send it to me, and I'll
 * see about adding it.
 *
 * Alexander Dupuy <dupuy@cs.columbia.edu>
 *
 */

#ifdef BSD
#include <strings.h>
#else
#include <string.h>
#include <memory.h>
#define index strchr
#define bcopy(b1,b2,len) memcpy(b2,b1,len)
#endif

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#ifndef NO_DOMAINS
#include <arpa/nameser.h>
#include <resolv.h>
#ifdef NO_HERROR
extern void herror ();
#endif
#endif
#include <stdio.h>
#include <signal.h>

#define MAXMXHOSTS 20

#define SYSERR 1
#define SMTPERR 2
#define BADUSER 3
#define BADHOST 4

char *fgets ();
char *index ();
struct hostent *gethostbyname ();

main (argc, argv)
    int argc;
    char **argv;
{
    int smtpfd;
    FILE *fin;				/* separate stdio streams to allow */
    FILE *fout;				/* mixed reads and writes w/o seeks */
    char buffer[2048];			/* stdio line buffer */

    char *user;				/* pieces of argv */
    char *orighost;
    char *host;

    char *command;			/* for expn/vrfy smtp commands */
    int exitstatus;

    struct hostent *hp;			/* inet networking */
    struct servent *sp;
    struct sockaddr_in server;

    char *bp;				/* utility infielder */

#ifndef NO_DOMAINS
    int n;				/* utility index */

    HEADER *dhp;			/* DNS reply header and fields*/
    int ancount;
    int qdcount;

    union				/* DNS reply and pointers */
    {
	HEADER hdr;
	u_char bytes[PACKETSZ];
    }     answer;
    u_char *cp;
    u_char *eom;

    char hostbuf[PACKETSZ];		/* hostnames buffer */
    int buflen;

    u_short pref;
    u_short type;

    char *hosts[MAXMXHOSTS];		/* arrays for multiple MX hosts */
    u_short prefs[MAXMXHOSTS];
    u_long bestpref;
    int besthost;			/* indexes into arrays */
    int i;
#endif

    if (argc != 2)
    {
	(void) fprintf (stderr, "Usage: %s user@host\n", argv[0]);
	return (SYSERR);
    }

    /*
     * Prevent "Broken Pipe" exit if an SMTP server dies while we are talking
     * to it.
     */

    signal (SIGPIPE, SIG_IGN);

    user = argv[1];
    if ((orighost = index (user, '@')) == 0)
	host = "localhost";
    else
    {
	*orighost = '\0';
	orighost++;
	host = orighost;
    }

    server.sin_family = AF_INET;

    /*
     * Get the smtp port using tcp.
     */

    sp = getservbyname ("smtp", "tcp");
    server.sin_port = sp->s_port;
    if (!server.sin_port)
    {
	(void) fprintf (stderr, "unknown service: smtp/tcp\n");
	return (SYSERR);
    }

#ifdef NO_DOMAINS

    /*
     * Now get the information for the @host part of the address.
     */

    if ((hp = gethostbyname (host)) == 0)
    {
	(void) fprintf (stderr, "%s: unknown host\n", host);
	return (BADHOST);
    }

#else

    besthost = -1;			/* don't retry MX */

    /*
     * Check MX records for the @host part of the address.
     */

    n = res_search (host, C_IN, T_MX, answer.bytes, sizeof (answer));
    if (n < 0)
	goto punt;

    /* find first satisfactory answer */

    dhp = &answer.hdr;
    cp = answer.bytes + sizeof (HEADER);
    eom = answer.bytes + n;
    for (qdcount = ntohs (dhp->qdcount); qdcount--; cp += n + QFIXEDSZ)
	if ((n = dn_skipname (cp, eom)) < 0)
	    goto punt;

    /* copy MX hosts and preferences into arrays */

    buflen = sizeof (hostbuf);
    bp = hostbuf;
    ancount = ntohs (dhp->ancount);
    i = 0;
    while (--ancount >= 0 && cp < eom && i < MAXMXHOSTS)
    {
	if ((n = dn_expand (&answer.hdr, eom, cp, bp, buflen)) < 0)
	    break;
	cp += n;
	GETSHORT (type, cp);
	cp += sizeof (u_short) + sizeof (u_long);
	GETSHORT (n, cp);
	if (type != T_MX)
	{
	    cp += n;
	    continue;
	}
	GETSHORT (pref, cp);
	if ((n = dn_expand (&answer.hdr, eom, cp, bp, buflen)) < 0)
	    break;
	cp += n;

	prefs[i] = pref;
	hosts[i] = bp;
	i++;

	n = strlen (bp) + 1;
	bp += n;
	buflen -= n;
    }
    hosts[i] = 0;			/* terminate host array */

  nextmxhost:

    i = 0;
    n = 0;
    bestpref = 65536;

    while (hosts[i] && i < MAXMXHOSTS)
    {
	if (prefs[i] < 65535)		/* count untried MX hosts */
	    n++;
	if (prefs[i] < bestpref)
	{
	    bestpref = prefs[i];
	    host = hosts[i];
	    besthost = i;
	}
	i++;
    }
    if (n <= 1)				/* don't retry if this is last host */
	besthost = -1;
    
  punt:

    /*
     * Now get the information for the @host part of the address.
     */

    if ((hp = gethostbyname (host)) == 0)
    {
        /*
	 * This is *not* a typo.  If gethostbyname fails, it leaves an error
	 * code in h_errno, not in errno, and does not use the error codes
	 * defined in <errno.h>.  So if the linker can't find herror() in your
	 * resolver library, use the herror() implementation at the end of this
	 * file.  Don't just change this to perror().
	 */
	herror (host);
	if (besthost >= 0)
	{
	    prefs[besthost] = 65535;
	    goto nextmxhost;
	}
	if (h_errno == TRY_AGAIN)
	    return (SYSERR);

	return (BADHOST);
    }

#endif

  reconnect:

    (void) bcopy (hp->h_addr, (char *) &server.sin_addr, hp->h_length);

    /*
     * One socket please...
     */

    if ((smtpfd = socket (AF_INET, SOCK_STREAM, 0)) < 0)
    {
	perror ("socket");
	return (SYSERR);
    }

    /*
     * Connecting to the socket might make things go a little easier...  :-)
     */

    while (connect (smtpfd, (struct sockaddr *) &server, sizeof (server)) < 0)
    {
#ifdef h_addr
	if (*++hp->h_addr_list)
	{
	    (void) close (smtpfd);
	    goto reconnect;
	}
#endif
	perror (host);
	if (besthost >= 0)
	{
	    prefs[besthost] = 65535;
	    (void) close (smtpfd);
	    goto nextmxhost;
	}
	return (SYSERR);
    }

    /*
     * Change them to streams 'cause I like 'em better.
     */

    fout = fdopen (smtpfd, "w");
    fin = fdopen (smtpfd, "r");

    /*
     * The format of SMTP reply codes is a three digit number followed by
     * either space or '-' minus.  The minus code indicates a multi-line
     * response, so we keep reading lines until we see one with a space.
     *
     * Reply codes beginning with 2 are positive, and can be ignored
     * (although 251 indicates a non-local user).
     * Reply codes beginning with 4 indicate transient errors.
     * Reply codes beginning with 5 indicate permanent errors
     * (although 551 will return a forward-path for a non-local user).
     *
     * Note that RFC-1123 defines a new reply code 252 which indicates that
     * the user cannot be verified, but the server will attempt forwarding.
     * This is pretty much the same as 251 except there is less assurance that
     * the address is valid.
     */

    /*
     * Wait for the smtp mailer to answer.  It should greet us with a 220 code
     */

    while (fgets (buffer, sizeof (buffer) - 1, fin) != NULL)
    {
	if (buffer[3] == '-')
	    continue;

	if (!strncmp (buffer, "220 ", 4))
	    break;
	else				/* some kind of mailer error */
	{
	    (void) fputs (buffer, stderr);
	    (void) putc ('\n', stderr);	/* fputs doesn't add a newline */
	    exitstatus = SMTPERR;
	    goto done;
	}
    }

    exitstatus = -1;			/* until we get positive response */

    if (orighost)
	command = "EXPN %s@%s\r\n";	/* RFC-821 requires CRLF */
    else
	command = "EXPN %s\r\n";

    /*
     * Now that we have the mailer's attention, tell it to 'verify' the user.
     * We have four ways of trying this, using EXPN or VRFY, and using @host
     * or not.  We will try all of them (only two if no @host specified).
     */

    (void) fprintf (fout, command, user, orighost);
    if (fflush (fout) == EOF)
    {
	perror (host);
#ifdef h_addr
	if (*++hp->h_addr_list)
	{
	    (void) fclose (fin);
	    (void) fclose (fout);
	    goto reconnect;
	}
#endif
	if (besthost >= 0)
	{
	    prefs[besthost] = 65535;
	    (void) fclose (fin);
	    (void) fclose (fout);
	    goto nextmxhost;
	}
	return (SYSERR);
    }

    /*
     * Now just go into a loop reading responses from the mailer.
     */

    while (fgets (buffer, sizeof (buffer) - 1, fin) != NULL)
    {
	/*
	 * Zap ^M from the lines.
	 */
	if ((bp = index (buffer, '\r')) != 0)
	    *bp = '\0';

	if (!strncmp (buffer, "250", 3))
	{
	    (void) puts (buffer + 4);	/* delete 250- code */
	}
	else if (!strncmp (buffer, "251", 3)) /* only given to VRFY */
	{
	    (void) puts (buffer);	/* unusual - leave 251 code in */
	}
	else if (!strncmp (buffer, "252", 3)) /* only given to VRFY */
	{				/* reconstruct the user name we sent */
	    if (orighost)
		(void) printf ("<%s@%s>\n", user, orighost);
	    else
		(void) puts (user);

	    (void) fputs (buffer, stderr);
	    (void) putc ('\n', stderr);	/* print warning to stderr */
	}
	else if (!strncmp (buffer, "551", 3)) /* only given to VRFY */
	{
	    (void) puts (buffer);	/* unusual - leave 551 code in */

	    (void) fputs (buffer, stderr);
	    (void) putc ('\n', stderr);	/* print warning to stderr */
	}
	else if (buffer[0] == '5' && (buffer[1] == '0' || buffer[2] == '0'))
	{				/* 50x syntax err; 550 list vs. user */
	    /*
	     * We try first with EXPN, then VRFY, since EXPN gives more info.
	     * A server which accepts EXPN but not VRFY can give misleading
	     * results for bad addresses.  The only server I have run across
	     * (@lists.psi.com) accepts VRFY but not EXPN, so this is okay.
	     * If some server does accept EXPN but not VRFY, then change this
	     * to retry only for 50x codes.  RFC-821 says that a VRFY on a list
	     * or an EXPN on a user is allowed to return 550, but I've never
	     * seen that happen.
	     */

	    switch (exitstatus)
	    {
	      case -1:			/* first failure; try without @host */
		if (orighost)		/* if we haven't already done so */
		{
		    command = "EXPN %s\r\n";
		    exitstatus = -2;
		    break;
		}
		/* fall through if !orighost */

	      case -2:			/* EXPN doesn't work; try VRFY */
		if (orighost)
		{
		    command = "VRFY %s@%s\r\n";
		    exitstatus = -3;
		    break;
		}
		/* fall through if !orighost */

	      case -3:
		command = "VRFY %s\r\n";
		exitstatus = -4;
		break;

	      case -4:
		if (buffer[1] == '5')	/* 55x implies bad username */
		    exitstatus = BADUSER;
		else
		    exitstatus = SMTPERR;

		(void) fputs (buffer, stderr);
		(void) putc ('\n', stderr);
		goto done;		/* we've had enough by now */
	    }
	    
	    (void) fprintf (fout, command, user, orighost);
	    if (fflush (fout) == EOF)
	    {
	        perror (host);
#ifdef h_addr
		if (*++hp->h_addr_list)
		{
		    (void) fclose (fin);
		    (void) fclose (fout);
		    goto reconnect;
		}
#endif
		if (besthost >= 0)
		{
		    prefs[besthost] = 65535;
		    (void) fclose (fin);
		    (void) fclose (fout);
		    goto nextmxhost;
		}
		return (SYSERR);
	    }
	    continue;
	}
	else
	{
	    (void) fputs (buffer, stderr);	/* failed bigtime */
	    (void) putc ('\n', stderr);	/* fputs doesn't add a newline */
	}
	
	if (buffer[3] == ' ')		/* no more responses coming */
	{
	    if (buffer[0] == '2')
		exitstatus = 0;		/* the last try worked */
	    else if (buffer[0] == '5' && buffer[1] == '5')
		exitstatus = BADUSER;	/* 55x implies bad username */
	    else
		exitstatus = SMTPERR;	/* SMTP error */
	    goto done;
	}
    }

    if (exitstatus < 0)
    {
        perror (host);
#ifdef h_addr
	if (*++hp->h_addr_list)
	{
	    (void) fclose (fin);
	    (void) fclose (fout);
	    goto reconnect;
	}
#endif
	if (besthost >= 0)
	{
	    prefs[besthost] = 65535;
	    (void) fclose (fin);
	    (void) fclose (fout);
	    goto nextmxhost;
	}
	exitstatus = SYSERR;		/* unexpected EOF */
    }

  done:

    /*
     * Close the SMTP connection gracefully.
     */

    (void) fputs ("QUIT\r\n", fout);
    (void) fflush (fout);
    (void) fclose (fout);
    (void) fclose (fin);

    if (fflush (stdout))		/* in case we couldn't write output */
	exitstatus = SYSERR;

    /*
     * And leave this nice program.
     */

    return (exitstatus);
}

#ifndef NO_DOMAINS
/*
 * I've been told that some resolver libraries don't provide herror().  If this
 * is the case, you probably have a very old and broken version of the resolver
 * libraries, and you should get a current copy of the BIND distribution and
 * install it.  If you really can't be bothered, though, you can use this
 * version of herror() by defining NO_HERROR.
 */

#ifdef NO_HERROR

char *h_errlist[] =
{
    "Error 0",
    "Unknown host",			/* 1 HOST_NOT_FOUND */
    "Host name lookup failure",		/* 2 TRY_AGAIN */
    "Unknown server error",		/* 3 NO_RECOVERY */
    "No address associated with name",	/* 4 NO_ADDRESS */
};

extern int h_errno;

void herror (message)
    char *message;
{
    if (h_errno < 0 || h_errno > 4)
        fprintf (stderr, "%s: Unknown error\n", message);
    else
        fprintf (stderr, "%s: %s\n", message, h_errlist[h_errno]);
}
#endif

#endif

