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