find recipients for sendmail
[mmh] / uip / spost.c
1 /*
2 ** spost.c -- feed messages to sendmail
3 **
4 ** This is a simpler, faster, replacement for "post" for use
5 ** when "sendmail" is the transport system.
6 **
7 ** This code is Copyright (c) 2002, by the authors of nmh.  See the
8 ** COPYRIGHT file in the root directory of the nmh distribution for
9 ** complete copyright information.
10 */
11
12 #include <h/mh.h>
13 #include <signal.h>
14 #include <h/addrsbr.h>
15 #include <h/aliasbr.h>
16 #include <h/dropsbr.h>
17 #include <h/tws.h>
18 #include <h/utils.h>
19 #include <unistd.h>
20 #include <locale.h>
21 #include <sysexits.h>
22 #include <errno.h>
23
24 #define MAX_SM_FIELD 1476  /* < largest hdr field sendmail will accept */
25
26 static struct swit switches[] = {
27 #define VERBSW  0
28         { "verbose", 0 },
29 #define NVERBSW  1
30         { "noverbose", 2 },
31 #define VERSIONSW  2
32         { "Version", 0 },
33 #define HELPSW  3
34         { "help", 0 },
35 #define DEBUGSW  4
36         { "debug", -5 },
37 #define DISTSW  5
38         { "dist", -4 },  /* interface from dist */
39         { NULL, 0 }
40 };
41
42
43 /* flags for headers->flags */
44 #define HNOP  0x0000  /* just used to keep .set around */
45 #define HBAD  0x0001  /* bad header - don't let it through */
46 #define HADR  0x0002  /* header has an address field */
47 #define HSUB  0x0004  /* Subject: header */
48 #define HTRY  0x0008  /* try to send to addrs on header */
49 #define HBCC  0x0010  /* don't output this header */
50 #define HFCC  0x0020  /* FCC: type header */
51 #define HIGN  0x0040  /* ignore this header */
52
53 /* flags for headers->set */
54 #define MFRM  0x0001  /* we've seen a From: */
55 #define MDAT  0x0002  /* we've seen a Date: */
56 #define MRFM  0x0004  /* we've seen a Resent-From: */
57 #define MVIS  0x0008  /* we've seen sighted addrs */
58 #define MINV  0x0010  /* we've seen blind addrs */
59 #define MRDT  0x0020  /* we've seen a Resent-Date: */
60 #define MFMM  0x0040  /* The Mail is From a Alternative-Mailbox Addresse */
61
62 struct headers {
63         char *value;
64         unsigned int flags;
65         unsigned int set;
66 };
67
68 static struct headers NHeaders[] = {
69         { "Return-Path", HBAD, 0 },
70         { "Received", HBAD, 0 },
71         { "Reply-To", HADR, 0 },
72         { "From", HADR, MFRM },
73         { "Sender", HADR|HBAD, 0 },
74         { "Date", HNOP, MDAT },
75         { "Subject", HSUB, 0 },
76         { "To", HADR|HTRY, MVIS },
77         { "Cc", HADR|HTRY, MVIS },
78         { "Bcc", HADR|HTRY|HBCC, MINV },
79         { "Message-Id", HBAD, 0 },
80         { "Fcc", HFCC, 0 },
81         { "Envelope-From", HIGN, 0 },
82         { NULL, 0, 0 }
83 };
84
85 static struct headers RHeaders[] = {
86         { "Resent-Reply-To",   HADR, 0 },
87         { "Resent-From", HADR, MRFM },
88         { "Resent-Sender", HADR|HBAD, 0 },
89         { "Resent-Date", HNOP, MRDT },
90         { "Resent-Subject", HSUB, 0 },
91         { "Resent-To", HADR|HTRY, MVIS },
92         { "Resent-Cc", HADR|HTRY, MVIS },
93         { "Resent-Bcc", HADR|HTRY|HBCC, MINV },
94         { "Resent-Message-Id", HBAD, 0 },
95         { "Resent-Fcc", HFCC, 0 },
96         { "Reply-To", HADR, 0 },
97         { "Fcc", HIGN, 0 },
98         { "Envelope-From", HIGN, 0 },
99         { NULL, 0, 0 }
100 };
101
102
103 static int badmsg = 0;
104 static int verbose = 0;
105 static int debug = 0;
106 static int aliasflg = 0;  /* if going to process aliases */
107
108 static unsigned msgflags = 0;  /* what we've seen */
109
110 static enum {
111         normal, resent
112 } msgstate = normal;
113
114 static char *tmpfil;
115
116 static char *subject = NULL;  /* the subject field for BCC'ing */
117 static char fccs[BUFSIZ] = "";
118 struct mailname *bccs = NULL;  /* list of the bcc recipients */
119 struct mailname *recipients = NULL;  /* list of the recipients */
120 size_t recipientsc = 0;
121 struct mailname *sender = NULL;
122
123 static struct headers *hdrtab;  /* table for the message we're doing */
124 static FILE *out;  /* output (temp) file */
125
126 /*
127 ** static prototypes
128 */
129 static void putfmt(char *, char *, FILE *);
130 static void finish_headers(FILE *);
131 static int get_header(char *, struct headers *);
132 static void putadr(char *, struct mailname *);
133 static int putone(char *, int, int);
134 static void process_fcc(char *);
135 static void fcc(char *, char *);
136 static void process_bccs(char *);
137 static size_t do_aliasing(struct mailname *, struct mailname **);
138
139
140 int
141 main(int argc, char **argv)
142 {
143         int state, compnum;
144         char *cp, *msg = NULL, **argp, **arguments;
145         char **sargv, buf[BUFSIZ], name[NAMESZ];
146         FILE *in;
147
148         setlocale(LC_ALL, "");
149         invo_name = mhbasename(argv[0]);
150
151         context_read();
152
153         arguments = getarguments(invo_name, argc, argv, 0);
154         argp = arguments;
155
156         while ((cp = *argp++)) {
157                 if (*cp == '-') {
158                         switch (smatch(++cp, switches)) {
159                         case AMBIGSW:
160                                 ambigsw(cp, switches);
161                                 exit(EX_USAGE);
162                         case UNKWNSW:
163                                 adios(EX_USAGE, NULL, "-%s unknown", cp);
164
165                         case HELPSW:
166                                 snprintf(buf, sizeof(buf),
167                                                 "%s [switches] file",
168                                                 invo_name);
169                                 print_help(buf, switches, 1);
170                                 exit(argc == 2 ? EX_OK : EX_USAGE);
171                         case VERSIONSW:
172                                 print_version(invo_name);
173                                 exit(argc == 2 ? EX_OK : EX_USAGE);
174
175                         case DEBUGSW:
176                                 debug++;
177                                 continue;
178
179                         case DISTSW:
180                                 msgstate = resent;
181                                 continue;
182
183                         case VERBSW:
184                                 verbose++;
185                                 continue;
186                         case NVERBSW:
187                                 verbose = 0;
188                                 continue;
189                         }
190                 }
191                 if (msg) {
192                         adios(EX_USAGE, NULL, "only one message at a time!");
193                 } else {
194                         msg = cp;
195                 }
196         }
197
198         if (!msg) {
199                 adios(EX_USAGE, NULL, "usage: %s [switches] file", invo_name);
200         }
201
202         if ((in = fopen(msg, "r")) == NULL) {
203                 adios(EX_IOERR, msg, "unable to open");
204         }
205
206         if (debug) {
207                 verbose++;
208                 out = stdout;
209         } else {
210                 tmpfil = getcpy(m_mktemp2("/tmp/", invo_name, NULL, &out));
211         }
212
213         /* check for "Aliasfile:" profile entry */
214         if ((cp = context_find("Aliasfile"))) {
215                 char *dp, **ap; 
216
217                 aliasflg = 1;
218                 for (ap=brkstring(dp=getcpy(cp), " ", "\n"); ap && *ap;
219                                 ap++) {
220                         if ((state = alias(etcpath(*ap))) != AK_OK) {
221                                 adios(EX_IOERR, NULL, "aliasing error in file %s: %s",
222                                                 *ap, akerror(state));
223                         }
224                 }
225         }
226
227
228         hdrtab = (msgstate == normal) ? NHeaders : RHeaders;
229
230         for (compnum = 1, state = FLD;;) {
231                 switch (state = m_getfld(state, name, buf, sizeof(buf), in)) {
232                 case FLD:
233                 case FLDPLUS:
234                         compnum++;
235                         cp = getcpy(buf);
236                         while (state == FLDPLUS) {
237                                 state = m_getfld(state, name, buf,
238                                                 sizeof(buf), in);
239                                 cp = add(buf, cp);
240                         }
241                         putfmt(name, cp, out);
242                         free(cp);
243                         continue;
244
245                 case BODY:
246                         finish_headers(out);
247                         fprintf(out, "\n%s", buf);
248                         while (state == BODY) {
249                                 state = m_getfld(state, name, buf,
250                                                 sizeof(buf), in);
251                                 fputs(buf, out);
252                         }
253                         break;
254
255                 case FILEEOF:
256                         finish_headers(out);
257                         break;
258
259                 case LENERR:
260                 case FMTERR:
261                         adios(EX_DATAERR, NULL, "message format error in component #%d",
262                                         compnum);
263
264                 default:
265                         adios(EX_SOFTWARE, NULL, "getfld() returned %d", state);
266                 }
267                 break;
268         }
269         fclose(in);
270
271         if (debug) {
272                 struct mailname *i = recipients;
273                 /* stop here */
274                 puts("----EOM----");
275                 while (i) {
276                         fputs(i->m_mbox, stdout);
277                         if (i->m_host) {
278                                 fputs("@", stdout);
279                                 fputs(i->m_host, stdout);
280                         }
281                         fputs("\n", stdout);
282                         i = i->m_next;
283                         mnfree(recipients);
284                         recipients = i;
285                 }
286                 exit(EX_OK);
287         }
288
289         fclose(out);
290
291         /* process Fcc */
292         if (*fccs) {
293                 fcc(tmpfil, fccs);
294         }
295
296         if (bccs) {
297                 process_bccs(tmpfil);
298                 if (!(msgflags & MVIS)) {
299                         /* only Bcc rcpts: we're finished here */
300                         unlink(tmpfil);
301                         exit(EX_OK);
302                 }
303         }
304
305         /*
306         ** re-open the temp file, unlink it and exec sendmail, giving it
307         ** the msg temp file as std in.
308         */
309         if (!freopen(tmpfil, "r", stdin)) {
310                 adios(EX_IOERR, tmpfil, "can't reopen for sendmail");
311         }
312         unlink(tmpfil);
313
314         if (recipientsc == 0) {
315                 adios(EX_DATAERR, NULL, "message has no recipients");
316         }
317
318         sargv = mh_xmalloc(sizeof(char **) * (recipientsc + 4));
319
320         argp = sargv;
321         *argp++ = "send-mail";
322         *argp++ = "-i";  /* don't stop on "." */
323         if (verbose) {
324                 *argp++ = "-v";
325         }
326
327         while (recipients != NULL) {
328                 cp = getcpy(recipients->m_mbox);
329                 if (recipients->m_host) {
330                         cp = add("@", cp);
331                         cp = add(recipients->m_host, cp);
332                 }
333                 *argp++ = cp;
334                 cp = NULL;
335                 recipients = recipients->m_next;
336         }
337         *argp = NULL;
338         execvp(sendmail, sargv);
339
340         if (errno == E2BIG) {
341                 adios(EX_DATAERR, sendmail, "too much arguments, probably to much recipients");
342         }
343
344         adios(EX_OSERR, sendmail, "can't exec");
345         return -1;
346 }
347
348
349 /* DRAFT GENERATION */
350
351 static void
352 putfmt(char *name, char *str, FILE *out)
353 {
354         int i;
355         struct headers *hdr;
356         struct mailname addr_start, *addr_end;
357         size_t addrc;
358
359         addr_end = &addr_start;
360         addr_end->m_next = NULL;
361
362         /* remove leading whitespace */
363         while (*str==' ' || *str=='\t') {
364                 str++;
365         }
366
367         if ((i = get_header(name, hdrtab)) == NOTOK) {
368                 /* no header we would care for */
369                 if (mh_strcasecmp(name, attach_hdr)==0) {
370                         return;
371                 }
372                 if (mh_strcasecmp(name, sign_hdr)==0) {
373                         return;
374                 }
375                 if (mh_strcasecmp(name, enc_hdr)==0) {
376                         return;
377                 }
378                 /* push it through */
379                 fprintf(out, "%s: %s", name, str);
380                 return;
381         }
382         /* it's one of the interesting headers */
383         hdr = &hdrtab[i];
384
385         if (hdr->flags & HIGN || strcmp(str, "\n")==0) {
386                 return;
387         }
388
389         if (hdr->flags & HBAD) {
390                 advise(NULL, "illegal header line -- %s:", name);
391                 badmsg++;
392                 return;
393         }
394
395         msgflags |= hdr->set;
396
397         if (hdr->flags & HFCC) {
398                 process_fcc(str);
399                 return;
400         }
401
402         if (hdr->flags & HSUB) {
403                 subject = getcpy(str);
404         }
405
406         if (!(hdr->flags & HADR)) {
407                 fprintf(out, "%s: %s", name, str);
408                 return;
409         }
410
411         addrc = getmboxes(str, &addr_end);
412
413         if (aliasflg) {
414                 addrc += do_aliasing(&addr_start, &addr_end);
415         }
416
417         if (hdr->flags & HBCC) {
418                 addr_end->m_next = bccs;
419                 bccs = addr_start.m_next;
420                 return;
421         }
422
423         if (hdr->set & MFRM) {
424                 struct mailname *mp = NULL;
425                 struct mailname *my = NULL;
426                 unsigned int fromcnt = 0;
427
428                 /* needed because the address parser holds global state */
429                 ismymbox(NULL);
430
431                 for ( mp = addr_start.m_next; mp; mp = mp->m_next) {
432                         if (ismymbox(mp)) {
433                                 msgflags |= MFMM;
434                                 if (my == NULL) {
435                                         my = mp;
436                                 }
437                         }
438                         fromcnt++;
439                 }
440
441                 if (fromcnt > 1) {
442                         sender = my;
443                 }
444         }
445
446         putadr(name, addr_start.m_next);
447
448         if (hdr->flags & HTRY) {
449                 addr_end->m_next = recipients;
450                 recipients = addr_start.m_next;
451                 recipientsc += i;
452         }
453 }
454
455
456 /*
457 ** Add yet missing headers.
458 */
459 static void
460 finish_headers(FILE *out)
461 {
462         char *cp;
463         char from[BUFSIZ];  /* my network address */
464         char signature[BUFSIZ];  /* my signature */
465         char *resentstr = (msgstate == resent) ? "Resent-" : "";
466
467         if (!(msgflags & MDAT)) {
468                 fprintf(out, "%sDate: %s\n", resentstr, dtimenow());
469         }
470
471         if (sender != NULL) {
472                 snprintf(signature, sizeof(signature), "%s", sender->m_text);
473         } else if ((cp = context_find("Default-From")) != NULL) {
474                 snprintf(signature, sizeof(signature), "%s", cp);
475         } else {
476                 snprintf(from, sizeof(from), "%s@%s", getusername(), LocalName());
477                 if ((cp = getfullname()) && *cp) {
478                         snprintf(signature, sizeof(signature), "%s <%s>", cp, from);
479                 } else {
480                         snprintf(signature, sizeof(signature), "%s", from);
481                 }
482         }
483         if (!(msgflags & MFRM)) {
484                 fprintf(out, "%sFrom: %s\n", resentstr, signature);
485         } else {
486                 /*
487                 ** Add a Sender: header because the From: header could
488                 ** be fake or contain multiple addresses.
489                 */
490                 if (!(msgflags & MFMM) || sender != NULL) {
491                         fprintf(out, "%sSender: %s\n", resentstr, signature);
492                 }
493         }
494         if (!(msgflags & MVIS)) {
495                 fprintf(out, "%sBcc: undisclosed-recipients:;\n", resentstr);
496         }
497         if (badmsg) {
498                 unlink(tmpfil);
499                 adios(EX_DATAERR, NULL, "re-format message and try again");
500         }
501 }
502
503
504 /*
505 ** Return index of the requested header in the table, or NOTOK if missing.
506 */
507 static int
508 get_header(char *header, struct headers *table)
509 {
510         struct headers *h;
511
512         for (h=table; h->value; h++) {
513                 if (mh_strcasecmp(header, h->value)==0) {
514                         return (h - table);
515                 }
516         }
517         return NOTOK;
518 }
519
520
521 /*
522 ** output the address list for header "name".  The address list
523 ** is a linked list of mailname structs.  "nl" points to the head
524 ** of the list.  Alias substitution should be done on nl.
525 */
526 static void
527 putadr(char *name, struct mailname *nl)
528 {
529         struct mailname *mp, *mp2;
530         int linepos;
531         int namelen;
532
533         fprintf(out, "%s: ", name);
534         namelen = strlen(name) + 2;
535         linepos = namelen;
536
537         for (mp = nl; mp; ) {
538                 if (linepos > MAX_SM_FIELD) {
539                         fprintf(out, "\n%s: ", name);
540                         linepos = namelen;
541                 }
542                 linepos = putone(mp->m_text, linepos, namelen);
543                 mp2 = mp;
544                 mp = mp->m_next;
545         }
546         putc('\n', out);
547 }
548
549 static int
550 putone(char *adr, int pos, int indent)
551 {
552         int len;
553         static int linepos;
554
555         len = strlen(adr);
556         if (pos == indent) {
557                 linepos = pos;
558         } else if (linepos+len > OUTPUTLINELEN) {
559                 fprintf(out, ",\n%*s", indent, "");
560                 linepos = indent;
561                 pos += indent + 2;
562         } else {
563                 fputs(", ", out);
564                 linepos += 2;
565                 pos += 2;
566         }
567         fputs(adr, out);
568
569         linepos += len;
570         return (pos+len);
571 }
572
573
574 static void
575 process_fcc(char *str)
576 {
577         char *cp, *pp;
578         int state = 0;
579
580         if (strlen(str)+strlen(fccs) > sizeof fccs /2) {
581                 adios(EX_DATAERR, NULL, "Too much Fcc data");
582         }
583         /* todo: better have three states: SEPARATOR, PLUS, WORD */
584         for (cp=pp=str; *cp; cp++) {
585                 switch (*cp) {
586                 case ' ':
587                 case '\t':
588                 case '\n':
589                 case ',':
590                         if (state != 0) {
591                                 state = 0;
592                                 *cp = '\0';
593                                 if (*pp=='+' || *pp=='@') {
594                                         strcat(fccs, " ");
595                                 } else {
596                                         strcat(fccs, " +");
597                                 }
598                                 strcat(fccs, pp);
599                         }
600                         break;
601                 default:
602                         if (state == 0) {
603                                 state = 1;
604                                 pp = cp;
605                         }
606                         break;
607                 }
608         }
609 }
610
611
612 static void
613 fcc(char *file, char *folders)
614 {
615         int status;
616         char cmd[BUFSIZ];
617
618         if (verbose) {
619                 printf("%sFcc: %s\n", msgstate == resent ? "Resent-" : "",
620                                 folders);
621                 fflush(stdout);
622         }
623         if (100+strlen(file)+strlen(folders) > sizeof cmd) {
624                 adios(EX_DATAERR, NULL, "Too much Fcc data");
625         }
626         /* hack: read from /dev/null and refile(1) won't question us */
627         snprintf(cmd, sizeof cmd, "</dev/null refile -link -file '%s' %s",
628                         file, folders);
629         status = system(cmd);
630         if (status == -1) {
631                 fprintf(stderr, "Skipped %sFcc %s: unable to system().\n",
632                                 msgstate == resent ? "Resent-" : "", folders);
633         } else if (status != 0) {
634                 fprintf(stderr, "%sFcc %s: Problems occured.\n",
635                                 msgstate == resent ? "Resent-" : "", folders);
636         }
637 }
638
639
640 /* BCC GENERATION */
641
642 static void
643 process_bccs(char *origmsg)
644 {
645         char *bccdraft = NULL;
646         struct mailname *mp = NULL;
647         FILE *out = NULL;
648
649         for (mp=bccs; mp; mp=mp->m_next) {
650                 bccdraft = getcpy(m_mktemp2("/tmp/", invo_name, NULL, &out));
651                 fprintf(out, "To: %s\n", mp->m_text);
652                 fprintf(out, "Subject: [BCC] %s", subject ? subject : "");
653                 fprintf(out, "%s: %s\n", attach_hdr, origmsg);
654                 fprintf(out, "------------\n");
655                 fclose(out);
656
657                 if (execprogl("send", "send", bccdraft, (char *)NULL) != 0) {
658                         admonish(invo_name, "Problems to send Bcc to %s",
659                                         mp->m_text);
660                         unlink(bccdraft);
661                 }
662         }
663 }
664
665 /*
666  * Do aliasing on a mailname linked list
667  * Begin at start->m_next
668  * End if m_next == NULL
669  * **end is set to the new end.
670  * Return the number of new mainames in the list
671  */
672
673 static size_t
674 do_aliasing(struct mailname *start, struct mailname **end)
675 {
676         struct mailname *prev, *cur;
677         char *cp;
678         size_t i = 0;
679
680         prev = start;
681         cur = prev->m_next;
682
683         while (cur != NULL) {
684                 if (cur->m_nohost) {
685                         cp = akvalue(cur->m_mbox);
686                         if (strcmp(cp, cur->m_mbox) != 0) {
687                                 prev->m_next = cur->m_next;
688                                 i += getmboxes(cp, &prev);
689                                 i -= 1;
690                                 mnfree(cur);
691                         } else {
692                                 prev = cur;
693                         }
694                 } else {
695                         prev = cur;
696                 }
697                 cur = prev->m_next;
698         }
699         *end = prev;
700         return i;
701 }