2 * attach.c -- routines to help attach files via whatnow
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.
13 static int get_line(FILE *, char *, size_t);
15 static char *get_file_info(const char *, const char *);
16 #endif /* MIMETYPEPROC */
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
,
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 */
40 * Open up the draft file.
43 if ((draft_file
= fopen(draft_file_name
, "r")) == (FILE *)0)
44 adios(NULL
, "can't open draft file `%s'.", draft_file_name
);
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.
51 field
= (char *)mh_xmalloc(field_size
= 256);
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.
61 length
= strlen(attachment_header_field_name
);
65 while (get_line(draft_file
, field
, field_size
) != EOF
&& *field
!= '\0' &&
67 if (strncasecmp(field
, attachment_header_field_name
, length
) == 0 &&
68 field
[length
] == ':') {
69 for (p
= field
+ length
+ 1; *p
== ' ' || *p
== '\t'; p
++)
77 if (has_attachment
== 0)
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.
87 while (get_line(draft_file
, field
, field_size
) != EOF
) {
88 for (p
= field
; *p
!= '\0'; p
++) {
89 if (*p
!= ' ' && *p
!= '\t') {
100 * Make names for the temporary files.
103 (void)strncpy(body_file_name
,
104 m_mktemp(m_maildir(invo_name
), NULL
, NULL
),
106 (void)strncpy(composition_file_name
,
107 m_mktemp(m_maildir(invo_name
), NULL
, NULL
),
108 composition_file_name_len
);
111 body_file
= fopen(body_file_name
, "w");
113 composition_file
= fopen(composition_file_name
, "w");
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.");
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.
128 while (get_line(draft_file
, field
, field_size
) != EOF
&& *field
!= '\0' &&
130 if (strncasecmp(field
, attachment_header_field_name
, length
) != 0 ||
131 field
[length
] != ':')
132 (void)fprintf(composition_file
, "%s\n", field
);
134 (void)fputs("--------\n", composition_file
);
137 * Copy the message body to a temporary file.
141 while ((c
= getc(draft_file
)) != EOF
)
144 (void)fclose(body_file
);
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.
155 * Make sure that the attachment file exists and is readable.
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
);
163 if ((build_directive
= construct_build_directive (body_file_name
,
165 attachformat
)) == NULL
) {
166 clean_up_temporary_files(body_file_name
, composition_file_name
);
167 adios (NULL
, "exiting due to failure in attach()");
169 (void) fputs(build_directive
, composition_file
);
170 free(build_directive
);
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.
181 while (get_line(draft_file
, field
, field_size
) != EOF
&& *field
!= '\0' &&
183 if (strncasecmp(field
, attachment_header_field_name
, length
) == 0 &&
184 field
[length
] == ':') {
185 for (p
= field
+ length
+ 1; *p
== ' ' || *p
== '\t'; p
++)
188 /* Skip empty attachment_header_field_name lines. */
189 if (strlen (p
) > 0) {
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()");
202 (void) fputs(build_directive
, composition_file
);
203 free(build_directive
);
206 adios (NULL
, "unable to attach %s, not a plain file",
210 adios (NULL
, "unable to access file \"%s\"", p
);
216 (void)fclose(composition_file
);
219 * We're ready to roll! Run mhbuild on the composition file.
220 * Note that mhbuild is in the context as buildmimeproc.
223 (void)sprintf(buf
, "%s %s", buildmimeproc
, composition_file_name
);
225 if (system(buf
) != 0) {
226 clean_up_temporary_files(body_file_name
, composition_file_name
);
234 clean_up_temporary_files(const char *body_file_name
,
235 const char *composition_file_name
)
237 (void) unlink(body_file_name
);
238 (void) unlink(composition_file_name
);
244 get_line(FILE *draft_file
, char *field
, size_t field_size
)
246 int c
; /* current character */
247 size_t n
; /* number of bytes in buffer */
248 char *p
; /* buffer pointer */
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.
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
);
264 if (++n
>= field_size
- 1) {
265 field
= (char *)mh_xrealloc((void *)field
, field_size
+= 256);
272 * NUL-terminate the field..
281 * Try to use external command to determine mime type, and possibly
282 * encoding. Caller is responsible for free'ing returned memory.
285 mime_type(const char *file_name
) {
286 char *content_type
= NULL
; /* mime content type */
291 if ((mimetype
= get_file_info(MIMETYPEPROC
, file_name
))) {
292 #ifdef MIMEENCODINGPROC
293 /* Try to append charset for text content. */
296 if (strncasecmp(mimetype
, "text", 4) == 0) {
297 if ((mimeencoding
= get_file_info(MIMEENCODINGPROC
, file_name
))) {
298 content_type
= concat(mimetype
, "; charset=", mimeencoding
,
301 content_type
= strdup(mimetype
);
304 content_type
= strdup(mimetype
);
306 #else /* MIMEENCODINGPROC */
307 content_type
= strdup(mimetype
);
308 #endif /* MIMEENCODINGPROC */
310 #else /* MIMETYPEPROC */
311 NMH_UNUSED(file_name
);
312 #endif /* MIMETYPEPROC */
320 * Get information using proc about a file.
323 get_file_info(const char *proc
, const char *file_name
) {
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");
338 cmd
= concat(proc
, " ", quotec
, file_name
, quotec
, NULL
);
339 if ((cmd
= concat(proc
, " ", quotec
, file_name
, quotec
, NULL
))) {
342 if ((fp
= popen(cmd
, "r")) != NULL
) {
343 char buf
[BUFSIZ
>= 2048 ? BUFSIZ
: 2048];
346 if (fgets(buf
, sizeof buf
, fp
)) {
349 /* Skip leading <filename>:<whitespace>, if present. */
350 if ((cp
= strchr(buf
, ':')) != NULL
) {
352 while (*cp
&& isblank((unsigned char) *cp
)) {
359 /* Truncate at newline (LF or CR), if present. */
360 if ((eol
= strpbrk(cp
, "\n\r")) != NULL
) {
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. */
373 advise(NULL
, "no output from %s", cmd
);
378 advise(NULL
, "concat with \"%s\" failed, out of memory?", proc
);
381 return cp
? strdup(cp
) : NULL
;
383 #endif /* MIMETYPEPROC */
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.
394 construct_build_directive (char *file_name
, const char *default_content_type
,
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 */
403 if ((content_type
= mime_type (file_name
)) == NULL
) {
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.
412 struct node
*np
; /* context scan node pointer */
413 static FILE *fp
= NULL
; /* pointer for mhn.defaults */
415 if (fp
== NULL
&& (fp
= fopen (p
= etcpath ("mhn.defaults"), "r"))) {
416 readconfig ((struct node
**) NULL
, fp
, p
, 0);
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);
430 if (content_type
== NULL
&& default_content_type
!= NULL
) {
431 content_type
= strdup (default_content_type
);
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.
441 if (content_type
== NULL
) {
442 int binary
; /* binary character found flag */
445 if ((fp
= fopen(file_name
, "r")) == (FILE *)0) {
446 advise(NULL
, "unable to access file \"%s\"", file_name
);
452 while ((c
= getc(fp
)) != EOF
) {
453 if (c
> 127 || c
< 0) {
462 strdup (binary
? "application/octet-stream" : "text/plain");
465 switch (attachformat
) {
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
);
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
)
482 "\"; x-unix-mode=0", m
, NULL
);
484 if (strlen(file_name
) > PATH_MAX
) {
485 advise(NULL
, "attachment file name `%s' too long.", file_name
);
489 (void) sprintf(cmd
, "file '%s'", file_name
);
491 if ((fp
= popen(cmd
, "r")) != NULL
&&
492 fgets(cmd
, sizeof (cmd
), fp
) != NULL
) {
493 *strchr(cmd
, '\n') = '\0';
496 * The output of the "file" command is of the form
500 * Strip off the "file:" and subsequent white space.
502 for (p
= cmd
; *p
!= '\0'; p
++) {
504 for (p
++; *p
!= '\0'; p
++) {
513 /* Insert Content-Description. */
515 concat (build_directive
, " [ ", p
, " ]", NULL
);
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
528 build_directive
= concat ("#", content_type
, " <>", NULL
);
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,
539 (p
== NULL
) ? file_name
: p
+ 1,
541 strcmp ("text/calendar", content_type
)
542 ? "attachment" : "inline",
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
553 build_directive
= concat ("#", content_type
, " <>", NULL
);
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. */
561 if (stat(file_name
, &st
) != OK
|| access(file_name
, R_OK
) != OK
) {
562 advise(NULL
, "unable to access file \"%s\"", file_name
);
566 p
= strrchr(file_name
, '/');
567 build_directive
= concat ("#", content_type
, "; name=\"",
568 (p
== NULL
) ? file_name
: p
+ 1,
570 (p
== NULL
) ? file_name
: p
+ 1,
572 strcmp ("text/calendar", content_type
)
573 ? "attachment" : "inline",
574 "; modification-date=\"",
575 dtime (&st
.st_mtime
, 0),
581 advise (NULL
, "unsupported attachformat %d", attachformat
);
587 * Finish up with the file name.
589 build_directive
= concat (build_directive
, " ", file_name
, "\n", NULL
);
591 return build_directive
;