Add/update copyright notice in all source code files.
[mmh] / mts / smtp / smtp.c
index 025bb96..ca56781 100644 (file)
@@ -1,20 +1,30 @@
-
 /*
  * smtp.c -- nmh SMTP interface
  *
  * $Id$
+ *
+ * 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 "smtp.h"
-#include <zotnet/mts/mts.h>
+#include <h/mts.h>
 #include <signal.h>
 #include <h/signals.h>
 #ifdef MPOP
 #include <errno.h>
 #endif
 
-
+#ifdef CYRUS_SASL
+#include <sasl.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <errno.h>
+#endif /* CYRUS_SASL */
 
 /*
  * This module implements an interface to SendMail very similar
@@ -44,6 +54,8 @@
 #define        TRUE    1
 #define        FALSE   0
 
+#define        NBITS ((sizeof (int)) * 8)
+
 /*
  * these codes must all be different!
  */
 #define        SM_DOT  180
 #define        SM_QUIT  30
 #define        SM_CLOS  10
+#define        SM_AUTH  45
 
 static int sm_addrs = 0;
 static int sm_alarmed = 0;
+static int sm_child = NOTOK;
 static int sm_debug = 0;
 static int sm_nl = TRUE;
 static int sm_verbose = 0;
@@ -72,12 +86,33 @@ static int sm_ispool = 0;
 static char sm_tmpfil[BUFSIZ];
 #endif /* MPOP */
 
+#ifdef CYRUS_SASL
+/*
+ * Some globals needed by SASL
+ */
+
+static sasl_conn_t *conn = NULL;       /* SASL connection state */
+static int sasl_complete = 0;          /* Has authentication succeded? */
+static sasl_ssf_t sasl_ssf;            /* Our security strength factor */
+static char *sasl_pw_context[2];       /* Context to pass into sm_get_pass */
+static int maxoutbuf;                  /* Maximum crypto output buffer */
+static int sm_get_user(void *, int, const char **, unsigned *);
+static int sm_get_pass(sasl_conn_t *, void *, int, sasl_secret_t **);
+
+static sasl_callback_t callbacks[] = {
+    { SASL_CB_USER, sm_get_user, NULL },
+#define SM_SASL_N_CB_USER 0
+    { SASL_CB_PASS, sm_get_pass, NULL },
+#define SM_SASL_N_CB_PASS 1
+    { SASL_CB_LIST_END, NULL, NULL },
+};
+#endif /* CYRUS_SASL */
+
 static char *sm_noreply = "No reply text given";
 static char *sm_moreply = "; ";
 
 struct smtp sm_reply;          /* global... */
 
-
 #define        MAXEHLO 20
 
 static int doingEHLO;
@@ -86,6 +121,10 @@ char *EHLOkeys[MAXEHLO + 1];
 /*
  * static prototypes
  */
+static int smtp_init (char *, char *, int, int, int, int, int, int,
+                     char *, char *);
+static int sendmail_init (char *, char *, int, int, int, int, int);
+
 static int rclient (char *, char *, char *);
 static int sm_ierror (char *fmt, ...);
 static int smtalk (int time, char *fmt, ...);
@@ -107,13 +146,38 @@ static int smail_brkany (char, char *);
 char **smail_copyip (char **, char **, int);
 #endif
 
-/* from zotnet/mts/client.c */
+#ifdef CYRUS_SASL
+/*
+ * Function prototypes needed for SASL
+ */
+
+static int sm_auth_sasl(char *, char *, char *);
+#endif /* CYRUS_SASL */
+
+/* from mts/generic/client.c */
 int client (char *, char *, char *, int, char *, int);
 
 int
 sm_init (char *client, char *server, int watch, int verbose,
-         int debug, int onex, int queued)
+         int debug, int onex, int queued, int sasl, char *saslmech,
+         char *user)
+{
+    if (sm_mts == MTS_SMTP)
+       return smtp_init (client, server, watch, verbose,
+                         debug, onex, queued, sasl, saslmech, user);
+    else
+       return sendmail_init (client, server, watch, verbose,
+                             debug, onex, queued);
+}
+
+static int
+smtp_init (char *client, char *server, int watch, int verbose,
+          int debug, int onex, int queued, int sasl, char *saslmech,
+          char *user)
 {
+#ifdef CYRUS_SASL
+    char *server_mechs;
+#endif /* CYRUS_SASL */
     int result, sd1, sd2;
 
     if (watch)
@@ -212,6 +276,35 @@ all_done: ;
        }
     }
 
+#ifdef CYRUS_SASL
+    /*
+     * If the user asked for SASL, then check to see if the SMTP server
+     * supports it.  Otherwise, error out (because the SMTP server
+     * might have been spoofed; we don't want to just silently not
+     * do authentication
+     */
+
+    if (sasl) {
+       if (! (server_mechs = EHLOset("AUTH"))) {
+           sm_end(NOTOK);
+           return sm_ierror("SMTP server does not support SASL");
+       }
+
+       if (saslmech && stringdex(saslmech, server_mechs) == -1) {
+           sm_end(NOTOK);
+           return sm_ierror("Requested SASL mech \"%s\" is not in the "
+                            "list of supported mechanisms:\n%s",
+                            saslmech, server_mechs);
+       }
+
+       if (sm_auth_sasl(user, saslmech ? saslmech : server_mechs,
+                        server) != RP_OK) {
+           sm_end(NOTOK);
+           return NOTOK;
+       }
+    }
+#endif /* CYRUS_SASL */
+
 send_options: ;
     if (watch && EHLOset ("XVRB"))
        smtalk (SM_HELO, "VERB on");
@@ -223,6 +316,138 @@ send_options: ;
     return RP_OK;
 }
 
+int
+sendmail_init (char *client, char *server, int watch, int verbose,
+               int debug, int onex, int queued)
+{
+    int i, result, vecp;
+    int pdi[2], pdo[2];
+    char *vec[15];
+
+    if (watch)
+       verbose = TRUE;
+
+    sm_verbose = verbose;
+    sm_debug = debug;
+    if (sm_rfp != NULL && sm_wfp != NULL)
+       return RP_OK;
+
+    if (client == NULL || *client == '\0') {
+       if (clientname)
+           client = clientname;
+       else
+           client = LocalName();       /* no clientname -> LocalName */
+    }
+
+#ifdef ZMAILER
+    if (client == NULL || *client == '\0')
+       client = "localhost";
+#endif
+
+    if (pipe (pdi) == NOTOK)
+       return sm_ierror ("no pipes");
+    if (pipe (pdo) == NOTOK) {
+       close (pdi[0]);
+       close (pdi[1]);
+       return sm_ierror ("no pipes");
+    }
+
+    for (i = 0; (sm_child = fork ()) == NOTOK && i < 5; i++)
+       sleep (5);
+
+    switch (sm_child) {
+       case NOTOK: 
+           close (pdo[0]);
+           close (pdo[1]);
+           close (pdi[0]);
+           close (pdi[1]);
+           return sm_ierror ("unable to fork");
+
+       case OK: 
+           if (pdo[0] != fileno (stdin))
+               dup2 (pdo[0], fileno (stdin));
+           if (pdi[1] != fileno (stdout))
+               dup2 (pdi[1], fileno (stdout));
+           if (pdi[1] != fileno (stderr))
+               dup2 (pdi[1], fileno (stderr));
+           for (i = fileno (stderr) + 1; i < NBITS; i++)
+               close (i);
+
+           vecp = 0;
+           vec[vecp++] = r1bindex (sendmail, '/');
+           vec[vecp++] = "-bs";
+#ifndef ZMAILER
+           vec[vecp++] = watch ? "-odi" : queued ? "-odq" : "-odb";
+           vec[vecp++] = "-oem";
+           vec[vecp++] = "-om";
+# ifndef RAND
+           if (verbose)
+               vec[vecp++] = "-ov";
+# endif /* not RAND */
+#endif /* not ZMAILER */
+           vec[vecp++] = NULL;
+
+           setgid (getegid ());
+           setuid (geteuid ());
+           execvp (sendmail, vec);
+           fprintf (stderr, "unable to exec ");
+           perror (sendmail);
+           _exit (-1);         /* NOTREACHED */
+
+       default: 
+           SIGNAL (SIGALRM, alrmser);
+           SIGNAL (SIGPIPE, SIG_IGN);
+
+           close (pdi[1]);
+           close (pdo[0]);
+           if ((sm_rfp = fdopen (pdi[0], "r")) == NULL
+                   || (sm_wfp = fdopen (pdo[1], "w")) == NULL) {
+               close (pdi[0]);
+               close (pdo[1]);
+               sm_rfp = sm_wfp = NULL;
+               return sm_ierror ("unable to fdopen");
+           }
+           sm_alarmed = 0;
+           alarm (SM_OPEN);
+           result = smhear ();
+           alarm (0);
+           switch (result) {
+               case 220: 
+                   break;
+
+               default: 
+                   sm_end (NOTOK);
+                   return RP_RPLY;
+           }
+
+           if (client && *client) {
+               doingEHLO = 1;
+               result = smtalk (SM_HELO, "EHLO %s", client);
+               doingEHLO = 0;
+
+               if (500 <= result && result <= 599)
+                   result = smtalk (SM_HELO, "HELO %s", client);
+
+               switch (result) {
+                   case 250:
+                       break;
+
+                   default:
+                       sm_end (NOTOK);
+                       return RP_RPLY;
+               }
+           }
+
+#ifndef ZMAILER
+           if (onex)
+               smtalk (SM_HELO, "ONEX");
+#endif
+           if (watch)
+               smtalk (SM_HELO, "VERB on");
+
+           return RP_OK;
+    }
+}
 
 #ifdef MPOP
 # define MAXARGS  1000
@@ -276,6 +501,14 @@ rclient (char *server, char *protocol, char *service)
     return NOTOK;
 }
 
+#ifdef CYRUS_SASL
+#include <sasl.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <errno.h>
+#endif /* CYRUS_SASL */
 
 int
 sm_winit (int mode, char *from)
@@ -435,6 +668,17 @@ sm_end (int type)
     int status;
     struct smtp sm_note;
 
+    if (sm_mts == MTS_SENDMAIL) {
+       switch (sm_child) {
+           case NOTOK: 
+           case OK: 
+               return RP_OK;
+
+           default: 
+               break;
+       }
+    }
+
     if (sm_rfp == NULL && sm_wfp == NULL)
        return RP_OK;
 
@@ -449,7 +693,13 @@ sm_end (int type)
        case DONE: 
            if (smtalk (SM_RSET, "RSET") == 250 && type == DONE)
                return RP_OK;
-           smtalk (SM_QUIT, "QUIT");
+           if (sm_mts == MTS_SMTP)
+               smtalk (SM_QUIT, "QUIT");
+           else {
+               kill (sm_child, SIGKILL);
+               discard (sm_rfp);
+               discard (sm_wfp);
+           }
            if (type == NOTOK) {
                sm_reply.code = sm_note.code;
                strncpy (sm_reply.text, sm_note.text, sm_reply.length = sm_note.length);
@@ -480,7 +730,17 @@ sm_end (int type)
        alarm (0);
     }
 
-    status = 0;
+    if (sm_mts == MTS_SMTP) {
+       status = 0;
+#ifdef CYRUS_SASL
+       if (conn)
+           sasl_dispose(&conn);
+#endif /* CYRUS_SASL */
+    } else {
+       status = pidwait (sm_child, OK);
+       sm_child = NOTOK;
+    }
+
     sm_rfp = sm_wfp = NULL;
     return (status ? RP_BHST : RP_OK);
 }
@@ -837,6 +1097,306 @@ no_dice:
 #endif /* MPOP */
 
 
+#ifdef CYRUS_SASL
+/*
+ * This function implements SASL authentication for SMTP.  If this function
+ * completes successfully, then authentication is successful and we've
+ * (optionally) negotiated a security layer.
+ *
+ * Right now we don't support session encryption.
+ */
+static int
+sm_auth_sasl(char *user, char *mechlist, char *host)
+{
+    int result, status, outlen;
+    unsigned int buflen;
+    char *buf, outbuf[BUFSIZ];
+    const char *chosen_mech;
+    sasl_security_properties_t secprops;
+    sasl_external_properties_t extprops;
+    sasl_ssf_t *ssf;
+    int *outbufmax;
+
+    /*
+     * Initialize the callback contexts
+     */
+
+    if (user == NULL)
+       user = getusername();
+
+    callbacks[SM_SASL_N_CB_USER].context = user;
+
+    /*
+     * This is a _bit_ of a hack ... but if the hostname wasn't supplied
+     * to us on the command line, then call getpeername and do a
+     * reverse-address lookup on the IP address to get the name.
+     */
+
+    if (!host) {
+       struct sockaddr_in sin;
+       int len = sizeof(sin);
+       struct hostent *hp;
+
+       if (getpeername(fileno(sm_wfp), (struct sockaddr *) &sin, &len) < 0) {
+           sm_ierror("getpeername on SMTP socket failed: %s",
+                     strerror(errno));
+           return NOTOK;
+       }
+
+       if ((hp = gethostbyaddr((void *) &sin.sin_addr, sizeof(sin.sin_addr),
+                               sin.sin_family)) == NULL) {
+           sm_ierror("DNS lookup on IP address %s failed",
+                     inet_ntoa(sin.sin_addr));
+           return NOTOK;
+       }
+
+       host = strdup(hp->h_name);
+    }
+
+    sasl_pw_context[0] = host;
+    sasl_pw_context[1] = user;
+
+    callbacks[SM_SASL_N_CB_PASS].context = sasl_pw_context;
+
+    result = sasl_client_init(callbacks);
+
+    if (result != SASL_OK) {
+       sm_ierror("SASL library initialization failed: %s",
+                 sasl_errstring(result, NULL, NULL));
+       return NOTOK;
+    }
+
+    result = sasl_client_new("smtp", host, NULL, SASL_SECURITY_LAYER, &conn);
+
+    if (result != SASL_OK) {
+       sm_ierror("SASL client initialization failed: %s",
+                 sasl_errstring(result, NULL, NULL));
+       return NOTOK;
+    }
+
+    /*
+     * Initialize the security properties
+     */
+
+    memset(&secprops, 0, sizeof(secprops));
+    secprops.maxbufsize = BUFSIZ;
+    secprops.max_ssf = 0;      /* XXX change this when we do encryption */
+    memset(&extprops, 0, sizeof(extprops));
+
+    result = sasl_setprop(conn, SASL_SEC_PROPS, &secprops);
+
+    if (result != SASL_OK) {
+       sm_ierror("SASL security property initialization failed: %s",
+                 sasl_errstring(result, NULL, NULL));
+       return NOTOK;
+    }
+
+    result = sasl_setprop(conn, SASL_SSF_EXTERNAL, &extprops);
+
+    if (result != SASL_OK) {
+       sm_ierror("SASL external property initialization failed: %s",
+                 sasl_errstring(result, NULL, NULL));
+       return NOTOK;
+    }
+
+    /*
+     * Start the actual protocol.  Feed the mech list into the library
+     * and get out a possible initial challenge
+     */
+
+    result = sasl_client_start(conn, mechlist, NULL, NULL, &buf, &buflen,
+                              &chosen_mech);
+
+    if (result != SASL_OK && result != SASL_CONTINUE) {
+       sm_ierror("SASL client start failed: %s",
+                 sasl_errstring(result, NULL, NULL));
+       return NOTOK;
+    }
+
+    /*
+     * If we got an initial challenge, send it as part of the AUTH
+     * command; otherwise, just send a plain AUTH command.
+     */
+
+    if (buflen) {
+       status = sasl_encode64(buf, buflen, outbuf, sizeof(outbuf), NULL);
+       free(buf);
+       if (status != SASL_OK) {
+           sm_ierror("SASL base64 encode failed: %s",
+                     sasl_errstring(status, NULL, NULL));
+           return NOTOK;
+       }
+
+       status = smtalk(SM_AUTH, "AUTH %s %s", chosen_mech, outbuf);
+    } else
+       status = smtalk(SM_AUTH, "AUTH %s", chosen_mech);
+
+    /*
+     * Now we loop until we either fail, get a SASL_OK, or a 235
+     * response code.  Receive the challenges and process them until
+     * we're all done.
+     */
+
+    while (result == SASL_CONTINUE) {
+
+       /*
+        * If we get a 235 response, that means authentication has
+        * succeeded and we need to break out of the loop (yes, even if
+        * we still get SASL_CONTINUE from sasl_client_step()).
+        *
+        * Otherwise, if we get a message that doesn't seem to be a
+        * valid response, then abort
+        */
+
+       if (status == 235)
+           break;
+       else if (status < 300 || status > 399)
+           return RP_BHST;
+       
+       /*
+        * Special case; a zero-length response from the SMTP server
+        * is returned as a single =.  If we get that, then set buflen
+        * to be zero.  Otherwise, just decode the response.
+        */
+       
+       if (strcmp("=", sm_reply.text) == 0) {
+           outlen = 0;
+       } else {
+           result = sasl_decode64(sm_reply.text, sm_reply.length,
+                                  outbuf, &outlen);
+       
+           if (result != SASL_OK) {
+               smtalk(SM_AUTH, "*");
+               sm_ierror("SASL base64 decode failed: %s",
+                         sasl_errstring(result, NULL, NULL));
+               return NOTOK;
+           }
+       }
+
+       result = sasl_client_step(conn, outbuf, outlen, NULL, &buf, &buflen);
+
+       if (result != SASL_OK && result != SASL_CONTINUE) {
+           smtalk(SM_AUTH, "*");
+           sm_ierror("SASL client negotiation failed: %s",
+                     sasl_errstring(result, NULL, NULL));
+           return NOTOK;
+       }
+
+       status = sasl_encode64(buf, buflen, outbuf, sizeof(outbuf), NULL);
+       free(buf);
+
+       if (status != SASL_OK) {
+           smtalk(SM_AUTH, "*");
+           sm_ierror("SASL base64 encode failed: %s",
+                     sasl_errstring(status, NULL, NULL));
+           return NOTOK;
+       }
+       
+       status = smtalk(SM_AUTH, outbuf);
+    }
+
+    /*
+     * Make sure that we got the correct response
+     */
+
+    if (status < 200 || status > 299)
+       return RP_BHST;
+
+    /*
+     * Depending on the mechanism, we need to do a FINAL call to
+     * sasl_client_step().  Do that now.
+     */
+
+    result = sasl_client_step(conn, NULL, 0, NULL, &buf, &buflen);
+
+    if (result != SASL_OK) {
+       sm_ierror("SASL final client negotiation failed: %s",
+                 sasl_errstring(result, NULL, NULL));
+       return NOTOK;
+    }
+
+    /*
+     * We _should_ have completed the authentication successfully.
+     * Get a few properties from the authentication exchange.
+     */
+
+    result = sasl_getprop(conn, SASL_MAXOUTBUF, (void **) &outbufmax);
+
+    if (result != SASL_OK) {
+       sm_ierror("Cannot retrieve SASL negotiated output buffer size: %s",
+                 sasl_errstring(result, NULL, NULL));
+       return NOTOK;
+    }
+
+    maxoutbuf = *outbufmax;
+
+    result = sasl_getprop(conn, SASL_SSF, (void **) &ssf);
+
+    sasl_ssf = *ssf;
+
+    if (result != SASL_OK) {
+       sm_ierror("Cannot retrieve SASL negotiated security strength "
+                 "factor: %s", sasl_errstring(result, NULL, NULL));
+       return NOTOK;
+    }
+
+    if (maxoutbuf == 0 || maxoutbuf > BUFSIZ)
+       maxoutbuf = BUFSIZ;
+
+    sasl_complete = 1;
+
+    return RP_OK;
+}
+
+/*
+ * Our callback functions to feed data to the SASL library
+ */
+
+static int
+sm_get_user(void *context, int id, const char **result, unsigned *len)
+{
+    char *user = (char *) context;
+
+    if (! result || id != SASL_CB_USER)
+       return SASL_BADPARAM;
+
+    *result = user;
+    if (len)
+       *len = strlen(user);
+
+    return SASL_OK;
+}
+
+static int
+sm_get_pass(sasl_conn_t *conn, void *context, int id,
+           sasl_secret_t **psecret)
+{
+    char **pw_context = (char **) context;
+    char *pass = NULL;
+    int len;
+
+    if (! psecret || id != SASL_CB_PASS)
+       return SASL_BADPARAM;
+
+    ruserpass(pw_context[0], &(pw_context[1]), &pass);
+
+    len = strlen(pass);
+
+    *psecret = (sasl_secret_t *) malloc(sizeof(sasl_secret_t) + len);
+
+    if (! *psecret) {
+       free(pass);
+       return SASL_NOMEM;
+    }
+
+    (*psecret)->len = len;
+    strcpy((*psecret)->data, pass);
+/*    free(pass); */
+
+    return SASL_OK;
+}
+#endif /* CYRUS_SASL */
+
 static int
 sm_ierror (char *fmt, ...)
 {
@@ -871,7 +1431,7 @@ smtalk (int time, char *fmt, ...)
 
 #ifdef MPOP
     if (sm_ispool) {
-       char    file[BUFSIZ];
+       char file[BUFSIZ];
 
        if (strcmp (buffer, ".") == 0)
            time = SM_DOT;
@@ -888,7 +1448,7 @@ smtalk (int time, char *fmt, ...)
                    char *bp;
 
                    snprintf (sm_reply.text, sizeof(sm_reply.text),
-                           "error renaming %s to %s: ", sm_tmpfil, file);
+                       "error renaming %s to %s: ", sm_tmpfil, file);
                    bp = sm_reply.text;
                    len = strlen (bp);
                    bp += len;
@@ -1125,9 +1685,10 @@ again: ;
 
        if ((i = min (bc, rc)) > 0) {
            strncpy (rp, bp, i);
-           rp += i, rc -= i;
+           rp += i;
+           rc -= i;
            if (more && rc > strlen (sm_moreply) + 1) {
-               strcpy (sm_reply.text + rc, sm_moreply);
+               strncpy (sm_reply.text + rc, sm_moreply, sizeof(sm_reply.text) - rc);
                rc += strlen (sm_moreply);
            }
        }
@@ -1175,11 +1736,18 @@ sm_rrecord (char *buffer, int *len)
 static int
 sm_rerror (void)
 {
-    sm_reply.length =
-       strlen (strcpy (sm_reply.text, sm_rfp == NULL ? "no socket opened"
-           : sm_alarmed ? "read from socket timed out"
-           : feof (sm_rfp) ? "premature end-of-file on socket"
-           : "error reading from socket"));
+    if (sm_mts == MTS_SMTP)
+       sm_reply.length =
+           strlen (strcpy (sm_reply.text, sm_rfp == NULL ? "no socket opened"
+               : sm_alarmed ? "read from socket timed out"
+               : feof (sm_rfp) ? "premature end-of-file on socket"
+               : "error reading from socket"));
+    else
+       sm_reply.length =
+           strlen (strcpy (sm_reply.text, sm_rfp == NULL ? "no pipe opened"
+               : sm_alarmed ? "read from pipe timed out"
+               : feof (sm_rfp) ? "premature end-of-file on pipe"
+               : "error reading from pipe"));
 
     return (sm_reply.code = NOTOK);
 }