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 *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 */
41 * Open up the draft file.
44 if ((draft_file
= fopen(draft_file_name
, "r")) == (FILE *)0)
45 adios(NULL
, "can't open draft file `%s'.", draft_file_name
);
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.
52 field
= (char *)mh_xmalloc(field_size
= 256);
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.
62 length
= strlen(attachment_header_field_name
);
66 while (get_line(draft_file
, field
, field_size
) != EOF
&& *field
!= '\0' &&
68 if (strncasecmp(field
, attachment_header_field_name
, length
) == 0 &&
69 field
[length
] == ':') {
70 for (p
= field
+ length
+ 1; *p
== ' ' || *p
== '\t'; p
++)
78 if (has_attachment
== 0)
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.
88 while (get_line(draft_file
, field
, field_size
) != EOF
) {
89 for (p
= field
; *p
!= '\0'; p
++) {
90 if (*p
!= ' ' && *p
!= '\t') {
101 * Make names for the temporary files.
104 (void)strncpy(body_file_name
,
105 m_mktemp(m_maildir(invo_name
), NULL
, NULL
),
107 (void)strncpy(composition_file_name
,
108 m_mktemp(m_maildir(invo_name
), NULL
, NULL
),
109 composition_file_name_len
);
112 body_file
= fopen(body_file_name
, "w");
114 composition_file
= fopen(composition_file_name
, "w");
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.");
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.
129 while (get_line(draft_file
, field
, field_size
) != EOF
&& *field
!= '\0' &&
131 if (strncasecmp(field
, attachment_header_field_name
, length
) != 0 ||
132 field
[length
] != ':')
133 (void)fprintf(composition_file
, "%s\n", field
);
135 (void)fputs("--------\n", composition_file
);
138 * Copy the message body to a temporary file.
142 while ((c
= getc(draft_file
)) != EOF
)
145 (void)fclose(body_file
);
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.
156 * Make sure that the attachment file exists and is readable.
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
);
164 if ((build_directive
= construct_build_directive (body_file_name
,
166 attachformat
)) == NULL
) {
167 clean_up_temporary_files(body_file_name
, composition_file_name
);
168 adios (NULL
, "exiting due to failure in attach()");
170 (void) fputs(build_directive
, composition_file
);
171 free(build_directive
);
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.
180 if ((fp
= fopen (p
= etcpath ("mhn.defaults"), "r"))) {
181 readconfig ((struct node
**) NULL
, fp
, p
, 0);
187 while (get_line(draft_file
, field
, field_size
) != EOF
&& *field
!= '\0' &&
189 if (strncasecmp(field
, attachment_header_field_name
, length
) == 0 &&
190 field
[length
] == ':') {
191 for (p
= field
+ length
+ 1; *p
== ' ' || *p
== '\t'; p
++)
194 /* Skip empty attachment_header_field_name lines. */
195 if (strlen (p
) > 0) {
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()");
208 (void) fputs(build_directive
, composition_file
);
209 free(build_directive
);
212 adios (NULL
, "unable to attach %s, not a plain file",
216 adios (NULL
, "unable to access file \"%s\"", p
);
222 (void)fclose(composition_file
);
225 * We're ready to roll! Run mhbuild on the composition file.
226 * Note that mhbuild is in the context as buildmimeproc.
229 (void)sprintf(buf
, "%s %s", buildmimeproc
, composition_file_name
);
231 if (system(buf
) != 0) {
232 clean_up_temporary_files(body_file_name
, composition_file_name
);
240 clean_up_temporary_files(const char *body_file_name
,
241 const char *composition_file_name
)
243 (void) unlink(body_file_name
);
244 (void) unlink(composition_file_name
);
250 get_line(FILE *draft_file
, char *field
, size_t field_size
)
252 int c
; /* current character */
253 size_t n
; /* number of bytes in buffer */
254 char *p
; /* buffer pointer */
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.
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
);
270 if (++n
>= field_size
- 1) {
271 field
= (char *)mh_xrealloc((void *)field
, field_size
+= 256);
278 * NUL-terminate the field..
287 * Try to use external command to determine mime type, and possibly
288 * encoding. Caller is responsible for free'ing returned memory.
291 mime_type(const char *file_name
) {
292 char *content_type
= NULL
; /* mime content type */
297 if ((mimetype
= get_file_info(MIMETYPEPROC
, file_name
))) {
298 #ifdef MIMEENCODINGPROC
299 /* Try to append charset for text content. */
302 if (strncasecmp(mimetype
, "text", 4) == 0) {
303 if ((mimeencoding
= get_file_info(MIMEENCODINGPROC
, file_name
))) {
304 content_type
= concat(mimetype
, "; charset=", mimeencoding
,
307 content_type
= strdup(mimetype
);
310 content_type
= strdup(mimetype
);
312 #else /* MIMEENCODINGPROC */
313 content_type
= strdup(mimetype
);
314 #endif /* MIMEENCODINGPROC */
316 #else /* MIMETYPEPROC */
317 NMH_UNUSED(file_name
);
318 #endif /* MIMETYPEPROC */
326 * Get information using proc about a file.
329 get_file_info(const char *proc
, const char *file_name
) {
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");
344 cmd
= concat(proc
, " ", quotec
, file_name
, quotec
, NULL
);
345 if ((cmd
= concat(proc
, " ", quotec
, file_name
, quotec
, NULL
))) {
348 if ((fp
= popen(cmd
, "r")) != NULL
) {
349 char buf
[BUFSIZ
>= 2048 ? BUFSIZ
: 2048];
352 if (fgets(buf
, sizeof buf
, fp
)) {
355 /* Skip leading <filename>:<whitespace>, if present. */
356 if ((cp
= strchr(buf
, ':')) != NULL
) {
358 while (*cp
&& isblank((unsigned char) *cp
)) {
365 /* Truncate at newline (LF or CR), if present. */
366 if ((eol
= strpbrk(cp
, "\n\r")) != NULL
) {
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. */
379 advise(NULL
, "no output from %s", cmd
);
384 advise(NULL
, "concat with \"%s\" failed, out of memory?", proc
);
387 return cp
? strdup(cp
) : NULL
;
389 #endif /* MIMETYPEPROC */
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.
400 construct_build_directive (char *file_name
, const char *default_content_type
,
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 */
408 int c
; /* current character */
410 if ((content_type
= mime_type (file_name
)) == NULL
) {
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.
419 struct node
*np
; /* context scan node pointer */
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);
431 if (content_type
== NULL
&& default_content_type
!= NULL
) {
432 content_type
= strdup (default_content_type
);
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.
442 if (content_type
== NULL
) {
443 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
) {
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
);
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
)
481 "\"; x-unix-mode=0", m
, NULL
);
483 if (strlen(file_name
) > PATH_MAX
) {
484 advise(NULL
, "attachment file name `%s' too long.", file_name
);
488 (void) sprintf(cmd
, "file '%s'", file_name
);
490 if ((fp
= popen(cmd
, "r")) != NULL
&&
491 fgets(cmd
, sizeof (cmd
), fp
) != NULL
) {
492 *strchr(cmd
, '\n') = '\0';
495 * The output of the "file" command is of the form
499 * Strip off the "file:" and subsequent white space.
501 for (p
= cmd
; *p
!= '\0'; p
++) {
503 for (p
++; *p
!= '\0'; p
++) {
512 /* Insert Content-Description. */
514 concat (build_directive
, " [ ", p
, " ]", NULL
);
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
527 build_directive
= concat ("#", content_type
, " <>", NULL
);
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,
538 (p
== NULL
) ? file_name
: p
+ 1,
540 strcmp ("text/calendar", content_type
)
541 ? "attachment" : "inline",
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
552 build_directive
= concat ("#", content_type
, " <>", NULL
);
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. */
560 if (stat(file_name
, &st
) != OK
|| access(file_name
, R_OK
) != OK
) {
561 advise(NULL
, "unable to access file \"%s\"", file_name
);
565 p
= strrchr(file_name
, '/');
566 build_directive
= concat ("#", content_type
, "; name=\"",
567 (p
== NULL
) ? file_name
: p
+ 1,
569 (p
== NULL
) ? file_name
: p
+ 1,
571 strcmp ("text/calendar", content_type
)
572 ? "attachment" : "inline",
573 "; modification-date=\"",
574 dtime (&st
.st_mtime
, 0),
580 advise (NULL
, "unsupported attachformat %d", attachformat
);
586 * Finish up with the file name.
588 build_directive
= concat (build_directive
, " ", file_name
, "\n", NULL
);
590 return build_directive
;