]> diplodocus.org Git - nmh/blob - uip/attach.c
Explictly return the exit code, so we can portably guarantee that
[nmh] / uip / attach.c
1 /*
2 * attach.c -- routines to help attach files via whatnow
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/utils.h>
11 #include <h/tws.h>
12
13 static int get_line(FILE *, char *, size_t);
14 #ifdef MIMETYPEPROC
15 static char *get_file_info(const char *, const char *);
16 #endif /* MIMETYPEPROC */
17
18 int
19 attach(char *attachment_header_field_name, char *draft_file_name,
20 char *body_file_name, size_t body_file_name_len,
21 char *composition_file_name, size_t composition_file_name_len,
22 int attachformat)
23 {
24 char buf[PATH_MAX + 6]; /* miscellaneous buffer */
25 int c; /* current character for body copy */
26 int has_attachment; /* draft has at least one attachment */
27 int has_body; /* draft has a message body */
28 int length; /* of attachment header field name */
29 char *p; /* miscellaneous string pointer */
30 struct stat st; /* file status buffer */
31 FILE *body_file = NULL; /* body file pointer */
32 FILE *draft_file; /* draft file pointer */
33 int field_size; /* size of header field buffer */
34 char *field; /* header field buffer */
35 FILE *composition_file; /* composition file pointer */
36 char *build_directive; /* mhbuild directive */
37
38
39 /*
40 * Open up the draft file.
41 */
42
43 if ((draft_file = fopen(draft_file_name, "r")) == (FILE *)0)
44 adios(NULL, "can't open draft file `%s'.", draft_file_name);
45
46 /*
47 * Allocate a buffer to hold the header components as they're read in.
48 * This buffer might need to be quite large, so we grow it as needed.
49 */
50
51 field = (char *)mh_xmalloc(field_size = 256);
52
53 /*
54 * Scan the draft file for a header field name, with a non-empty
55 * body, that matches the -attach argument. The existence of one
56 * indicates that the draft has attachments. Bail out if there
57 * are no attachments because we're done. Read to the end of the
58 * headers even if we have no attachments.
59 */
60
61 length = strlen(attachment_header_field_name);
62
63 has_attachment = 0;
64
65 while (get_line(draft_file, field, field_size) != EOF && *field != '\0' &&
66 *field != '-') {
67 if (strncasecmp(field, attachment_header_field_name, length) == 0 &&
68 field[length] == ':') {
69 for (p = field + length + 1; *p == ' ' || *p == '\t'; p++)
70 ;
71 if (strlen (p) > 0) {
72 has_attachment = 1;
73 }
74 }
75 }
76
77 if (has_attachment == 0)
78 return (DONE);
79
80 /*
81 * We have at least one attachment. Look for at least one non-blank line
82 * in the body of the message which indicates content in the body.
83 */
84
85 has_body = 0;
86
87 while (get_line(draft_file, field, field_size) != EOF) {
88 for (p = field; *p != '\0'; p++) {
89 if (*p != ' ' && *p != '\t') {
90 has_body = 1;
91 break;
92 }
93 }
94
95 if (has_body)
96 break;
97 }
98
99 /*
100 * Make names for the temporary files.
101 */
102
103 (void)strncpy(body_file_name,
104 m_mktemp(m_maildir(invo_name), NULL, NULL),
105 body_file_name_len);
106 (void)strncpy(composition_file_name,
107 m_mktemp(m_maildir(invo_name), NULL, NULL),
108 composition_file_name_len);
109
110 if (has_body)
111 body_file = fopen(body_file_name, "w");
112
113 composition_file = fopen(composition_file_name, "w");
114
115 if ((has_body && body_file == (FILE *)0) || composition_file == (FILE *)0) {
116 clean_up_temporary_files(body_file_name, composition_file_name);
117 adios(NULL, "unable to open all of the temporary files.");
118 }
119
120 /*
121 * Start at the beginning of the draft file. Copy all
122 * non-attachment header fields to the temporary composition
123 * file. Then add the dashed line separator.
124 */
125
126 rewind(draft_file);
127
128 while (get_line(draft_file, field, field_size) != EOF && *field != '\0' &&
129 *field != '-')
130 if (strncasecmp(field, attachment_header_field_name, length) != 0 ||
131 field[length] != ':')
132 (void)fprintf(composition_file, "%s\n", field);
133
134 (void)fputs("--------\n", composition_file);
135
136 /*
137 * Copy the message body to a temporary file.
138 */
139
140 if (has_body) {
141 while ((c = getc(draft_file)) != EOF)
142 putc(c, body_file);
143
144 (void)fclose(body_file);
145 }
146
147 /*
148 * Add a mhbuild MIME composition file line for the body if there was one.
149 * Set the default content type to text/plain so that mhbuild takes care
150 * of any necessary encoding.
151 */
152
153 if (has_body)
154 /*
155 * Make sure that the attachment file exists and is readable.
156 */
157 if (stat(body_file_name, &st) != OK ||
158 access(body_file_name, R_OK) != OK) {
159 advise(NULL, "unable to access file \"%s\"", body_file_name);
160 return NOTOK;
161 }
162
163 if ((build_directive = construct_build_directive (body_file_name,
164 "text/plain",
165 attachformat)) == NULL) {
166 clean_up_temporary_files(body_file_name, composition_file_name);
167 adios (NULL, "exiting due to failure in attach()");
168 } else {
169 (void) fputs(build_directive, composition_file);
170 free(build_directive);
171 }
172
173 /*
174 * Now, go back to the beginning of the draft file and look for
175 * header fields that specify attachments. Add a mhbuild MIME
176 * composition file for each.
177 */
178
179 rewind(draft_file);
180
181 while (get_line(draft_file, field, field_size) != EOF && *field != '\0' &&
182 *field != '-') {
183 if (strncasecmp(field, attachment_header_field_name, length) == 0 &&
184 field[length] == ':') {
185 for (p = field + length + 1; *p == ' ' || *p == '\t'; p++)
186 ;
187
188 /* Skip empty attachment_header_field_name lines. */
189 if (strlen (p) > 0) {
190 struct stat st;
191 if (stat(p, &st) == OK && access(p, R_OK) == OK) {
192 if (S_ISREG (st.st_mode)) {
193 /* Don't set the default content type so that
194 construct_build_directive() will try to infer
195 it from the file type. */
196 if ((build_directive = construct_build_directive (p, 0,
197 attachformat)) == NULL) {
198 clean_up_temporary_files(body_file_name,
199 composition_file_name);
200 adios (NULL, "exiting due to failure in attach()");
201 } else {
202 (void) fputs(build_directive, composition_file);
203 free(build_directive);
204 }
205 } else {
206 adios (NULL, "unable to attach %s, not a plain file",
207 p);
208 }
209 } else {
210 adios (NULL, "unable to access file \"%s\"", p);
211 }
212 }
213 }
214 }
215
216 (void)fclose(composition_file);
217
218 /*
219 * We're ready to roll! Run mhbuild on the composition file.
220 * Note that mhbuild is in the context as buildmimeproc.
221 */
222
223 (void)sprintf(buf, "%s %s", buildmimeproc, composition_file_name);
224
225 if (system(buf) != 0) {
226 clean_up_temporary_files(body_file_name, composition_file_name);
227 return NOTOK;
228 }
229
230 return OK;
231 }
232
233 void
234 clean_up_temporary_files(const char *body_file_name,
235 const char *composition_file_name)
236 {
237 (void) unlink(body_file_name);
238 (void) unlink(composition_file_name);
239
240 return;
241 }
242
243 static int
244 get_line(FILE *draft_file, char *field, size_t field_size)
245 {
246 int c; /* current character */
247 size_t n; /* number of bytes in buffer */
248 char *p; /* buffer pointer */
249
250 /*
251 * Get a line from the input file, growing the field buffer as
252 * needed. We do this so that we can fit an entire line in the
253 * buffer making it easy to do a string comparison on both the
254 * field name and the field body which might be a long path name.
255 */
256
257 for (n = 0, p = field; (c = getc(draft_file)) != EOF; *p++ = c) {
258 if (c == '\n' && (c = getc(draft_file)) != ' ' && c != '\t') {
259 (void)ungetc(c, draft_file);
260 c = '\n';
261 break;
262 }
263
264 if (++n >= field_size - 1) {
265 field = (char *)mh_xrealloc((void *)field, field_size += 256);
266
267 p = field + n - 1;
268 }
269 }
270
271 /*
272 * NUL-terminate the field..
273 */
274
275 *p = '\0';
276
277 return (c);
278 }
279
280 /*
281 * Try to use external command to determine mime type, and possibly
282 * encoding. Caller is responsible for free'ing returned memory.
283 */
284 char *
285 mime_type(const char *file_name) {
286 char *content_type = NULL; /* mime content type */
287
288 #ifdef MIMETYPEPROC
289 char *mimetype;
290
291 if ((mimetype = get_file_info(MIMETYPEPROC, file_name))) {
292 #ifdef MIMEENCODINGPROC
293 /* Try to append charset for text content. */
294 char *mimeencoding;
295
296 if (strncasecmp(mimetype, "text", 4) == 0) {
297 if ((mimeencoding = get_file_info(MIMEENCODINGPROC, file_name))) {
298 content_type = concat(mimetype, "; charset=", mimeencoding,
299 NULL);
300 } else {
301 content_type = strdup(mimetype);
302 }
303 } else {
304 content_type = strdup(mimetype);
305 }
306 #else /* MIMEENCODINGPROC */
307 content_type = strdup(mimetype);
308 #endif /* MIMEENCODINGPROC */
309 }
310 #else /* MIMETYPEPROC */
311 NMH_UNUSED(file_name);
312 #endif /* MIMETYPEPROC */
313
314 return content_type;
315 }
316
317
318 #ifdef MIMETYPEPROC
319 /*
320 * Get information using proc about a file.
321 */
322 static char *
323 get_file_info(const char *proc, const char *file_name) {
324 char *cmd, *cp;
325 char *quotec = "'";
326
327 if ((cp = strchr(file_name, '\''))) {
328 /* file_name contains a single quote. */
329 if (strchr(file_name, '"')) {
330 advise(NULL, "filenames containing both single and double quotes "
331 "are unsupported for attachment");
332 return NULL;
333 } else {
334 quotec = "\"";
335 }
336 }
337
338 cmd = concat(proc, " ", quotec, file_name, quotec, NULL);
339 if ((cmd = concat(proc, " ", quotec, file_name, quotec, NULL))) {
340 FILE *fp;
341
342 if ((fp = popen(cmd, "r")) != NULL) {
343 char buf[BUFSIZ >= 2048 ? BUFSIZ : 2048];
344
345 buf[0] = '\0';
346 if (fgets(buf, sizeof buf, fp)) {
347 char *eol;
348
349 /* Skip leading <filename>:<whitespace>, if present. */
350 if ((cp = strchr(buf, ':')) != NULL) {
351 ++cp;
352 while (*cp && isblank((unsigned char) *cp)) {
353 ++cp;
354 }
355 } else {
356 cp = buf;
357 }
358
359 /* Truncate at newline (LF or CR), if present. */
360 if ((eol = strpbrk(cp, "\n\r")) != NULL) {
361 *eol = '\0';
362 }
363 } else if (buf[0] == '\0') {
364 /* This can happen on Cygwin if the popen()
365 mysteriously fails. Return NULL so that the caller
366 will use another method to determine the info. */
367 free (cp);
368 cp = NULL;
369 }
370
371 (void) pclose(fp);
372 } else {
373 advise(NULL, "no output from %s", cmd);
374 }
375
376 free(cmd);
377 } else {
378 advise(NULL, "concat with \"%s\" failed, out of memory?", proc);
379 }
380
381 return cp ? strdup(cp) : NULL;
382 }
383 #endif /* MIMETYPEPROC */
384
385
386 /*
387 * Construct an mhbuild directive for the draft file. This starts
388 * with the content type. Append a file name attribute, and depending
389 * on attachformat value a private x-unix-mode attribute and a
390 * description obtained (if possible) by running the "file" command on
391 * the file. Caller is responsible for free'ing returned memory.
392 */
393 char *
394 construct_build_directive (char *file_name, const char *default_content_type,
395 int attachformat) {
396 char *build_directive = NULL; /* Return value. */
397 char *content_type; /* mime content type */
398 char cmd[PATH_MAX + 8]; /* file command buffer */
399 struct stat st; /* file status buffer */
400 char *p; /* miscellaneous temporary variables */
401 int c; /* current character */
402
403 if ((content_type = mime_type (file_name)) == NULL) {
404 /*
405 * Check the file name for a suffix. Scan the context for
406 * that suffix on a mhshow-suffix- entry. We use these
407 * entries to be compatible with mhnshow, and there's no
408 * reason to make the user specify each suffix twice. Context
409 * entries of the form "mhshow-suffix-contenttype" in the name
410 * have the suffix in the field, including the dot.
411 */
412 struct node *np; /* context scan node pointer */
413 static FILE *fp = NULL; /* pointer for mhn.defaults */
414
415 if (fp == NULL && (fp = fopen (p = etcpath ("mhn.defaults"), "r"))) {
416 readconfig ((struct node **) NULL, fp, p, 0);
417 fclose(fp);
418 }
419
420 if ((p = strrchr(file_name, '.')) != NULL) {
421 for (np = m_defs; np; np = np->n_next) {
422 if (strncasecmp(np->n_name, "mhshow-suffix-", 14) == 0 &&
423 strcasecmp(p, np->n_field ? np->n_field : "") == 0) {
424 content_type = strdup (np->n_name + 14);
425 break;
426 }
427 }
428 }
429
430 if (content_type == NULL && default_content_type != NULL) {
431 content_type = strdup (default_content_type);
432 }
433 }
434
435 /*
436 * No content type was found, either because there was no matching
437 * entry in the context or because the file name has no suffix.
438 * Open the file and check for non-ASCII characters. Choose the
439 * content type based on this check.
440 */
441 if (content_type == NULL) {
442 int binary; /* binary character found flag */
443 FILE *fp;
444
445 if ((fp = fopen(file_name, "r")) == (FILE *)0) {
446 advise(NULL, "unable to access file \"%s\"", file_name);
447 return NULL;
448 }
449
450 binary = 0;
451
452 while ((c = getc(fp)) != EOF) {
453 if (c > 127 || c < 0) {
454 binary = 1;
455 break;
456 }
457 }
458
459 (void) fclose(fp);
460
461 content_type =
462 strdup (binary ? "application/octet-stream" : "text/plain");
463 }
464
465 switch (attachformat) {
466 case 0: {
467 struct stat st;
468 FILE *fp;
469 char m[4];
470
471 /* Insert name, file mode, and Content-Id. */
472 if (stat(file_name, &st) != OK || access(file_name, R_OK) != OK) {
473 advise(NULL, "unable to access file \"%s\"", file_name);
474 return NULL;
475 }
476
477 snprintf (m, sizeof m, "%.3ho", (unsigned short)(st.st_mode & 0777));
478 build_directive = concat ("#", content_type, "; name=\"",
479 ((p = strrchr(file_name, '/')) == NULL)
480 ? file_name
481 : p + 1,
482 "\"; x-unix-mode=0", m, NULL);
483
484 if (strlen(file_name) > PATH_MAX) {
485 advise(NULL, "attachment file name `%s' too long.", file_name);
486 return NULL;
487 }
488
489 (void) sprintf(cmd, "file '%s'", file_name);
490
491 if ((fp = popen(cmd, "r")) != NULL &&
492 fgets(cmd, sizeof (cmd), fp) != NULL) {
493 *strchr(cmd, '\n') = '\0';
494
495 /*
496 * The output of the "file" command is of the form
497 *
498 * file: description
499 *
500 * Strip off the "file:" and subsequent white space.
501 */
502 for (p = cmd; *p != '\0'; p++) {
503 if (*p == ':') {
504 for (p++; *p != '\0'; p++) {
505 if (*p != '\t')
506 break;
507 }
508 break;
509 }
510 }
511
512 if (*p != '\0') {
513 /* Insert Content-Description. */
514 build_directive =
515 concat (build_directive, " [ ", p, " ]", NULL);
516 }
517
518 (void) pclose(fp);
519 }
520 break;
521 }
522 case 1:
523 if (stringdex (m_maildir(invo_name), file_name) == 0) {
524 /* Content had been placed by send into a temp file.
525 Don't generate Content-Disposition header, because
526 it confuses Microsoft Outlook, Build 10.0.6626, at
527 least. */
528 build_directive = concat ("#", content_type, " <>", NULL);
529 } else {
530 /* Suppress Content-Id, insert simple Content-Disposition
531 and Content-Description with filename.
532 The Content-Disposition type needs to be "inline" for
533 MS Outlook and BlackBerry calendar programs to properly
534 handle a text/calendar attachment. */
535 p = strrchr(file_name, '/');
536 build_directive = concat ("#", content_type, "; name=\"",
537 (p == NULL) ? file_name : p + 1,
538 "\" <> [",
539 (p == NULL) ? file_name : p + 1,
540 "]{",
541 strcmp ("text/calendar", content_type)
542 ? "attachment" : "inline",
543 "}", NULL);
544 }
545
546 break;
547 case 2:
548 if (stringdex (m_maildir(invo_name), file_name) == 0) {
549 /* Content had been placed by send into a temp file.
550 Don't generate Content-Disposition header, because
551 it confuses Microsoft Outlook, Build 10.0.6626, at
552 least. */
553 build_directive = concat ("#", content_type, " <>", NULL);
554 } else {
555 /* Suppress Content-Id, insert Content-Disposition with
556 modification date and Content-Description wtih filename.
557 The Content-Disposition type needs to be "inline" for
558 MS Outlook and BlackBerry calendar programs to properly
559 handle a text/calendar attachment. */
560
561 if (stat(file_name, &st) != OK || access(file_name, R_OK) != OK) {
562 advise(NULL, "unable to access file \"%s\"", file_name);
563 return NULL;
564 }
565
566 p = strrchr(file_name, '/');
567 build_directive = concat ("#", content_type, "; name=\"",
568 (p == NULL) ? file_name : p + 1,
569 "\" <> [",
570 (p == NULL) ? file_name : p + 1,
571 "]{",
572 strcmp ("text/calendar", content_type)
573 ? "attachment" : "inline",
574 "; modification-date=\"",
575 dtime (&st.st_mtime, 0),
576 "}", NULL);
577 }
578
579 break;
580 default:
581 advise (NULL, "unsupported attachformat %d", attachformat);
582 }
583
584 free(content_type);
585
586 /*
587 * Finish up with the file name.
588 */
589 build_directive = concat (build_directive, " ", file_name, "\n", NULL);
590
591 return build_directive;
592 }