2 ** anno.c -- annotate messages
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.
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.
21 static enum { MODE_ADD, MODE_DEL, MODE_LIST } mode = MODE_ADD;
23 static struct swit switches[] = {
25 { "component field", 0 },
46 #define NOPRESERVESW 11
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);
62 main(int argc, char **argv)
68 unsigned char *comp = NULL;
69 char *text = NULL, *folder = NULL, buf[BUFSIZ];
71 char **argp, **arguments;
72 struct msgs_array msgs = { 0, 0, NULL };
74 int append = 0; /* append annotations instead of default prepend */
75 int number = 0; /* delete specific number of like elements if set */
77 setlocale(LC_ALL, "");
78 invo_name = mhbasename(argv[0]);
81 arguments = getarguments(invo_name, argc, argv, 1);
84 while ((cp = *argp++)) {
86 switch (smatch(++cp, switches)) {
88 ambigsw(cp, switches);
89 /* sysexits.h EX_USAGE */
92 adios(NULL, "-%s unknown", cp);
95 snprintf(buf, sizeof(buf),
96 "%s [+folder] [msgs] [switches]",
98 print_help(buf, switches, 1);
101 print_version(invo_name);
104 case DELETESW: /* delete annotations */
108 case LISTSW: /* produce a listing */
114 adios(NULL, "only one component at a time!");
115 if (!(comp = *argp++) || *comp == '-')
116 adios(NULL, "missing argument to %s",
122 adios(NULL, "only one body at a time!");
123 if (!(text = *argp++) || *text == '-')
124 adios(NULL, "missing argument to %s",
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.");
132 if (mode == MODE_LIST) {
138 adios(NULL, "only one number at a time!");
140 if (*argp && strcmp(*argp, "all")==0) {
145 if (!*argp || !(number = atoi(*argp))) {
146 adios(NULL, "missing argument to %s",
150 adios(NULL, "invalid number (%d).",
176 if (*cp == '+' || *cp == '@') {
178 adios(NULL, "only one folder at a time!");
180 folder = getcpy(expandfol(cp));
181 } else if (*cp == '/' || *cp == '.') {
183 adios(NULL, "only one file at a time!");
186 app_msgarg(&msgs, cp);
190 if (file && (folder || msgs.size)) {
191 adios(NULL, "Don't intermix files and messages.");
193 if (!datesw && !text) {
194 adios(NULL, "-nodate without -text is a no-op.");
196 if (number && text) {
197 adios(NULL, "Don't combine -number with -text.");
201 if (mode == MODE_LIST)
202 annolist(file, comp, number);
204 annotate(file, comp, text, datesw, number,
210 app_msgarg(&msgs, seq_cur);
212 folder = getcurfol();
213 maildir = toabsdir(folder);
215 if (chdir(maildir) == NOTOK)
216 adios(maildir, "unable to change directory to");
218 /* read folder and create message structure */
219 if (!(mp = folder_read(folder)))
220 adios(NULL, "unable to read folder %s", folder);
222 /* check for empty folder */
224 adios(NULL, "no messages in %s", folder);
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 */
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);
240 annotate(m_name(msgnum), comp, text, datesw,
241 number, append, preserve);
245 context_replace(curfolder, folder);
246 seq_setcur(mp, mp->lowsel);
254 make_comp(unsigned char **ap)
260 printf("Enter component name: ");
263 if (!fgets(buffer, sizeof buffer, stdin)) {
264 /* sysexits.h EX_IOERR */
267 *ap = trimcpy(buffer);
270 if ((cp = *ap + strlen(*ap) - 1) > *ap && *cp == ':')
272 if (strlen(*ap) == 0)
273 adios(NULL, "null component name");
275 adios(NULL, "invalid component name %s", *ap);
276 if (strlen(*ap) >= NAMESZ)
277 adios(NULL, "too large component name %s", *ap);
279 for (cp = *ap; *cp; cp++)
280 if (!isalnum(*cp) && *cp != '-')
281 adios(NULL, "invalid component name %s", *ap);
286 ** Produce a listing of all header fields (annotations) whose field
287 ** name matches comp. Number the listing if number is set.
290 annolist(char *file, unsigned char *comp, int number)
293 int count = 1; /* header field (annotation) counter */
299 int n; /* number of bytes written */
301 if ((fp = fopen(file, "r")) == NULL) {
302 adios(file, "unable to open");
305 /* We'll grow this buffer as needed. */
306 field = (char *)mh_xmalloc(field_size = 256);
309 length = strlen(comp); /* Convenience copy. */
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
319 for (n = 0, cp = field; (c = getc(fp)) != EOF; *cp++ = c) {
320 if (c == '\n' && (c = getc(fp)) != ' ' && c != '\t') {
325 if (++n >= field_size - 1) {
326 field = (char *)mh_xrealloc(field,
333 if (strncasecmp(field, comp, length)==0 &&
334 field[length] == ':') {
335 cp = trim(field + length + 1);
337 printf("%d\t", count++);
342 } while (*field && *field != '-');
352 annotate(char *file, unsigned char *comp, char *text, int datesw,
353 int number, int append, int preserve)
362 /* open and lock the file to be annotated */
363 if ((fd = lkopen(file, O_RDWR, 0)) == NOTOK) {
368 admonish(file, "unable to lock and open");
374 if (stat(file, &st) == -1) {
375 advise("can't get access and modification times for %s", file);
378 b.actime = st.st_atime;
379 b.modtime = st.st_mtime;
381 perms = fstat(fd, &st) != NOTOK ?
382 (int)(st.st_mode & 0777) : m_gmprot();
384 strncpy(tmpfil, m_mktemp2(file, "annotate", NULL, &tmp),
386 chmod(tmpfil, perms);
390 if (mode == MODE_DEL) {
391 dodel(fd, comp, text, tmp, number);
393 if (mode == MODE_ADD) {
394 doadd(fd, comp, text, tmp, datesw, append);
397 cpydata(fd, fileno(tmp), file, tmpfil);
400 if ((tmpfd = open(tmpfil, O_RDONLY)) == NOTOK) {
401 adios(tmpfil, "unable to open for re-reading");
403 lseek(fd, (off_t) 0, SEEK_SET);
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.
409 if (mode == MODE_DEL && ftruncate(fd, 0) == -1) {
410 adios(tmpfil, "unable to truncate.");
412 cpydata(tmpfd, fd, tmpfil, file);
416 if (preserve && utime(file, &b) == -1) {
417 advise("can't set access and modification times for %s", file);
424 ** We're trying to delete a header field (annotation).
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.
439 dodel(int fd, unsigned char *comp, char *text, FILE *tmp, int number)
441 int length = strlen(comp); /* convenience copy */
442 int count = 1; /* Number of matching header line. */
446 int field_size = 256;
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.
457 if ((fp = fdopen(fd, "r")) == NULL) {
458 adios(NULL, "unable to fdopen file.");
460 field = (char *)mh_xmalloc(field_size);
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.
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
479 for (n=0, cp=field; (c=getc(fp)) != EOF; *cp++ = c) {
480 if (c == '\n' && (c = getc(fp)) != ' ' &&
487 if (++n >= field_size - 1) {
488 field = (char *) mh_xrealloc(field,
495 if (strncasecmp(field, comp, length)==0 &&
496 field[length] == ':') {
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.
506 if (!number && !text) {
507 /* this first one is it */
512 /* delete all of them */
514 } else if (number == count++) {
515 /* delete this specific one */
520 /* delete the first matching one */
522 while (*cp==' ' || *cp=='\t') {
523 cp++; /* eat leading whitespace */
525 if (*text == '/' && strcmp(text, cp)==0) {
526 break; /* full path matches */
527 } else if (strcmp(text, mhbasename(cp))==0) {
528 break; /* basename matches */
532 ** Although the compoment name mached, it
533 ** wasn't the right one.
538 if ((n = fputs(field, tmp)) == EOF ||
539 (c=='\n' && fputc('\n', tmp)==EOF)) {
540 adios(NULL, "unable to write temporary file.");
543 } while (*field && *field != '-');
548 fflush(fp); /* The underlying fd will be closed by lkclose() */
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,
556 if (lseek(fd, (off_t)ftell(fp), SEEK_SET) == (off_t)-1) {
557 adios(NULL, "can't seek.");
563 doadd(int fd, unsigned char *comp, char *text, FILE *tmp, int datesw,
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.
580 if ((fp = fdopen(fd, "r")) == NULL) {
581 adios(NULL, "unable to fdopen file.");
583 /* Find the end of the headers. */
584 if ((c = getc(fp)) == '\n') {
585 /* Special check for no headers is needed. */
589 ** Copy lines from the input file to the
590 ** temporary file until we reach the end
594 while ((c = getc(fp)) != EOF) {
597 ungetc(c = getc(fp), fp);
598 if (c == '\n' || c == '-') {
607 fprintf(tmp, "%s: %s\n", comp, dtimenow());
610 /* Add body text header */
612 while (*cp == ' ' || *cp == '\t') {
616 while (*cp && *cp++ != '\n') {
620 fprintf(tmp, "%s: %*.*s", comp,
625 if (cp[-1] != '\n' && cp != text) {
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,
638 if (lseek(fd, (off_t)ftell(fp), SEEK_SET) == (off_t)-1) {
639 adios(NULL, "can't seek.");