Replace mh_xmalloc() with mh_xcalloc()
[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 /*
57 ** static prototypes
58 */
59 static void make_comp(unsigned char **);
60 static int annotate(char *, unsigned char *, char *, int, int, int, int);
61 static void annolist(char *, unsigned char *, int);
62 static void dodel(int, unsigned char *, char *, FILE *, int);
63 static void doadd(int, unsigned char *, char *, FILE *, int, int);
64
65
66 int
67 main(int argc, char **argv)
68 {
69         int datesw = 1;
70         int preserve = 0;
71         int msgnum;
72         char *cp, *maildir;
73         unsigned char *comp = NULL;
74         char *text = NULL, *folder = NULL, buf[BUFSIZ];
75         char *file = NULL;
76         char **argp, **arguments;
77         struct msgs_array msgs = { 0, 0, NULL };
78         struct msgs *mp;
79         int append = 0;  /* append annotations instead of default prepend */
80         int number = 0; /* delete specific number of like elements if set */
81
82         setlocale(LC_ALL, "");
83         invo_name = mhbasename(argv[0]);
84         context_read();
85
86         arguments = getarguments(invo_name, argc, argv, 1);
87         argp = arguments;
88
89         while ((cp = *argp++)) {
90                 if (*cp == '-') {
91                         switch (smatch(++cp, switches)) {
92                         case AMBIGSW:
93                                 ambigsw(cp, switches);
94                                 exit(EX_USAGE);
95                         case UNKWNSW:
96                                 adios(EX_USAGE, NULL, "-%s unknown", cp);
97
98                         case HELPSW:
99                                 snprintf(buf, sizeof(buf),
100                                         "%s [+folder] [msgs] [switches]",
101                                         invo_name);
102                                 print_help(buf, switches, 1);
103                                 exit(argc == 2 ? EX_OK : EX_USAGE);
104                         case VERSIONSW:
105                                 print_version(invo_name);
106                                 exit(argc == 2 ? EX_OK : EX_USAGE);
107
108                         case DELETESW:  /* delete annotations */
109                                 mode = MODE_DEL;
110                                 continue;
111
112                         case LISTSW:  /* produce a listing */
113                                 mode = MODE_LIST;
114                                 continue;
115
116                         case COMPSW:
117                                 if (comp)
118                                         adios(EX_USAGE, NULL, "only one component at a time!");
119                                 if (!(comp = *argp++) || *comp == '-')
120                                         adios(EX_USAGE, NULL, "missing argument to %s",
121                                                         argp[-2]);
122                                 continue;
123
124                         case TEXTSW:
125                                 if (text)
126                                         adios(EX_USAGE, NULL, "only one body at a time!");
127                                 if (!(text = *argp++) || *text == '-')
128                                         adios(EX_USAGE, NULL, "missing argument to %s",
129                                                         argp[-2]);
130                                 continue;
131
132                         case NUMBERSW: /* number listing or delete by number */
133                                 if (mode == MODE_ADD) {
134                                         adios(EX_USAGE, NULL, "-number switch must appear after -list or -delete, only.");
135                                 }
136                                 if (mode == MODE_LIST) {
137                                         number = 1;
138                                         continue;
139                                 }
140                                 /* MODE_DEL */
141                                 if (number) {
142                                         adios(EX_USAGE, NULL, "only one number at a time!");
143                                 }
144                                 if (*argp && strcmp(*argp, "all")==0) {
145                                         number = -1;
146                                         argp++;
147                                         continue;
148                                 }
149                                 if (!*argp || !(number = atoi(*argp))) {
150                                         adios(EX_USAGE, NULL, "missing argument to %s",
151                                                         argp[-1]);
152                                 }
153                                 if (number < 0) {
154                                         adios(EX_USAGE, NULL, "invalid number (%d).",
155                                                         number);
156                                 }
157                                 argp++;
158                                 continue;
159
160                         case DATESW:
161                                 datesw++;
162                                 continue;
163                         case NDATESW:
164                                 datesw = 0;
165                                 continue;
166
167                         case APPENDSW:
168                                 append = 1;
169                                 continue;
170
171                         case PRESERVESW:
172                                 preserve = 1;
173                                 continue;
174
175                         case NOPRESERVESW:
176                                 preserve = 0;
177                                 continue;
178                         }
179                 }
180                 if (*cp == '+' || *cp == '@') {
181                         if (folder)
182                                 adios(EX_USAGE, NULL, "only one folder at a time!");
183                         else
184                                 folder = getcpy(expandfol(cp));
185                 } else if (*cp == '/' || *cp == '.') {
186                         if (file)
187                                 adios(EX_USAGE, NULL, "only one file at a time!");
188                         file = cp;
189                 } else {
190                         app_msgarg(&msgs, cp);
191                 }
192         }
193
194         if (file && (folder || msgs.size)) {
195                 adios(EX_USAGE, NULL, "Don't intermix files and messages.");
196         }
197         if (!datesw && !text) {
198                 adios(EX_USAGE, NULL, "-nodate without -text is a no-op.");
199         }
200         if (number && text) {
201                 adios(EX_USAGE, NULL, "Don't combine -number with -text.");
202         }
203
204         if (file) {
205                 if (mode == MODE_LIST)
206                         annolist(file, comp, number);
207                 else
208                         annotate(file, comp, text, datesw, number,
209                                         append, preserve);
210                 exit(EX_OK);
211         }
212
213         if (!msgs.size)
214                 app_msgarg(&msgs, seq_cur);
215         if (!folder)
216                 folder = getcurfol();
217         maildir = toabsdir(folder);
218
219         if (chdir(maildir) == NOTOK)
220                 adios(EX_OSERR, maildir, "unable to change directory to");
221
222         /* read folder and create message structure */
223         if (!(mp = folder_read(folder)))
224                 adios(EX_IOERR, NULL, "unable to read folder %s", folder);
225
226         /* check for empty folder */
227         if (mp->nummsg == 0)
228                 adios(EX_DATAERR, NULL, "no messages in %s", folder);
229
230         /* parse all the message ranges/sequences and set SELECTED */
231         for (msgnum = 0; msgnum < msgs.size; msgnum++) {
232                 if (!m_convert(mp, msgs.msgs[msgnum])) {
233                         exit(EX_SOFTWARE);
234                 }
235         }
236
237         /* annotate all the SELECTED messages */
238         for (msgnum = mp->lowsel; msgnum <= mp->hghsel; msgnum++) {
239                 if (is_selected(mp, msgnum)) {
240                         if (mode == MODE_LIST)
241                                 annolist(m_name(msgnum), comp, number);
242                         else
243                                 annotate(m_name(msgnum), comp, text, datesw,
244                                                 number, append, preserve);
245                 }
246         }
247
248         context_replace(curfolder, folder);
249         seq_setcur(mp, mp->lowsel);
250         seq_save(mp);
251         folder_free(mp);
252         context_save();
253         return 0;
254 }
255
256 static void
257 make_comp(unsigned char **ap)
258 {
259         unsigned char *cp;
260         char buffer[BUFSIZ];
261
262         if (!*ap) {
263                 printf("Enter component name: ");
264                 fflush(stdout);
265
266                 if (!fgets(buffer, sizeof buffer, stdin)) {
267                         exit(EX_IOERR);
268                 }
269                 *ap = trimcpy(buffer);
270         }
271
272         if ((cp = *ap + strlen(*ap) - 1) > *ap && *cp == ':')
273                 *cp = '\0';
274         if (strlen(*ap) == 0)
275                 adios(EX_SOFTWARE, NULL, "null component name");
276         if (**ap == '-')
277                 adios(EX_CONFIG, NULL, "invalid component name %s", *ap);
278         if (strlen(*ap) >= NAMESZ)
279                 adios(EX_SOFTWARE, NULL, "too large component name %s", *ap);
280
281         for (cp = *ap; *cp; cp++)
282                 if (!isalnum(*cp) && *cp != '-')
283                         adios(EX_CONFIG, NULL, "invalid component name %s", *ap);
284 }
285
286
287 /*
288 **  Produce a listing of all header fields (annotations) whose field
289 **  name matches comp.  Number the listing if number is set.
290 */
291 static void
292 annolist(char *file, unsigned char *comp, int number)
293 {
294         int c;
295         int count = 1;  /* header field (annotation) counter */
296         char *cp;
297         char *field;
298         int field_size;
299         FILE *fp;
300         int length;
301         int n;  /* number of bytes written */
302
303         if ((fp = fopen(file, "r")) == NULL) {
304                 adios(EX_IOERR, file, "unable to open");
305         }
306
307         /* We'll grow this buffer as needed. */
308         field = (char *)mh_xcalloc(field_size = 256, sizeof(char));
309
310         make_comp(&comp);
311         length = strlen(comp); /* Convenience copy. */
312
313         do {
314                 /*
315                 ** Get a line from the input file, growing the field buffer
316                 ** as needed.  We do this so that we can fit an entire line
317                 ** in the buffer making it easy to do a string comparison
318                 ** on both the field name and the field body which might be
319                 ** a long path name.
320                 */
321                 for (n = 0, cp = field; (c = getc(fp)) != EOF; *cp++ = c) {
322                         if (c == '\n' && (c = getc(fp)) != ' ' && c != '\t') {
323                                 ungetc(c, fp);
324                                 c = '\n';
325                                 break;
326                         }
327                         if (++n >= field_size - 1) {
328                                 field = (char *)mh_xrealloc(field,
329                                                 field_size += 256);
330                                 cp = field + n - 1;
331                         }
332                 }
333                 *cp = '\0';
334
335                 if (strncasecmp(field, comp, length)==0 &&
336                                 field[length] == ':') {
337                         cp = trim(field + length + 1);
338                         if (number) {
339                                 printf("%d\t", count++);
340                         }
341                         printf("%s\n", cp);
342                 }
343
344         } while (*field && *field != '-');
345
346         free(field);
347         fclose(fp);
348
349         return;
350 }
351
352
353 static int
354 annotate(char *file, unsigned char *comp, char *text, int datesw,
355                 int number, int append, int preserve)
356 {
357         int fd;
358         struct utimbuf b;
359         int perms, tmpfd;
360         char tmpfil[BUFSIZ];
361         struct stat st;
362         FILE *tmp;
363
364         /* open and lock the file to be annotated */
365         if ((fd = lkopen(file, O_RDWR, 0)) == NOTOK) {
366                 switch (errno) {
367                 case ENOENT:
368                         break;
369                 default:
370                         admonish(file, "unable to lock and open");
371                         break;
372                 }
373                 return 1;
374         }
375
376         if (stat(file, &st) == -1) {
377                 advise("can't get access and modification times for %s", file);
378                 preserve = 0;
379         }
380         b.actime = st.st_atime;
381         b.modtime = st.st_mtime;
382
383         perms = fstat(fd, &st) != NOTOK ?
384                         (int)(st.st_mode & 0777) : m_gmprot();
385
386         strncpy(tmpfil, m_mktemp2(file, "annotate", NULL, &tmp),
387                         sizeof(tmpfil));
388         chmod(tmpfil, perms);
389
390         make_comp(&comp);
391
392         if (mode == MODE_DEL) {
393                 dodel(fd, comp, text, tmp, number);
394         }
395         if (mode == MODE_ADD) {
396                 doadd(fd, comp, text, tmp, datesw, append);
397         }
398
399         cpydata(fd, fileno(tmp), file, tmpfil);
400         fclose(tmp);
401
402         if ((tmpfd = open(tmpfil, O_RDONLY)) == NOTOK) {
403                 adios(EX_IOERR, tmpfil, "unable to open for re-reading");
404         }
405         lseek(fd, (off_t) 0, SEEK_SET);
406
407         /*
408         **  We're making the file shorter if we're deleting a header field
409         **  so the file has to be truncated or it will contain garbage.
410         */
411         if (mode == MODE_DEL && ftruncate(fd, 0) == -1) {
412                 adios(EX_IOERR, tmpfil, "unable to truncate.");
413         }
414         cpydata(tmpfd, fd, tmpfil, file);
415         close(tmpfd);
416         unlink(tmpfil);
417
418         if (preserve && utime(file, &b) == -1) {
419                 advise("can't set access and modification times for %s", file);
420         }
421         lkclose(fd, file);
422         return 0;
423 }
424
425 /*
426 ** We're trying to delete a header field (annotation).
427 **
428 ** - If number is greater than zero,
429 **   we're deleting the nth header field that matches
430 **   the field (component) name.
431 ** - If number is zero and text is NULL,
432 **   we're deleting the first field in which the field name
433 **   matches the component name.
434 ** - If number is zero and text is set,
435 **   we're deleting the first field in which both the field name
436 **   matches the component name and the field body matches the text.
437 ** - If number is -1,
438 **   we delete all matching fields.
439 */
440 static void
441 dodel(int fd, unsigned char *comp, char *text, FILE *tmp, int number)
442 {
443         int length = strlen(comp);  /* convenience copy */
444         int count = 1;  /* Number of matching header line. */
445         int c, n;
446         char *cp;
447         char *field = NULL;
448         int field_size = 256;
449         FILE *fp;
450
451         /*
452         ** We're going to need to copy some of the message file to the
453         ** temporary file while examining the contents.  Convert the
454         ** message file descriptor to a file pointer since it's a lot
455         ** easier and more efficient to use stdio for this.  Also allocate
456         ** a buffer to hold the header components as they're read in.
457         ** This buffer is grown as needed later.
458         */
459         if ((fp = fdopen(fd, "r")) == NULL) {
460                 adios(EX_IOERR, NULL, "unable to fdopen file.");
461         }
462         field = (char *)mh_xcalloc(field_size, sizeof(char));
463
464         /*
465         **  Copy lines from the input file to the temporary file
466         **  until we either find the one that we're looking
467         **  for (which we don't copy) or we reach the end of
468         **  the headers.  Both a blank line and a line beginning
469         **  with a - terminate the headers so that we can handle
470         **  both drafts and RFC-2822 format messages.
471         */
472         do {
473                 /*
474                 ** Get a line from the input file, growing the
475                 ** field buffer as needed.  We do this so that
476                 ** we can fit an entire line in the buffer making
477                 ** it easy to do a string comparison on both the
478                 ** field name and the field body which might be
479                 ** a long path name.
480                 */
481                 for (n=0, cp=field; (c=getc(fp)) != EOF; *cp++ = c) {
482                         if (c == '\n' && (c = getc(fp)) != ' ' &&
483                                         c != '\t') {
484                                 ungetc(c, fp);
485                                 c = '\n';
486                                 break;
487                         }
488
489                         if (++n >= field_size - 1) {
490                                 field = (char *) mh_xrealloc(field,
491                                                 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         free(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 }