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