ead1bd5cd47aa232d8c8d552d54956a0a0bda282
[mmh] / uip / whatnow.c
1 /*
2 ** whatnow.c -- the WhatNow shell
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 **  The inclusion of attachments is eased by
9 **  using the header field name mechanism added to anno and send.
10 **  The header field name for attachments is predefined.
11 **
12 **  Several commands have been added at the whatnow prompt:
13 **
14 **        cd [ directory ]        This option works just like the shell's
15 **                                cd command and lets the user change the
16 **                                directory from which attachments are
17 **                                taken so that long path names are not
18 **                                needed with every file.
19 **
20 **        ls [ ls-options ]       This option works just like the normal
21 **                                ls command and exists to allow the user
22 **                                to verify file names in the directory.
23 **
24 **        pwd                     This option works just like the normal
25 **                                pwd command and exists to allow the user
26 **                                to verify the directory.
27 **
28 **        attach files            This option attaches the named files to
29 **                                the draft.
30 **
31 **        alist                   This option lists the attachments on the
32 **                                draft.
33 **
34 **        detach files            This option removes attachments from the
35 **        detach -n numbers       draft.  This can be done by file name or
36 **                                by attachment number.
37 */
38
39 #include <h/mh.h>
40 #include <fcntl.h>
41 #include <signal.h>
42 #include <h/mime.h>
43 #include <h/utils.h>
44
45 static struct swit whatnowswitches[] = {
46 #define EDITRSW  0
47         { "editor editor", 0 },
48 #define PRMPTSW  1
49         { "prompt string", 0 },
50 #define VERSIONSW  2
51         { "Version", 0 },
52 #define HELPSW  3
53         { "help", 0 },
54         { NULL, 0 }
55 };
56
57 /*
58 ** Options at the "whatnow" prompt
59 */
60 static struct swit aleqs[] = {
61 #define EDITSW  0
62         { "edit [<editor> <switches>]", 0 },
63 #define REFILEOPT  1
64         { "refile [<switches>] +folder", 0 },
65 #define DISPSW  2
66         { "display", 0 },
67 #define LISTSW  3
68         { "list", 0 },
69 #define SENDSW  4
70         { "send [<switches>]", 0 },
71 #define QUITSW  5
72         { "quit", 0 },
73 #define DELETESW  6
74         { "delete", 0 },
75 #define CDCMDSW  7
76         { "cd [directory]", 0 },
77 #define PWDCMDSW  8
78         { "pwd", 0 },
79 #define LSCMDSW  9
80         { "ls", 0 },
81 #define ATTACHCMDSW  10
82         { "attach", 0 },
83 #define DETACHCMDSW  11
84         { "detach [-n]", 0 },
85 #define ALISTCMDSW  12
86         { "alist", 0 },
87         { NULL, 0 }
88 };
89
90 static char *myprompt = "\nWhat now? ";
91
92 /*
93 ** static prototypes
94 */
95 static int editfile(char **, char **, char *, int);
96 static int sendfile(char **, char *);
97 static int refile(char **, char *);
98 static int removefile(char *);
99 static void writelscmd(char *, int, char **);
100 static void writesomecmd(char *buf, int bufsz, char *cmd, char *trailcmd, char **argp);
101 static FILE* popen_in_dir(const char *dir, const char *cmd, const char *type);
102 static int system_in_dir(const char *dir, const char *cmd);
103
104
105 int
106 main(int argc, char **argv)
107 {
108         int use = 0;
109         char *cp;
110         char *ed = NULL, *drft = NULL;
111         char buf[BUFSIZ], prompt[BUFSIZ];
112         char **argp, **arguments;
113         struct stat st;
114         char cwd[MAXPATHLEN + 1];  /* current working directory */
115         char file[MAXPATHLEN + 1];  /* file name buffer */
116         char shell[MAXPATHLEN + 1];  /* shell response buffer */
117         FILE *f;  /* read pointer for bgnd proc */
118         int n;  /* set on -n to detach command */
119
120 #ifdef LOCALE
121         setlocale(LC_ALL, "");
122 #endif
123
124         invo_name = mhbasename(argv[0]);
125
126         /* read user profile/context */
127         context_read();
128
129         arguments = getarguments(invo_name, argc, argv, 1);
130         argp = arguments;
131
132         /*
133         ** Get the initial current working directory.
134         */
135
136         if (!getcwd(cwd, sizeof (cwd))) {
137                 adios("getcwd", "could not get working directory");
138         }
139
140         while ((cp = *argp++)) {
141                 if (*cp == '-') {
142                         switch (smatch(++cp, whatnowswitches)) {
143                         case AMBIGSW:
144                                 ambigsw(cp, whatnowswitches);
145                                 done(1);
146                         case UNKWNSW:
147                                 adios(NULL, "-%s unknown", cp);
148
149                         case HELPSW:
150                                 snprintf(buf, sizeof(buf),
151                                                 "%s [switches] [file]",
152                                                 invo_name);
153                                 print_help(buf, whatnowswitches, 1);
154                                 done(1);
155                         case VERSIONSW:
156                                 print_version(invo_name);
157                                 done(1);
158
159                         case EDITRSW:
160                                 if (!(ed = *argp++) || *ed == '-')
161                                         adios(NULL, "missing argument to %s",
162                                                         argp[-2]);
163                                 continue;
164
165                         case PRMPTSW:
166                                 if (!(myprompt = *argp++) || *myprompt == '-')
167                                         adios(NULL, "missing argument to %s",
168                                                         argp[-2]);
169                                 continue;
170
171                         }
172                 }
173                 if (drft)
174                         adios(NULL, "only one draft at a time!");
175                 else
176                         drft = cp;
177         }
178
179         if ((!drft && !(drft = getenv("mhdraft"))) || !*drft)
180                 drft = getcpy(m_draft(seq_cur));
181
182         if ((cp = getenv("mhuse")) && *cp)
183                 use = atoi(cp);
184
185         if (!ed && !(ed = getenv("mheditor"))) {
186                 ed = "";  /* Don't initially edit the draft */
187         }
188
189         /* start editing the draft, unless editor is the empty string */
190         if (*ed) {
191                 if (editfile(&ed, NULL, drft, use) <0) {
192                         if (!use) {
193                                 unlink(drft);
194                         }
195                         advise(NULL, "Try again.");
196                         done(1);
197                 }
198         }
199
200         snprintf(prompt, sizeof(prompt), myprompt, invo_name);
201         for (;;) {
202                 if (!(argp = getans(prompt, aleqs))) {
203                         done(1);
204                 }
205                 switch (smatch(*argp, aleqs)) {
206                 case DISPSW:
207                         /* display the msg being replied to or distributed */
208                         if ((cp = getenv("mhaltmsg")) && *cp) {
209                                 snprintf(buf, sizeof buf, "%s '%s'",
210                                                 listproc, cp);
211                                 system(buf);
212                         } else {
213                                 advise(NULL, "no alternate message to display");
214                         }
215                         break;
216
217                 case EDITSW:
218                         /* Call an editor on the draft file */
219                         if (*++argp)
220                                 ed = *argp++;
221                         editfile(&ed, argp, drft, NOUSE);
222                         break;
223
224                 case LISTSW:
225                         /* display the draft file */
226                         snprintf(buf, sizeof buf, "%s '%s'", listproc, drft);
227                         system(buf);
228                         break;
229
230                 case QUITSW:
231                         /* quit */
232                         if (stat(drft, &st) != NOTOK) {
233                                 advise(NULL, "draft left on %s", drft);
234                         }
235                         done(1);
236
237                 case DELETESW:
238                         /* Delete draft and exit */
239                         removefile(drft);
240                         done(1);
241
242                 case SENDSW:
243                         /* Send draft */
244                         sendfile(++argp, drft);
245                         break;
246
247                 case REFILEOPT:
248                         /* Refile the draft */
249                         if (refile(++argp, drft) == 0) {
250                                 done(0);
251                         }
252                         break;
253
254                 case CDCMDSW:
255                         /*
256                         ** Change the working directory for attachments
257                         **
258                         ** Run the directory through the user's shell
259                         ** so that we can take advantage of any syntax
260                         ** that the user is accustomed to.  Read back
261                         ** the absolute path.
262                         */
263
264                         if (*(argp+1) == NULL) {
265                                 sprintf(buf, "$SHELL -c \"cd;pwd\"");
266                         } else {
267                                 writesomecmd(buf, BUFSIZ, "cd", "pwd", argp);
268                         }
269                         if ((f = popen_in_dir(cwd, buf, "r"))) {
270                                 fgets(cwd, sizeof (cwd), f);
271
272                                 if (strchr(cwd, '\n'))
273                                         *strchr(cwd, '\n') = '\0';
274
275                                 pclose(f);
276                         } else {
277                                 advise("popen", "could not get directory");
278                         }
279
280                         break;
281
282                 case PWDCMDSW:
283                         /* Print the working directory for attachments */
284                         printf("%s\n", cwd);
285                         break;
286
287                 case LSCMDSW:
288                         /*
289                         ** List files in the current attachment working
290                         ** directory
291                         **
292                         ** Use the user's shell so that we can take
293                         ** advantage of any syntax that the user is
294                         ** accustomed to.
295                         */
296                         writelscmd(buf, sizeof(buf), argp);
297                         system_in_dir(cwd, buf);
298                         break;
299
300                 case ALISTCMDSW:
301                         /*
302                         ** List attachments on current draft.
303                         */
304                         snprintf(buf, sizeof buf, "anno -list -comp '%s' "
305                                         "-number all -text IGNORE '%s'",
306                                         attach_hdr, drft);
307                         if (system(buf) != 0) {
308                                 advise(NULL, "Could not list attachment headers.");
309                         }
310                         break;
311
312                 case ATTACHCMDSW:
313                         /*
314                         ** Attach files to current draft.
315                         */
316
317                         if (*(argp+1) == NULL) {
318                                 advise(NULL, "attach command requires file argument(s).");
319                                 break;
320                         }
321
322                         /*
323                         ** Build a command line that causes the user's
324                         ** shell to list the file name arguments.
325                         ** This handles and wildcard expansion, tilde
326                         ** expansion, etc.
327                         */
328                         writelscmd(buf, sizeof(buf), argp);
329
330                         /*
331                         ** Read back the response from the shell,
332                         ** which contains a number of lines with one
333                         ** file name per line.  Remove off the newline.
334                         ** Determine whether we have an absolute or
335                         ** relative path name.  Prepend the current
336                         ** working directory to relative path names.
337                         ** Add the attachment annotation to the draft.
338                         */
339                         if ((f = popen_in_dir(cwd, buf, "r"))) {
340                                 char buf[BUFSIZ];
341
342                                 while (fgets(shell, sizeof(shell), f)) {
343                                         *(strchr(shell, '\n')) = '\0';
344
345                                         if (*shell == '/')
346                                                 sprintf(file, "%s", shell);
347                                         else {
348                                                 sprintf(file, "%s/%s", cwd,
349                                                                 shell);
350                                         }
351                                         snprintf(buf, sizeof buf,
352                                                         "anno -nodate -append "
353                                                         "-comp '%s' -text '%s'"
354                                                         " '%s'",
355                                                         attach_hdr, file,
356                                                         drft);
357                                         if (system(buf) != 0) {
358                                                 advise(NULL, "Could not add attachment header.");
359                                         }
360                                 }
361
362                                 pclose(f);
363                         } else {
364                                 advise("popen", "could not get file from shell");
365                         }
366
367                         break;
368
369                 case DETACHCMDSW:
370                         /*
371                         ** Detach files from current draft.
372                         **
373                         ** Scan the arguments for a -n.  Mixed file
374                         ** names and numbers aren't allowed, so this
375                         ** catches a -n anywhere in the argument list.
376                         */
377                         for (n = 0, arguments = argp + 1;
378                                         *arguments != NULL;
379                                         arguments++) {
380                                 if (strcmp(*arguments, "-n") == 0) {
381                                                 n = 1;
382                                                 break;
383                                 }
384                         }
385
386                         /*
387                         ** A -n was found so interpret the arguments as
388                         ** attachment numbers.  Decrement any remaining
389                         ** argument number that is greater than the one
390                         ** just processed after processing each one so
391                         ** that the numbering stays correct.
392                         */
393                         if (n == 1) {
394                                 for (arguments=argp+1; *arguments;
395                                                 arguments++) {
396                                         if (strcmp(*arguments, "-n") == 0)
397                                                 continue;
398
399                                         if (**arguments != '\0') {
400                                                 char buf[BUFSIZ];
401
402                                                 n = atoi(*arguments);
403                                                 snprintf(buf, sizeof buf, "anno -delete -comp '%s' -number '%d' '%s'", attach_hdr, n, drft);
404                                                 if (system(buf) != 0) {
405                                                         advise(NULL, "Could not delete attachment header.");
406                                                 }
407
408                                                 for (argp=arguments+1; *argp;
409                                                                 argp++) {
410                                                         if (atoi(*argp) > n) {
411                                                                 if (atoi(*argp) == 1)
412                                                                         *argp = "";
413                                                                 else
414                                                                         sprintf(*argp, "%d", atoi(*argp) - 1);
415                                                         }
416                                                 }
417                                         }
418                                 }
419                                 break;
420                         }
421                         /* else */
422
423                         /*
424                         ** The arguments are interpreted as file names.
425                         ** Run them through the user's shell for wildcard
426                         ** expansion and other goodies.  Do this from
427                         ** the current working directory if the argument
428                         ** is not an absolute path name (does not begin
429                         ** with a /).
430                         **
431                         ** We feed all the file names to the shell at
432                         ** once, otherwise you can't provide a file name
433                         ** with a space in it.
434                         */
435                         writelscmd(buf, sizeof(buf), argp);
436                         if ((f = popen_in_dir(cwd, buf, "r"))) {
437                                 while (fgets(shell, sizeof (shell), f)) {
438                                         *(strchr(shell, '\n')) = '\0';
439                                         snprintf(buf, sizeof buf,
440                                                         "anno -delete -comp "
441                                                         "'%s' -text '%s' '%s'",
442                                                         attach_hdr, shell,
443                                                         drft);
444                                         if (system(buf) != 0) {
445                                                 advise(NULL, "Could not delete attachment header.");
446                                         }
447                                 }
448                                 pclose(f);
449                         } else {
450                                 advise("popen", "could not get file from shell");
451                         }
452
453                         break;
454
455                 default:
456                         /* Unknown command */
457                         advise(NULL, "say what?");
458                         break;
459                 }
460         }
461         /*NOTREACHED*/
462 }
463
464
465
466 /*
467 ** Build a command line of the form $SHELL -c "cd 'cwd'; cmd argp ... ;
468 ** trailcmd".
469 */
470 static void
471 writesomecmd(char *buf, int bufsz, char *cmd, char *trailcmd, char **argp)
472 {
473         char *cp;
474         /*
475         ** Note that we do not quote -- the argp from the user
476         ** is assumed to be quoted as they desire. (We can't treat
477         ** it as pure literal as that would prevent them using ~,
478         ** wildcards, etc.) The buffer produced by this function
479         ** should be given to popen_in_dir() or system_in_dir() so
480         ** that the current working directory is set correctly.
481         */
482         int ln = snprintf(buf, bufsz, "$SHELL -c \"%s", cmd);
483         /*
484         ** NB that some snprintf() return -1 on overflow rather than the
485         ** new C99 mandated 'number of chars that would have been written'
486         */
487         /*
488         ** length checks here and inside the loop allow for the
489         ** trailing ';', trailcmd, '"' and NUL
490         */
491         int trailln = strlen(trailcmd) + 3;
492         if (ln < 0 || ln + trailln > bufsz)
493                 adios(NULL, "arguments too long");
494
495         cp = buf + ln;
496
497         while (*++argp) {
498                 ln = strlen(*argp);
499                 /* +1 for leading space */
500                 if (ln + trailln + 1 > bufsz - (cp-buf))
501                         adios(NULL, "arguments too long");
502                 *cp++ = ' ';
503                 memcpy(cp, *argp, ln+1);
504                 cp += ln;
505         }
506         if (*trailcmd) {
507                 *cp++ = ';';
508                 strcpy(cp, trailcmd);
509                 cp += trailln - 3;
510         }
511         *cp++ = '"';
512         *cp = 0;
513 }
514
515 /*
516 ** Build a command line that causes the user's shell to list the file name
517 ** arguments.  This handles and wildcard expansion, tilde expansion, etc.
518 */
519 static void
520 writelscmd(char *buf, int bufsz, char **argp)
521 {
522         writesomecmd(buf, bufsz, "ls", "", argp);
523 }
524
525 /*
526 ** Like system(), but run the command in directory dir.
527 ** This assumes the program is single-threaded!
528 */
529 static int
530 system_in_dir(const char *dir, const char *cmd)
531 {
532         char olddir[BUFSIZ];
533         int r;
534         if (getcwd(olddir, sizeof(olddir)) == 0)
535                 adios("getcwd", "could not get working directory");
536         if (chdir(dir) != 0)
537                 adios("chdir", "could not change working directory");
538         r = system(cmd);
539         if (chdir(olddir) != 0)
540                 adios("chdir", "could not change working directory");
541         return r;
542 }
543
544 /* ditto for popen() */
545 static FILE*
546 popen_in_dir(const char *dir, const char *cmd, const char *type)
547 {
548         char olddir[BUFSIZ];
549         FILE *f;
550         if (getcwd(olddir, sizeof(olddir)) == 0)
551                 adios("getcwd", "could not get working directory");
552         if (chdir(dir) != 0)
553                 adios("chdir", "could not change working directory");
554         f = popen(cmd, type);
555         if (chdir(olddir) != 0)
556                 adios("chdir", "could not change working directory");
557         return f;
558 }
559
560
561 /*
562 ** EDIT
563 */
564
565 static char *edsave = NULL;  /* the editor we used previously */
566
567
568 static int
569 editfile(char **ed, char **arg, char *file, int use)
570 {
571         int pid, status, vecp;
572         char *cp, *vec[MAXARGS];
573
574         if (!*ed || !**ed) {
575                 /* We have no explicit editor. */
576                 if (edsave) {
577                         /* Use the previous editor ... */
578                         *ed = edsave;
579                         if (!(cp = mhbasename(*ed)))
580                                 cp = *ed;
581
582                         /* but prefer one specified via "editor-next" */
583                         cp = concat(cp, "-next", NULL);
584                         if ((cp = context_find(cp)))
585                                 *ed = cp;
586                 } else {
587                         /* set initial editor */
588                         *ed = defaulteditor;
589                 }
590         }
591
592         context_save();
593         fflush(stdout);
594
595         switch (pid = fork()) {
596         case NOTOK:
597                 advise("fork", "unable to");
598                 status = NOTOK;
599                 break;
600
601         case OK:
602                 vecp = 0;
603                 vec[vecp++] = mhbasename(*ed);
604                 if (arg)
605                         while (*arg)
606                                 vec[vecp++] = *arg++;
607                 vec[vecp++] = file;
608                 vec[vecp] = NULL;
609
610                 execvp(*ed, vec);
611                 fprintf(stderr, "%s: unable to exec ", invo_name);
612                 perror(*ed);
613                 _exit(-1);
614
615         default:
616                 if ((status = pidwait(pid, NOTOK))) {
617                         if ((status & 0xff00) == 0xff00) {
618                                 /* cmd not found or pidwait() failed */
619                                 status = -1;
620                                 break;
621                         }
622                         if (status & 0x00ff) {
623                                 /* terminated by signal */
624                                 advise(NULL, "%s terminated by signal %d",
625                                                 *ed, status & 0x7f);
626                         } else {
627                                 /* failure exit */
628                                 advise(NULL, "%s exited with return code %d",
629                                                 *ed, (status & 0xff00) >> 8);
630                         }
631                         status = -1;
632                         break;
633                 }
634         }
635
636         /* remember which editor we used */
637         edsave = getcpy(*ed);
638
639         *ed = NULL;
640
641         return status;
642 }
643
644
645 /*
646 ** SEND
647 */
648
649 static int
650 sendfile(char **arg, char *file)
651 {
652         pid_t child_id;
653         int vecp;
654         char *vec[MAXARGS];
655
656         context_save();  /* save the context file */
657         fflush(stdout);
658
659         switch (child_id = fork()) {
660         case NOTOK:
661                 advise(NULL, "unable to fork, so sending directly...");
662                 /* fall */
663         case OK:
664                 vecp = 0;
665                 vec[vecp++] = invo_name;
666                 if (arg)
667                         while (*arg)
668                                 vec[vecp++] = *arg++;
669                 vec[vecp++] = file;
670                 vec[vecp] = NULL;
671
672                 execvp("send", vec);
673                 fprintf(stderr, "%s: unable to exec ", invo_name);
674                 perror("send");
675                 _exit(-1);
676
677         default:
678                 if (pidwait(child_id, OK) == 0)
679                         done(0);
680                 return 1;
681         }
682 }
683
684
685 /*
686 ** refile msg into another folder
687 */
688 static int
689 refile(char **arg, char *file)
690 {
691         pid_t pid;
692         register int vecp;
693         char *vec[MAXARGS];
694
695         vecp = 0;
696         vec[vecp++] = "refile";
697         vec[vecp++] = "-nolink";  /* override bad .mh_profile defaults */
698         vec[vecp++] = "-file";
699         vec[vecp++] = file;
700
701         while (arg && *arg) {
702                 vec[vecp++] = *arg++;
703         }
704         vec[vecp] = NULL;
705
706         context_save();  /* save the context file */
707         fflush(stdout);
708
709         switch (pid = fork()) {
710         case -1:
711                 advise("fork", "unable to");
712                 return -1;
713
714         case 0:
715                 execvp(*vec, vec);
716                 fprintf(stderr, "%s: unable to exec ", invo_name);
717                 perror(*vec);
718                 _exit(-1);
719
720         default:
721                 return (pidwait(pid, -1));
722         }
723 }
724
725
726 /*
727 ** Remove the draft file
728 */
729
730 static int
731 removefile(char *drft)
732 {
733         if (unlink(drft) == NOTOK)
734                 adios(drft, "unable to unlink");
735
736         return OK;
737 }