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