Removed the -[no]watch flags of send and post.
[mmh] / uip / send.c
1 /*
2 ** send.c -- send a composed message
3 **
4 ** This code is Copyright (c) 2002, by the authors of nmh.  See the
5 ** COPYRIGHT file in the root directory of the nmh distribution for
6 ** complete copyright information.
7 */
8
9 #include <h/mh.h>
10 #include <fcntl.h>
11 #include <errno.h>
12 #include <signal.h>
13 #include <h/signals.h>
14 #include <setjmp.h>
15 #include <h/mime.h>
16 #include <h/tws.h>
17 #include <h/utils.h>
18
19 #ifdef HAVE_SYS_TIME_H
20 # include <sys/time.h>
21 #endif
22 #include <time.h>
23
24 int debugsw = 0;  /* global */
25 char *altmsg   = NULL;
26 char *annotext = NULL;
27 char *distfile = NULL;
28
29 static jmp_buf env;
30
31 /* name of temp file for body content */
32 static char body_file_name[MAXPATHLEN + 1];
33 /* name of mhbuild composition temporary file */
34 static char composition_file_name[MAXPATHLEN + 1]; 
35 static int field_size;  /* size of header field buffer */
36 static char *field;  /* header field buffer */
37 static FILE *draft_file;  /* draft file pointer */
38 static FILE *body_file;  /* body file pointer */
39 static FILE *composition_file;  /* composition file pointer */
40
41 /*
42 ** static prototypes
43 */
44 static int sendsbr(char **, int, char *, struct stat *);
45 static void armed_done(int) NORETURN;
46 static void anno(struct stat *);
47 static int sendaux(char **, int, char *, struct stat *);
48 static int attach(char *);
49 static void clean_up_temporary_files(void);
50 static int get_line(void);
51 static void make_mime_composition_file_entry(char *);
52
53
54 static struct swit switches[] = {
55 #define ALIASW  0
56         { "alias aliasfile", 0 },
57 #define DEBUGSW  1
58         { "debug", -5 },
59 #define VERBSW  2
60         { "verbose", 0 },
61 #define NVERBSW  3
62         { "noverbose", 2 },
63 #define VERSIONSW  4
64         { "Version", 0 },
65 #define HELPSW  5
66         { "help", 0 },
67         { NULL, 0 }
68 };
69
70
71 int
72 main(int argc, char **argv)
73 {
74         int msgp = 0, nfiles = 0, distsw = 0, vecp = 1;
75         int msgnum, status;
76         int in, out;
77         int n;
78         char *cp, *maildir = NULL;
79         char buf[BUFSIZ], **ap, **argp, **arguments;
80         char *msgs[MAXARGS], *vec[MAXARGS];
81         char *files[MAXARGS];
82         struct msgs *mp;
83         struct stat st;
84         struct stat st2;
85
86         setlocale(LC_ALL, "");
87         invo_name = mhbasename(argv[0]);
88
89         /* read user profile/context */
90         context_read();
91
92         arguments = getarguments(invo_name, argc, argv, 1);
93         argp = arguments;
94
95         vec[vecp++] = "-library";
96         vec[vecp++] = getcpy(toabsdir("+"));
97
98         while ((cp = *argp++)) {
99                 if (*cp == '-') {
100                         switch (smatch(++cp, switches)) {
101                         case AMBIGSW:
102                                 ambigsw(cp, switches);
103                                 done(1);
104                         case UNKWNSW:
105                                 adios(NULL, "-%s unknown\n", cp);
106
107                         case HELPSW:
108                                 snprintf(buf, sizeof(buf),
109                                                 "%s [file] [switches]",
110                                                 invo_name);
111                                 print_help(buf, switches, 1);
112                                 done(1);
113                         case VERSIONSW:
114                                 print_version(invo_name);
115                                 done(1);
116
117                         case DEBUGSW:
118                                 debugsw++;
119                                 /* fall */
120                         case VERBSW:
121                         case NVERBSW:
122                                 vec[vecp++] = --cp;
123                                 continue;
124
125                         case ALIASW:
126                                 vec[vecp++] = --cp;
127                                 if (!(cp = *argp++) || *cp == '-') {
128                                         adios(NULL, "missing argument to %s",
129                                                         argp[-2]);
130                                 }
131                                 vec[vecp++] = cp;
132                                 continue;
133
134                         }
135                 } else {
136                         if (*cp == '/') {
137                                 files[nfiles++] = cp;
138                         } else {
139                                 msgs[msgp++] = cp;
140                         }
141                 }
142         }
143
144         /* check for "Aliasfile:" profile entry */
145         if ((cp = context_find("Aliasfile"))) {
146                 char *dp = NULL;
147
148                 for (ap=brkstring(dp=getcpy(cp), " ", "\n"); ap && *ap; ap++) {
149                         vec[vecp++] = "-alias";
150                         vec[vecp++] = getcpy(etcpath(*ap));
151                 }
152         }
153
154         if (!msgp && !nfiles) {
155                 msgs[msgp++] = seq_cur;
156         }
157         maildir = toabsdir(draftfolder);
158
159         if (chdir(maildir) == NOTOK) {
160                 adios(maildir, "unable to change directory to");
161         }
162
163         if (!(mp = folder_read(draftfolder))) {
164                 adios(NULL, "unable to read draft folder %s", draftfolder);
165         }
166         if (mp->nummsg == 0) {
167                 adios(NULL, "no messages in draft folder %s", draftfolder);
168         }
169         /* parse all the message ranges/sequences and set SELECTED */
170         for (msgnum = 0; msgnum < msgp; msgnum++) {
171                 if (!m_convert(mp, msgs[msgnum])) {
172                         done(1);
173                 }
174         }
175         seq_setprev(mp);
176
177         for (msgp = 0, msgnum = mp->lowsel; msgnum <= mp->hghsel; msgnum++) {
178                 if (is_selected(mp, msgnum)) {
179                         files[nfiles++] = getcpy(m_name(msgnum));
180                         unset_exists(mp, msgnum);
181                 }
182         }
183
184         mp->msgflags |= SEQMOD;
185         seq_save(mp);
186
187         if (!(cp = getenv("SIGNATURE")) || !*cp) {
188                 if ((cp = context_find("signature")) && *cp) {
189                         m_putenv("SIGNATURE", cp);
190                 }
191         }
192
193         for (msgnum = 0; msgnum < nfiles; msgnum++) {
194                 if (stat(files[msgnum], &st) == NOTOK) {
195                         adios(files[msgnum], "unable to stat draft file");
196                 }
197         }
198
199         if (!(annotext = getenv("mhannotate")) || !*annotext) {
200                 annotext = NULL;
201         }
202         if (!(altmsg = getenv("mhaltmsg")) || !*altmsg) {
203                 altmsg = NULL;  /* used by dist interface - see below */
204         }
205
206         if ((cp = getenv("mhdist")) && *cp && (distsw = atoi(cp)) && altmsg) {
207                 vec[vecp++] = "-dist";
208                 if ((in = open(altmsg, O_RDONLY)) == NOTOK) {
209                         adios(altmsg, "unable to open for reading");
210                 }
211                 fstat(in, &st2);
212                 distfile = getcpy(m_mktemp2(NULL, invo_name, NULL, NULL));
213                 if ((out = creat(distfile, (int)st2.st_mode & 0777))==NOTOK) {
214                         adios(distfile, "unable to open for writing");
215                 }
216                 cpydata(in, out, altmsg, distfile);
217                 close(in);
218                 close(out);
219         } else {
220                 distfile = NULL;
221         }
222
223         if (!altmsg || stat(altmsg, &st) == NOTOK) {
224                 st.st_mtime = 0;
225                 st.st_dev = 0;
226                 st.st_ino = 0;
227         }
228         status = 0;
229         vec[0] = "spost";
230         for (n=3; n<OPEN_MAX; n++) {
231                 close(n);
232         }
233
234         for (msgnum = 0; msgnum < nfiles; msgnum++) {
235                 switch (sendsbr(vec, vecp, files[msgnum], &st)) {
236                 case DONE:
237                         done(++status);
238                 case NOTOK:
239                         status++;  /* fall */
240                 case OK:
241                         break;
242                 }
243         }
244
245         context_save();
246         done(status);
247         return 1;
248 }
249
250
251 /*
252 ** message sending back-end
253 */
254 static int
255 sendsbr(char **vec, int vecp, char *drft, struct stat *st)
256 {
257         int status;
258         char buffer[BUFSIZ];
259         char *original_draft;
260         char *p;  /* string pointer for building file name */
261
262         /*
263         ** Save the original name of the draft file.  The name of the
264         ** draft file is changed to a temporary file containing the built
265         ** MIME message if there are attachments.  We need the original
266         ** name so that it can be renamed after the message is sent.
267         */
268         original_draft = drft;
269
270         /*
271         ** Convert the draft to a MIME message.
272         ** Use the mhbuild composition file for the draft if there was
273         ** a successful conversion because that now contains the MIME
274         ** message.  A nice side effect of this is that it leaves the
275         ** original draft file untouched so that it can be retrieved
276         ** and modified if desired.
277         */
278         switch (attach(drft)) {
279         case OK:
280                 drft = composition_file_name;
281                 break;
282
283         case NOTOK:
284                 return (NOTOK);
285
286         case DONE:
287                 break;
288         }
289
290         done=armed_done;
291         switch (setjmp(env)) {
292         case OK:
293                 status = sendaux(vec, vecp, drft, st) ? NOTOK : OK;
294                 /* rename the original draft */
295                 if (status == OK) {
296                         strncpy(buffer, m_backup(original_draft),
297                                         sizeof(buffer));
298                         if (rename(original_draft, buffer) == NOTOK) {
299                                 advise(buffer, "unable to rename %s to", drft);
300                         }
301                 }
302                 break;
303
304         default:
305                 status = DONE;
306                 break;
307         }
308
309         done=exit;
310         if (distfile) {
311                 unlink(distfile);
312         }
313
314         /*
315         ** Get rid of any temporary files that we created for attachments.
316         ** Also get rid of the renamed composition file that mhbuild
317         ** leaves as a turd.  It looks confusing, but we use the body
318         ** file name to help build the renamed composition file name.
319         */
320         if (drft == composition_file_name) {
321                 clean_up_temporary_files();
322
323                 if (strlen(composition_file_name) >=
324                                 sizeof (composition_file_name) - 6) {
325                         advise(NULL, "unable to remove original composition file.");
326                 } else {
327                         if (!(p = strrchr(composition_file_name, '/'))) {
328                                 p = composition_file_name;
329                         } else {
330                                 p++;
331                         }
332                         strcpy(body_file_name, p);
333                         *p++ = ',';
334                         strcpy(p, body_file_name);
335                         strcat(p, ".orig");
336
337                         unlink(composition_file_name);
338                 }
339         }
340
341         return status;
342 }
343
344 static int
345 attach(char *draft_file_name)
346 {
347         char buf[MAXPATHLEN + 6];
348         int c;
349         int has_attachment;
350         int has_body;
351         int non_ascii; /* msg body contains non-ASCII chars */
352         int length;  /* length of attachment header field name */
353         char *p;
354
355         if (!(draft_file = fopen(draft_file_name, "r"))) {
356                 adios(NULL, "can't open draft file `%s'.", draft_file_name);
357         }
358
359         /* We'll grow the buffer as needed. */
360         field = (char *)mh_xmalloc(field_size = 256);
361
362         /*
363         ** Scan the draft file for an attachment header field name.
364         */
365         length = strlen(attach_hdr);
366         has_attachment = 0;
367         while (get_line() != EOF && *field != '\0' && *field != '-') {
368                 if (strncasecmp(field, attach_hdr, length)==0 &&
369                                 field[length] == ':') {
370                         has_attachment = 1;
371                 }
372         }
373
374         /*
375         ** Look for at least one non-blank line in the body of the
376         ** message which indicates content in the body.
377         ** Check if body contains at least one non-blank (= not empty)
378         ** and if it contains any non-ASCII chars (= need MIME).
379         */
380         has_body = 0;
381         non_ascii = 0;
382         while (get_line() != EOF) {
383                 for (p = field; *p != '\0'; p++) {
384                         if (*p != ' ' && *p != '\t') {
385                                 has_body = 1;
386                         }
387                         if (*p > 127 || *p < 0) {
388                                 non_ascii = 1;
389                         }
390                 }
391                 if (has_body && non_ascii) {
392                         break;  /* that's been already enough information */
393                 }
394         }
395
396         if (!has_attachment && non_ascii==0) {
397                 /* We don't need to convert it to MIME. */
398                 return DONE;
399         }
400
401         /*
402         ** Else: mimify
403         */
404
405         /* Make names for the temporary files.  */
406         strncpy(body_file_name,
407                         m_mktemp(toabsdir(invo_name), NULL, NULL),
408                         sizeof (body_file_name));
409         strncpy(composition_file_name,
410                         m_mktemp(toabsdir(invo_name), NULL, NULL),
411                         sizeof (composition_file_name));
412
413         if (has_body) {
414                 body_file = fopen(body_file_name, "w");
415         }
416         composition_file = fopen(composition_file_name, "w");
417
418         if ((has_body && !body_file) || !composition_file) {
419                 clean_up_temporary_files();
420                 adios(NULL, "unable to open all of the temporary files.");
421         }
422
423         /* Copy non-attachment header fields to the temp composition file. */
424         rewind(draft_file);
425         while (get_line() != EOF && *field && *field != '-') {
426                 if (strncasecmp(field, attach_hdr, length) != 0 ||
427                                 field[length] != ':') {
428                         fprintf(composition_file, "%s\n", field);
429                 }
430         }
431         fputs("--------\n", composition_file);
432
433         if (has_body) {
434                 /* Copy the message body to the temporary file. */
435                 while ((c = getc(draft_file)) != EOF) {
436                         putc(c, body_file);
437                 }
438                 fclose(body_file);
439
440                 /* Add a mhbuild MIME composition file line for the body */
441                 /* charset will be discovered/guessed by mhbuild */
442                 fprintf(composition_file, "#text/plain %s\n", body_file_name);
443         }
444
445         /*
446         ** Now, go back to the beginning of the draft file and look for
447         ** header fields that specify attachments.  Add a mhbuild MIME
448         ** composition file for each.
449         */
450         rewind(draft_file);
451         while (get_line() != EOF && *field && *field != '-') {
452                 if (strncasecmp(field, attach_hdr, length) == 0 &&
453                                 field[length] == ':') {
454                         for (p = field+length+1; *p==' ' || *p=='\t'; p++) {
455                                 continue;
456                         }
457                         if (*p == '+') {
458                                 /* forwarded message */
459                                 fprintf(composition_file, "#forw [forwarded message(s)] %s\n", p);
460                         } else {
461                                 /* regular attachment */
462                                 make_mime_composition_file_entry(p);
463                         }
464                 }
465         }
466         fclose(composition_file);
467
468         /* We're ready to roll! */
469         sprintf(buf, "mhbuild %s", composition_file_name);
470         if (system(buf) != 0) {
471                 /* some problem */
472                 clean_up_temporary_files();
473                 return (NOTOK);
474         }
475
476         return (OK);
477 }
478
479 static void
480 clean_up_temporary_files(void)
481 {
482         unlink(body_file_name);
483         unlink(composition_file_name);
484
485         return;
486 }
487
488 static int
489 get_line(void)
490 {
491         int c;  /* current character */
492         int n;  /* number of bytes in buffer */
493         char *p;
494
495         /*
496         ** Get a line from the input file, growing the field buffer as
497         ** needed.  We do this so that we can fit an entire line in the
498         ** buffer making it easy to do a string comparison on both the
499         ** field name and the field body which might be a long path name.
500         */
501         for (n = 0, p = field; (c = getc(draft_file)) != EOF; *p++ = c) {
502                 if (c == '\n' && (c = getc(draft_file)) != ' ' && c != '\t') {
503                         ungetc(c, draft_file);
504                         c = '\n';
505                         break;
506                 }
507                 if (++n >= field_size - 1) {
508                         field = (char *)mh_xrealloc(field, field_size += 256);
509                         p = field + n - 1;
510                 }
511         }
512         *p = '\0';
513
514         return (c);
515 }
516
517 static void
518 make_mime_composition_file_entry(char *file_name)
519 {
520         FILE *fp;
521         struct node *np;
522         char *cp;
523         char content_type[BUFSIZ];
524         char cmdbuf[BUFSIZ];
525         char *cmd = mimetypequeryproc;
526
527         for (np = m_defs; np; np = np->n_next) {
528                 if (strcasecmp(np->n_name, mimetypequery)==0) {
529                         cmd = np->n_field;
530                         break;
531                 }
532         }
533         snprintf(cmdbuf, sizeof cmdbuf, "%s %s", cmd, file_name);
534
535         if (!(fp = popen(cmdbuf, "r"))) {
536                 clean_up_temporary_files();
537                 adios(NULL, "unable to determine content type with `%s'",
538                                 cmdbuf);
539         }
540         if (fgets(content_type, sizeof content_type, fp) &&
541                         (cp = strrchr(content_type, '\n'))) {
542                 *cp = '\0';
543         } else {
544                 strcpy(content_type, "application/octet-stream");
545                 admonish(NULL, "problems with `%s', using fall back type `%s'",
546                                 cmdbuf, content_type);
547         }
548         pclose(fp);
549
550         /* TODO: don't use access(2) because it checks for ruid, not euid */
551         if (access(file_name, R_OK) != 0) {
552                 clean_up_temporary_files();
553                 adios(NULL, "unable to access file `%s'", file_name);
554         }
555
556         cp = (!(cp = strrchr(file_name, '/'))) ? file_name : cp + 1;
557         fprintf(composition_file, "#%s; name=\"%s\" <> [%s] {attachment}",
558                         content_type, cp, cp);
559
560         fprintf(composition_file, " %s\n", file_name);
561
562         return;
563 }
564
565 /*
566 ** The back-end of the message sending back-end.
567 ** Annotate original message, and call `spost' to send message.
568 */
569 static int
570 sendaux(char **vec, int vecp, char *drft, struct stat *st)
571 {
572         pid_t child_id;
573         int status;
574         char backup[BUFSIZ];
575
576         vec[vecp++] = drft;
577         if (distfile && distout(drft, distfile, backup) == NOTOK) {
578                 done(1);
579         }
580         vec[vecp] = NULL;
581
582         switch (child_id = fork()) {
583         case -1:
584                 /* oops -- fork error */
585                 adios("fork", "unable to");
586                 break;  /* NOT REACHED */
587
588         case 0:
589                 /* child process -- send it */
590                 execvp(*vec, vec);
591                 fprintf(stderr, "unable to exec ");
592                 perror(*vec);
593                 _exit(-1);
594                 break;  /* NOT REACHED */
595
596         default:
597                 /* parent process -- wait for it */
598                 if ((status = pidwait(child_id, NOTOK)) == OK) {
599                         if (annotext) {
600                                 anno(st);
601                         }
602                 } else {
603                         /* spost failed */
604                         advise(NULL, "message not delivered to anyone");
605                         if (distfile) {
606                                 unlink(drft);
607                                 if (rename(backup, drft) == NOTOK) {
608                                         advise(drft, "unable to rename %s to",
609                                                         backup);
610                                 }
611                         }
612                 }
613                 break;
614         }
615
616         return status;
617 }
618
619
620 static void
621 anno(struct stat *st)
622 {
623         struct stat st2;
624         char *msgs, *folder;
625         char buf[BUFSIZ];
626
627         if (altmsg && (stat(altmsg, &st2) == NOTOK ||
628                         st->st_mtime != st2.st_mtime ||
629                         st->st_dev != st2.st_dev ||
630                         st->st_ino != st2.st_ino)) {
631                 if (debugsw) {
632                         admonish(NULL, "$mhaltmsg mismatch");
633                 }
634                 return;
635         }
636
637         if (!(folder = getenv("mhfolder")) || !*folder) {
638                 if (debugsw) {
639                         admonish(NULL, "$mhfolder not set");
640                 }
641                 return;
642         }
643         if (!(msgs = getenv("mhmessages")) || !*msgs) {
644                 if (debugsw) {
645                         admonish(NULL, "$mhmessages not set");
646                 }
647                 return;
648         }
649         if (debugsw) {
650                 advise(NULL, "annotate as `%s': %s %s", annotext,
651                                 folder, msgs);
652         }
653         snprintf(buf, sizeof buf, "anno -comp '%s' '+%s' %s",
654                         annotext, folder, msgs);
655         if (system(buf) != 0) {
656                 advise(NULL, "unable to annotate");
657         }
658 }
659
660
661 static void
662 armed_done(int status)
663 {
664         longjmp(env, status ? status : NOTOK);
665
666         exit(status);
667 }