99422e6b11ea80e0bf8776127cbc2c9299f2b6af
[mmh] / uip / annosbr.c
1 /*
2 ** annosbr.c -- prepend annotation to 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
9 #include <h/mh.h>
10 #include <h/tws.h>
11 #include <h/utils.h>
12 #include <fcntl.h>
13 #include <errno.h>
14 #include <utime.h>
15
16
17 /*
18 ** static prototypes
19 */
20 static int annosbr(int, char *, char *, char *, int, int, int);
21
22 /*
23 ** This "local" global and the annopreserve() function are a hack that
24 ** allows additional functionality to be added to anno without piling
25 ** on yet another annotate() argument.
26 */
27
28 /* set to preserve access and modification times on annotated message */
29 static int preserve_actime_and_modtime = 0;
30
31 int
32 annotate(char *file, char *comp, char *text, int datesw,
33         int delete, int append)
34 {
35         int i, fd;
36         struct utimbuf b;
37         struct stat s;
38
39         /* open and lock the file to be annotated */
40         if ((fd = lkopen(file, O_RDWR, 0)) == NOTOK) {
41                 switch (errno) {
42                 case ENOENT:
43                         break;
44
45                 default:
46                         admonish(file, "unable to lock and open");
47                         break;
48                 }
49                 return 1;
50         }
51
52         if (stat(file, &s) == -1) {
53                 advise("can't get access and modification times for %s", file);
54                         preserve_actime_and_modtime = 0;
55         }
56
57         b.actime = s.st_atime;
58         b.modtime = s.st_mtime;
59
60         i = annosbr(fd, file, comp, text, datesw, delete, append);
61
62         if (preserve_actime_and_modtime && utime(file, &b) == -1)
63                 advise("can't set access and modification times for %s", file);
64
65         lkclose(fd, file);
66         return i;
67 }
68
69 /*
70 **  Produce a listing of all header fields (annotations) whose field
71 **  name matches comp.  Number the listing if number is set.  Treate the
72 **  field bodies as path names and just output the last component unless
73 **  text is non-NULL.  We don't care what text is set to.
74 */
75
76 void
77 annolist(char *file, char *comp, char *text, int number)
78 {
79         int c;  /* current character */
80         int count;  /* header field (annotation) counter */
81         char *cp;  /* miscellaneous character pointer */
82         char *field;  /* buffer for header field */
83         int field_size;  /* size of field buffer */
84         FILE *fp;  /* file pointer made from locked file descriptor */
85         int length;  /* length of field name */
86         int n;  /* number of bytes written */
87         char *sp;  /* another miscellaneous character pointer */
88
89         if ((fp = fopen(file, "r")) == (FILE *)0)
90                 adios(file, "unable to open");
91
92         /*
93         **  Allocate a buffer to hold the header components as they're read in.
94         **  This buffer might need to be quite large, so we grow it as needed.
95         */
96         field = (char *)mh_xmalloc(field_size = 256);
97
98         /*
99         **  Get the length of the field name since we use it often.
100         */
101         length = strlen(comp);
102
103         count = 0;
104
105         do {
106                 /*
107                 ** Get a line from the input file, growing the field buffer
108                 ** as needed.  We do this so that we can fit an entire line
109                 ** in the buffer making it easy to do a string comparison
110                 ** on both the field name and the field body which might be
111                 ** a long path name.
112                 */
113
114                 for (n = 0, cp = field; (c = getc(fp)) != EOF; *cp++ = c) {
115                         if (c == '\n' && (c = getc(fp)) != ' ' && c != '\t') {
116                                 ungetc(c, fp);
117                                 c = '\n';
118                                 break;
119                         }
120
121                         if (++n >= field_size - 1) {
122                                 field = (char *) mh_xrealloc((void *)field, field_size += 256);
123
124                                 cp = field + n - 1;
125                         }
126                 }
127
128                 /* NUL-terminate the field */
129                 *cp = '\0';
130
131                 if (strncasecmp(field, comp, length) == 0 && field[length] == ':') {
132                         for (cp = field + length + 1; *cp == ' ' || *cp == '\t'; cp++)
133                                 ;
134
135                         if (number)
136                                 printf("%d\t", ++count);
137
138                         if (text == NULL && (sp = strrchr(cp, '/')) != NULL)
139                                 cp = sp + 1;
140
141                         printf("%s\n", cp);
142                 }
143
144         } while (*field != '\0' && *field != '-');
145
146         /* Clean up. */
147         free(field);
148         fclose(fp);
149
150         return;
151 }
152
153 /*
154 ** Set the preserve-times flag.  This hack eliminates the need for an
155 ** additional argument to annotate().
156 */
157 void
158 annopreserve(int preserve)
159 {
160         preserve_actime_and_modtime = preserve;
161         return;
162 }
163
164 static int
165 annosbr(int fd, char *file, char *comp, char *text, int datesw, int delete,
166                 int append)
167 {
168         int mode, tmpfd;
169         char *cp, *sp;
170         char tmpfil[BUFSIZ];
171         struct stat st;
172         FILE *tmp;
173         int c;  /* current character */
174         int count;  /* header field (annotation) counter */
175         char *field = NULL;  /* buffer for header field */
176         int field_size = 0;  /* size of field buffer */
177         FILE *fp = NULL;  /* file pointer made from locked file descriptor */
178         int length;  /* length of field name */
179         int n;  /* number of bytes written */
180
181         mode = fstat(fd, &st) != NOTOK ? (st.st_mode & 0777) : m_gmprot();
182
183         strncpy(tmpfil, m_mktemp2(file, "annotate", NULL, &tmp), sizeof(tmpfil));
184         chmod(tmpfil, mode);
185
186         /*
187         ** We're going to need to copy some of the message file to the
188         ** temporary file while examining the contents.  Convert the
189         ** message file descriptor to a file pointer since it's a lot
190         ** easier and more efficient to use stdio for this.  Also allocate
191         ** a buffer to hold the header components as they're read in.
192         ** This buffer is grown as needed later.
193         */
194
195         if (delete >= -1 || append != 0) {
196                 if ((fp = fdopen(fd, "r")) == (FILE *)0)
197                         adios(NULL, "unable to fdopen file.");
198
199                 field = (char *)mh_xmalloc(field_size = 256);
200         }
201
202         /*
203         ** We're trying to delete a header field (annotation )if the
204         ** delete flag is not -2 or less.  A  value greater than zero
205         ** means that we're deleting the nth header field that matches
206         ** the field (component) name.  A value of zero means that
207         ** we're deleting the first field in which both the field name
208         ** matches the component name and the field body matches the text.
209         ** The text is matched in its entirety if it begins with a slash;
210         ** otherwise the text is matched against whatever portion of the
211         ** field body follows the last slash.  This allows matching of
212         ** both absolute and relative path names.  This is because this
213         ** functionality was added to support attachments.  It might be
214         ** worth having a separate flag to indicate path name matching
215         ** to make it more general.  A value of -1 means to delete all
216         ** matching fields.
217         */
218
219         if (delete >= -1) {
220                 /* Get the length of the field name since we use it often. */
221                 length = strlen(comp);
222
223                 /*
224                 **  Initialize the field counter.  This is only used if
225                 **  we're deleting by number.
226                 */
227                 count = 0;
228
229                 /*
230                 **  Copy lines from the input file to the temporary file
231                 **  until we either find the one that we're looking
232                 **  for (which we don't copy) or we reach the end of
233                 **  the headers.  Both a blank line and a line beginning
234                 **  with a - terminate the headers so that we can handle
235                 **  both drafts and RFC-2822 format messages.
236                 */
237
238                 do {
239                         /*
240                         ** Get a line from the input file, growing the
241                         ** field buffer as needed.  We do this so that
242                         ** we can fit an entire line in the buffer making
243                         ** it easy to do a string comparison on both the
244                         ** field name and the field body which might be
245                         ** a long path name.
246                         */
247
248                         for (n = 0, cp = field; (c = getc(fp)) != EOF; *cp++ = c) {
249                                 if (c == '\n' && (c = getc(fp)) != ' ' && c != '\t') {
250                                         ungetc(c, fp);
251                                         c = '\n';
252                                         break;
253                                 }
254
255                                 if (++n >= field_size - 1) {
256                                         field = (char *) mh_xrealloc((void *)field, field_size *= 2);
257
258                                         cp = field + n - 1;
259                                 }
260                         }
261
262                         /* NUL-terminate the field */
263                         *cp = '\0';
264
265                         /*
266                         ** Check for a match on the field name.  We delete
267                         ** the line by not copying it to the temporary
268                         ** file if
269                         **
270                         **  o  The delete flag is 0, meaning that we're
271                         **     going to delete the first matching
272                         **     field, and the text is NULL meaning that
273                         **     we don't care about the field body.
274                         **
275                         **  o  The delete flag is 0, meaning that we're
276                         **     going to delete the first matching
277                         **     field, and the text begins with a / meaning
278                         **     that we're looking for a full path name,
279                         **     and the text matches the field body.
280                         **
281                         **  o  The delete flag is 0, meaning that we're
282                         **     going to delete the first matching
283                         **     field, the text does not begin with a /
284                         **     meaning that we're looking for the last
285                         **     path name component, and the last path
286                         **     name component matches the text.
287                         **
288                         **  o  The delete flag is positive meaning that
289                         **     we're going to delete the nth field
290                         **     with a matching field name, and this is
291                         **     the nth matching field name.
292                         **
293                         **  o  The delete flag is -1 meaning that we're
294                         **     going to delete all fields with a
295                         **     matching field name.
296                         */
297
298                         if (strncasecmp(field, comp, length) == 0 && field[length] == ':') {
299                                 if (delete == 0) {
300                                         if (text == NULL)
301                                                 break;
302
303                                         for (cp = field + length + 1; *cp == ' ' || *cp == '\t'; cp++)
304                                                 ;
305
306                                         if (*text == '/') {
307                                                 if (strcmp(cp, text) == 0)
308                                                                 break;
309                                         }
310                                         else {
311                                                 if ((sp = strrchr(cp, '/')) != NULL)
312                                                         cp = sp + 1;
313
314                                                 if (strcmp(cp, text) == 0)
315                                                         break;
316                                         }
317                                 }
318
319                                 else if (delete == -1)
320                                                 continue;
321
322                                 else if (++count == delete)
323                                         break;
324                         }
325
326                         /*
327                         ** This line wasn't a match so copy it to the
328                         ** temporary file.
329                         */
330
331                         if ((n = fputs(field, tmp)) == EOF || (c == '\n' && fputc('\n', tmp) == EOF))
332                                 adios(NULL, "unable to write temporary file.");
333
334                 } while (*field != '\0' && *field != '-');
335
336                 /*
337                 **  Get rid of the field buffer because we're done with it.
338                 */
339
340                 free((void *)field);
341
342         } else {
343                 /*
344                 **  Find the end of the headers before adding the
345                 **  annotations if we're appending instead of the default
346                 **  prepending.  A special check for no headers is needed
347                 **  if appending.
348                 */
349
350                 if (append) {
351                         /*
352                         ** Copy lines from the input file to the temporary
353                         ** file until we reach the end of the headers.
354                         */
355
356                         if ((c = getc(fp)) == '\n')
357                                 rewind(fp);
358
359                         else {
360                                 putc(c, tmp);
361
362                                 while ((c = getc(fp)) != EOF) {
363                                         putc(c, tmp);
364
365                                         if (c == '\n') {
366                                                 ungetc(c = getc(fp), fp);
367
368                                                 if (c == '\n' || c == '-')
369                                                         break;
370                                         }
371                                 }
372                         }
373                 }
374
375                 if (datesw)
376                         fprintf(tmp, "%s: %s\n", comp, dtimenow(0));
377                 if ((cp = text)) {
378                         do {
379                                 while (*cp == ' ' || *cp == '\t')
380                                         cp++;
381                                 sp = cp;
382                                 while (*cp && *cp++ != '\n')
383                                         continue;
384                                 if (cp - sp)
385                                         fprintf(tmp, "%s: %*.*s", comp, (int)(cp - sp), (int)(cp - sp), sp);
386                         } while (*cp);
387                         if (cp[-1] != '\n' && cp != text)
388                                 putc('\n', tmp);
389                 }
390         }
391
392         fflush(tmp);
393
394         /*
395         ** We've been messing with the input file position.  Move the
396         ** input file descriptor to the current place in the file
397         ** because the stock data copying routine uses the descriptor,
398         ** not the pointer.
399         */
400
401         if (append || delete >= -1) {
402                 if (lseek(fd, (off_t)ftell(fp), SEEK_SET) == (off_t)-1)
403                         adios(NULL, "can't seek.");
404         }
405
406         cpydata(fd, fileno(tmp), file, tmpfil);
407         fclose(tmp);
408
409         if ((tmpfd = open(tmpfil, O_RDONLY)) == NOTOK)
410                 adios(tmpfil, "unable to open for re-reading");
411
412         lseek(fd, (off_t) 0, SEEK_SET);
413
414         /*
415         **  We're making the file shorter if we're deleting a
416         **  header field so the file has to be truncated or it
417         **  will contain garbage.
418         */
419
420         if (delete >= -1 && ftruncate(fd, 0) == -1)
421                 adios(tmpfil, "unable to truncate.");
422
423         cpydata(tmpfd, fd, tmpfil, file);
424         close(tmpfd);
425         unlink(tmpfil);
426
427         /*
428         ** Close the delete file so that we don't run out of file pointers if
429         ** we're doing piles of files.  Note that this will make the close() in
430         ** lkclose() fail, but that failure is ignored so it's not a problem.
431         */
432
433         if (delete >= -1)
434                 fclose(fp);
435
436         return 0;
437 }