Fix mhsign for gpg2: Expiry date format
[mmh] / uip / anno.c
1 /*
2 ** anno.c -- annotate messages
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 ** Three new options have been added: delete, list, and number. Adding
9 ** features to generalize the anno command seemed to be a better approach
10 ** than the creation of a new command whose features would overlap with
11 ** those of the anno command.
12 */
13
14 #include <h/mh.h>
15 #include <h/utils.h>
16 #include <h/tws.h>
17 #include <fcntl.h>
18 #include <errno.h>
19 #include <utime.h>
20 #include <unistd.h>
21 #include <ctype.h>
22 #include <sys/stat.h>
23 #include <locale.h>
24 #include <sysexits.h>
25
26 static enum { MODE_ADD, MODE_DEL, MODE_LIST } mode = MODE_ADD;
27
28 static struct swit switches[] = {
29 #define COMPSW 0
30         { "component field", 0 },
31 #define DATESW 1
32         { "date", 0 },
33 #define NDATESW 2
34         { "nodate", 2 },
35 #define TEXTSW 3
36         { "text body", 0 },
37 #define VERSIONSW 4
38         { "Version", 0 },
39 #define HELPSW 5
40         { "help", 0 },
41 #define LISTSW 6
42         { "list", 0 },
43 #define DELETESW 7
44         { "delete", 0 },
45 #define NUMBERSW 8
46         { "number", 0 },
47 #define APPENDSW 9
48         { "append", 0 },
49 #define PRESERVESW 10
50         { "preserve", 0 },
51 #define NOPRESERVESW 11
52         { "nopreserve", 2 },
53         { NULL, 0 }
54 };
55
56 char *version=VERSION;
57
58 /*
59 ** static prototypes
60 */
61 static void make_comp(unsigned char **);
62 static int annotate(char *, unsigned char *, char *, int, int, int, int);
63 static void annolist(char *, unsigned char *, int);
64 static void dodel(int, unsigned char *, char *, FILE *, int);
65 static void doadd(int, unsigned char *, char *, FILE *, int, int);
66
67
68 int
69 main(int argc, char **argv)
70 {
71         int datesw = 1;
72         int preserve = 0;
73         int msgnum;
74         char *cp, *maildir;
75         unsigned char *comp = NULL;
76         char *text = NULL, *folder = NULL, buf[BUFSIZ];
77         char *file = NULL;
78         char **argp, **arguments;
79         struct msgs_array msgs = { 0, 0, NULL };
80         struct msgs *mp;
81         int append = 0;  /* append annotations instead of default prepend */
82         int number = 0; /* delete specific number of like elements if set */
83
84         setlocale(LC_ALL, "");
85         invo_name = mhbasename(argv[0]);
86         context_read();
87
88         arguments = getarguments(invo_name, argc, argv, 1);
89         argp = arguments;
90
91         while ((cp = *argp++)) {
92                 if (*cp == '-') {
93                         switch (smatch(++cp, switches)) {
94                         case AMBIGSW:
95                                 ambigsw(cp, switches);
96                                 exit(EX_USAGE);
97                         case UNKWNSW:
98                                 adios(EX_USAGE, NULL, "-%s unknown", cp);
99
100                         case HELPSW:
101                                 snprintf(buf, sizeof(buf),
102                                         "%s [+folder] [msgs] [switches]",
103                                         invo_name);
104                                 print_help(buf, switches, 1);
105                                 exit(argc == 2 ? EX_OK : EX_USAGE);
106                         case VERSIONSW:
107                                 print_version(invo_name);
108                                 exit(argc == 2 ? EX_OK : EX_USAGE);
109
110                         case DELETESW:  /* delete annotations */
111                                 mode = MODE_DEL;
112                                 continue;
113
114                         case LISTSW:  /* produce a listing */
115                                 mode = MODE_LIST;
116                                 continue;
117
118                         case COMPSW:
119                                 if (comp)
120                                         adios(EX_USAGE, NULL, "only one component at a time!");
121                                 if (!(comp = *argp++) || *comp == '-')
122                                         adios(EX_USAGE, NULL, "missing argument to %s",
123                                                         argp[-2]);
124                                 continue;
125
126                         case TEXTSW:
127                                 if (text)
128                                         adios(EX_USAGE, NULL, "only one body at a time!");
129                                 if (!(text = *argp++) || *text == '-')
130                                         adios(EX_USAGE, NULL, "missing argument to %s",
131                                                         argp[-2]);
132                                 continue;
133
134                         case NUMBERSW: /* number listing or delete by number */
135                                 if (mode == MODE_ADD) {
136                                         adios(EX_USAGE, NULL, "-number switch must appear after -list or -delete, only.");
137                                 }
138                                 if (mode == MODE_LIST) {
139                                         number = 1;
140                                         continue;
141                                 }
142                                 /* MODE_DEL */
143                                 if (number) {
144                                         adios(EX_USAGE, NULL, "only one number at a time!");
145                                 }
146                                 if (*argp && strcmp(*argp, "all")==0) {
147                                         number = -1;
148                                         argp++;
149                                         continue;
150                                 }
151                                 if (!*argp || !(number = atoi(*argp))) {
152                                         adios(EX_USAGE, NULL, "missing argument to %s",
153                                                         argp[-1]);
154                                 }
155                                 if (number < 0) {
156                                         adios(EX_USAGE, NULL, "invalid number (%d).",
157                                                         number);
158                                 }
159                                 argp++;
160                                 continue;
161
162                         case DATESW:
163                                 datesw++;
164                                 continue;
165                         case NDATESW:
166                                 datesw = 0;
167                                 continue;
168
169                         case APPENDSW:
170                                 append = 1;
171                                 continue;
172
173                         case PRESERVESW:
174                                 preserve = 1;
175                                 continue;
176
177                         case NOPRESERVESW:
178                                 preserve = 0;
179                                 continue;
180                         }
181                 }
182                 if (*cp == '+' || *cp == '@') {
183                         if (folder)
184                                 adios(EX_USAGE, NULL, "only one folder at a time!");
185                         else
186                                 folder = mh_xstrdup(expandfol(cp));
187                 } else if (*cp == '/' || *cp == '.') {
188                         if (file)
189                                 adios(EX_USAGE, NULL, "only one file at a time!");
190                         file = cp;
191                 } else {
192                         app_msgarg(&msgs, cp);
193                 }
194         }
195
196         if (file && (folder || msgs.size)) {
197                 adios(EX_USAGE, NULL, "Don't intermix files and messages.");
198         }
199         if (!datesw && !text) {
200                 adios(EX_USAGE, NULL, "-nodate without -text is a no-op.");
201         }
202         if (number && text) {
203                 adios(EX_USAGE, NULL, "Don't combine -number with -text.");
204         }
205
206         if (file) {
207                 if (mode == MODE_LIST)
208                         annolist(file, comp, number);
209                 else
210                         annotate(file, comp, text, datesw, number,
211                                         append, preserve);
212                 exit(EX_OK);
213         }
214
215         if (!msgs.size)
216                 app_msgarg(&msgs, seq_cur);
217         if (!folder)
218                 folder = getcurfol();
219         maildir = toabsdir(folder);
220
221         if (chdir(maildir) == NOTOK)
222                 adios(EX_OSERR, maildir, "unable to change directory to");
223
224         /* read folder and create message structure */
225         if (!(mp = folder_read(folder)))
226                 adios(EX_IOERR, NULL, "unable to read folder %s", folder);
227
228         /* check for empty folder */
229         if (mp->nummsg == 0)
230                 adios(EX_DATAERR, NULL, "no messages in %s", folder);
231
232         /* parse all the message ranges/sequences and set SELECTED */
233         for (msgnum = 0; msgnum < msgs.size; msgnum++) {
234                 if (!m_convert(mp, msgs.msgs[msgnum])) {
235                         exit(EX_SOFTWARE);
236                 }
237         }
238
239         /* annotate all the SELECTED messages */
240         for (msgnum = mp->lowsel; msgnum <= mp->hghsel; msgnum++) {
241                 if (is_selected(mp, msgnum)) {
242                         if (mode == MODE_LIST)
243                                 annolist(m_name(msgnum), comp, number);
244                         else
245                                 annotate(m_name(msgnum), comp, text, datesw,
246                                                 number, append, preserve);
247                 }
248         }
249
250         context_replace(curfolder, folder);
251         seq_setcur(mp, mp->lowsel);
252         seq_save(mp);
253         folder_free(mp);
254         context_save();
255         return 0;
256 }
257
258 static void
259 make_comp(unsigned char **ap)
260 {
261         unsigned char *cp;
262         char buffer[BUFSIZ];
263
264         if (!*ap) {
265                 printf("Enter component name: ");
266                 fflush(stdout);
267
268                 if (!fgets(buffer, sizeof buffer, stdin)) {
269                         exit(EX_IOERR);
270                 }
271                 *ap = trimcpy(buffer);
272         }
273
274         if ((cp = *ap + strlen(*ap) - 1) > *ap && *cp == ':')
275                 *cp = '\0';
276         if (strlen(*ap) == 0)
277                 adios(EX_SOFTWARE, NULL, "null component name");
278         if (**ap == '-')
279                 adios(EX_CONFIG, NULL, "invalid component name %s", *ap);
280         if (strlen(*ap) >= NAMESZ)
281                 adios(EX_SOFTWARE, NULL, "too large component name %s", *ap);
282
283         for (cp = *ap; *cp; cp++)
284                 if (!isalnum(*cp) && *cp != '-')
285                         adios(EX_CONFIG, NULL, "invalid component name %s", *ap);
286 }
287
288
289 /*
290 **  Produce a listing of all header fields (annotations) whose field
291 **  name matches comp.  Number the listing if number is set.
292 */
293 static void
294 annolist(char *file, unsigned char *comp, int number)
295 {
296         int c;
297         int count = 1;  /* header field (annotation) counter */
298         char *cp;
299         char *field;
300         int field_size;
301         FILE *fp;
302         int length;
303         int n;  /* number of bytes written */
304
305         if ((fp = fopen(file, "r")) == NULL) {
306                 adios(EX_IOERR, file, "unable to open");
307         }
308
309         /* We'll grow this buffer as needed. */
310         field = mh_xcalloc(field_size = 256, sizeof(char));
311
312         make_comp(&comp);
313         length = strlen(comp); /* Convenience copy. */
314
315         do {
316                 /*
317                 ** Get a line from the input file, growing the field buffer
318                 ** as needed.  We do this so that we can fit an entire line
319                 ** in the buffer making it easy to do a string comparison
320                 ** on both the field name and the field body which might be
321                 ** a long path name.
322                 */
323                 for (n = 0, cp = field; (c = getc(fp)) != EOF; *cp++ = c) {
324                         if (c == '\n' && (c = getc(fp)) != ' ' && c != '\t') {
325                                 ungetc(c, fp);
326                                 c = '\n';
327                                 break;
328                         }
329                         if (++n >= field_size - 1) {
330                                 field = mh_xrealloc(field, field_size += 256);
331                                 cp = field + n - 1;
332                         }
333                 }
334                 *cp = '\0';
335
336                 if (strncasecmp(field, comp, length)==0 &&
337                                 field[length] == ':') {
338                         cp = trim(field + length + 1);
339                         if (number) {
340                                 printf("%d\t", count++);
341                         }
342                         printf("%s\n", cp);
343                 }
344
345         } while (*field && *field != '-');
346
347         mh_free0(&field);
348         fclose(fp);
349
350         return;
351 }
352
353
354 static int
355 annotate(char *file, unsigned char *comp, char *text, int datesw,
356                 int number, int append, int preserve)
357 {
358         int fd;
359         struct utimbuf b;
360         int perms, tmpfd;
361         char tmpfil[BUFSIZ];
362         struct stat st;
363         FILE *tmp;
364
365         /* open and lock the file to be annotated */
366         if ((fd = lkopen(file, O_RDWR, 0)) == NOTOK) {
367                 switch (errno) {
368                 case ENOENT:
369                         break;
370                 default:
371                         admonish(file, "unable to lock and open");
372                         break;
373                 }
374                 return 1;
375         }
376
377         if (stat(file, &st) == -1) {
378                 advise("can't get access and modification times for %s", file);
379                 preserve = 0;
380         }
381         b.actime = st.st_atime;
382         b.modtime = st.st_mtime;
383
384         perms = fstat(fd, &st) != NOTOK ?
385                         (int)(st.st_mode & 0777) : m_gmprot();
386
387         strncpy(tmpfil, m_mktemp2(file, "annotate", NULL, &tmp),
388                         sizeof(tmpfil));
389         chmod(tmpfil, perms);
390
391         make_comp(&comp);
392
393         if (mode == MODE_DEL) {
394                 dodel(fd, comp, text, tmp, number);
395         }
396         if (mode == MODE_ADD) {
397                 doadd(fd, comp, text, tmp, datesw, append);
398         }
399
400         cpydata(fd, fileno(tmp), file, tmpfil);
401         fclose(tmp);
402
403         if ((tmpfd = open(tmpfil, O_RDONLY)) == NOTOK) {
404                 adios(EX_IOERR, tmpfil, "unable to open for re-reading");
405         }
406         lseek(fd, (off_t) 0, SEEK_SET);
407
408         /*
409         **  We're making the file shorter if we're deleting a header field
410         **  so the file has to be truncated or it will contain garbage.
411         */
412         if (mode == MODE_DEL && ftruncate(fd, 0) == -1) {
413                 adios(EX_IOERR, tmpfil, "unable to truncate.");
414         }
415         cpydata(tmpfd, fd, tmpfil, file);
416         close(tmpfd);
417         unlink(tmpfil);
418
419         if (preserve && utime(file, &b) == -1) {
420                 advise("can't set access and modification times for %s", file);
421         }
422         lkclose(fd, file);
423         return 0;
424 }
425
426 /*
427 ** We're trying to delete a header field (annotation).
428 **
429 ** - If number is greater than zero,
430 **   we're deleting the nth header field that matches
431 **   the field (component) name.
432 ** - If number is zero and text is NULL,
433 **   we're deleting the first field in which the field name
434 **   matches the component name.
435 ** - If number is zero and text is set,
436 **   we're deleting the first field in which both the field name
437 **   matches the component name and the field body matches the text.
438 ** - If number is -1,
439 **   we delete all matching fields.
440 */
441 static void
442 dodel(int fd, unsigned char *comp, char *text, FILE *tmp, int number)
443 {
444         int length = strlen(comp);  /* convenience copy */
445         int count = 1;  /* Number of matching header line. */
446         int c, n;
447         char *cp;
448         char *field = NULL;
449         int field_size = 256;
450         FILE *fp;
451
452         /*
453         ** We're going to need to copy some of the message file to the
454         ** temporary file while examining the contents.  Convert the
455         ** message file descriptor to a file pointer since it's a lot
456         ** easier and more efficient to use stdio for this.  Also allocate
457         ** a buffer to hold the header components as they're read in.
458         ** This buffer is grown as needed later.
459         */
460         if ((fp = fdopen(fd, "r")) == NULL) {
461                 adios(EX_IOERR, NULL, "unable to fdopen file.");
462         }
463         field = mh_xcalloc(field_size, sizeof(char));
464
465         /*
466         **  Copy lines from the input file to the temporary file
467         **  until we either find the one that we're looking
468         **  for (which we don't copy) or we reach the end of
469         **  the headers.  Both a blank line and a line beginning
470         **  with a - terminate the headers so that we can handle
471         **  both drafts and RFC-2822 format messages.
472         */
473         do {
474                 /*
475                 ** Get a line from the input file, growing the
476                 ** field buffer as needed.  We do this so that
477                 ** we can fit an entire line in the buffer making
478                 ** it easy to do a string comparison on both the
479                 ** field name and the field body which might be
480                 ** a long path name.
481                 */
482                 for (n=0, cp=field; (c=getc(fp)) != EOF; *cp++ = c) {
483                         if (c == '\n' && (c = getc(fp)) != ' ' &&
484                                         c != '\t') {
485                                 ungetc(c, fp);
486                                 c = '\n';
487                                 break;
488                         }
489
490                         if (++n >= field_size - 1) {
491                                 field = mh_xrealloc(field, field_size *= 2);
492                                 cp = field + n - 1;
493                         }
494                 }
495                 *cp = '\0';
496
497                 if (strncasecmp(field, comp, length)==0 &&
498                                 field[length] == ':') {
499                         /*
500                         ** This component matches and thus is a candidate.
501                         ** We delete the line by not copying it to the
502                         ** temporary file. Thus:
503                         ** - Break if we've found the one to delete.
504                         ** - Continue if this is one to delete, but
505                         **   there'll be further ones.
506                         */
507
508                         if (!number && !text) {
509                                 /* this first one is it */
510                                 break;
511                         }
512
513                         if (number == -1) {
514                                 /* delete all of them */
515                                 continue;
516                         } else if (number == count++) {
517                                 /* delete this specific one */
518                                 break;
519                         }
520
521                         if (text) {
522                                 /* delete the first matching one */
523                                 cp = field+length+1;
524                                 while (*cp==' ' || *cp=='\t') {
525                                         cp++;  /* eat leading whitespace */
526                                 }
527                                 if (*text == '/' && strcmp(text, cp)==0) {
528                                         break;  /* full path matches */
529                                 } else if (strcmp(text, mhbasename(cp))==0) {
530                                         break;  /* basename matches */
531                                 }
532                         }
533                         /*
534                         ** Although the compoment name mached, it
535                         ** wasn't the right one.
536                         */
537                 }
538
539                 /* Copy it. */
540                 if ((n = fputs(field, tmp)) == EOF ||
541                                 (c=='\n' && fputc('\n', tmp)==EOF)) {
542                         adios(EX_CANTCREAT, NULL, "unable to write temporary file.");
543                 }
544
545         } while (*field && *field != '-');
546
547         mh_free0(&field);
548
549         fflush(tmp);
550         fflush(fp); /* The underlying fd will be closed by lkclose() */
551
552         /*
553         ** We've been messing with the input file position.  Move the
554         ** input file descriptor to the current place in the file
555         ** because the stock data copying routine uses the descriptor,
556         ** not the pointer.
557         */
558         if (lseek(fd, (off_t)ftell(fp), SEEK_SET) == (off_t)-1) {
559                 adios(EX_IOERR, NULL, "can't seek.");
560         }
561 }
562
563
564 static void
565 doadd(int fd, unsigned char *comp, char *text, FILE *tmp, int datesw,
566                 int append)
567 {
568         char *cp, *sp;
569         int c;
570         FILE *fp = NULL;
571
572         if (append) {
573                 /*
574                 ** We're going to need to copy some of the message
575                 ** file to the temporary file while examining the
576                 ** contents.  Convert the message file descriptor to
577                 ** a file pointer since it's a lot easier and more
578                 ** efficient to use stdio for this.  Also allocate
579                 ** a buffer to hold the header components as they're
580                 ** read in.  This buffer is grown as needed later.
581                 */
582                 if ((fp = fdopen(fd, "r")) == NULL) {
583                         adios(EX_IOERR, NULL, "unable to fdopen file.");
584                 }
585                 /* Find the end of the headers. */
586                 if ((c = getc(fp)) == '\n') {
587                         /* Special check for no headers is needed. */
588                         rewind(fp);
589                 } else {
590                         /*
591                         ** Copy lines from the input file to the
592                         ** temporary file until we reach the end
593                         ** of the headers.
594                         */
595                         putc(c, tmp);
596                         while ((c = getc(fp)) != EOF) {
597                                 putc(c, tmp);
598                                 if (c == '\n') {
599                                         ungetc(c = getc(fp), fp);
600                                         if (c == '\n' || c == '-') {
601                                                 break;
602                                         }
603                                 }
604                         }
605                 }
606         }
607
608         if (datesw) {
609                 fprintf(tmp, "%s: %s\n", comp, dtimenow());
610         }
611         if ((cp = text)) {
612                 /* Add body text header */
613                 do {
614                         while (*cp == ' ' || *cp == '\t') {
615                                 cp++;
616                         }
617                         sp = cp;
618                         while (*cp && *cp++ != '\n') {
619                                 continue;
620                         }
621                         if (cp - sp) {
622                                 fprintf(tmp, "%s: %*.*s", comp,
623                                         (int)(cp - sp),
624                                         (int)(cp - sp), sp);
625                         }
626                 } while (*cp);
627                 if (cp[-1] != '\n' && cp != text) {
628                         putc('\n', tmp);
629                 }
630         }
631         fflush(tmp);
632
633         /*
634         ** We've been messing with the input file position.  Move the
635         ** input file descriptor to the current place in the file
636         ** because the stock data copying routine uses the descriptor,
637         ** not the pointer.
638         */
639         if (append) {
640                 if (lseek(fd, (off_t)ftell(fp), SEEK_SET) == (off_t)-1) {
641                         adios(EX_IOERR, NULL, "can't seek.");
642                 }
643         }
644 }