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