]> diplodocus.org Git - nmh/blob - docs/contrib/sendfrom.c
Added consts to adios(), advise(), and advertise() to sync master
[nmh] / docs / contrib / sendfrom.c
1 /*
2 * sendfrom.c -- postproc that selects user, and then mail server, from draft
3 *
4 * Author: David Levine <levinedl@acm.org>
5 *
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.
9 *
10 * To use:
11 *
12 * 1) Add profile entries of the form:
13 *
14 * sendfrom-<email address or domain name>: <post(1) switches>
15 *
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.
20 *
21 * Example profile entry using OAuth for account hosted by gmail:
22 *
23 * sendfrom-gmail_address@example.com: -saslmech xoauth2 -authservice gmail -tls
24 * -server smtp.gmail.com -user gmail_login@example.com
25 *
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.
28 *
29 * Example profile entries that use an nmh credentials file:
30 *
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
36 *
37 * where nmhcreds is in the user's nmh directory (from the Path profile component) and contains:
38 *
39 * machine smtp.sendgrid.net
40 * login sendgrid_login@example.com
41 * password ********
42 * machine outbound.att.net
43 * login att_login@example.com
44 * password ********
45 * machine smtps-proxy.messagingengine.com
46 * login fastmail_login@example.com
47 * password ********
48 *
49 * 2) To enable, add a line like this to your profile:
50 *
51 * postproc: <docdir>/contrib/sendfrom
52 *
53 * with <docdir> expanded. This source file is in docdir. Also, "mhparam docdir" will show it.
54 *
55 * For more information on authentication to mail servers, see mhlogin(1) for OAuth
56 * services, and mh-profile(5) for login credentials.
57 *
58 * This program follows these steps:
59 * 1) Loads profile.
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.
65 */
66
67 #include <h/mh.h>
68 #include <h/fmt_scan.h>
69 #include <h/fmt_compile.h>
70 #include <h/utils.h>
71
72 #ifdef OAUTH_SUPPORT
73 #include <h/oauth.h>
74
75 static int setup_oauth_params(char *vec[], int *, int, const char **);
76 #endif /* OAUTH_SUPPORT */
77
78 extern char *mhlibexecdir; /* from config.c */
79
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 **);
84
85 int
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;
91 char *program, **vec;
92 int vecp = 0;
93 char *msg = NULL;
94
95 /* Load profile. */
96 if (nmh_init(argv[0], 1)) { return NOTOK; }
97
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) {
103 dryrun = 1;
104 } else {
105 vec[vecp++] = getcpy(*argp);
106
107 if (strcmp(*argp, "-snoop") == 0) {
108 snoop = 1;
109 } else if (strcmp(*argp, "-whom") == 0) {
110 whom = 1;
111 } else {
112 /* Need to keep filename, from last arg (though it's not used below with -whom). */
113 msg = *argp;
114 }
115 }
116 }
117
118 free(arguments);
119
120 if (! whom && msg == NULL) {
121 adios(NULL, "requires exactly 1 filename argument");
122 } else {
123 char **vp;
124
125 /* With post, but without -whom, need to keep filename as last arg. */
126 if (! whom) {
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. */
129 free(vec[--vecp]);
130 }
131 /* Null terminate vec because it's used below. */
132 vec[vecp] = NULL;
133
134 if (! whom) {
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. */
138
139 const char *addr, *host;
140 const char *message;
141
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);
145 }
146
147 /* Merge in any address or host specific switches to post(1) from profile. */
148 merge_profile_entry(addr, host, vec, &vecp);
149 free((void *) host);
150 free((void *) addr);
151 }
152
153 vec[vecp] = NULL;
154 for (vp = vec; *vp; ++vp) {
155 if (strcmp(*vp, "xoauth2") == 0) {
156 #ifdef OAUTH_SUPPORT
157 const char *message;
158
159 if (setup_oauth_params(vec, &vecp, snoop, &message) != OK) {
160 adios(NULL, message);
161 }
162 break;
163 #else
164 NMH_UNUSED(snoop);
165 NMH_UNUSED(vp);
166 adios(NULL, "sendfrom built without OAUTH_SUPPORT, so -saslmech xoauth2 is not supported");
167 #endif /* OAUTH_SUPPORT */
168 }
169 }
170
171 if (! whom) {
172 /* Append filename as last non-null vec entry. */
173 vec[vecp++] = getcpy(msg);
174 vec[vecp] = NULL;
175 }
176
177 /* Call post, or with -dryrun, echo how it would be called. */
178 if (dryrun) {
179 /* Show the program name, because run_program() won't. */
180 printf("%s ", realpost);
181 fflush(stdout);
182 }
183 if (run_program(dryrun ? "echo" : realpost, vec) != OK) {
184 adios(realpost, "failure in ");
185 }
186
187 arglist_free(program, vec);
188 free(realpost);
189 }
190
191 return 0;
192 }
193
194
195
196 #ifdef OAUTH_SUPPORT
197 /*
198 * For XOAUTH2, append access token, from mh_oauth_do_xoauth(), for the user to vec.
199 */
200 static
201 int
202 setup_oauth_params(char *vec[], int *vecp, int snoop, const char **message) {
203 const char *saslmech = NULL, *user = NULL, *auth_svc = NULL;
204 int i;
205
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
209 are added. */
210 if (! strcmp(vec[i-1], "-saslmech")) {
211 saslmech = vec[i];
212 } else if (! strcmp(vec[i-1], "-user")) {
213 user = vec[i];
214 } else if (! strcmp(vec[i-1], "-authservice")) {
215 auth_svc = vec[i];
216 }
217 }
218
219 if (auth_svc == NULL) {
220 if (saslmech && ! strcasecmp(saslmech, "xoauth2")) {
221 *message = "must specify -authservice with -saslmech xoauth2";
222 return NOTOK;
223 }
224 } else {
225 if (user == NULL) {
226 *message = "must specify -user with -saslmech xoauth2";
227 return NOTOK;
228 }
229
230 vec[(*vecp)++] = getcpy("-authservice");
231 if (saslmech && ! strcasecmp(saslmech, "xoauth2")) {
232 vec[(*vecp)++] = mh_oauth_do_xoauth(user, auth_svc, snoop ? stderr : NULL);
233 } else {
234 vec[(*vecp)++] = getcpy(auth_svc);
235 }
236 }
237
238 return 0;
239 }
240 #endif /* OAUTH_SUPPORT */
241
242
243 /*
244 * Extract user and domain from From: header line in draft.
245 */
246 static
247 int
248 get_from_header_info(const char *filename, const char **addr, const char **host, const char **message) {
249 struct stat st;
250 FILE *in;
251
252 if (stat (filename, &st) == NOTOK) {
253 *message = "unable to stat draft file";
254 return NOTOK;
255 }
256
257 if ((in = fopen(filename, "r")) != NULL) {
258 char *addrformat = "%(addr{from})", *hostformat = "%(host{from})";
259
260 if ((*addr = get_message_header_info(in, addrformat)) == NULL) {
261 *message = "unable to find From: address in";
262 return NOTOK;
263 }
264 rewind(in);
265
266 if ((*host = get_message_header_info(in, hostformat)) == NULL) {
267 fclose(in);
268 *message = "unable to find From: host in";
269 return NOTOK;
270 }
271 fclose(in);
272
273 return OK;
274 } else {
275 *message = "unable to open";
276 return NOTOK;
277 }
278 }
279
280
281 /*
282 * Get formatted information from header of a message.
283 * Adapted from process_single_file() in uip/fmttest.c.
284 */
285 static
286 const char *
287 get_message_header_info(FILE *in, char *format) {
288 int dat[5];
289 struct format *fmt;
290 struct stat st;
291 int parsing_header;
292 m_getfld_state_t gstate = 0;
293 charstring_t buffer = charstring_create(0);
294 char *retval;
295
296 dat[0] = dat[1] = dat[4] = 0;
297 dat[2] = fstat(fileno(in), &st) == 0 ? st.st_size : 0;
298 dat[3] = INT_MAX;
299
300 (void) fmt_compile(new_fs(NULL, NULL, format), &fmt, 1);
301 free_fs();
302
303 /*
304 * Read in the message and process the header.
305 */
306 parsing_header = 1;
307 do {
308 char name[NAMESZ], rbuf[NMH_BUFSIZ];
309 int bufsz = sizeof rbuf;
310 int state = m_getfld(&gstate, name, rbuf, &bufsz, in);
311
312 switch (state) {
313 case FLD:
314 case FLDPLUS: {
315 int bucket = fmt_addcomptext(name, rbuf);
316
317 if (bucket != -1) {
318 while (state == FLDPLUS) {
319 bufsz = sizeof rbuf;
320 state = m_getfld(&gstate, name, rbuf, &bufsz, in);
321 fmt_appendcomp(bucket, name, rbuf);
322 }
323 }
324
325 while (state == FLDPLUS) {
326 bufsz = sizeof rbuf;
327 state = m_getfld(&gstate, name, rbuf, &bufsz, in);
328 }
329 break;
330 }
331 default:
332 parsing_header = 0;
333 }
334 } while (parsing_header);
335 m_getfld_state_destroy(&gstate);
336
337 fmt_scan(fmt, buffer, INT_MAX, dat, NULL);
338 fmt_free(fmt, 1);
339
340 /* Trim trailing newline, if any. */
341 retval = rtrim(charstring_buffer_copy((buffer)));
342 charstring_free(buffer);
343
344 return retval;
345 }
346
347
348 /*
349 * Look in profile for entry corresponding to addr or host, and add its contents to vec.
350 *
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.
357 */
358 static
359 void
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);
363
364 free(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);
368
369 profile_entry = context_find(host_entry);
370 free(host_entry);
371 }
372
373 /* Use argsplit() to do the real work of splitting the args in the profile entry. */
374 if (profile_entry && strlen(profile_entry) > 0) {
375 int profile_vecp;
376 char *file;
377 char **profile_vec = argsplit(profile_entry, &file, &profile_vecp);
378 int i;
379
380 for (i = 0; i < profile_vecp; ++i) {
381 vec[(*vecp)++] = getcpy(profile_vec[i]);
382 }
383
384 arglist_free(file, profile_vec);
385 }
386 }
387
388
389 /*
390 * Fork and exec.
391 */
392 static
393 int
394 run_program(char *realpost, char **vec) {
395 pid_t child_id;
396 int i;
397
398 for (i = 0; (child_id = fork()) == NOTOK && i < 5; ++i) { sleep(5); }
399
400 switch (child_id) {
401 case -1:
402 /* oops -- fork error */
403 return NOTOK;
404 case 0:
405 (void) execvp(realpost, vec);
406 fprintf(stderr, "unable to exec ");
407 perror(realpost);
408 _exit(-1);
409 default:
410 if (pidXwait(child_id, realpost)) {
411 return NOTOK;
412 }
413 }
414
415 return OK;
416 }