]> diplodocus.org Git - nmh/commitdiff
Added sendfrom to docs/contrib.
authorDavid Levine <david.levine@gonift.com>
Sun, 29 May 2016 13:40:12 +0000 (09:40 -0400)
committerDavid Levine <david.levine@gonift.com>
Sun, 29 May 2016 13:40:12 +0000 (09:40 -0400)
.gitignore
Makefile.am
docs/contrib/sendfrom.c [new file with mode: 0644]

index f251aef..4aa2623 100644 (file)
@@ -48,6 +48,7 @@
 /RPM/
 a.out.dSYM/
 /config/version.c
+/docs/contrib/sendfrom
 /etc/gen-ctype-checked
 /etc/gen-ctype-checked.exe
 /etc/mhn.defaults
index 80e9cc7..2ec9d5c 100644 (file)
@@ -236,10 +236,11 @@ dist_doc_DATA = COPYRIGHT INSTALL NEWS README VERSION \
 ##
 ## Contribs that get installed in docdir/contrib/
 ##
-contribdir = $(docdir)/contrib
-dist_contrib_SCRIPTS = docs/contrib/replyfilter docs/contrib/build_nmh \
-                      docs/contrib/ml docs/contrib/vpick
-dist_contrib_DATA = docs/contrib/replaliases
+docs_contribdir = $(docdir)/contrib
+dist_docs_contrib_SCRIPTS = docs/contrib/replyfilter docs/contrib/build_nmh \
+                           docs/contrib/ml docs/contrib/vpick
+dist_docs_contrib_DATA = docs/contrib/replaliases docs/contrib/sendfrom.c
+docs_contrib_PROGRAMS = docs/contrib/sendfrom
 
 ##
 ## Our man pages
@@ -436,7 +437,7 @@ uip_whom_SOURCES = uip/whom.c uip/distsbr.c
 uip_whom_LDADD = $(LDADD) $(POSTLINK)
 
 ##
-## Programs definitions for all utilities that end up in $(nmhlibexecdir)
+## Program definitions for all utilities that end up in $(nmhlibexecdir)
 ##
 
 uip_ap_SOURCES = uip/ap.c
@@ -482,6 +483,10 @@ uip_viamail_SOURCES = uip/viamail.c uip/mhmisc.c uip/sendsbr.c \
                      uip/annosbr.c uip/distsbr.c
 uip_viamail_LDADD = $(LDADD) $(POSTLINK)
 
+##
+## Other program definitions
+##
+
 test_getfullname_SOURCES = test/getfullname.c
 test_getfullname_LDADD = $(LDADD) $(POSTLINK)
 
@@ -503,6 +508,9 @@ test_getcwidth_LDADD = $(POSTLINK)
 etc_gen_ctype_checked_SOURCES = etc/gen-ctype-checked.c
 etc_gen_ctype_checked_LDADD = $(POSTLINK)
 
+docs_contrib_sendfrom_SOURCES = docs/contrib/sendfrom.c
+docs_contrib_sendfrom_LDADD = $(LDADD) $(TERMLIB) $(CURLLIB) $(POSTLINK)
+
 ##
 ## Our rebuild rules for files that aren't built via the normal mechanisms
 ##
diff --git a/docs/contrib/sendfrom.c b/docs/contrib/sendfrom.c
new file mode 100644 (file)
index 0000000..451a792
--- /dev/null
@@ -0,0 +1,415 @@
+/*
+ * sendfrom.c -- postproc that selects user, and then mail server, from draft
+ *
+ * Author:  David Levine <levinedl@acm.org>
+ *
+ * This program fits between send(1) and post(1), as a postproc.  It makes
+ * up for the facts that send doesn't parse the message draft and post
+ * doesn't load the profile.
+ *
+ * To use:
+ *
+ * 1) Add profile entries of the form:
+ *
+ *        sendfrom-<email address or domain name>:  <post(1) switches>
+ *
+ *    The email address is extracted from the From: header line of the message draft.  Multiple
+ *    profile entries, with different email addresses or domain names, are supported.  This
+ *    allows different switches to post(1), such as -user, to be associated with different email
+ *    addresses.  If a domain name is used, it matches all users in that domain.
+ *
+ *    Example profile entry using OAuth for account hosted by gmail:
+ *
+ *       sendfrom-gmail_address@example.com: -saslmech xoauth2 -authservice gmail -tls
+ *           -server smtp.gmail.com -user gmail_login@example.com
+ *
+ *    (Indentation indicates a continued line, as supported in MH profiles.)  The username
+ *    need not be the same as the sender address, which was extracted from the From: header line.
+ *
+ *    Example profile entries that use an nmh credentials file:
+ *
+ *       credentials: file:nmhcreds
+ *       sendfrom-sendgrid_address@example.com: -sasl -tls -server smtp.sendgrid.net
+ *       sendfrom-outbound.att.net: -sasl -initialtls -server outbound.att.net -port 465
+ *       sendfrom-fastmail.com: -initialtls -sasl -saslmech LOGIN
+ *           -server smtps-proxy.messagingengine.com -port 80
+ *
+ *    where nmhcreds is in the user's nmh directory (from the Path profile component) and contains:
+ *
+ *       machine smtp.sendgrid.net
+ *           login sendgrid_login@example.com
+ *           password ********
+ *       machine outbound.att.net
+ *           login att_login@example.com
+ *           password ********
+ *       machine smtps-proxy.messagingengine.com
+ *           login fastmail_login@example.com
+ *           password ********
+ *
+ * 2) To enable, add a line like this to your profile:
+ *
+ *        postproc: <docdir>/contrib/sendfrom
+ *
+ *    with <docdir> expanded.  This source file is in docdir.  Also, "mhparam docdir" will show it.
+ *
+ * For more information on authentication to mail servers, see mhlogin(1) for OAuth
+ * services, and mh-profile(5) for login credentials.
+ *
+ * This program follows these steps:
+ * 1) Loads profile.
+ * 2) Saves command line arguments in vec.
+ * 3) Extracts address and domain name from From: header line in draft.
+ * 4) Extracts any address or host specific switches to post(1) from profile.  Appends
+      to vec, along with implied switches, such as OAUTH access token.
+ * 5) Calls post, or with -dryrun, echos how it would be called.
+ */
+
+#include <h/mh.h>
+#include <h/fmt_scan.h>
+#include <h/fmt_compile.h>
+#include <h/utils.h>
+
+#ifdef OAUTH_SUPPORT
+#include <h/oauth.h>
+
+static int setup_oauth_params(char *vec[], int *, int, const char **);
+#endif /* OAUTH_SUPPORT */
+
+extern char *mhlibexecdir;      /* from config.c */
+
+static int get_from_header_info(const char *, const char **, const char **, const char **);
+static const char *get_message_header_info(FILE *, char *);
+static void merge_profile_entry(const char *, const char *, char *vec[], int *vecp);
+static int run_program(char *, char **);
+
+int
+main(int argc, char **argv) {
+    /* Make sure that nmh's post gets called by specifying its full path. */
+    char *realpost = concat(mhlibexecdir, "/", "post", NULL);
+    int whom = 0, dryrun = 0, snoop = 0;
+    char **arguments, **argp;
+    char *program, **vec;
+    int vecp = 0;
+    char *msg = NULL;
+
+    /* Load profile. */
+    if (nmh_init(argv[0], 1)) { return NOTOK; }
+
+    /* Save command line arguments in vec, except for the last argument, if it's the draft file path. */
+    vec = argsplit(realpost, &program, &vecp);
+    for (argp = arguments = getarguments(invo_name, argc, argv, 1); *argp; ++argp) {
+        /* Don't pass -dryrun to post by putting it in vec. */
+        if (strcmp(*argp, "-dryrun") == 0) {
+            dryrun = 1;
+        } else {
+            vec[vecp++] = getcpy(*argp);
+
+            if (strcmp(*argp, "-snoop") == 0) {
+                snoop = 1;
+            } else if (strcmp(*argp, "-whom") == 0) {
+                whom = 1;
+            } else {
+                /* Need to keep filename, from last arg (though it's not used below with -whom). */
+                msg = *argp;
+            }
+        }
+    }
+
+    free(arguments);
+
+    if (! whom  &&  msg == NULL) {
+        adios(NULL, "requires exactly 1 filename argument");
+    } else {
+        char **vp;
+
+        /* With post, but without -whom, need to keep filename as last arg. */
+        if (! whom) {
+            /* At this point, vecp points to the next argument to be added.  Back it up to
+               remove the last one, which is the filename.  Add it back at the end of vec below. */
+            free(vec[--vecp]);
+        }
+        /* Null terminate vec because it's used below. */
+        vec[vecp] = NULL;
+
+        if (! whom) {
+            /* Some users might need the switch from the profile entry with -whom, but that would
+               require figuring out which command line argument has the filename.  With -whom, it's
+               not necessarily the last one. */
+
+            const char *addr, *host;
+            const char *message;
+
+            /* Extract address and host from From: header line in draft. */
+            if (get_from_header_info(msg, &addr, &host, &message) != OK) {
+                adios(msg, message);
+            }
+
+            /* Merge in any address or host specific switches to post(1) from profile. */
+            merge_profile_entry(addr, host, vec, &vecp);
+            free((void *) host);
+            free((void *) addr);
+        }
+
+        vec[vecp] = NULL;
+        for (vp = vec; *vp; ++vp) {
+            if (strcmp(*vp, "xoauth2") == 0) {
+#ifdef OAUTH_SUPPORT
+                const char *message;
+
+                if (setup_oauth_params(vec, &vecp, snoop, &message) != OK) {
+                    adios(NULL, message);
+                }
+                break;
+#else
+                NMH_UNUSED(snoop);
+                NMH_UNUSED(vp);
+                adios(NULL, "sendfrom built without OAUTH_SUPPORT, so -saslmech xoauth2 is not supported");
+#endif /* OAUTH_SUPPORT */
+            }
+        }
+
+        if (! whom) {
+            /* Append filename as last non-null vec entry. */
+            vec[vecp++] = getcpy(msg);
+            vec[vecp] = NULL;
+        }
+
+        /* Call post, or with -dryrun, echo how it would be called. */
+        if (dryrun) {
+            /* Show the program name, because run_program() won't. */
+            printf("%s ", realpost);
+            fflush(stdout);
+        }
+        if (run_program(dryrun ? "echo" : realpost, vec) != OK) {
+            adios(realpost, "failure in ");
+        }
+
+        arglist_free(program, vec);
+        free(realpost);
+    }
+
+    return 0;
+}
+
+
+
+#ifdef OAUTH_SUPPORT
+/*
+ * For XOAUTH2, append access token, from mh_oauth_do_xoauth(), for the user to vec.
+ */
+static
+int
+setup_oauth_params(char *vec[], int *vecp, int snoop, const char **message) {
+    const char *saslmech = NULL, *user = NULL, *auth_svc = NULL;
+    int i;
+
+    /* Make sure we have all the information we need. */
+    for (i = 1; i < *vecp; ++i) {
+        /* Don't support abbreviated switches, to avoid collisions in the future if new ones
+           are added. */
+        if (! strcmp(vec[i-1], "-saslmech")) {
+            saslmech = vec[i];
+        } else if (! strcmp(vec[i-1], "-user")) {
+            user = vec[i];
+        } else if (! strcmp(vec[i-1], "-authservice")) {
+            auth_svc = vec[i];
+        }
+    }
+
+    if (auth_svc == NULL) {
+        if (saslmech  &&  ! strcasecmp(saslmech, "xoauth2")) {
+            *message = "must specify -authservice with -saslmech xoauth2";
+            return NOTOK;
+        }
+    } else {
+        if (user == NULL) {
+            *message = "must specify -user with -saslmech xoauth2";
+            return NOTOK;
+        }
+
+        vec[(*vecp)++] = getcpy("-authservice");
+        if (saslmech  &&  ! strcasecmp(saslmech, "xoauth2")) {
+            vec[(*vecp)++] = mh_oauth_do_xoauth(user, auth_svc, snoop ? stderr : NULL);
+        } else {
+            vec[(*vecp)++] = getcpy(auth_svc);
+        }
+    }
+
+    return 0;
+}
+#endif /* OAUTH_SUPPORT */
+
+
+/*
+ * Extract user and domain from From: header line in draft.
+ */
+static
+int
+get_from_header_info(const char *filename, const char **addr, const char **host, const char **message) {
+    struct stat st;
+    FILE *in;
+
+    if (stat (filename, &st) == NOTOK) {
+        *message = "unable to stat draft file";
+        return NOTOK;
+    }
+
+    if ((in = fopen(filename, "r")) != NULL) {
+        char *addrformat = "%(addr{from})", *hostformat = "%(host{from})";
+
+        if ((*addr = get_message_header_info(in, addrformat)) == NULL) {
+            *message = "unable to find From: address in";
+            return NOTOK;
+        }
+        rewind(in);
+
+        if ((*host = get_message_header_info(in, hostformat)) == NULL) {
+            fclose(in);
+            *message = "unable to find From: host in";
+            return NOTOK;
+        }
+        fclose(in);
+
+        return OK;
+    } else {
+        *message = "unable to open";
+        return NOTOK;
+    }
+}
+
+
+/*
+ * Get formatted information from header of a message.
+ * Adapted from process_single_file() in uip/fmttest.c.
+ */
+static
+const char *
+get_message_header_info(FILE *in, char *format) {
+    int dat[5];
+    struct format *fmt;
+    struct stat st;
+    int parsing_header;
+    m_getfld_state_t gstate = 0;
+    charstring_t buffer = charstring_create(0);
+    char *retval;
+
+    dat[0] = dat[1] = dat[4] = 0;
+    dat[2] = fstat(fileno(in), &st) == 0  ?  st.st_size  :  0;
+    dat[3] = INT_MAX;
+
+    (void) fmt_compile(new_fs(NULL, NULL, format), &fmt, 1);
+
+    /*
+     * Read in the message and process the header.
+     */
+    parsing_header = 1;
+    do {
+        char name[NAMESZ], rbuf[NMH_BUFSIZ];
+        int bufsz = sizeof rbuf;
+        int state = m_getfld(&gstate, name, rbuf, &bufsz, in);
+
+        switch (state) {
+        case FLD:
+        case FLDPLUS: {
+            int bucket = fmt_addcomptext(name, rbuf);
+
+            if (bucket != -1) {
+                while (state == FLDPLUS) {
+                    bufsz = sizeof rbuf;
+                    state = m_getfld(&gstate, name, rbuf, &bufsz, in);
+                    fmt_appendcomp(bucket, name, rbuf);
+                }
+            }
+
+            while (state == FLDPLUS) {
+                bufsz = sizeof rbuf;
+                state = m_getfld(&gstate, name, rbuf, &bufsz, in);
+            }
+            break;
+        }
+        default:
+            parsing_header = 0;
+        }
+    } while (parsing_header);
+    m_getfld_state_destroy(&gstate);
+
+    fmt_scan(fmt, buffer, INT_MAX, dat, NULL);
+    fmt_free(fmt, 1);
+
+    /* Trim trailing newline, if any. */
+    retval = rtrim(charstring_buffer_copy((buffer)));
+    charstring_free(buffer);
+
+    return retval;
+}
+
+
+/*
+ * Look in profile for entry corresponding to addr or host, and add its contents to vec.
+ *
+ * Could do some of this automatically, by looking for:
+ * 1) access-$(mbox{from}) in oauth-svc file using mh_oauth_cred_load(), which isn't
+ *    static and doesn't have side effects; free the result with mh_oauth_cred_free())
+ * 2) machine $(mbox{from}) in creds
+ * If no -server passed in from profile or commandline, could use smtp.<svc>.com for gmail,
+ * but that might not generalize for other svcs.
+ */
+static
+void
+merge_profile_entry(const char *addr, const char *host, char *vec[], int *vecp) {
+    char *addr_entry = concat("sendfrom-", addr, NULL);
+    char *profile_entry = context_find(addr_entry);
+
+    free(addr_entry);
+    if (profile_entry == NULL) {
+        /* No entry for the user.  Look for one for the host. */
+        char *host_entry = concat("sendfrom-", host, NULL);
+
+        profile_entry = context_find(host_entry);
+        free(host_entry);
+    }
+
+    /* Use argsplit() to do the real work of splitting the args in the profile entry. */
+    if (profile_entry  &&  strlen(profile_entry) > 0) {
+        int profile_vecp;
+        char *file;
+        char **profile_vec = argsplit(profile_entry, &file, &profile_vecp);
+        int i;
+
+        for (i = 0; i < profile_vecp; ++i) {
+            vec[(*vecp)++] = getcpy(profile_vec[i]);
+        }
+
+        arglist_free(file, profile_vec);
+    }
+}
+
+
+/*
+ * Fork and exec.
+ */
+static
+int
+run_program(char *realpost, char **vec) {
+    pid_t child_id;
+    int i;
+
+    for (i = 0; (child_id = fork()) == NOTOK && i < 5; ++i) { sleep(5); }
+
+    switch (child_id) {
+    case -1:
+        /* oops -- fork error */
+        return NOTOK;
+    case 0:
+        (void) execvp(realpost, vec);
+        fprintf(stderr, "unable to exec ");
+        perror(realpost);
+        _exit(-1);
+    default:
+        if (pidXwait(child_id, realpost)) {
+            return NOTOK;
+        }
+    }
+
+    return OK;
+}