]>
diplodocus.org Git - nmh/blob - docs/contrib/sendfrom.c
2 * sendfrom.c -- postproc that selects user, and then mail server, from draft
4 * Author: David Levine <levinedl@acm.org>
6 * This program fits between send(1) and post(1), as a postproc. It makes
7 * up for the facts that send doesn't parse the message draft and post
8 * doesn't load the profile.
12 * 1) Add profile entries of the form:
14 * sendfrom-<email address or domain name>: <post(1) switches>
16 * The email address is extracted from the From: header line of the message draft. Multiple
17 * profile entries, with different email addresses or domain names, are supported. This
18 * allows different switches to post(1), such as -user, to be associated with different email
19 * addresses. If a domain name is used, it matches all users in that domain.
21 * Example profile entry using OAuth for account hosted by gmail:
23 * sendfrom-gmail_address@example.com: -saslmech xoauth2 -authservice gmail -tls
24 * -server smtp.gmail.com -user gmail_login@example.com
26 * (Indentation indicates a continued line, as supported in MH profiles.) The username
27 * need not be the same as the sender address, which was extracted from the From: header line.
29 * Example profile entries that use an nmh credentials file:
31 * credentials: file:nmhcreds
32 * sendfrom-sendgrid_address@example.com: -sasl -tls -server smtp.sendgrid.net
33 * sendfrom-outbound.att.net: -sasl -initialtls -server outbound.att.net -port 465
34 * sendfrom-fastmail.com: -initialtls -sasl -saslmech LOGIN
35 * -server smtps-proxy.messagingengine.com -port 80
37 * where nmhcreds is in the user's nmh directory (from the Path profile component) and contains:
39 * machine smtp.sendgrid.net
40 * login sendgrid_login@example.com
42 * machine outbound.att.net
43 * login att_login@example.com
45 * machine smtps-proxy.messagingengine.com
46 * login fastmail_login@example.com
49 * 2) To enable, add a line like this to your profile:
51 * postproc: <docdir>/contrib/sendfrom
53 * with <docdir> expanded. This source file is in docdir. Also, "mhparam docdir" will show it.
55 * For more information on authentication to mail servers, see mhlogin(1) for OAuth
56 * services, and mh-profile(5) for login credentials.
58 * This program follows these steps:
60 * 2) Saves command line arguments in vec.
61 * 3) Extracts address and domain name from From: header line in draft.
62 * 4) Extracts any address or host specific switches to post(1) from profile. Appends
63 to vec, along with implied switches, such as OAUTH access token.
64 * 5) Calls post, or with -dryrun, echos how it would be called.
68 #include <h/fmt_scan.h>
69 #include <h/fmt_compile.h>
75 static int setup_oauth_params(char *vec
[], int *, int, const char **);
76 #endif /* OAUTH_SUPPORT */
78 extern char *mhlibexecdir
; /* from config.c */
80 static int get_from_header_info(const char *, const char **, const char **, const char **);
81 static const char *get_message_header_info(FILE *, char *);
82 static void merge_profile_entry(const char *, const char *, char *vec
[], int *vecp
);
83 static int run_program(char *, char **);
86 main(int argc
, char **argv
) {
87 /* Make sure that nmh's post gets called by specifying its full path. */
88 char *realpost
= concat(mhlibexecdir
, "/", "post", NULL
);
89 int whom
= 0, dryrun
= 0, snoop
= 0;
90 char **arguments
, **argp
;
96 if (nmh_init(argv
[0], 1)) { return NOTOK
; }
98 /* Save command line arguments in vec, except for the last argument, if it's the draft file path. */
99 vec
= argsplit(realpost
, &program
, &vecp
);
100 for (argp
= arguments
= getarguments(invo_name
, argc
, argv
, 1); *argp
; ++argp
) {
101 /* Don't pass -dryrun to post by putting it in vec. */
102 if (strcmp(*argp
, "-dryrun") == 0) {
105 vec
[vecp
++] = getcpy(*argp
);
107 if (strcmp(*argp
, "-snoop") == 0) {
109 } else if (strcmp(*argp
, "-whom") == 0) {
112 /* Need to keep filename, from last arg (though it's not used below with -whom). */
120 if (! whom
&& msg
== NULL
) {
121 adios(NULL
, "requires exactly 1 filename argument");
125 /* With post, but without -whom, need to keep filename as last arg. */
127 /* At this point, vecp points to the next argument to be added. Back it up to
128 remove the last one, which is the filename. Add it back at the end of vec below. */
131 /* Null terminate vec because it's used below. */
135 /* Some users might need the switch from the profile entry with -whom, but that would
136 require figuring out which command line argument has the filename. With -whom, it's
137 not necessarily the last one. */
139 const char *addr
, *host
;
142 /* Extract address and host from From: header line in draft. */
143 if (get_from_header_info(msg
, &addr
, &host
, &message
) != OK
) {
144 adios(msg
, (char *) message
);
147 /* Merge in any address or host specific switches to post(1) from profile. */
148 merge_profile_entry(addr
, host
, vec
, &vecp
);
154 for (vp
= vec
; *vp
; ++vp
) {
155 if (strcmp(*vp
, "xoauth2") == 0) {
159 if (setup_oauth_params(vec
, &vecp
, snoop
, &message
) != OK
) {
160 adios(NULL
, message
);
166 adios(NULL
, "sendfrom built without OAUTH_SUPPORT, so -saslmech xoauth2 is not supported");
167 #endif /* OAUTH_SUPPORT */
172 /* Append filename as last non-null vec entry. */
173 vec
[vecp
++] = getcpy(msg
);
177 /* Call post, or with -dryrun, echo how it would be called. */
179 /* Show the program name, because run_program() won't. */
180 printf("%s ", realpost
);
183 if (run_program(dryrun
? "echo" : realpost
, vec
) != OK
) {
184 adios(realpost
, "failure in ");
187 arglist_free(program
, vec
);
198 * For XOAUTH2, append access token, from mh_oauth_do_xoauth(), for the user to vec.
202 setup_oauth_params(char *vec
[], int *vecp
, int snoop
, const char **message
) {
203 const char *saslmech
= NULL
, *user
= NULL
, *auth_svc
= NULL
;
206 /* Make sure we have all the information we need. */
207 for (i
= 1; i
< *vecp
; ++i
) {
208 /* Don't support abbreviated switches, to avoid collisions in the future if new ones
210 if (! strcmp(vec
[i
-1], "-saslmech")) {
212 } else if (! strcmp(vec
[i
-1], "-user")) {
214 } else if (! strcmp(vec
[i
-1], "-authservice")) {
219 if (auth_svc
== NULL
) {
220 if (saslmech
&& ! strcasecmp(saslmech
, "xoauth2")) {
221 *message
= "must specify -authservice with -saslmech xoauth2";
226 *message
= "must specify -user with -saslmech xoauth2";
230 vec
[(*vecp
)++] = getcpy("-authservice");
231 if (saslmech
&& ! strcasecmp(saslmech
, "xoauth2")) {
232 vec
[(*vecp
)++] = mh_oauth_do_xoauth(user
, auth_svc
, snoop
? stderr
: NULL
);
234 vec
[(*vecp
)++] = getcpy(auth_svc
);
240 #endif /* OAUTH_SUPPORT */
244 * Extract user and domain from From: header line in draft.
248 get_from_header_info(const char *filename
, const char **addr
, const char **host
, const char **message
) {
252 if (stat (filename
, &st
) == NOTOK
) {
253 *message
= "unable to stat draft file";
257 if ((in
= fopen(filename
, "r")) != NULL
) {
258 char *addrformat
= "%(addr{from})", *hostformat
= "%(host{from})";
260 if ((*addr
= get_message_header_info(in
, addrformat
)) == NULL
) {
261 *message
= "unable to find From: address in";
266 if ((*host
= get_message_header_info(in
, hostformat
)) == NULL
) {
268 *message
= "unable to find From: host in";
275 *message
= "unable to open";
282 * Get formatted information from header of a message.
283 * Adapted from process_single_file() in uip/fmttest.c.
287 get_message_header_info(FILE *in
, char *format
) {
292 m_getfld_state_t gstate
= 0;
293 charstring_t buffer
= charstring_create(0);
296 dat
[0] = dat
[1] = dat
[4] = 0;
297 dat
[2] = fstat(fileno(in
), &st
) == 0 ? st
.st_size
: 0;
300 (void) fmt_compile(new_fs(NULL
, NULL
, format
), &fmt
, 1);
304 * Read in the message and process the header.
308 char name
[NAMESZ
], rbuf
[NMH_BUFSIZ
];
309 int bufsz
= sizeof rbuf
;
310 int state
= m_getfld(&gstate
, name
, rbuf
, &bufsz
, in
);
315 int bucket
= fmt_addcomptext(name
, rbuf
);
318 while (state
== FLDPLUS
) {
320 state
= m_getfld(&gstate
, name
, rbuf
, &bufsz
, in
);
321 fmt_appendcomp(bucket
, name
, rbuf
);
325 while (state
== FLDPLUS
) {
327 state
= m_getfld(&gstate
, name
, rbuf
, &bufsz
, in
);
334 } while (parsing_header
);
335 m_getfld_state_destroy(&gstate
);
337 fmt_scan(fmt
, buffer
, INT_MAX
, dat
, NULL
);
340 /* Trim trailing newline, if any. */
341 retval
= rtrim(charstring_buffer_copy((buffer
)));
342 charstring_free(buffer
);
349 * Look in profile for entry corresponding to addr or host, and add its contents to vec.
351 * Could do some of this automatically, by looking for:
352 * 1) access-$(mbox{from}) in oauth-svc file using mh_oauth_cred_load(), which isn't
353 * static and doesn't have side effects; free the result with mh_oauth_cred_free())
354 * 2) machine $(mbox{from}) in creds
355 * If no -server passed in from profile or commandline, could use smtp.<svc>.com for gmail,
356 * but that might not generalize for other svcs.
360 merge_profile_entry(const char *addr
, const char *host
, char *vec
[], int *vecp
) {
361 char *addr_entry
= concat("sendfrom-", addr
, NULL
);
362 char *profile_entry
= context_find(addr_entry
);
365 if (profile_entry
== NULL
) {
366 /* No entry for the user. Look for one for the host. */
367 char *host_entry
= concat("sendfrom-", host
, NULL
);
369 profile_entry
= context_find(host_entry
);
373 /* Use argsplit() to do the real work of splitting the args in the profile entry. */
374 if (profile_entry
&& strlen(profile_entry
) > 0) {
377 char **profile_vec
= argsplit(profile_entry
, &file
, &profile_vecp
);
380 for (i
= 0; i
< profile_vecp
; ++i
) {
381 vec
[(*vecp
)++] = getcpy(profile_vec
[i
]);
384 arglist_free(file
, profile_vec
);
394 run_program(char *realpost
, char **vec
) {
398 for (i
= 0; (child_id
= fork()) == NOTOK
&& i
< 5; ++i
) { sleep(5); }
402 /* oops -- fork error */
405 (void) execvp(realpost
, vec
);
406 fprintf(stderr
, "unable to exec ");
410 if (pidXwait(child_id
, realpost
)) {