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