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