/* * sendfrom.c -- postproc that selects user, and then mail server, from draft * * Author: David Levine * * 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-: * * 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: /contrib/sendfrom * * with 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 #include #include #include #ifdef OAUTH_SUPPORT #include 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, (char *) 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); free_fs(); /* * 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..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; }