Various rework in spost.
[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
20 #define MAX_SM_FIELD 1476  /* < largest hdr field sendmail will accept */
21 #define FCCS 10  /* max number of fccs allowed */
22
23 struct swit switches[] = {
24 #define VERBSW  0
25         { "verbose", 0 },
26 #define NVERBSW  1
27         { "noverbose", 0 },
28 #define WATCSW  2
29         { "watch", 0 },
30 #define NWATCSW  3
31         { "nowatch", 0 },
32 #define ALIASW  4
33         { "alias aliasfile", 0 },
34 #define NALIASW  5
35         { "noalias", 0 },
36 #define VERSIONSW  6
37         { "version", 0 },
38 #define HELPSW  7
39         { "help", 0 },
40 #define DEBUGSW  8
41         { "debug", -5 },
42 #define DISTSW  9
43         { "dist", -4 },  /* interface from dist */
44 #define PUSHSW  10  /* fork to sendmail then exit */
45         { "push", -4 },
46 #define NPUSHSW  11  /* exec sendmail */
47         { "nopush", -6 },
48 #define LIBSW  12
49         { "library directory", -7 },
50         { NULL, 0 }
51 };
52
53
54 /* flags for headers->flags */
55 #define HNOP  0x0000  /* just used to keep .set around */
56 #define HBAD  0x0001  /* bad header - don't let it through */
57 #define HADR  0x0002  /* header has an address field */
58 #define HSUB  0x0004  /* Subject: header */
59 #define HTRY  0x0008  /* try to send to addrs on header */
60 #define HBCC  0x0010  /* don't output this header */
61 #define HFCC  0x0020  /* FCC: type header */
62 #define HIGN  0x0040  /* ignore this header */
63
64 /* flags for headers->set */
65 #define MFRM  0x0001  /* we've seen a From: */
66 #define MDAT  0x0002  /* we've seen a Date: */
67 #define MRFM  0x0004  /* we've seen a Resent-From: */
68 #define MVIS  0x0008  /* we've seen sighted addrs */
69 #define MINV  0x0010  /* we've seen blind addrs */
70 #define MRDT  0x0020  /* we've seen a Resent-Date: */
71
72 struct headers {
73         char *value;
74         unsigned int flags;
75         unsigned int set;
76 };
77
78
79 static struct headers NHeaders[] = {
80         { "Return-Path", HBAD, 0 },
81         { "Received", HBAD, 0 },
82         { "Reply-To", HADR, 0 },
83         { "From", HADR, MFRM },
84         { "Sender", HADR|HBAD, 0 },
85         { "Date", HNOP, MDAT },
86         { "Subject", HSUB, 0 },
87         { "To", HADR|HTRY, MVIS },
88         { "Cc", HADR|HTRY, MVIS },
89         { "Bcc", HADR|HTRY|HBCC, MINV },
90         { "Message-Id", HBAD, 0 },
91         { "Fcc", HFCC, 0 },
92         { NULL, 0, 0 }
93 };
94
95 static struct headers RHeaders[] = {
96         { "Resent-Reply-To",   HADR, 0 },
97         { "Resent-From", HADR, MRFM },
98         { "Resent-Sender", HADR|HBAD, 0 },
99         { "Resent-Date", HNOP, MRDT },
100         { "Resent-Subject", HSUB, 0 },
101         { "Resent-To", HADR|HTRY, MVIS },
102         { "Resent-Cc", HADR|HTRY, MVIS },
103         { "Resent-Bcc", HADR|HTRY|HBCC, MINV },
104         { "Resent-Message-Id", HBAD, 0 },
105         { "Resent-Fcc", HFCC, 0 },
106         { "Reply-To", HADR, 0 },
107         { "Fcc", HIGN, 0 },
108         { NULL, 0, 0 }
109 };
110
111
112 static int badmsg = 0;  /* message has bad semantics */
113 static int verbose = 0;  /* spell it out */
114 static int debug = 0;  /* debugging post */
115 static int watch = 0;  /* watch the delivery process */
116 static int pushflg = 0;  /* if going to fork to sendmail */
117 static int aliasflg = 0;  /* if going to process aliases */
118
119 static unsigned msgflags = 0;  /* what we've seen */
120
121 static enum {
122         normal, resent
123 } msgstate = normal;
124
125 static char tmpfil[] = "/tmp/spost-XXXXXX";
126 static char bccfil[] = "/tmp/spost-bcc-XXXXXX";
127
128 static char *subject = NULL;  /* the subject field for BCC'ing */
129 static char fccs[BUFSIZ] = "";
130 struct mailname *bccs = NULL;
131
132 static struct headers *hdrtab;  /* table for the message we're doing */
133 static FILE *out;  /* output (temp) file */
134
135 extern char *sendmail;
136
137 /*
138 ** static prototypes
139 */
140 static void putfmt(char *, char *, FILE *);
141 static void finish_headers(FILE *);
142 static int get_header(char *, struct headers *);
143 static void putadr(char *, struct mailname *);
144 static int putone(char *, int, int);
145 static void process_fcc(char *);
146 static void fcc(char *, char *);
147 static void make_bcc_draft(char *);
148
149
150 int
151 main(int argc, char **argv)
152 {
153         int state, i, pid, compnum;
154         char *cp, *msg = NULL, **argp, **arguments;
155         char *sargv[16], buf[BUFSIZ], name[NAMESZ];
156         FILE *in;
157
158 #ifdef LOCALE
159         setlocale(LC_ALL, "");
160 #endif
161         invo_name = mhbasename(argv[0]);
162
163         /* foil search of user profile/context */
164         if (context_foil(NULL) == -1)
165                 done(1);
166
167         arguments = getarguments(invo_name, argc, argv, 0);
168         argp = arguments;
169
170         while ((cp = *argp++)) {
171                 if (*cp == '-') {
172                         switch (smatch(++cp, switches)) {
173                         case AMBIGSW:
174                                 ambigsw(cp, switches);
175                                 done(1);
176                         case UNKWNSW:
177                                 adios(NULL, "-%s unknown", cp);
178
179                         case HELPSW:
180                                 snprintf(buf, sizeof(buf),
181                                                 "%s [switches] file",
182                                                 invo_name);
183                                 print_help(buf, switches, 1);
184                                 done(1);
185                         case VERSIONSW:
186                                 print_version(invo_name);
187                                 done(1);
188
189                         case DEBUGSW:
190                                 debug++;
191                                 continue;
192
193                         case DISTSW:
194                                 msgstate = resent;
195                                 continue;
196
197                         case VERBSW:
198                                 verbose++;
199                                 continue;
200                         case NVERBSW:
201                                 verbose = 0;
202                                 continue;
203
204                         case WATCSW:
205                                 watch++;
206                                 continue;
207                         case NWATCSW:
208                                 watch = 0;
209                                 continue;
210
211                         case PUSHSW:
212                                 pushflg++;
213                                 continue;
214                         case NPUSHSW:
215                                 pushflg = 0;
216                                 continue;
217
218                         case ALIASW:
219                                 if (!(cp = *argp++) || *cp == '-')
220                                         adios(NULL, "missing argument to %s",
221                                                         argp[-2]);
222                                 aliasflg = 1;
223                                 if ((state = alias(cp)) != AK_OK)
224                                         adios(NULL, "aliasing error in file %s - %s", cp, akerror(state));
225                                 continue;
226                         case NALIASW:
227                                 aliasflg = 0;
228                                 continue;
229
230                         case LIBSW:
231                                 if (!(cp = *argp++) || *cp == '-')
232                                         adios(NULL, "missing argument to %s",
233                                                         argp[-2]);
234                                 /* create a minimal context */
235                                 if (context_foil(cp) == -1)
236                                         done(1);
237                                 continue;
238                         }
239                 }
240                 if (msg)
241                         adios(NULL, "only one message at a time!");
242                 else
243                         msg = cp;
244         }
245
246         if (!msg)
247                 adios(NULL, "usage: %s [switches] file", invo_name);
248
249         if ((in = fopen(msg, "r")) == NULL)
250                 adios(msg, "unable to open");
251
252         if (debug) {
253                 verbose++;
254                 out = stdout;
255         } else {
256 #ifdef HAVE_MKSTEMP
257                 if ((out = fdopen(mkstemp(tmpfil), "w")) == NULL)
258                         adios(tmpfil, "unable to create");
259 #else
260                 mktemp(tmpfil);
261                 if ((out = fopen(tmpfil, "w")) == NULL)
262                         adios(tmpfil, "unable to create");
263                 chmod(tmpfil, 0600);
264 #endif
265         }
266
267         hdrtab = (msgstate == normal) ? NHeaders : RHeaders;
268
269         for (compnum = 1, state = FLD;;) {
270                 switch (state = m_getfld(state, name, buf, sizeof(buf), in)) {
271                 case FLD:
272                         compnum++;
273                         putfmt(name, buf, out);
274                         continue;
275
276                 case FLDPLUS:
277                         compnum++;
278                         cp = add(buf, cp);
279                         while (state == FLDPLUS) {
280                                 state = m_getfld(state, name, buf,
281                                                 sizeof(buf), in);
282                                 cp = add(buf, cp);
283                         }
284                         putfmt(name, cp, out);
285                         free(cp);
286                         continue;
287
288                 case BODY:
289                         finish_headers(out);
290                         fprintf(out, "\n%s", buf);
291                         while (state == BODY) {
292                                 state = m_getfld(state, name, buf,
293                                                 sizeof(buf), in);
294                                 fputs(buf, out);
295                         }
296                         break;
297
298                 case FILEEOF:
299                         finish_headers(out);
300                         break;
301
302                 case LENERR:
303                 case FMTERR:
304                         adios(NULL, "message format error in component #%d",
305                                         compnum);
306
307                 default:
308                         adios(NULL, "getfld() returned %d", state);
309                 }
310                 break;
311         }
312         fclose(in);
313
314         if (debug) {
315                 /* stop here */
316                 done(0);
317         }
318
319         fclose(out);
320
321         /* process Fcc */
322         if (*fccs) {
323                 fcc(tmpfil, fccs);
324         }
325
326         /* process Bcc */
327         if (bccs) {
328                 make_bcc_draft(tmpfil);
329         }
330
331         /*
332         ** re-open the temp file, unlink it and exec sendmail, giving it
333         ** the msg temp file as std in.
334         */
335         if (!freopen(tmpfil, "r", stdin)) {
336                 adios(tmpfil, "can't reopen for sendmail");
337         }
338         unlink(tmpfil);
339
340         argp = sargv;
341         *argp++ = "send-mail";
342         *argp++ = "-m";  /* send to me too */
343         *argp++ = "-t";  /* read msg for recipients */
344         *argp++ = "-i";  /* don't stop on "." */
345         if (watch || verbose) {
346                 *argp++ = "-v";
347                 pushflg = 0;
348         }
349         *argp = NULL;
350
351         if (pushflg) {
352                 /* fork to a child to run sendmail */
353                 for (i=0; (pid = fork()) == NOTOK && i < 5; i++)
354                         sleep(5);
355                 switch (pid) {
356                 case NOTOK:
357                         fprintf(verbose ? stdout : stderr,
358                                         "%s: can't fork to %s\n",
359                                         invo_name, sendmail);
360                         exit(-1);
361                 case OK:
362                         /* we're the child .. */
363                         break;
364                 default:
365                         exit(0);
366                 }
367         }
368
369         if (0 /*bccs*/) {
370                 /* fork to a child to run sendmail */
371                 for (i=0; (pid = fork()) == NOTOK && i < 5; i++)
372                         sleep(5);
373                 switch (pid) {
374                 case NOTOK:
375                         fprintf(verbose ? stdout : stderr,
376                                         "%s: can't fork to %s\n",
377                                         invo_name, sendmail);
378                         exit(-1);
379                 case OK:
380                         /* we're the child .. */
381                         exit(0);
382                 default:
383                         exit(0);
384                 }
385         }
386
387         execv(sendmail, sargv);
388         adios(sendmail, "can't exec");
389         return 0;  /* dead code to satisfy the compiler */
390 }
391
392 /* DRAFT GENERATION */
393
394 static void
395 putfmt(char *name, char *str, FILE *out)
396 {
397         int i;
398         char *cp;
399         struct headers *hdr;
400
401         /* remove all leading whitespace (even newlines) */
402         while (*str==' ' || *str=='\t' || *str=='\n') {
403                 str++;
404         }
405
406         if ((i = get_header(name, hdrtab)) == NOTOK) {
407                 /* no header we would care for: push it through */
408                 fprintf(out, "%s: %s", name, str);
409                 return;
410         }
411         /* it's one of the interesting headers */
412         hdr = &hdrtab[i];
413
414         if (hdr->flags & HIGN || !*str) {
415                 return;
416         }
417
418         if (hdr->flags & HBAD) {
419                 advise(NULL, "illegal header line -- %s:", name);
420                 badmsg++;
421                 return;
422         }
423
424         msgflags |= hdr->set;
425
426         if (hdr->flags & HFCC) {
427                 process_fcc(str);
428                 return;
429         }
430
431         if (hdr->flags & HBCC) {
432                 struct mailname *mp = NULL;
433
434                 /* Create list of Bcc addrs. */
435                 while ((cp = getname(str))) {
436                         mp = getm(cp, NULL, 0, AD_HOST, NULL);
437                         mp->m_next = bccs;  /* push */
438                         bccs = mp;
439                 }
440                 return;
441         }
442
443         if (aliasflg && hdr->flags & HTRY) {
444                 /*
445                 ** This header contains address(es) that we have to do
446                 ** alias expansion on.  Because of the saved state in
447                 ** getname we have to put all the addresses into a list.
448                 **/
449                 struct mailname *f = NULL;
450                 struct mailname *mp = NULL;
451
452                 while ((cp = getname(str))) {
453                         mp = getm(cp, NULL, 0, AD_HOST, NULL);
454                         if (!f) {
455                                 f = mp;
456                                 mp->m_next = mp;
457                         } else {
458                                 mp->m_next = f->m_next;
459                                 f->m_next = mp;
460                                 f = mp;
461                         }
462                 }
463                 f = mp->m_next;
464                 mp->m_next = NULL;
465                 /* Now munch on the list, possibly expanding aliases */
466                 putadr(name, f);
467                 return;
468         }
469
470         /*
471         ** The author(s) of spost decided that alias substitution wasn't
472         ** necessary for the non-HTRY headers.  Unfortunately, one of
473         ** those headers is "From:", and having alias substitution work on
474         ** that is extremely useful for someone with a lot of POP3 email
475         ** accounts or aliases.  post supports aliasing of "From:"...
476         **
477         ** Since "From:"-processing is incompletely implemented in this
478         ** unsupported and undocumented spost backend, I'm not going
479         ** to take the time to implement my new draft-From:-based email
480         ** address masquerading.  If I do ever implement it here, I'd almost
481         ** certainly want to implement "From:" line alias processing as well.
482         ** -- Dan Harkless <dan-nmh@dilvish.speed.net>
483         */
484         /*
485         ** Although there is no masquerading anymore in mmh, we might want
486         ** to have aliasing of From: addresses. Think about it.
487         ** -- meillo@marmaro.de  2012-02
488         */
489
490         if (hdr->flags & HSUB) {
491                 subject = getcpy(str);
492         }
493         fprintf(out, "%s: %s", name, str);
494 }
495
496
497 /*
498 ** Add yet missing headers.
499 */
500 static void
501 finish_headers(FILE *out)
502 {
503         char *cp;
504         char from[BUFSIZ];  /* my network address */
505         char signature[BUFSIZ];  /* my signature */
506         char *resentstr = (msgstate == resent) ? "Resent-" : "";
507
508         if (!(msgflags & MDAT)) {
509                 fprintf(out, "%sDate: %s\n", resentstr, dtimenow(0));
510         }
511
512         strncpy(from, getusername(), sizeof(from));
513         if ((cp = getfullname()) && *cp) {
514                 snprintf(signature, sizeof(signature), "%s <%s>", cp, from);
515         } else {
516                 snprintf(signature, sizeof(signature), "%s", from);
517         }
518         if (!(msgflags & MFRM)) {
519                 fprintf(out, "%sFrom: %s\n", resentstr, signature);
520         } else {
521                 /* In case the From: header contains multiple addresses. */
522                 fprintf(out, "%sSender: %s\n", resentstr, from);
523         }
524         if (!(msgflags & MVIS)) {
525                 fprintf(out, "%sBcc: Blind Distribution List: ;\n", resentstr);
526         }
527         if (badmsg) {
528                 adios(NULL, "re-format message and try again");
529         }
530 }
531
532
533 /*
534 ** Return index of the requested header in the table, or NOTOK if missing.
535 */
536 static int
537 get_header(char *header, struct headers *table)
538 {
539         struct headers *h;
540
541         for (h=table; h->value; h++) {
542                 if (mh_strcasecmp(header, h->value)==0) {
543                         return (h - table);
544                 }
545         }
546
547         return NOTOK;
548 }
549
550
551 /*
552 ** output the address list for header "name".  The address list
553 ** is a linked list of mailname structs.  "nl" points to the head
554 ** of the list.  Alias substitution should be done on nl.
555 */
556 static void
557 putadr(char *name, struct mailname *nl)
558 {
559         struct mailname *mp, *mp2;
560         int linepos;
561         char *cp;
562         int namelen;
563
564         fprintf(out, "%s: ", name);
565         namelen = strlen(name) + 2;
566         linepos = namelen;
567
568         for (mp = nl; mp; ) {
569                 if (linepos > MAX_SM_FIELD) {
570                         fprintf(out, "\n%s: ", name);
571                         linepos = namelen;
572                 }
573                 if (mp->m_nohost) {
574                         /* a local name - see if it's an alias */
575                         cp = akvalue(mp->m_mbox);
576                         if (cp == mp->m_mbox) {
577                                 /* wasn't an alias - use what the user typed */
578                                 linepos = putone(mp->m_text, linepos, namelen);
579                         } else {
580                                 /* an alias - expand it */
581                                 while ((cp = getname(cp))) {
582                                         if (linepos > MAX_SM_FIELD) {
583                                                 fprintf(out, "\n%s: ", name);
584                                                 linepos = namelen;
585                                         }
586                                         mp2 = getm(cp, NULL, 0, AD_HOST, NULL);
587                                         if (akvisible()) {
588                                                 mp2->m_pers = getcpy(mp->m_mbox);
589                                                 linepos = putone(adrformat(mp2), linepos, namelen);
590                                         } else {
591                                                 linepos = putone(mp2->m_text,
592                                                                 linepos,
593                                                                 namelen);
594                                         }
595                                         mnfree(mp2);
596                                 }
597                         }
598                 } else {
599                         /* not a local name - use what the user typed */
600                         linepos = putone(mp->m_text, linepos, namelen);
601                 }
602                 mp2 = mp;
603                 mp = mp->m_next;
604                 mnfree(mp2);
605         }
606         putc('\n', out);
607 }
608
609 static int
610 putone(char *adr, int pos, int indent)
611 {
612         register int len;
613         static int linepos;
614
615         len = strlen(adr);
616         if (pos == indent)
617                 linepos = pos;
618         else if (linepos+len > OUTPUTLINELEN) {
619                 fprintf(out, ",\n%*s", indent, "");
620                 linepos = indent;
621                 pos += indent + 2;
622         } else {
623                 fputs(", ", out);
624                 linepos += 2;
625                 pos += 2;
626         }
627         fputs(adr, out);
628
629         linepos += len;
630         return (pos+len);
631 }
632
633
634 static void
635 process_fcc(char *str)
636 {
637         char *cp, *pp;
638         int state = 0;
639
640         if (strlen(str)+strlen(fccs) > sizeof fccs /2) {
641                 adios(NULL, "Too much Fcc data");
642         }
643         /* todo: better have three states: SEPARATOR, PLUS, WORD */
644         for (cp=pp=str; *cp; cp++) {
645                 switch (*cp) {
646                 case ' ':
647                 case '\t':
648                 case '\n':
649                 case ',':
650                         if (state != 0) {
651                                 state = 0;
652                                 *cp = '\0';
653                                 if (*pp=='+' || *pp=='@') {
654                                         strcat(fccs, " ");
655                                 } else {
656                                         strcat(fccs, " +");
657                                 }
658                                 strcat(fccs, pp);
659                         }
660                         break;
661                 default:
662                         if (state == 0) {
663                                 state = 1;
664                                 pp = cp;
665                         }
666                         break;
667                 }
668         }
669 }
670
671
672 static void
673 fcc(char *file, char *folders)
674 {
675         int status;
676         char cmd[BUFSIZ];
677
678         if (verbose) {
679                 printf("%sFcc: %s\n", msgstate == resent ? "Resent-" : "",
680                                 folders);
681                 fflush(stdout);
682         }
683         if (strlen(fileproc)+strlen(file)+strlen(folders)+100 > sizeof cmd) {
684                 adios(NULL, "Too much Fcc data");
685         }
686         /* hack: read from /dev/null and refile(1) won't question us */
687         snprintf(cmd, sizeof cmd, "</dev/null %s -link -file %s %s",
688                         fileproc, file, folders);
689         status = system(cmd);
690         if (status == -1) {
691                 fprintf(stderr, "Skipped %sFcc %s: unable to system().\n",
692                                 msgstate == resent ? "Resent-" : "", folders);
693         } else if (status != 0) {
694                 fprintf(stderr, "%sFcc %s: Problems occured.\n",
695                                 msgstate == resent ? "Resent-" : "", folders);
696         }
697 }
698
699
700 /* BCC GENERATION */
701
702 static void
703 make_bcc_draft(char *draft)
704 {
705         int fd;
706         FILE *out;
707
708 #ifdef HAVE_MKSTEMP
709         fd = mkstemp(bccfil);
710         if (fd == -1 || (out = fdopen(fd, "w")) == NULL)
711                 adios(bccfil, "unable to create");
712 #else
713         mktemp(bccfil);
714         if ((out = fopen(bccfil, "w")) == NULL)
715                 adios(bccfil, "unable to create");
716 #endif
717         chmod(bccfil, 0600);
718
719         if (subject) {
720                 fprintf(out, "Subject: BCC: %s", subject);
721         } else {
722                 fprintf(out, "Subject: BCC: message without subject");
723         }
724         fprintf(out, "%s: %s\n", attach_hdr, draft);
725         fprintf(out, "------------\n");
726         fclose(out);
727 }