Changed types and added casts so that build is clean with gcc -Wsign-compare.
[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.
9 ** Message header fields are used by the new MIME attachment code in
10 ** the send command.  Adding features to generalize the anno command
11 ** seemed to be a better approach than the creation of a new command
12 ** whose features would overlap with those of the anno command.
13 **
14 ** The -delete option deletes header elements that match the -component
15 ** field name.  If -delete is used without the -text option, the first
16 ** header field whose field name matches the component name is deleted.
17 ** If the -delete is used with the -text option, and the -text argument
18 ** begins with a /, the first header field whose field name matches the
19 ** component name and whose field body matches the text is deleted.  If
20 ** the -text argument does not begin with a /, then the text is assumed
21 ** to be the last component of a path name, and the first header field
22 ** whose field name matches the component name and a field body whose
23 ** last path name component matches the text is deleted.  If the -delete
24 ** option is used with the new -number option described below, the nth
25 ** header field whose field name matches the component name is deleted.
26 ** No header fields are deleted if none of the above conditions are met.
27 **
28 ** The -list option outputs the field bodies from each header field whose
29 ** field name matches the component name, one per line.  If no -text
30 ** option is specified, only the last path name component of each field
31 ** body is output.  The entire field body is output if the -text option
32 ** is used; the contents of the -text argument are ignored.  If the -list
33 ** option is used in conjuction with the new -number option described
34 ** below, each line is numbered starting with 1.  A tab separates the
35 ** number from the field body.
36 **
37 ** The -number option works with both the -delete and -list options as
38 ** described above.  The -number option takes an optional argument.  A
39 ** value of 1 is assumed if this argument is absent.
40 */
41
42 #include <h/mh.h>
43 #include <h/utils.h>
44 #include <h/tws.h>
45 #include <fcntl.h>
46 #include <errno.h>
47 #include <utime.h>
48
49
50 static struct swit switches[] = {
51 #define COMPSW 0
52         { "component field", 0 },
53 #define DATESW 1
54         { "date", 0 },
55 #define NDATESW 2
56         { "nodate", 0 },
57 #define TEXTSW 3
58         { "text body", 0 },
59 #define VERSIONSW 4
60         { "version", 0 },
61 #define HELPSW 5
62         { "help", 0 },
63 #define LISTSW 6
64         { "list", 1 },
65 #define DELETESW 7
66         { "delete", 2 },
67 #define NUMBERSW 8
68         { "number", 2 },
69 #define APPENDSW 9
70         { "append", 1 },
71 #define PRESERVESW 10
72         { "preserve", 1 },
73 #define NOPRESERVESW 11
74         { "nopreserve", 3 },
75         { NULL, 0 }
76 };
77
78 /*
79 ** static prototypes
80 */
81 static void make_comp(unsigned char **);
82 static int annosbr(int, char *, char *, char *, int, int, int);
83 static int annotate(char *, char *, char *, int, int, int, int);
84 static void annolist(char *, char *, char *, int);
85
86
87 int
88 main(int argc, char **argv)
89 {
90         int datesw = 1;
91         int preserve = 0;
92         int msgnum;
93         char *cp, *maildir;
94         unsigned char *comp = NULL;
95         char *text = NULL, *folder = NULL, buf[BUFSIZ];
96         char *file = NULL;
97         char **argp, **arguments;
98         struct msgs_array msgs = { 0, 0, NULL };
99         struct msgs *mp;
100         int append = 0;  /* append annotations instead of default prepend */
101         int delete = -2;  /* delete header element if set */
102         int list = 0;  /* list header elements if set */
103         int number = 0; /* delete specific number of like elements if set */
104         int havemsgs = 0;
105
106 #ifdef LOCALE
107         setlocale(LC_ALL, "");
108 #endif
109         invo_name = mhbasename(argv[0]);
110
111         /* read user profile/context */
112         context_read();
113
114         arguments = getarguments(invo_name, argc, argv, 1);
115         argp = arguments;
116
117         while ((cp = *argp++)) {
118                 if (*cp == '-') {
119                         switch (smatch(++cp, switches)) {
120                         case AMBIGSW:
121                                 ambigsw(cp, switches);
122                                 done(1);
123                         case UNKWNSW:
124                                 adios(NULL, "-%s unknown", cp);
125
126                         case HELPSW:
127                                 snprintf(buf, sizeof(buf),
128                                         "%s [+folder] [msgs] [switches]",
129                                         invo_name);
130                                 print_help(buf, switches, 1);
131                                 done(1);
132                         case VERSIONSW:
133                                 print_version(invo_name);
134                                 done(1);
135
136                         case COMPSW:
137                                 if (comp)
138                                         adios(NULL, "only one component at a time!");
139                                 if (!(comp = *argp++) || *comp == '-')
140                                         adios(NULL, "missing argument to %s",
141                                                         argp[-2]);
142                                 continue;
143
144                         case DATESW:
145                                 datesw++;
146                                 continue;
147                         case NDATESW:
148                                 datesw = 0;
149                                 continue;
150
151                         case TEXTSW:
152                                 if (text)
153                                         adios(NULL, "only one body at a time!");
154                                 if (!(text = *argp++) || *text == '-')
155                                         adios(NULL, "missing argument to %s",
156                                                         argp[-2]);
157                                 continue;
158
159                         case DELETESW:  /* delete annotations */
160                                 delete = 0;
161                                 continue;
162
163                         case LISTSW:  /* produce a listing */
164                                 list = 1;
165                                 continue;
166
167                         case NUMBERSW: /* number listing or delete by number */
168                                 if (number != 0)
169                                         adios(NULL, "only one number at a time!");
170
171                                 if (argp-arguments == argc-1 || **argp == '-')
172                                         number = 1;
173
174                                 else {
175                                         if (strcmp(*argp, "all") == 0)
176                                                 number = -1;
177                                         else if (!(number = atoi(*argp)))
178                                                 /* FIXME: fails for
179                                                 ** `-list -number l:10'
180                                                 ** but okay if we add `all'.
181                                                 */
182                                                 adios(NULL, "missing argument to %s", argp[-1]);
183                                         argp++;
184                                 }
185
186                                 delete = number;
187                                 continue;
188
189                         case APPENDSW:
190                                 append = 1;
191                                 continue;
192
193                         case PRESERVESW:
194                                 preserve = 1;
195                                 continue;
196
197                         case NOPRESERVESW:
198                                 preserve = 0;
199                                 continue;
200                         }
201                 }
202                 if (*cp == '+' || *cp == '@') {
203                         if (folder)
204                                 adios(NULL, "only one folder at a time!");
205                         else
206                                 folder = getcpy(expandfol(cp));
207                 } else if (*cp == '/' || *cp == '.') {
208                         if (file)
209                                 adios(NULL, "only one file at a time!");
210                         file = cp;
211                 } else {
212                         app_msgarg(&msgs, cp);
213                         havemsgs = 1;
214                 }
215         }
216
217         if (file && (folder || havemsgs)) {
218                 adios(NULL, "Don't intermix files and messages.");
219         }
220
221         make_comp(&comp);
222
223         if (file) {
224                 if (list)
225                         annolist(file, comp, text, number);
226                 else
227                         annotate(file, comp, text,
228                                         datesw, delete, append, preserve);
229                 done(0);
230         }
231
232         if (!msgs.size)
233                 app_msgarg(&msgs, seq_cur);
234         if (!folder)
235                 folder = getcurfol();
236         maildir = toabsdir(folder);
237
238         if (chdir(maildir) == NOTOK)
239                 adios(maildir, "unable to change directory to");
240
241         /* read folder and create message structure */
242         if (!(mp = folder_read(folder)))
243                 adios(NULL, "unable to read folder %s", folder);
244
245         /* check for empty folder */
246         if (mp->nummsg == 0)
247                 adios(NULL, "no messages in %s", folder);
248
249         /* parse all the message ranges/sequences and set SELECTED */
250         for (msgnum = 0; msgnum < msgs.size; msgnum++)
251                 if (!m_convert(mp, msgs.msgs[msgnum]))
252                         done(1);
253
254         /* annotate all the SELECTED messages */
255         for (msgnum = mp->lowsel; msgnum <= mp->hghsel; msgnum++) {
256                 if (is_selected(mp, msgnum)) {
257                         if (list)
258                                 annolist(m_name(msgnum), comp, text, number);
259                         else
260                                 annotate(m_name(msgnum), comp, text, datesw,
261                                                 delete, append, preserve);
262                 }
263         }
264
265         context_replace(curfolder, folder);
266         seq_setcur(mp, mp->lowsel);
267         seq_save(mp);
268         folder_free(mp);
269         context_save();
270         done(0);
271         return 1;
272 }
273
274 static void
275 make_comp(unsigned char **ap)
276 {
277         register unsigned char *cp;
278         char buffer[BUFSIZ];
279
280         if (*ap == NULL) {
281                 printf("Enter component name: ");
282                 fflush(stdout);
283
284                 if (fgets(buffer, sizeof buffer, stdin) == NULL)
285                         done(1);
286                 *ap = trimcpy(buffer);
287         }
288
289         if ((cp = *ap + strlen(*ap) - 1) > *ap && *cp == ':')
290                 *cp = 0;
291         if (strlen(*ap) == 0)
292                 adios(NULL, "null component name");
293         if (**ap == '-')
294                 adios(NULL, "invalid component name %s", *ap);
295         if (strlen(*ap) >= NAMESZ)
296                 adios(NULL, "too large component name %s", *ap);
297
298         for (cp = *ap; *cp; cp++)
299                 if (!isalnum(*cp) && *cp != '-')
300                         adios(NULL, "invalid component name %s", *ap);
301 }
302
303
304 static int
305 annotate(char *file, char *comp, char *text, int datesw,
306         int delete, int append, int preserve)
307 {
308         int i, fd;
309         struct utimbuf b;
310         struct stat s;
311
312         /* open and lock the file to be annotated */
313         if ((fd = lkopen(file, O_RDWR, 0)) == NOTOK) {
314                 switch (errno) {
315                 case ENOENT:
316                         break;
317                 default:
318                         admonish(file, "unable to lock and open");
319                         break;
320                 }
321                 return 1;
322         }
323
324         if (stat(file, &s) == -1) {
325                 advise("can't get access and modification times for %s", file);
326                 preserve = 0;
327         }
328
329         b.actime = s.st_atime;
330         b.modtime = s.st_mtime;
331
332         i = annosbr(fd, file, comp, text, datesw, delete, append);
333
334         if (preserve && utime(file, &b) == -1) {
335                 advise("can't set access and modification times for %s", file);
336         }
337         lkclose(fd, file);
338         return i;
339 }
340
341 /*
342 **  Produce a listing of all header fields (annotations) whose field
343 **  name matches comp.  Number the listing if number is set.  Treat the
344 **  field bodies as path names and just output the last component unless
345 **  text is non-NULL.  We don't care what text is set to.
346 */
347 static void
348 annolist(char *file, char *comp, char *text, int number)
349 {
350         int c;
351         int count;  /* header field (annotation) counter */
352         char *cp;
353         char *field;
354         int field_size;
355         FILE *fp;
356         int length;
357         int n;  /* number of bytes written */
358         char *sp;
359
360         if ((fp = fopen(file, "r")) == NULL) {
361                 adios(file, "unable to open");
362         }
363
364         /* We'll grow this buffer as needed. */
365         field = (char *)mh_xmalloc(field_size = 256);
366
367         length = strlen(comp); /* Convenience copy. */
368         count = 0;
369
370         do {
371                 /*
372                 ** Get a line from the input file, growing the field buffer
373                 ** as needed.  We do this so that we can fit an entire line
374                 ** in the buffer making it easy to do a string comparison
375                 ** on both the field name and the field body which might be
376                 ** a long path name.
377                 */
378                 for (n = 0, cp = field; (c = getc(fp)) != EOF; *cp++ = c) {
379                         if (c == '\n' && (c = getc(fp)) != ' ' && c != '\t') {
380                                 ungetc(c, fp);
381                                 c = '\n';
382                                 break;
383                         }
384                         if (++n >= field_size - 1) {
385                                 field = (char *)mh_xrealloc(field,
386                                                 field_size += 256);
387                                 cp = field + n - 1;
388                         }
389                 }
390                 *cp = '\0';
391
392                 if (strncasecmp(field, comp, length)==0 &&
393                                 field[length] == ':') {
394                         for (cp = field + length + 1;
395                                         *cp == ' ' || *cp == '\t'; cp++) {
396                                 continue;
397                         }
398                         if (number) {
399                                 printf("%d\t", ++count);
400                         }
401                         if (!text && (sp = strrchr(cp, '/'))) {
402                                 cp = sp + 1;
403                         }
404                         printf("%s\n", cp);
405                 }
406
407         } while (*field && *field != '-');
408
409         free(field);
410         fclose(fp);
411
412         return;
413 }
414
415 static int
416 annosbr(int fd, char *file, char *comp, char *text, int datesw, int delete,
417                 int append)
418 {
419         int mode, tmpfd;
420         char *cp, *sp;
421         char tmpfil[BUFSIZ];
422         struct stat st;
423         FILE *tmp;
424         int c;
425         int count;  /* header field (annotation) counter */
426         char *field = NULL;
427         int field_size = 0;
428         FILE *fp = NULL;
429         int length;
430         int n;  /* number of bytes written */
431
432         mode = fstat(fd, &st) != NOTOK ? (int)(st.st_mode & 0777) : m_gmprot();
433
434         strncpy(tmpfil, m_mktemp2(file, "annotate", NULL, &tmp),
435                         sizeof(tmpfil));
436         chmod(tmpfil, mode);
437
438         /*
439         ** We're going to need to copy some of the message file to the
440         ** temporary file while examining the contents.  Convert the
441         ** message file descriptor to a file pointer since it's a lot
442         ** easier and more efficient to use stdio for this.  Also allocate
443         ** a buffer to hold the header components as they're read in.
444         ** This buffer is grown as needed later.
445         */
446         if (delete >= -1 || append != 0) {
447                 if ((fp = fdopen(fd, "r")) == NULL) {
448                         adios(NULL, "unable to fdopen file.");
449                 }
450                 field = (char *)mh_xmalloc(field_size = 256);
451         }
452
453         /*
454         ** We're trying to delete a header field (annotation) if the
455         ** delete flag is greater -2.  A value greater than zero
456         ** means that we're deleting the nth header field that matches
457         ** the field (component) name.  A value of zero means that
458         ** we're deleting the first field in which both the field name
459         ** matches the component name and the field body matches the text.
460         ** The text is matched in its entirety if it begins with a slash;
461         ** otherwise the text is matched against whatever portion of the
462         ** field body follows the last slash.  This allows matching of
463         ** both absolute and relative path names.  This is because this
464         ** functionality was added to support attachments.  It might be
465         ** worth having a separate flag to indicate path name matching
466         ** to make it more general.  A value of -1 means to delete all
467         ** matching fields.
468         */
469         if (delete >= -1) {
470                 length = strlen(comp);  /* convenience copy */
471                 count = 0; /* Only used if we're deleting by number. */
472
473                 /*
474                 **  Copy lines from the input file to the temporary file
475                 **  until we either find the one that we're looking
476                 **  for (which we don't copy) or we reach the end of
477                 **  the headers.  Both a blank line and a line beginning
478                 **  with a - terminate the headers so that we can handle
479                 **  both drafts and RFC-2822 format messages.
480                 */
481                 do {
482                         /*
483                         ** Get a line from the input file, growing the
484                         ** field buffer as needed.  We do this so that
485                         ** we can fit an entire line in the buffer making
486                         ** it easy to do a string comparison on both the
487                         ** field name and the field body which might be
488                         ** a long path name.
489                         */
490                         for (n=0, cp=field; (c=getc(fp)) != EOF; *cp++ = c) {
491                                 if (c == '\n' && (c = getc(fp)) != ' ' &&
492                                                 c != '\t') {
493                                         ungetc(c, fp);
494                                         c = '\n';
495                                         break;
496                                 }
497
498                                 if (++n >= field_size - 1) {
499                                         field = (char *) mh_xrealloc(field,
500                                                         field_size *= 2);
501                                         cp = field + n - 1;
502                                 }
503                         }
504                         *cp = '\0';
505
506                         /*
507                         ** Check for a match on the field name.  We delete
508                         ** the line by not copying it to the temporary
509                         ** file if
510                         **
511                         **  o  The delete flag is 0, meaning that we're
512                         **     going to delete the first matching
513                         **     field, and the text is NULL meaning that
514                         **     we don't care about the field body.
515                         **
516                         **  o  The delete flag is 0, meaning that we're
517                         **     going to delete the first matching
518                         **     field, and the text begins with a / meaning
519                         **     that we're looking for a full path name,
520                         **     and the text matches the field body.
521                         **
522                         **  o  The delete flag is 0, meaning that we're
523                         **     going to delete the first matching
524                         **     field, the text does not begin with a /
525                         **     meaning that we're looking for the last
526                         **     path name component, and the last path
527                         **     name component matches the text.
528                         **
529                         **  o  The delete flag is positive meaning that
530                         **     we're going to delete the nth field
531                         **     with a matching field name, and this is
532                         **     the nth matching field name.
533                         **
534                         **  o  The delete flag is -1 meaning that we're
535                         **     going to delete all fields with a
536                         **     matching field name.
537                         */
538                         if (strncasecmp(field, comp, length)==0 &&
539                                         field[length] == ':') {
540                                 if (!delete) {
541                                         if (!text) {
542                                                 break;
543                                         }
544                                         for (cp=field+length+1;
545                                                         *cp==' ' || *cp=='\t';
546                                                         cp++) {
547                                                 continue;
548                                         }
549                                         if (*text == '/' &&
550                                                         strcmp(cp, text)==0) {
551                                                 break;
552                                         } else {
553                                                 if ((sp = strrchr(cp, '/'))) {
554                                                         cp = sp + 1;
555                                                 }
556                                                 if (strcmp(cp, text)==0) {
557                                                         break;
558                                                 }
559                                         }
560                                 } else if (delete == -1) {
561                                         continue;
562                                 } else if (++count == delete) {
563                                         break;
564                                 }
565                         }
566
567                         /*
568                         ** This line wasn't a match so copy it to the
569                         ** temporary file.
570                         */
571                         if ((n = fputs(field, tmp)) == EOF ||
572                                         (c=='\n' && fputc('\n', tmp)==EOF)) {
573                                 adios(NULL, "unable to write temporary file.");
574                         }
575                 } while (*field && *field != '-');
576                 free(field);
577
578         } else {
579                 /*
580                 **  Find the end of the headers before adding the
581                 **  annotations if we're appending instead of the default
582                 **  prepending.  A special check for no headers is needed
583                 **  if appending.
584                 */
585                 if (append) {
586                         /*
587                         ** Copy lines from the input file to the temporary
588                         ** file until we reach the end of the headers.
589                         */
590                         if ((c = getc(fp)) == '\n') {
591                                 rewind(fp);
592                         } else {
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(0));
608                 }
609                 if ((cp = text)) {
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         }
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 || delete >= -1) {
638                 if (lseek(fd, (off_t)ftell(fp), SEEK_SET) == (off_t)-1) {
639                         adios(NULL, "can't seek.");
640                 }
641         }
642
643         cpydata(fd, fileno(tmp), file, tmpfil);
644         fclose(tmp);
645
646         if ((tmpfd = open(tmpfil, O_RDONLY)) == NOTOK) {
647                 adios(tmpfil, "unable to open for re-reading");
648         }
649         lseek(fd, (off_t) 0, SEEK_SET);
650
651         /*
652         **  We're making the file shorter if we're deleting a header field
653         **  so the file has to be truncated or it will contain garbage.
654         */
655         if (delete >= -1 && ftruncate(fd, 0) == -1) {
656                 adios(tmpfil, "unable to truncate.");
657         }
658         cpydata(tmpfd, fd, tmpfil, file);
659         close(tmpfd);
660         unlink(tmpfil);
661
662         /*
663         ** Close the delete file so that we don't run out of file pointers if
664         ** we're doing piles of files.  Note that this will make the close() in
665         ** lkclose() fail, but that failure is ignored so it's not a problem.
666         */
667         if (delete >= -1) {
668                 fclose(fp);
669         }
670         return 0;
671 }