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