]> diplodocus.org Git - nmh/blob - uip/mhfixmsg.c
a1ea169c1dbbaae9e7ec572e92ef01ce719b0a31
[nmh] / uip / mhfixmsg.c
1 /* mhfixmsg.c -- rewrite a message with various transformations
2 *
3 * This code is Copyright (c) 2002 and 2013, by the authors of nmh.
4 * See the COPYRIGHT file in the root directory of the nmh
5 * distribution for complete copyright information.
6 */
7
8 #include "h/mh.h"
9 #include "sbr/readconfig.h"
10 #include "sbr/ambigsw.h"
11 #include "sbr/path.h"
12 #include "sbr/print_version.h"
13 #include "sbr/print_help.h"
14 #include "sbr/error.h"
15 #include "h/fmt_scan.h"
16 #include "h/mime.h"
17 #include "h/mhparse.h"
18 #include "h/done.h"
19 #include "h/utils.h"
20 #include "h/signals.h"
21 #include "sbr/m_maildir.h"
22 #include "sbr/m_mktemp.h"
23 #include "sbr/mime_type.h"
24 #include "mhmisc.h"
25 #include "mhfree.h"
26 #include "mhoutsbr.h"
27 #include "mhshowsbr.h"
28 #include <fcntl.h>
29
30 #define MHFIXMSG_SWITCHES \
31 X("decodetext 8bit|7bit|binary", 0, DECODETEXTSW) \
32 X("nodecodetext", 0, NDECODETEXTSW) \
33 X("decodetypes", 0, DECODETYPESW) \
34 X("crlflinebreaks", 0, CRLFLINEBREAKSSW) \
35 X("nocrlflinebreaks", 0, NCRLFLINEBREAKSSW) \
36 X("textcharset", 0, TEXTCHARSETSW) \
37 X("notextcharset", 0, NTEXTCHARSETSW) \
38 X("reformat", 0, REFORMATSW) \
39 X("noreformat", 0, NREFORMATSW) \
40 X("replacetextplain", 0, REPLACETEXTPLAINSW) \
41 X("noreplacetextplain", 0, NREPLACETEXTPLAINSW) \
42 X("fixboundary", 0, FIXBOUNDARYSW) \
43 X("nofixboundary", 0, NFIXBOUNDARYSW) \
44 X("fixcte", 0, FIXCOMPOSITECTESW) \
45 X("nofixcte", 0, NFIXCOMPOSITECTESW) \
46 X("fixtype mimetype", 0, FIXTYPESW) \
47 X("file file", 0, FILESW) \
48 X("outfile file", 0, OUTFILESW) \
49 X("rmmproc program", 0, RPROCSW) \
50 X("normmproc", 0, NRPRCSW) \
51 X("changecur", 0, CHGSW) \
52 X("nochangecur", 0, NCHGSW) \
53 X("verbose", 0, VERBSW) \
54 X("noverbose", 0, NVERBSW) \
55 X("version", 0, VERSIONSW) \
56 X("help", 0, HELPSW) \
57
58 #define X(sw, minchars, id) id,
59 DEFINE_SWITCH_ENUM(MHFIXMSG);
60 #undef X
61
62 #define X(sw, minchars, id) { sw, minchars, id },
63 DEFINE_SWITCH_ARRAY(MHFIXMSG, switches);
64 #undef X
65
66
67 int verbosw;
68 int debugsw; /* Needed by mhparse.c. */
69
70 #define quitser pipeser
71
72 /*
73 * static prototypes
74 */
75 typedef struct fix_transformations {
76 int fixboundary;
77 int fixcompositecte;
78 svector_t fixtypes;
79 int reformat;
80 int replacetextplain;
81 int decodetext;
82 char *decodetypes;
83 /* Whether to use CRLF linebreaks, per RFC 2046 Sec. 4.1.1, par.1. */
84 int lf_line_endings;
85 char *textcharset;
86 } fix_transformations;
87
88 static int mhfixmsgsbr (CT *, char *, const fix_transformations *,
89 FILE **, char *, FILE **);
90 static int fix_boundary (CT *, int *);
91 static int copy_input_to_output (const char *, FILE *, const char *, FILE *);
92 static int get_multipart_boundary (CT, char **);
93 static int replace_boundary (CT, char *, char *);
94 static int fix_types (CT, svector_t, int *);
95 static char *replace_substring (char **, const char *, const char *);
96 static char *remove_parameter (char *, const char *);
97 static int fix_composite_cte (CT, int *);
98 static int set_ce (CT, int);
99 static int ensure_text_plain (CT *, CT, int *, int);
100 static int find_textplain_sibling (CT, int, int *);
101 static int insert_new_text_plain_part (CT, int, CT);
102 static CT build_text_plain_part (CT);
103 static int insert_into_new_mp_alt (CT *, int *);
104 static CT divide_part (CT);
105 static void copy_ctinfo (CI, CI);
106 static int decode_part (CT);
107 static int reformat_part (CT, char *, char *, char *, int);
108 static CT build_multipart_alt (CT, CT, int, int);
109 static int boundary_in_content (FILE **, char *, const char *);
110 static void transfer_noncontent_headers (CT, CT);
111 static int set_ct_type (CT, int type, int subtype, int encoding);
112 static int decode_text_parts (CT, int, const char *, int *);
113 static int should_decode(const char *, const char *, const char *);
114 static int content_encoding (CT, const char **);
115 static int strip_crs (CT, int *);
116 static void update_cte (CT);
117 static int least_restrictive_encoding (CT) PURE;
118 static int less_restrictive (int, int);
119 static int convert_charsets (CT, char *, int *);
120 static int fix_always (CT, int *);
121 static int fix_filename_param (char *, char *, PM *, PM *);
122 static int fix_filename_encoding (CT);
123 static int write_content (CT, const char *, char *, FILE *, int, int);
124 static void set_text_ctparams(CT, char *, int);
125 static int remove_file (const char *);
126 static void report (char *, char *, char *, char *, ...)
127 CHECK_PRINTF(4, 5);
128 static void pipeser (int);
129
130
131 int
132 main (int argc, char **argv)
133 {
134 int msgnum;
135 char *cp, *file = NULL, *folder = NULL;
136 char *maildir = NULL, buf[100], *outfile = NULL;
137 char **argp, **arguments;
138 struct msgs_array msgs = { 0, 0, NULL };
139 struct msgs *mp = NULL;
140 CT *ctp;
141 FILE *fp, *infp = NULL, *outfp = NULL;
142 bool using_stdin = false;
143 bool chgflag = true;
144 int status = OK;
145 fix_transformations fx;
146 fx.reformat = fx.fixcompositecte = fx.fixboundary = 1;
147 fx.fixtypes = NULL;
148 fx.replacetextplain = 0;
149 fx.decodetext = CE_8BIT;
150 fx.decodetypes = "text,application/ics"; /* Default, per man page. */
151 fx.lf_line_endings = 0;
152 fx.textcharset = NULL;
153
154 if (nmh_init(argv[0], true, false)) { return 1; }
155
156 arguments = getarguments (invo_name, argc, argv, 1);
157 argp = arguments;
158
159 /*
160 * Parse arguments
161 */
162 while ((cp = *argp++)) {
163 if (*cp == '-') {
164 switch (smatch (++cp, switches)) {
165 case AMBIGSW:
166 ambigsw (cp, switches);
167 done (1);
168 case UNKWNSW:
169 die("-%s unknown", cp);
170
171 case HELPSW:
172 snprintf (buf, sizeof buf, "%s [+folder] [msgs] [switches]",
173 invo_name);
174 print_help (buf, switches, 1);
175 done (0);
176 case VERSIONSW:
177 print_version(invo_name);
178 done (0);
179
180 case DECODETEXTSW:
181 if (! (cp = *argp++) || *cp == '-') {
182 die("missing argument to %s", argp[-2]);
183 }
184 if (! strcasecmp (cp, "8bit")) {
185 fx.decodetext = CE_8BIT;
186 } else if (! strcasecmp (cp, "7bit")) {
187 fx.decodetext = CE_7BIT;
188 } else if (! strcasecmp (cp, "binary")) {
189 fx.decodetext = CE_BINARY;
190 } else {
191 die("invalid argument to %s", argp[-2]);
192 }
193 continue;
194 case NDECODETEXTSW:
195 fx.decodetext = 0;
196 continue;
197 case DECODETYPESW:
198 if (! (cp = *argp++) || *cp == '-') {
199 die("missing argument to %s", argp[-2]);
200 }
201 fx.decodetypes = cp;
202 continue;
203 case CRLFLINEBREAKSSW:
204 fx.lf_line_endings = 0;
205 continue;
206 case NCRLFLINEBREAKSSW:
207 fx.lf_line_endings = 1;
208 continue;
209 case TEXTCHARSETSW:
210 if (! (cp = *argp++) || (*cp == '-' && cp[1])) {
211 die("missing argument to %s", argp[-2]);
212 }
213 fx.textcharset = cp;
214 continue;
215 case NTEXTCHARSETSW:
216 fx.textcharset = 0;
217 continue;
218 case FIXBOUNDARYSW:
219 fx.fixboundary = 1;
220 continue;
221 case NFIXBOUNDARYSW:
222 fx.fixboundary = 0;
223 continue;
224 case FIXCOMPOSITECTESW:
225 fx.fixcompositecte = 1;
226 continue;
227 case NFIXCOMPOSITECTESW:
228 fx.fixcompositecte = 0;
229 continue;
230 case FIXTYPESW:
231 if (! (cp = *argp++) || (*cp == '-' && cp[1])) {
232 die("missing argument to %s", argp[-2]);
233 }
234 if (! strncasecmp (cp, "multipart/", 10) ||
235 ! strncasecmp (cp, "message/", 8))
236 die("-fixtype %s not allowed", cp);
237 if (! strchr (cp, '/'))
238 die("-fixtype requires type/subtype");
239 if (fx.fixtypes == NULL) { fx.fixtypes = svector_create (10); }
240 svector_push_back (fx.fixtypes, cp);
241 continue;
242 case REFORMATSW:
243 fx.reformat = 1;
244 continue;
245 case NREFORMATSW:
246 fx.reformat = 0;
247 continue;
248 case REPLACETEXTPLAINSW:
249 fx.replacetextplain = 1;
250 continue;
251 case NREPLACETEXTPLAINSW:
252 fx.replacetextplain = 0;
253 continue;
254 case FILESW:
255 if (! (cp = *argp++) || (*cp == '-' && cp[1])) {
256 die("missing argument to %s", argp[-2]);
257 }
258 file = *cp == '-' ? mh_xstrdup (cp) : path (cp, TFILE);
259 continue;
260 case OUTFILESW:
261 if (! (cp = *argp++) || (*cp == '-' && cp[1])) {
262 die("missing argument to %s", argp[-2]);
263 }
264 outfile = *cp == '-' ? mh_xstrdup (cp) : path (cp, TFILE);
265 continue;
266 case RPROCSW:
267 if (!(rmmproc = *argp++) || *rmmproc == '-') {
268 die("missing argument to %s", argp[-2]);
269 }
270 continue;
271 case NRPRCSW:
272 rmmproc = NULL;
273 continue;
274 case CHGSW:
275 chgflag = true;
276 continue;
277 case NCHGSW:
278 chgflag = false;
279 continue;
280 case VERBSW:
281 verbosw = 1;
282 continue;
283 case NVERBSW:
284 verbosw = 0;
285 continue;
286 }
287 }
288 if (*cp == '+' || *cp == '@') {
289 if (folder)
290 die("only one folder at a time!");
291 folder = pluspath (cp);
292 } else {
293 if (*cp == '/') {
294 /* Interpret a full path as a filename, not a message. */
295 file = mh_xstrdup (cp);
296 } else {
297 app_msgarg (&msgs, cp);
298 }
299 }
300 }
301
302 SIGNAL (SIGQUIT, quitser);
303 SIGNAL (SIGPIPE, pipeser);
304
305 /*
306 * Read the standard profile setup
307 */
308 if ((fp = fopen (cp = etcpath ("mhn.defaults"), "r"))) {
309 readconfig(NULL, fp, cp, 0);
310 fclose (fp);
311 }
312
313 suppress_bogus_mp_content_warning = skip_mp_cte_check = true;
314 suppress_extraneous_trailing_semicolon_warning = true;
315
316 if (! context_find ("path")) {
317 free (path ("./", TFOLDER));
318 }
319
320 if (file && msgs.size) {
321 die("cannot specify msg and file at same time!");
322 }
323
324 if (outfile) {
325 /* Open the outfile now, so we don't have to risk opening it
326 after running out of fds. */
327 if (strcmp (outfile, "-") == 0) {
328 outfp = stdout;
329 } else if ((outfp = fopen (outfile, "w")) == NULL) {
330 adios (outfile, "unable to open for writing");
331 }
332 }
333
334 /*
335 * check if message is coming from file
336 */
337 if (file) {
338 /* If file is stdin, create a tmp file name before parse_mime()
339 has a chance, because it might put in on a different
340 filesystem than the output file. Instead, put it in the
341 user's preferred tmp directory. */
342 CT ct;
343
344 if (! strcmp ("-", file)) {
345 int fd;
346 char *cp;
347
348 using_stdin = true;
349
350 if ((cp = m_mktemp2 (NULL, invo_name, &fd, NULL)) == NULL) {
351 die("unable to create temporary file in %s",
352 get_temp_dir());
353 } else {
354 free (file);
355 file = mh_xstrdup (cp);
356 cpydata (STDIN_FILENO, fd, "-", file);
357 }
358
359 if (close (fd)) {
360 (void) m_unlink (file);
361 die("failed to write temporary file");
362 }
363 }
364
365 cts = mh_xcalloc(2, sizeof *cts);
366 ctp = cts;
367
368 if ((ct = parse_mime (file))) {
369 set_text_ctparams(ct, fx.decodetypes, fx.lf_line_endings);
370 *ctp++ = ct;
371 } else {
372 inform("unable to parse message from file %s", file);
373 status = NOTOK;
374
375 /* If there's an outfile, pass the input message unchanged, so the
376 message won't get dropped from a pipeline. */
377 if (outfile) {
378 /* Something went wrong. Output might be expected, such as if
379 this were run as a filter. Just copy the input to the
380 output. */
381 if ((infp = fopen (file, "r")) == NULL) {
382 adios (file, "unable to open for reading");
383 }
384
385 if (copy_input_to_output (file, infp, outfile, outfp) != OK) {
386 inform("unable to copy message to %s, "
387 "it might be lost\n", outfile);
388 }
389
390 fclose (infp);
391 infp = NULL;
392 }
393 }
394 } else {
395 /*
396 * message(s) are coming from a folder
397 */
398 CT ct;
399
400 if (! msgs.size) {
401 app_msgarg(&msgs, "cur");
402 }
403 if (! folder) {
404 folder = getfolder (1);
405 }
406 maildir = mh_xstrdup(m_maildir (folder));
407
408 /* chdir so that error messages, esp. from MIME parser, just
409 refer to the message and not its path. */
410 if (chdir (maildir) == NOTOK) {
411 adios (maildir, "unable to change directory to");
412 }
413
414 /* read folder and create message structure */
415 if (! (mp = folder_read (folder, 1))) {
416 die("unable to read folder %s", folder);
417 }
418
419 /* check for empty folder */
420 if (mp->nummsg == 0) {
421 die("no messages in %s", folder);
422 }
423
424 /* parse all the message ranges/sequences and set SELECTED */
425 for (msgnum = 0; msgnum < msgs.size; msgnum++)
426 if (! m_convert (mp, msgs.msgs[msgnum])) {
427 done (1);
428 }
429 seq_setprev (mp); /* set the previous-sequence */
430
431 cts = mh_xcalloc(mp->numsel + 1, sizeof *cts);
432 ctp = cts;
433
434 for (msgnum = mp->lowsel; msgnum <= mp->hghsel; msgnum++) {
435 if (is_selected(mp, msgnum)) {
436 char *msgnam = m_name (msgnum);
437
438 if ((ct = parse_mime (msgnam))) {
439 set_text_ctparams(ct, fx.decodetypes, fx.lf_line_endings);
440 *ctp++ = ct;
441 } else {
442 inform("unable to parse message %s", msgnam);
443 status = NOTOK;
444
445 /* If there's an outfile, pass the input message
446 unchanged, so the message won't get dropped from a
447 pipeline. */
448 if (outfile) {
449 /* Something went wrong. Output might be expected,
450 such as if this were run as a filter. Just copy
451 the input to the output. */
452 /* Can't use path() here because 1) it might have been
453 called before and it caches the pwd, and 2) we call
454 chdir() after that. */
455 char *input_filename =
456 concat (maildir, "/", msgnam, NULL);
457
458 if ((infp = fopen (input_filename, "r")) == NULL) {
459 adios (input_filename,
460 "unable to open for reading");
461 }
462
463 if (copy_input_to_output (input_filename, infp,
464 outfile, outfp) != OK) {
465 inform("unable to copy message to %s, "
466 "it might be lost\n", outfile);
467 }
468
469 fclose (infp);
470 infp = NULL;
471 free (input_filename);
472 }
473 }
474 }
475 }
476
477 if (chgflag) {
478 seq_setcur (mp, mp->hghsel); /* update current message */
479 }
480 seq_save (mp); /* synchronize sequences */
481 context_replace (pfolder, folder);/* update current folder */
482 context_save (); /* save the context file */
483 }
484
485 if (*cts) {
486 for (ctp = cts; *ctp; ++ctp) {
487 status =
488 mhfixmsgsbr (ctp, maildir, &fx, &infp, outfile, &outfp) == OK
489 ? 0
490 : 1;
491 free_content (*ctp);
492
493 if (using_stdin) {
494 (void) m_unlink (file);
495
496 if (! outfile) {
497 /* Just calling m_backup() unlinks the backup file. */
498 (void) m_backup (file);
499 }
500 }
501 }
502 } else {
503 status = 1;
504 }
505
506 free(maildir);
507 free (cts);
508
509 if (fx.fixtypes != NULL) { svector_free (fx.fixtypes); }
510 if (infp) { fclose (infp); } /* even if stdin */
511 if (outfp) { fclose (outfp); } /* even if stdout */
512 free (outfile);
513 free (file);
514 free (folder);
515 free (arguments);
516
517 done (status == OK ? 0 : 1);
518 return NOTOK;
519 }
520
521
522 /*
523 * Apply transformations to one message.
524 */
525 static int
526 mhfixmsgsbr (CT *ctp, char *maildir, const fix_transformations *fx,
527 FILE **infp, char *outfile, FILE **outfp)
528 {
529 /* Store input filename in case one of the transformations, i.e.,
530 fix_boundary(), rewrites to a tmp file. */
531 char *input_filename = maildir
532 ? concat (maildir, "/", (*ctp)->c_file, NULL)
533 : mh_xstrdup ((*ctp)->c_file);
534 bool modify_inplace = false;
535 int message_mods = 0;
536 int status = OK;
537
538 /* Though the input file won't need to be opened if everything goes
539 well, do it here just in case there's a failure, and that failure is
540 running out of file descriptors. */
541 if ((*infp = fopen (input_filename, "r")) == NULL) {
542 adios (input_filename, "unable to open for reading");
543 }
544
545 if (outfile == NULL) {
546 modify_inplace = true;
547
548 if ((*ctp)->c_file) {
549 char *tempfile;
550 /* outfp will be closed by the caller */
551 if ((tempfile = m_mktemp2 (NULL, invo_name, NULL, outfp)) ==
552 NULL) {
553 die("unable to create temporary file in %s",
554 get_temp_dir());
555 }
556 outfile = mh_xstrdup (tempfile);
557 } else {
558 die("missing both input and output filenames\n");
559 }
560 } /* else *outfp was defined by caller */
561
562 reverse_alternative_parts (*ctp);
563 status = fix_always (*ctp, &message_mods);
564 if (status == OK && fx->fixboundary) {
565 status = fix_boundary (ctp, &message_mods);
566 }
567 if (status == OK && fx->fixtypes != NULL) {
568 status = fix_types (*ctp, fx->fixtypes, &message_mods);
569 }
570 if (status == OK && fx->fixcompositecte) {
571 status = fix_composite_cte (*ctp, &message_mods);
572 }
573 if (status == OK && fx->reformat) {
574 status =
575 ensure_text_plain (ctp, NULL, &message_mods, fx->replacetextplain);
576 }
577 if (status == OK && fx->decodetext) {
578 status = decode_text_parts (*ctp, fx->decodetext, fx->decodetypes,
579 &message_mods);
580 update_cte (*ctp);
581 }
582 if (status == OK && fx->textcharset != NULL) {
583 status = convert_charsets (*ctp, fx->textcharset, &message_mods);
584 }
585
586 if (status == OK && ! (*ctp)->c_umask) {
587 /* Set the umask for the contents file. This currently
588 isn't used but just in case it is in the future. */
589 struct stat st;
590
591 if (stat ((*ctp)->c_file, &st) != NOTOK) {
592 (*ctp)->c_umask = ~(st.st_mode & 0777);
593 } else {
594 (*ctp)->c_umask = ~m_gmprot();
595 }
596 }
597
598 /*
599 * Write the content to a file
600 */
601 if (status == OK) {
602 status = write_content (*ctp, input_filename, outfile, *outfp,
603 modify_inplace, message_mods);
604 } else if (! modify_inplace) {
605 /* Something went wrong. Output might be expected, such
606 as if this were run as a filter. Just copy the input
607 to the output. */
608 if (copy_input_to_output (input_filename, *infp, outfile,
609 *outfp) != OK) {
610 inform("unable to copy message to %s, it might be lost\n",
611 outfile);
612 }
613 }
614
615 if (modify_inplace) {
616 if (status != OK) { (void) m_unlink (outfile); }
617 free (outfile);
618 outfile = NULL;
619 }
620
621 fclose (*infp);
622 *infp = NULL;
623 free (input_filename);
624
625 return status;
626 }
627
628
629 /*
630 * Copy input message to output. Assumes not modifying in place, so this
631 * might be running as part of a pipeline.
632 */
633 static int
634 copy_input_to_output (const char *input_filename, FILE *infp,
635 const char *output_filename, FILE *outfp)
636 {
637 int in = fileno (infp);
638 int out = fileno (outfp);
639 int status = OK;
640
641 if (in != -1 && out != -1) {
642 cpydata (in, out, input_filename, output_filename);
643 } else {
644 status = NOTOK;
645 }
646
647 return status;
648 }
649
650
651 /*
652 * Fix mismatched outer level boundary.
653 */
654 static int
655 fix_boundary (CT *ct, int *message_mods)
656 {
657 struct multipart *mp;
658 int status = OK;
659
660 if (ct && (*ct)->c_type == CT_MULTIPART && bogus_mp_content) {
661 mp = (struct multipart *) (*ct)->c_ctparams;
662
663 /*
664 * 1) Get boundary at end of part.
665 * 2) Get boundary at beginning of part and compare to the end-of-part
666 * boundary.
667 * 3) Write out contents of ct to tmp file, replacing boundary in
668 * header with boundary from part. Set c_unlink to 1.
669 * 4) Free ct.
670 * 5) Call parse_mime() on the tmp file, replacing ct.
671 */
672
673 if (mp && mp->mp_start) {
674 char *part_boundary;
675
676 if (get_multipart_boundary (*ct, &part_boundary) == OK) {
677 char *fixed;
678
679 if ((fixed = m_mktemp2 (NULL, invo_name, NULL, &(*ct)->c_fp))) {
680 if (replace_boundary (*ct, fixed, part_boundary) == OK) {
681 char *filename = mh_xstrdup ((*ct)->c_file);
682 CT fixed_ct;
683
684 free_content (*ct);
685 if ((fixed_ct = parse_mime (fixed))) {
686 *ct = fixed_ct;
687 (*ct)->c_unlink = 1;
688
689 ++*message_mods;
690 if (verbosw) {
691 report (NULL, NULL, filename,
692 "fix multipart boundary");
693 }
694 } else {
695 *ct = NULL;
696 inform("unable to parse fixed part");
697 status = NOTOK;
698 }
699 free (filename);
700 } else {
701 inform("unable to replace broken boundary");
702 status = NOTOK;
703 }
704 } else {
705 inform("unable to create temporary file in %s",
706 get_temp_dir());
707 status = NOTOK;
708 }
709
710 free (part_boundary);
711 } else {
712 /* Couldn't fix the boundary. Report failure so that mhfixmsg
713 doesn't modify the message. */
714 status = NOTOK;
715 }
716 } else {
717 /* No multipart struct, even though the content type is
718 CT_MULTIPART. Report failure so that mhfixmsg doesn't modify
719 the message. */
720 status = NOTOK;
721 }
722 }
723
724 return status;
725 }
726
727
728 /*
729 * Find boundary at end of multipart.
730 */
731 static int
732 get_multipart_boundary (CT ct, char **part_boundary)
733 {
734 char buffer[NMH_BUFSIZ];
735 char *end_boundary = NULL;
736 off_t begin = (off_t) ct->c_end > (off_t) (ct->c_begin + sizeof buffer)
737 ? (off_t) (ct->c_end - sizeof buffer)
738 : (off_t) ct->c_begin;
739 size_t bytes_read;
740 int status = OK;
741
742 /* This will fail if the boundary spans fread() calls. NMH_BUFSIZ should
743 be big enough, even if it's just 1024, to make that unlikely. */
744
745 /* free_content() will close ct->c_fp if bogus MP boundary is fixed. */
746 if (! ct->c_fp && (ct->c_fp = fopen (ct->c_file, "r")) == NULL) {
747 advise (ct->c_file, "unable to open for reading");
748 return NOTOK;
749 }
750
751 /* Get boundary at end of multipart. */
752 while (begin >= (off_t) ct->c_begin) {
753 fseeko (ct->c_fp, begin, SEEK_SET);
754 while ((bytes_read = fread (buffer, 1, sizeof buffer, ct->c_fp)) > 0) {
755 char *cp = rfind_str (buffer, bytes_read, "--");
756
757 if (cp) {
758 char *end;
759
760 /* Trim off trailing "--" and anything beyond. */
761 *cp-- = '\0';
762 if ((end = rfind_str (buffer, cp - buffer, "\n"))) {
763 if (strlen (end) > 3 && *end++ == '\n' &&
764 *end++ == '-' && *end++ == '-') {
765 end_boundary = mh_xstrdup (end);
766 break;
767 }
768 }
769 }
770 }
771
772 if (end_boundary || begin <= (off_t) (ct->c_begin + sizeof buffer))
773 break;
774 begin -= sizeof buffer;
775 }
776
777 /* Get boundary at beginning of multipart. */
778 if (end_boundary) {
779 fseeko (ct->c_fp, ct->c_begin, SEEK_SET);
780 while ((bytes_read = fread (buffer, 1, sizeof buffer, ct->c_fp)) > 0) {
781 if (bytes_read >= strlen (end_boundary)) {
782 char *cp = find_str (buffer, bytes_read, end_boundary);
783
784 if (cp && cp - buffer >= 2 && *--cp == '-' &&
785 *--cp == '-' && (cp > buffer && *--cp == '\n')) {
786 status = OK;
787 break;
788 }
789 } else {
790 /* The start and end boundaries didn't match, or the
791 start boundary doesn't begin with "\n--" (or "--"
792 if at the beginning of buffer). Keep trying. */
793 status = NOTOK;
794 }
795 }
796 } else {
797 status = NOTOK;
798 }
799
800 if (ct->c_fp) {
801 fclose (ct->c_fp);
802 ct->c_fp = NULL;
803 }
804
805 if (status == OK) {
806 *part_boundary = end_boundary;
807 } else {
808 *part_boundary = NULL;
809 free (end_boundary);
810 }
811
812 return status;
813 }
814
815
816 /*
817 * Open and copy ct->c_file to file, replacing the multipart boundary.
818 */
819 static int
820 replace_boundary (CT ct, char *file, char *boundary)
821 {
822 FILE *fpin, *fpout;
823 int compnum, state;
824 char buf[NMH_BUFSIZ], name[NAMESZ];
825 char *np, *vp;
826 m_getfld_state_t gstate;
827 int status = OK;
828
829 if (ct->c_file == NULL) {
830 inform("missing input filename");
831 return NOTOK;
832 }
833
834 if ((fpin = fopen (ct->c_file, "r")) == NULL) {
835 advise (ct->c_file, "unable to open for reading");
836 return NOTOK;
837 }
838
839 if ((fpout = fopen (file, "w")) == NULL) {
840 fclose (fpin);
841 advise (file, "unable to open for writing");
842 return NOTOK;
843 }
844
845 gstate = m_getfld_state_init(fpin);
846 for (compnum = 1;;) {
847 int bufsz = (int) sizeof buf;
848
849 switch (state = m_getfld2(&gstate, name, buf, &bufsz)) {
850 case FLD:
851 case FLDPLUS:
852 compnum++;
853
854 /* get copies of the buffers */
855 np = mh_xstrdup (name);
856 vp = mh_xstrdup (buf);
857
858 /* if necessary, get rest of field */
859 while (state == FLDPLUS) {
860 bufsz = sizeof buf;
861 state = m_getfld2(&gstate, name, buf, &bufsz);
862 vp = add (buf, vp); /* add to previous value */
863 }
864
865 if (strcasecmp (TYPE_FIELD, np)) {
866 fprintf (fpout, "%s:%s", np, vp);
867 } else {
868 char *new_ctline, *new_params;
869
870 replace_param(&ct->c_ctinfo.ci_first_pm,
871 &ct->c_ctinfo.ci_last_pm, "boundary",
872 boundary, 0);
873
874 new_ctline = concat(" ", ct->c_ctinfo.ci_type, "/",
875 ct->c_ctinfo.ci_subtype, NULL);
876 new_params = output_params(LEN(TYPE_FIELD) +
877 strlen(new_ctline) + 1,
878 ct->c_ctinfo.ci_first_pm, NULL, 0);
879 fprintf (fpout, "%s:%s%s\n", np, new_ctline,
880 FENDNULL(new_params));
881 free(new_ctline);
882 free(new_params);
883 }
884
885 free (vp);
886 free (np);
887
888 continue;
889
890 case BODY:
891 putc('\n', fpout);
892 /* buf will have a terminating NULL, skip it. */
893 if ((int) fwrite (buf, 1, bufsz-1, fpout) < bufsz-1) {
894 advise (file, "fwrite");
895 }
896 continue;
897
898 case FILEEOF:
899 break;
900
901 case LENERR:
902 case FMTERR:
903 inform("message format error in component #%d", compnum);
904 status = NOTOK;
905 break;
906
907 default:
908 inform("getfld() returned %d", state);
909 status = NOTOK;
910 break;
911 }
912
913 break;
914 }
915
916 m_getfld_state_destroy (&gstate);
917 fclose (fpout);
918 fclose (fpin);
919
920 return status;
921 }
922
923
924 /*
925 * Fix Content-Type header to reflect the content of its part.
926 */
927 static int
928 fix_types (CT ct, svector_t fixtypes, int *message_mods)
929 {
930 int status = OK;
931
932 switch (ct->c_type) {
933 case CT_MULTIPART: {
934 struct multipart *m = (struct multipart *) ct->c_ctparams;
935 struct part *part;
936
937 for (part = m->mp_parts; status == OK && part; part = part->mp_next) {
938 status = fix_types (part->mp_part, fixtypes, message_mods);
939 }
940 break;
941 }
942
943 case CT_MESSAGE:
944 if (ct->c_subtype == MESSAGE_EXTERNAL) {
945 struct exbody *e = (struct exbody *) ct->c_ctparams;
946
947 status = fix_types (e->eb_content, fixtypes, message_mods);
948 }
949 break;
950
951 default: {
952 char **typep, *type;
953
954 if (ct->c_ctinfo.ci_type && ct->c_ctinfo.ci_subtype) {
955 for (typep = svector_strs (fixtypes);
956 typep && (type = *typep);
957 ++typep) {
958 char *type_subtype =
959 concat (ct->c_ctinfo.ci_type, "/", ct->c_ctinfo.ci_subtype,
960 NULL);
961
962 if (! strcasecmp (type, type_subtype) &&
963 decode_part (ct) == OK &&
964 ct->c_cefile.ce_file != NULL) {
965 char *ct_type_subtype = mime_type (ct->c_cefile.ce_file);
966 char *cp;
967
968 if ((cp = strchr (ct_type_subtype, ';'))) {
969 /* Truncate to remove any parameter list from
970 mime_type () result. */
971 *cp = '\0';
972 }
973
974 if (strcasecmp (type, ct_type_subtype)) {
975 char *ct_type, *ct_subtype;
976 HF hf;
977
978 /* The Content-Type header does not match the
979 content, so update these struct Content
980 fields to match:
981 * c_type, c_subtype
982 * c_ctinfo.ci_type, c_ctinfo.ci_subtype
983 * c_ctline
984 */
985 /* Extract type and subtype from type/subtype. */
986 ct_type = mh_xstrdup(ct_type_subtype);
987 if ((cp = strchr (ct_type, '/'))) {
988 *cp = '\0';
989 ct_subtype = mh_xstrdup(++cp);
990 } else {
991 inform("missing / in MIME type of %s %s",
992 ct->c_file, ct->c_partno);
993 free (ct_type);
994 return NOTOK;
995 }
996
997 ct->c_type = ct_str_type (ct_type);
998 ct->c_subtype = ct_str_subtype (ct->c_type, ct_subtype);
999
1000 free (ct->c_ctinfo.ci_type);
1001 ct->c_ctinfo.ci_type = ct_type;
1002 free (ct->c_ctinfo.ci_subtype);
1003 ct->c_ctinfo.ci_subtype = ct_subtype;
1004 if (! replace_substring (&ct->c_ctline, type,
1005 ct_type_subtype)) {
1006 inform("did not find %s in %s",
1007 type, ct->c_ctline);
1008 }
1009
1010 /* Update Content-Type header field. */
1011 for (hf = ct->c_first_hf; hf; hf = hf->next) {
1012 if (! strcasecmp (TYPE_FIELD, hf->name)) {
1013 if (replace_substring (&hf->value, type,
1014 ct_type_subtype)) {
1015 ++*message_mods;
1016 if (verbosw) {
1017 report (NULL, ct->c_partno, ct->c_file,
1018 "change Content-Type in header "
1019 "from %s to %s",
1020 type, ct_type_subtype);
1021 }
1022 break;
1023 }
1024 inform("did not find %s in %s", type, hf->value);
1025 }
1026 }
1027 }
1028 free (ct_type_subtype);
1029 }
1030 free (type_subtype);
1031 }
1032 }
1033 }}
1034
1035 return status;
1036 }
1037
1038
1039 /*
1040 * Replace a substring, allocating space to hold the new one.
1041 */
1042 char *
1043 replace_substring (char **str, const char *old, const char *new)
1044 {
1045 char *cp;
1046
1047 if ((cp = strstr (*str, old))) {
1048 char *remainder = cp + strlen (old);
1049 char *prefix, *new_str;
1050
1051 if (cp - *str) {
1052 prefix = mh_xstrdup(*str);
1053 *(prefix + (cp - *str)) = '\0';
1054 new_str = concat (prefix, new, remainder, NULL);
1055 free (prefix);
1056 } else {
1057 new_str = concat (new, remainder, NULL);
1058 }
1059
1060 free (*str);
1061
1062 return *str = new_str;
1063 }
1064
1065 return NULL;
1066 }
1067
1068
1069 /*
1070 * Remove a name=value parameter, given just its name, from a header value.
1071 */
1072 char *
1073 remove_parameter (char *str, const char *name)
1074 {
1075 /* It looks to me, based on the BNF in RFC 2045, than there can't
1076 be whitespace between the parameter name and the "=", or
1077 between the "=" and the parameter value. */
1078 char *param_name = concat (name, "=", NULL);
1079 char *cp;
1080
1081 if ((cp = strstr (str, param_name))) {
1082 char *start, *end;
1083 size_t count = 1;
1084
1085 /* Remove any leading spaces, before the parameter name. */
1086 for (start = cp;
1087 start > str && isspace ((unsigned char) *(start-1));
1088 --start) {
1089 continue;
1090 }
1091 /* Remove a leading semicolon. */
1092 if (start > str && *(start-1) == ';') { --start; }
1093
1094 end = cp + strlen (name) + 1;
1095 if (*end == '"') {
1096 /* Skip past the quoted value, and then the final quote. */
1097 for (++end ; *end && *end != '"'; ++end) { continue; }
1098 ++end;
1099 } else {
1100 /* Skip past the value. */
1101 for (++end ; *end && ! isspace ((unsigned char) *end); ++end) {}
1102 }
1103
1104 /* Count how many characters need to be moved. Include
1105 trailing null, which is accounted for by the
1106 initialization of count to 1. */
1107 for (cp = end; *cp; ++cp) { ++count; }
1108 (void) memmove (start, end, count);
1109 }
1110
1111 free (param_name);
1112
1113 return str;
1114 }
1115
1116
1117 /*
1118 * Fix Content-Transfer-Encoding of composite,, e.g., message or multipart, part.
1119 * According to RFC 2045 Sec. 6.4, it must be 7bit, 8bit, or binary. Set it to
1120 * 8 bit.
1121 */
1122 static int
1123 fix_composite_cte (CT ct, int *message_mods)
1124 {
1125 int status = OK;
1126
1127 if (ct->c_type == CT_MESSAGE || ct->c_type == CT_MULTIPART) {
1128 if (ct->c_encoding != CE_7BIT && ct->c_encoding != CE_8BIT &&
1129 ct->c_encoding != CE_BINARY) {
1130 HF hf;
1131
1132 for (hf = ct->c_first_hf; hf; hf = hf->next) {
1133 char *name = hf->name;
1134 for (; isspace((unsigned char)*name); ++name) {
1135 continue;
1136 }
1137
1138 if (! strncasecmp (name, ENCODING_FIELD,
1139 LEN(ENCODING_FIELD))) {
1140 char *prefix = "Nmh-REPLACED-INVALID-";
1141 HF h;
1142
1143 NEW(h);
1144 h->name = mh_xstrdup (hf->name);
1145 h->hf_encoding = hf->hf_encoding;
1146 h->next = hf->next;
1147 hf->next = h;
1148
1149 /* Retain old header but prefix its name. */
1150 free (hf->name);
1151 hf->name = concat (prefix, h->name, NULL);
1152
1153 ++*message_mods;
1154 if (verbosw) {
1155 char *encoding = cpytrim (hf->value);
1156 report (NULL, ct->c_partno, ct->c_file,
1157 "replace Content-Transfer-Encoding of %s "
1158 "with 8 bit", encoding);
1159 free (encoding);
1160 }
1161
1162 h->value = mh_xstrdup (" 8bit\n");
1163
1164 /* Don't need to warn for multiple C-T-E header
1165 fields, parse_mime() already does that. But
1166 if there are any, fix them all as necessary. */
1167 hf = h;
1168 }
1169 }
1170
1171 set_ce (ct, CE_8BIT);
1172 }
1173
1174 if (ct->c_type == CT_MULTIPART) {
1175 struct multipart *m;
1176 struct part *part;
1177
1178 m = (struct multipart *) ct->c_ctparams;
1179 for (part = m->mp_parts; part; part = part->mp_next) {
1180 if (fix_composite_cte (part->mp_part, message_mods) != OK) {
1181 status = NOTOK;
1182 break;
1183 }
1184 }
1185 }
1186 }
1187
1188 return status;
1189 }
1190
1191
1192 /*
1193 * Set content encoding.
1194 */
1195 static int
1196 set_ce (CT ct, int encoding)
1197 {
1198 const char *ce = ce_str (encoding);
1199 const struct str2init *ctinit = get_ce_method (ce);
1200
1201 if (ctinit) {
1202 char *cte = concat (" ", ce, "\n", NULL);
1203 bool found_cte = false;
1204 HF hf;
1205 /* Decoded contents might be in ct->c_cefile.ce_file, if the
1206 caller is decode_text_parts (). Save because we'll
1207 overwrite below. */
1208 struct cefile decoded_content_info = ct->c_cefile;
1209
1210 ct->c_encoding = encoding;
1211
1212 ct->c_ctinitfnx = ctinit->si_init;
1213 /* This will assign ct->c_cefile with an all-0 struct, which
1214 is what we want. */
1215 (*ctinit->si_init) (ct);
1216 /* After returning, the caller should set
1217 ct->c_cefile.ce_file to the name of the file containing
1218 the contents. */
1219
1220 if (ct->c_ceclosefnx) {
1221 (*ct->c_ceclosefnx) (ct);
1222 }
1223
1224 /* Restore the cefile. */
1225 ct->c_cefile = decoded_content_info;
1226
1227 /* Update/add Content-Transfer-Encoding header field. */
1228 for (hf = ct->c_first_hf; hf; hf = hf->next) {
1229 if (! strcasecmp (ENCODING_FIELD, hf->name)) {
1230 found_cte = true;
1231 free (hf->value);
1232 hf->value = cte;
1233 }
1234 }
1235 if (! found_cte) {
1236 add_header (ct, mh_xstrdup (ENCODING_FIELD), cte);
1237 }
1238
1239 /* Update c_celine. It's used only by mhlist -debug. */
1240 free (ct->c_celine);
1241 ct->c_celine = mh_xstrdup (cte);
1242
1243 return OK;
1244 }
1245
1246 return NOTOK;
1247 }
1248
1249
1250 /*
1251 * Make sure each text part has a corresponding text/plain part.
1252 */
1253 static int
1254 ensure_text_plain (CT *ct, CT parent, int *message_mods, int replacetextplain)
1255 {
1256 int status = OK;
1257
1258 switch ((*ct)->c_type) {
1259 case CT_TEXT: {
1260 /* Nothing to do for text/plain. */
1261 if ((*ct)->c_subtype == TEXT_PLAIN) { return OK; }
1262
1263 if (parent && parent->c_type == CT_MULTIPART &&
1264 parent->c_subtype == MULTI_ALTERNATE) {
1265 int new_subpart_number = 1;
1266 int has_text_plain =
1267 find_textplain_sibling (parent, replacetextplain,
1268 &new_subpart_number);
1269
1270 if (! has_text_plain) {
1271 /* Parent is a multipart/alternative. Insert a new
1272 text/plain subpart. */
1273 const int inserted =
1274 insert_new_text_plain_part (*ct, new_subpart_number,
1275 parent);
1276 if (inserted) {
1277 ++*message_mods;
1278 if (verbosw) {
1279 report (NULL, parent->c_partno, parent->c_file,
1280 "insert text/plain part");
1281 }
1282 } else {
1283 status = NOTOK;
1284 }
1285 }
1286 } else if (parent && parent->c_type == CT_MULTIPART &&
1287 parent->c_subtype == MULTI_RELATED) {
1288 char *type_subtype =
1289 concat ((*ct)->c_ctinfo.ci_type, "/",
1290 (*ct)->c_ctinfo.ci_subtype, NULL);
1291 const char *parent_type =
1292 get_param (parent->c_ctinfo.ci_first_pm, "type", '?', 1);
1293 int new_subpart_number = 1;
1294 int has_text_plain = 0;
1295
1296 /* Have to do string comparison on the subtype because we
1297 don't enumerate all of them in c_subtype values.
1298 parent_type will be NULL if the multipart/related part
1299 doesn't have a type parameter. The type parameter must
1300 be specified according to RFC 2387 Sec. 3.1 but not all
1301 messages comply. */
1302 if (parent_type && strcasecmp (type_subtype, parent_type) == 0) {
1303 /* The type of this part matches the root type of the
1304 parent multipart/related. Look to see if there's
1305 text/plain sibling. */
1306 has_text_plain =
1307 find_textplain_sibling (parent, replacetextplain,
1308 &new_subpart_number);
1309 }
1310
1311 free (type_subtype);
1312
1313 if (! has_text_plain) {
1314 struct multipart *mp = (struct multipart *) parent->c_ctparams;
1315 struct part *part;
1316 int siblings = 0;
1317
1318 for (part = mp->mp_parts; part; part = part->mp_next) {
1319 if (*ct != part->mp_part) {
1320 ++siblings;
1321 }
1322 }
1323
1324 if (siblings) {
1325 /* Parent is a multipart/related. Insert a new
1326 text/plain subpart in a new multipart/alternative. */
1327 if (insert_into_new_mp_alt (ct, message_mods)) {
1328 /* Not an error if text/plain couldn't be added. */
1329 }
1330 } else {
1331 /* There are no siblings, so insert a new text/plain
1332 subpart, and change the parent type from
1333 multipart/related to multipart/alternative. */
1334 const int inserted =
1335 insert_new_text_plain_part (*ct, new_subpart_number,
1336 parent);
1337
1338 if (inserted) {
1339 HF hf;
1340
1341 parent->c_subtype = MULTI_ALTERNATE;
1342 free (parent->c_ctinfo.ci_subtype);
1343 parent->c_ctinfo.ci_subtype = mh_xstrdup("alternative");
1344 if (! replace_substring (&parent->c_ctline, "/related",
1345 "/alternative")) {
1346 inform("did not find multipart/related in %s",
1347 parent->c_ctline);
1348 }
1349
1350 /* Update Content-Type header field. */
1351 for (hf = parent->c_first_hf; hf; hf = hf->next) {
1352 if (! strcasecmp (TYPE_FIELD, hf->name)) {
1353 if (replace_substring (&hf->value, "/related",
1354 "/alternative")) {
1355 ++*message_mods;
1356 if (verbosw) {
1357 report (NULL, parent->c_partno,
1358 parent->c_file,
1359 "insert text/plain part");
1360 }
1361
1362 /* Remove, e.g., type="text/html" from
1363 multipart/alternative. */
1364 remove_parameter (hf->value, "type");
1365 break;
1366 }
1367 inform("did not find multipart/"
1368 "related in header %s", hf->value);
1369 }
1370 }
1371 } else {
1372 /* Not an error if text/plain couldn't be inserted. */
1373 }
1374 }
1375 }
1376 } else {
1377 if (insert_into_new_mp_alt (ct, message_mods)) {
1378 status = NOTOK;
1379 }
1380 }
1381 break;
1382 }
1383
1384 case CT_MULTIPART: {
1385 struct multipart *mp = (struct multipart *) (*ct)->c_ctparams;
1386 struct part *part;
1387
1388 for (part = mp->mp_parts; status == OK && part; part = part->mp_next) {
1389 if ((*ct)->c_type == CT_MULTIPART) {
1390 status = ensure_text_plain (&part->mp_part, *ct, message_mods,
1391 replacetextplain);
1392 }
1393 }
1394 break;
1395 }
1396
1397 case CT_MESSAGE:
1398 if ((*ct)->c_subtype == MESSAGE_EXTERNAL) {
1399 struct exbody *e = (struct exbody *) (*ct)->c_ctparams;
1400
1401 status = ensure_text_plain (&e->eb_content, *ct, message_mods,
1402 replacetextplain);
1403 }
1404 break;
1405 }
1406
1407 return status;
1408 }
1409
1410
1411 /*
1412 * See if there is a sibling text/plain, and return its subpart number.
1413 */
1414 static int
1415 find_textplain_sibling (CT parent, int replacetextplain,
1416 int *new_subpart_number)
1417 {
1418 struct multipart *mp = (struct multipart *) parent->c_ctparams;
1419 struct part *part, *prev;
1420 bool has_text_plain = false;
1421
1422 for (prev = part = mp->mp_parts; part; part = part->mp_next) {
1423 ++*new_subpart_number;
1424 if (part->mp_part->c_type == CT_TEXT &&
1425 part->mp_part->c_subtype == TEXT_PLAIN) {
1426 if (replacetextplain) {
1427 struct part *old_part;
1428 if (part == mp->mp_parts) {
1429 old_part = mp->mp_parts;
1430 mp->mp_parts = part->mp_next;
1431 } else {
1432 old_part = prev->mp_next;
1433 prev->mp_next = part->mp_next;
1434 }
1435 if (verbosw) {
1436 report (NULL, parent->c_partno, parent->c_file,
1437 "remove text/plain part %s",
1438 old_part->mp_part->c_partno);
1439 }
1440 free_content (old_part->mp_part);
1441 free (old_part);
1442 } else {
1443 has_text_plain = true;
1444 }
1445 break;
1446 }
1447 prev = part;
1448 }
1449
1450 return has_text_plain;
1451 }
1452
1453
1454 /*
1455 * Insert a new text/plain part.
1456 */
1457 static int
1458 insert_new_text_plain_part (CT ct, int new_subpart_number, CT parent)
1459 {
1460 struct multipart *mp = (struct multipart *) parent->c_ctparams;
1461 struct part *new_part;
1462
1463 NEW(new_part);
1464 if ((new_part->mp_part = build_text_plain_part (ct))) {
1465 char buffer[16];
1466 snprintf (buffer, sizeof buffer, "%d", new_subpart_number);
1467
1468 new_part->mp_next = mp->mp_parts;
1469 mp->mp_parts = new_part;
1470 new_part->mp_part->c_partno =
1471 concat (parent->c_partno ? parent->c_partno : "1", ".",
1472 buffer, NULL);
1473
1474 return 1;
1475 }
1476
1477 free_content (new_part->mp_part);
1478 free (new_part);
1479
1480 return 0;
1481 }
1482
1483
1484 /*
1485 * Create a text/plain part to go along with non-plain sibling part.
1486 */
1487 static CT
1488 build_text_plain_part (CT encoded_part)
1489 {
1490 CT tp_part = divide_part (encoded_part);
1491 char *tmp_plain_file = NULL;
1492
1493 if (decode_part (tp_part) == OK) {
1494 /* Now, tp_part->c_cefile.ce_file is the name of the tmp file that
1495 contains the decoded contents. And the decoding function, such
1496 as openQuoted, will have set ...->ce_unlink to 1 so that it will
1497 be unlinked by free_content (). */
1498 char *tempfile;
1499
1500 /* This m_mktemp2() call closes the temp file. */
1501 if ((tempfile = m_mktemp2 (NULL, invo_name, NULL, NULL)) == NULL) {
1502 inform("unable to create temporary file in %s",
1503 get_temp_dir());
1504 } else {
1505 tmp_plain_file = mh_xstrdup (tempfile);
1506 if (reformat_part (tp_part, tmp_plain_file,
1507 tp_part->c_ctinfo.ci_type,
1508 tp_part->c_ctinfo.ci_subtype,
1509 tp_part->c_type) == OK) {
1510 return tp_part;
1511 }
1512 }
1513 }
1514
1515 free_content (tp_part);
1516 if (tmp_plain_file) { (void) m_unlink (tmp_plain_file); }
1517 free (tmp_plain_file);
1518
1519 return NULL;
1520 }
1521
1522
1523 /*
1524 * Slip new text/plain part into a new multipart/alternative.
1525 */
1526 static int
1527 insert_into_new_mp_alt (CT *ct, int *message_mods)
1528 {
1529 CT tp_part = build_text_plain_part (*ct);
1530 int status = OK;
1531
1532 if (tp_part) {
1533 CT mp_alt = build_multipart_alt (*ct, tp_part, CT_MULTIPART,
1534 MULTI_ALTERNATE);
1535 if (mp_alt) {
1536 struct multipart *mp = (struct multipart *) mp_alt->c_ctparams;
1537
1538 if (mp && mp->mp_parts) {
1539 mp->mp_parts->mp_part = tp_part;
1540 /* Make the new multipart/alternative the parent. */
1541 *ct = mp_alt;
1542
1543 ++*message_mods;
1544 if (verbosw) {
1545 report (NULL, (*ct)->c_partno, (*ct)->c_file,
1546 "insert text/plain part");
1547 }
1548 } else {
1549 free_content (tp_part);
1550 free_content (mp_alt);
1551 status = NOTOK;
1552 }
1553 } else {
1554 status = NOTOK;
1555 }
1556 } else {
1557 /* Not an error if text/plain couldn't be built. */
1558 }
1559
1560 return status;
1561 }
1562
1563
1564 /*
1565 * Clone a MIME part.
1566 */
1567 static CT
1568 divide_part (CT ct)
1569 {
1570 CT new_part;
1571
1572 NEW0(new_part);
1573 /* Just copy over what is needed for decoding. c_vrsn and
1574 c_celine aren't necessary. */
1575 new_part->c_file = mh_xstrdup (ct->c_file);
1576 new_part->c_begin = ct->c_begin;
1577 new_part->c_end = ct->c_end;
1578 copy_ctinfo (&new_part->c_ctinfo, &ct->c_ctinfo);
1579 new_part->c_type = ct->c_type;
1580 new_part->c_cefile = ct->c_cefile;
1581 new_part->c_encoding = ct->c_encoding;
1582 new_part->c_ctinitfnx = ct->c_ctinitfnx;
1583 new_part->c_ceopenfnx = ct->c_ceopenfnx;
1584 new_part->c_ceclosefnx = ct->c_ceclosefnx;
1585 new_part->c_cesizefnx = ct->c_cesizefnx;
1586
1587 /* c_ctline is used by reformat__part(), so it can preserve
1588 anything after the type/subtype. */
1589 new_part->c_ctline = mh_xstrdup (ct->c_ctline);
1590
1591 return new_part;
1592 }
1593
1594
1595 /*
1596 * Copy the content info from one part to another.
1597 */
1598 static void
1599 copy_ctinfo (CI dest, CI src)
1600 {
1601 PM s_pm, d_pm;
1602
1603 dest->ci_type = src->ci_type ? mh_xstrdup (src->ci_type) : NULL;
1604 dest->ci_subtype = src->ci_subtype ? mh_xstrdup (src->ci_subtype) : NULL;
1605
1606 for (s_pm = src->ci_first_pm; s_pm; s_pm = s_pm->pm_next) {
1607 d_pm = add_param(&dest->ci_first_pm, &dest->ci_last_pm, s_pm->pm_name,
1608 s_pm->pm_value, 0);
1609 if (s_pm->pm_charset) {
1610 d_pm->pm_charset = mh_xstrdup(s_pm->pm_charset);
1611 }
1612 if (s_pm->pm_lang) {
1613 d_pm->pm_lang = mh_xstrdup(s_pm->pm_lang);
1614 }
1615 }
1616
1617 dest->ci_comment = src->ci_comment ? mh_xstrdup (src->ci_comment) : NULL;
1618 dest->ci_magic = src->ci_magic ? mh_xstrdup (src->ci_magic) : NULL;
1619 }
1620
1621
1622 /*
1623 * Decode content.
1624 */
1625 static int
1626 decode_part (CT ct)
1627 {
1628 char *tmp_decoded;
1629 int status;
1630 FILE *file;
1631 char *tempfile;
1632
1633 if ((tempfile = m_mktemp2 (NULL, invo_name, NULL, &file)) == NULL) {
1634 die("unable to create temporary file in %s", get_temp_dir());
1635 }
1636 tmp_decoded = mh_xstrdup (tempfile);
1637 /* The following call will load ct->c_cefile.ce_file with the tmp
1638 filename of the decoded content. tmp_decoded will contain the
1639 encoded output, get rid of that. */
1640 status = output_message_fp (ct, file, tmp_decoded);
1641 (void) m_unlink (tmp_decoded);
1642 free (tmp_decoded);
1643 if (fclose (file)) {
1644 inform("unable to close temporary file %s, continuing...", tempfile);
1645 }
1646
1647 return status;
1648 }
1649
1650
1651 /*
1652 * Reformat content as plain text.
1653 * Some of the arguments aren't really needed now, but maybe will
1654 * be in the future for other than text types.
1655 */
1656 static int
1657 reformat_part (CT ct, char *file, char *type, char *subtype, int c_type)
1658 {
1659 int output_subtype, output_encoding;
1660 const char *reason = NULL;
1661 char *cp, *cf;
1662 int status;
1663
1664 /* Hacky: this redirects the output from whatever command is used
1665 to show the part to a file. So, the user can't have any output
1666 redirection in that command.
1667 Could show_multi() in mhshowsbr.c avoid this? */
1668
1669 /* Check for invo_name-format-type/subtype. */
1670 if ((cf = context_find_by_type ("format", type, subtype)) == NULL) {
1671 if (verbosw) {
1672 inform("Don't know how to convert %s, there is no "
1673 "%s-format-%s/%s profile entry",
1674 ct->c_file, invo_name, type, subtype);
1675 }
1676 return NOTOK;
1677 }
1678 if (strchr (cf, '>')) {
1679 inform("'>' prohibited in \"%s\",\nplease fix your "
1680 "%s-format-%s/%s profile entry", cf, invo_name, type,
1681 FENDNULL(subtype));
1682
1683 return NOTOK;
1684 }
1685
1686 cp = concat (cf, " >", file, NULL);
1687 status = show_content_aux (ct, 0, cp, NULL, NULL);
1688 free (cp);
1689
1690 /* Unlink decoded content tmp file and free its filename to avoid
1691 leaks. The file stream should already have been closed. */
1692 if (ct->c_cefile.ce_unlink) {
1693 (void) m_unlink (ct->c_cefile.ce_file);
1694 free (ct->c_cefile.ce_file);
1695 ct->c_cefile.ce_file = NULL;
1696 ct->c_cefile.ce_unlink = 0;
1697 }
1698
1699 if (c_type == CT_TEXT) {
1700 output_subtype = TEXT_PLAIN;
1701 } else {
1702 /* Set subtype to 0, which is always an UNKNOWN subtype. */
1703 output_subtype = 0;
1704 }
1705
1706 output_encoding = content_encoding (ct, &reason);
1707 if (status == OK &&
1708 set_ct_type (ct, c_type, output_subtype, output_encoding) == OK) {
1709 ct->c_cefile.ce_file = file;
1710 ct->c_cefile.ce_unlink = 1;
1711 } else {
1712 ct->c_cefile.ce_unlink = 0;
1713 status = NOTOK;
1714 }
1715
1716 return status;
1717 }
1718
1719
1720 /*
1721 * Fill in a multipart/alternative part.
1722 */
1723 static CT
1724 build_multipart_alt (CT first_alt, CT new_part, int type, int subtype)
1725 {
1726 char *boundary_prefix = "----=_nmh-multipart";
1727 char *boundary = concat (boundary_prefix, first_alt->c_partno, NULL);
1728 char *boundary_indicator = "; boundary=";
1729 char *typename, *subtypename, *name;
1730 CT ct;
1731 struct part *p;
1732 struct multipart *m;
1733 const struct str2init *ctinit;
1734
1735 NEW0(ct);
1736
1737 /* Set up the multipart/alternative part. These fields of *ct were
1738 initialized to 0 by mh_xcalloc():
1739 c_fp, c_unlink, c_begin, c_end,
1740 c_vrsn, c_ctline, c_celine,
1741 c_id, c_descr, c_dispo, c_partno,
1742 c_ctinfo.ci_comment, c_ctinfo.ci_magic,
1743 c_cefile, c_encoding,
1744 c_digested, c_digest[16], c_ctexbody,
1745 c_ctinitfnx, c_ceopenfnx, c_ceclosefnx, c_cesizefnx,
1746 c_umask, c_rfc934,
1747 c_showproc, c_termproc, c_storeproc, c_storage, c_folder
1748 */
1749
1750 ct->c_file = mh_xstrdup (first_alt->c_file);
1751 ct->c_type = type;
1752 ct->c_subtype = subtype;
1753
1754 ctinit = get_ct_init (ct->c_type);
1755
1756 typename = ct_type_str (type);
1757 subtypename = ct_subtype_str (type, subtype);
1758
1759 {
1760 int serial = 0;
1761 int found_boundary = 1;
1762
1763 while (found_boundary && serial < 1000000) {
1764 found_boundary = 0;
1765
1766 /* Ensure that the boundary doesn't appear in the decoded
1767 content. */
1768 if (new_part->c_cefile.ce_file) {
1769 if ((found_boundary =
1770 boundary_in_content (&new_part->c_cefile.ce_fp,
1771 new_part->c_cefile.ce_file,
1772 boundary)) == NOTOK) {
1773 goto return_null;
1774 }
1775 }
1776
1777 /* Ensure that the boundary doesn't appear in the encoded
1778 content. */
1779 if (! found_boundary && new_part->c_file) {
1780 if ((found_boundary =
1781 boundary_in_content (&new_part->c_fp,
1782 new_part->c_file,
1783 boundary)) == NOTOK) {
1784 goto return_null;
1785 }
1786 }
1787
1788 if (found_boundary) {
1789 /* Try a slightly different boundary. */
1790 char buffer2[16];
1791
1792 free (boundary);
1793 ++serial;
1794 snprintf (buffer2, sizeof buffer2, "%d", serial);
1795 boundary =
1796 concat (boundary_prefix,
1797 FENDNULL(first_alt->c_partno),
1798 "-", buffer2, NULL);
1799 }
1800 }
1801
1802 if (found_boundary) {
1803 inform("giving up trying to find a unique boundary");
1804 goto return_null;
1805 }
1806 }
1807
1808 name = concat (" ", typename, "/", subtypename, boundary_indicator, "\"",
1809 boundary, "\"", NULL);
1810
1811 /* Load c_first_hf and c_last_hf. */
1812 transfer_noncontent_headers (first_alt, ct);
1813 add_header (ct, mh_xstrdup (TYPE_FIELD), concat (name, "\n", NULL));
1814 free (name);
1815
1816 /* Load c_partno. */
1817 if (first_alt->c_partno) {
1818 ct->c_partno = mh_xstrdup (first_alt->c_partno);
1819 free (first_alt->c_partno);
1820 first_alt->c_partno = concat (ct->c_partno, ".1", NULL);
1821 new_part->c_partno = concat (ct->c_partno, ".2", NULL);
1822 } else {
1823 first_alt->c_partno = mh_xstrdup ("1");
1824 new_part->c_partno = mh_xstrdup ("2");
1825 }
1826
1827 if (ctinit) {
1828 ct->c_ctinfo.ci_type = mh_xstrdup (typename);
1829 ct->c_ctinfo.ci_subtype = mh_xstrdup (subtypename);
1830 }
1831
1832 add_param(&ct->c_ctinfo.ci_first_pm, &ct->c_ctinfo.ci_last_pm,
1833 "boundary", boundary, 0);
1834
1835 NEW(p);
1836 NEW(p->mp_next);
1837 p->mp_next->mp_next = NULL;
1838 p->mp_next->mp_part = first_alt;
1839
1840 NEW0(m);
1841 m->mp_start = concat (boundary, "\n", NULL);
1842 m->mp_stop = concat (boundary, "--\n", NULL);
1843 m->mp_parts = p;
1844 ct->c_ctparams = m;
1845
1846 free (boundary);
1847
1848 return ct;
1849
1850 return_null:
1851 free_content(ct);
1852 free(boundary);
1853 return NULL;
1854 }
1855
1856
1857 /*
1858 * Check that the boundary does not appear in the content.
1859 */
1860 static int
1861 boundary_in_content (FILE **fp, char *file, const char *boundary)
1862 {
1863 char buffer[NMH_BUFSIZ];
1864 size_t bytes_read;
1865 bool found_boundary = false;
1866
1867 /* free_content() will close *fp if we fopen it here. */
1868 if (! *fp && (*fp = fopen (file, "r")) == NULL) {
1869 advise (file, "unable to open %s for reading", file);
1870 return NOTOK;
1871 }
1872
1873 fseeko (*fp, 0L, SEEK_SET);
1874 while ((bytes_read = fread (buffer, 1, sizeof buffer, *fp)) > 0) {
1875 if (find_str (buffer, bytes_read, boundary)) {
1876 found_boundary = true;
1877 break;
1878 }
1879 }
1880
1881 return found_boundary;
1882 }
1883
1884
1885 /*
1886 * Remove all non-Content headers.
1887 */
1888 static void
1889 transfer_noncontent_headers (CT old, CT new)
1890 {
1891 HF hp, hp_prev;
1892
1893 hp_prev = hp = old->c_first_hf;
1894 while (hp) {
1895 HF next = hp->next;
1896
1897 if (strncasecmp (XXX_FIELD_PRF, hp->name, LEN(XXX_FIELD_PRF))) {
1898 if (hp == old->c_last_hf) {
1899 if (hp == old->c_first_hf) {
1900 old->c_last_hf = old->c_first_hf = NULL;
1901 } else {
1902 hp_prev->next = NULL;
1903 old->c_last_hf = hp_prev;
1904 }
1905 } else {
1906 if (hp == old->c_first_hf) {
1907 old->c_first_hf = next;
1908 } else {
1909 hp_prev->next = next;
1910 }
1911 }
1912
1913 /* Put node hp in the new CT. */
1914 if (new->c_first_hf == NULL) {
1915 new->c_first_hf = hp;
1916 } else {
1917 new->c_last_hf->next = hp;
1918 }
1919 new->c_last_hf = hp;
1920 } else {
1921 /* A Content- header, leave in old. */
1922 hp_prev = hp;
1923 }
1924
1925 hp = next;
1926 }
1927 }
1928
1929
1930 /*
1931 * Set content type.
1932 */
1933 static int
1934 set_ct_type (CT ct, int type, int subtype, int encoding)
1935 {
1936 char *typename = ct_type_str (type);
1937 char *subtypename = ct_subtype_str (type, subtype);
1938 /* E.g, " text/plain" */
1939 char *type_subtypename = concat (" ", typename, "/", subtypename, NULL);
1940 /* E.g, " text/plain\n" */
1941 char *name_plus_nl = concat (type_subtypename, "\n", NULL);
1942 bool found_content_type = false;
1943 HF hf;
1944 const char *cp = NULL;
1945 char *ctline;
1946 int status;
1947
1948 /* Update/add Content-Type header field. */
1949 for (hf = ct->c_first_hf; hf; hf = hf->next) {
1950 if (! strcasecmp (TYPE_FIELD, hf->name)) {
1951 found_content_type = true;
1952 free (hf->value);
1953 hf->value = (cp = strchr (ct->c_ctline, ';'))
1954 ? concat (type_subtypename, cp, "\n", NULL)
1955 : mh_xstrdup (name_plus_nl);
1956 }
1957 }
1958 if (! found_content_type) {
1959 add_header (ct, mh_xstrdup (TYPE_FIELD),
1960 (cp = strchr (ct->c_ctline, ';'))
1961 ? concat (type_subtypename, cp, "\n", NULL)
1962 : mh_xstrdup (name_plus_nl));
1963 }
1964
1965 /* Some of these might not be used, but set them anyway. */
1966 ctline = cp
1967 ? concat (type_subtypename, cp, NULL)
1968 : concat (type_subtypename, NULL);
1969 free (ct->c_ctline);
1970 ct->c_ctline = ctline;
1971 /* Leave other ctinfo members as they were. */
1972 free (ct->c_ctinfo.ci_type);
1973 ct->c_ctinfo.ci_type = mh_xstrdup (typename);
1974 free (ct->c_ctinfo.ci_subtype);
1975 ct->c_ctinfo.ci_subtype = mh_xstrdup (subtypename);
1976 ct->c_type = type;
1977 ct->c_subtype = subtype;
1978
1979 free (name_plus_nl);
1980 free (type_subtypename);
1981
1982 status = set_ce (ct, encoding);
1983
1984 return status;
1985 }
1986
1987
1988 /*
1989 * It's not necessary to update the charset parameter of a Content-Type
1990 * header for a text part. According to RFC 2045 Sec. 6.4, the body
1991 * (content) was originally in the specified charset, "and will be in
1992 * that character set again after decoding."
1993 */
1994 static int
1995 decode_text_parts (CT ct, int encoding, const char *decodetypes,
1996 int *message_mods)
1997 {
1998 int status = OK;
1999 int lf_line_endings = 0;
2000
2001 switch (ct->c_type) {
2002 case CT_MULTIPART: {
2003 struct multipart *m = (struct multipart *) ct->c_ctparams;
2004 struct part *part;
2005
2006 /* Should check to see if the body for this part is encoded?
2007 For now, it gets passed along as-is by InitMultiPart(). */
2008 for (part = m->mp_parts; status == OK && part; part = part->mp_next) {
2009 status = decode_text_parts (part->mp_part, encoding, decodetypes,
2010 message_mods);
2011 }
2012 break;
2013 }
2014
2015 case CT_MESSAGE:
2016 if (ct->c_subtype == MESSAGE_EXTERNAL) {
2017 struct exbody *e = (struct exbody *) ct->c_ctparams;
2018
2019 status = decode_text_parts (e->eb_content, encoding, decodetypes,
2020 message_mods);
2021 }
2022 break;
2023
2024 default:
2025 if (! should_decode(decodetypes, ct->c_ctinfo.ci_type, ct->c_ctinfo.ci_subtype)) {
2026 break;
2027 }
2028
2029 lf_line_endings =
2030 ct->c_ctparams && ((struct text *) ct->c_ctparams)->lf_line_endings;
2031
2032 switch (ct->c_encoding) {
2033 case CE_BASE64:
2034 case CE_QUOTED: {
2035 int ct_encoding;
2036
2037 if (decode_part (ct) == OK && ct->c_cefile.ce_file) {
2038 const char *reason = NULL;
2039
2040 if ((ct_encoding = content_encoding (ct, &reason)) == CE_BINARY
2041 && encoding != CE_BINARY) {
2042 /* The decoding isn't acceptable so discard it.
2043 Leave status as OK to allow other transformations. */
2044 if (verbosw) {
2045 report (NULL, ct->c_partno, ct->c_file,
2046 "will not decode%s because it is binary (%s)",
2047 ct->c_partno ? ""
2048 : (FENDNULL(ct->c_ctline)),
2049 reason);
2050 }
2051 (void) m_unlink (ct->c_cefile.ce_file);
2052 free (ct->c_cefile.ce_file);
2053 ct->c_cefile.ce_file = NULL;
2054 } else if (ct->c_encoding == CE_QUOTED &&
2055 ct_encoding == CE_8BIT && encoding == CE_7BIT) {
2056 /* The decoding isn't acceptable so discard it.
2057 Leave status as OK to allow other transformations. */
2058 if (verbosw) {
2059 report (NULL, ct->c_partno, ct->c_file,
2060 "will not decode%s because it is 8bit",
2061 ct->c_partno ? ""
2062 : (FENDNULL(ct->c_ctline)));
2063 }
2064 (void) m_unlink (ct->c_cefile.ce_file);
2065 free (ct->c_cefile.ce_file);
2066 ct->c_cefile.ce_file = NULL;
2067 } else {
2068 int enc;
2069
2070 if (ct_encoding == CE_BINARY) {
2071 enc = CE_BINARY;
2072 } else if (ct_encoding == CE_8BIT && encoding == CE_7BIT) {
2073 enc = CE_QUOTED;
2074 } else {
2075 enc = ct_encoding;
2076 }
2077 if (set_ce (ct, enc) == OK) {
2078 ++*message_mods;
2079 if (verbosw) {
2080 report (NULL, ct->c_partno, ct->c_file, "decode%s",
2081 FENDNULL(ct->c_ctline));
2082 }
2083 if (lf_line_endings) {
2084 strip_crs (ct, message_mods);
2085 }
2086 } else {
2087 status = NOTOK;
2088 }
2089 }
2090 } else {
2091 status = NOTOK;
2092 }
2093 break;
2094 }
2095 case CE_8BIT:
2096 case CE_7BIT:
2097 if (lf_line_endings) {
2098 strip_crs (ct, message_mods);
2099 }
2100 break;
2101 default:
2102 break;
2103 }
2104
2105 break;
2106 }
2107
2108 return status;
2109 }
2110
2111
2112 /*
2113 * Determine if the part with type[/subtype] should be decoded, according to
2114 * decodetypes (which came from the -decodetypes switch).
2115 */
2116 static int
2117 should_decode(const char *decodetypes, const char *type, const char *subtype)
2118 {
2119 /* Quick search for matching type[/subtype] in decodetypes: bracket
2120 decodetypes with commas, then search for ,type, and ,type/subtype, in
2121 it. */
2122
2123 bool found_match = false;
2124 char *delimited_decodetypes = concat(",", decodetypes, ",", NULL);
2125 char *delimited_type = concat(",", type, ",", NULL);
2126
2127 if (nmh_strcasestr(delimited_decodetypes, delimited_type)) {
2128 found_match = true;
2129 } else if (subtype != NULL) {
2130 char *delimited_type_subtype =
2131 concat(",", type, "/", subtype, ",", NULL);
2132
2133 if (nmh_strcasestr(delimited_decodetypes, delimited_type_subtype)) {
2134 found_match = true;
2135 }
2136 free(delimited_type_subtype);
2137 }
2138
2139 free(delimited_type);
2140 free(delimited_decodetypes);
2141
2142 return found_match;
2143 }
2144
2145
2146 /*
2147 * See if the decoded content is 7bit, 8bit, or binary. It's binary
2148 * if it has any NUL characters, a CR not followed by a LF, or lines
2149 * greater than 998 characters in length. If binary, reason is set
2150 * to a string explaining why.
2151 */
2152 static int
2153 content_encoding (CT ct, const char **reason)
2154 {
2155 CE ce = &ct->c_cefile;
2156 int encoding = CE_7BIT;
2157
2158 if (ce->ce_file) {
2159 size_t line_len = 0;
2160 char buffer[NMH_BUFSIZ];
2161 size_t inbytes;
2162
2163 if (! ce->ce_fp && (ce->ce_fp = fopen (ce->ce_file, "r")) == NULL) {
2164 advise (ce->ce_file, "unable to open for reading");
2165 return CE_UNKNOWN;
2166 }
2167
2168 fseeko (ce->ce_fp, 0L, SEEK_SET);
2169 while (encoding != CE_BINARY &&
2170 (inbytes = fread (buffer, 1, sizeof buffer, ce->ce_fp)) > 0) {
2171 char *cp;
2172 size_t i;
2173 int last_char_was_cr = 0;
2174
2175 for (i = 0, cp = buffer; i < inbytes; ++i, ++cp) {
2176 if (*cp == '\0' || ++line_len > 998 ||
2177 (*cp != '\n' && last_char_was_cr)) {
2178 encoding = CE_BINARY;
2179 if (*cp == '\0') {
2180 *reason = "null character";
2181 } else if (line_len > 998) {
2182 *reason = "line length > 998";
2183 } else if (*cp != '\n' && last_char_was_cr) {
2184 *reason = "CR not followed by LF";
2185 } else {
2186 /* Should not reach this. */
2187 *reason = "";
2188 }
2189 break;
2190 }
2191 if (*cp == '\n') {
2192 line_len = 0;
2193 } else if (! isascii ((unsigned char) *cp)) {
2194 encoding = CE_8BIT;
2195 }
2196
2197 last_char_was_cr = *cp == '\r';
2198 }
2199 }
2200
2201 fclose (ce->ce_fp);
2202 ce->ce_fp = NULL;
2203 } /* else should never happen */
2204
2205 return encoding;
2206 }
2207
2208
2209 /*
2210 * Strip carriage returns from content.
2211 */
2212 static int
2213 strip_crs (CT ct, int *message_mods)
2214 {
2215 char *charset = content_charset (ct);
2216 int status = OK;
2217
2218 /* Only strip carriage returns if content is ASCII or another
2219 charset that has the same readily recognizable CR followed by a
2220 LF. We can include UTF-8 here because if the high-order bit of
2221 a UTF-8 byte is 0, then it must be a single-byte ASCII
2222 character. */
2223 if (! strcasecmp (charset, "US-ASCII") ||
2224 ! strcasecmp (charset, "UTF-8") ||
2225 ! strncasecmp (charset, "ISO-8859-", 9) ||
2226 ! strncasecmp (charset, "WINDOWS-12", 10)) {
2227 char **file = NULL;
2228 FILE **fp = NULL;
2229 size_t begin;
2230 size_t end;
2231 bool has_crs = false;
2232 bool opened_input_file = false;
2233
2234 if (ct->c_cefile.ce_file) {
2235 file = &ct->c_cefile.ce_file;
2236 fp = &ct->c_cefile.ce_fp;
2237 begin = end = 0;
2238 } else if (ct->c_file) {
2239 file = &ct->c_file;
2240 fp = &ct->c_fp;
2241 begin = (size_t) ct->c_begin;
2242 end = (size_t) ct->c_end;
2243 } /* else don't know where the content is */
2244
2245 if (file && *file && fp) {
2246 if (! *fp) {
2247 if ((*fp = fopen (*file, "r")) == NULL) {
2248 advise (*file, "unable to open for reading");
2249 status = NOTOK;
2250 } else {
2251 opened_input_file = true;
2252 }
2253 }
2254 }
2255
2256 if (fp && *fp) {
2257 char buffer[NMH_BUFSIZ];
2258 size_t bytes_read;
2259 size_t bytes_to_read =
2260 end > 0 && end > begin ? end - begin : sizeof buffer;
2261
2262 fseeko (*fp, begin, SEEK_SET);
2263 while ((bytes_read = fread (buffer, 1,
2264 min (bytes_to_read, sizeof buffer),
2265 *fp)) > 0) {
2266 /* Look for CR followed by a LF. This is supposed to
2267 be text so there should be LF's. If not, don't
2268 modify the content. */
2269 char *cp;
2270 size_t i;
2271 bool last_char_was_cr = false;
2272
2273 if (end > 0) { bytes_to_read -= bytes_read; }
2274
2275 for (i = 0, cp = buffer; i < bytes_read; ++i, ++cp) {
2276 if (*cp == '\n' && last_char_was_cr) {
2277 has_crs = true;
2278 break;
2279 }
2280
2281 last_char_was_cr = *cp == '\r';
2282 }
2283 }
2284
2285 if (has_crs) {
2286 int fd;
2287 char *stripped_content_file;
2288 char *tempfile = m_mktemp2 (NULL, invo_name, &fd, NULL);
2289
2290 if (tempfile == NULL) {
2291 die("unable to create temporary file in %s",
2292 get_temp_dir());
2293 }
2294 stripped_content_file = mh_xstrdup (tempfile);
2295
2296 /* Strip each CR before a LF from the content. */
2297 fseeko (*fp, begin, SEEK_SET);
2298 while ((bytes_read = fread (buffer, 1, sizeof buffer, *fp)) >
2299 0) {
2300 char *cp;
2301 size_t i;
2302 bool last_char_was_cr = false;
2303
2304 for (i = 0, cp = buffer; i < bytes_read; ++i, ++cp) {
2305 if (*cp == '\r') {
2306 last_char_was_cr = true;
2307 } else if (last_char_was_cr) {
2308 if (*cp != '\n') {
2309 if (write (fd, "\r", 1) < 0) {
2310 advise (tempfile, "CR write");
2311 }
2312 }
2313 if (write (fd, cp, 1) < 0) {
2314 advise (tempfile, "write");
2315 }
2316 last_char_was_cr = false;
2317 } else {
2318 if (write (fd, cp, 1) < 0) {
2319 advise (tempfile, "write");
2320 }
2321 last_char_was_cr = false;
2322 }
2323 }
2324 }
2325
2326 if (close (fd)) {
2327 inform("unable to write temporary file %s, continuing...",
2328 stripped_content_file);
2329 (void) m_unlink (stripped_content_file);
2330 free(stripped_content_file);
2331 status = NOTOK;
2332 } else {
2333 /* Replace the decoded file with the converted one. */
2334 if (ct->c_cefile.ce_file && ct->c_cefile.ce_unlink)
2335 (void) m_unlink (ct->c_cefile.ce_file);
2336
2337 free(ct->c_cefile.ce_file);
2338 ct->c_cefile.ce_file = stripped_content_file;
2339 ct->c_cefile.ce_unlink = 1;
2340
2341 ++*message_mods;
2342 if (verbosw) {
2343 report (NULL, ct->c_partno,
2344 begin == 0 && end == 0 ? "" : *file,
2345 "stripped CRs");
2346 }
2347 }
2348 }
2349
2350 if (opened_input_file) {
2351 fclose (*fp);
2352 *fp = NULL;
2353 }
2354 }
2355 }
2356
2357 free (charset);
2358
2359 return status;
2360 }
2361
2362
2363 /*
2364 * Add/update, if necessary, the message C-T-E, based on the least restrictive
2365 * of the part C-T-E's.
2366 */
2367 static void
2368 update_cte (CT ct)
2369 {
2370 const int least_restrictive_enc = least_restrictive_encoding (ct);
2371
2372 if (least_restrictive_enc != CE_UNKNOWN &&
2373 least_restrictive_enc != CE_7BIT) {
2374 char *cte = concat (" ", ce_str (least_restrictive_enc), "\n", NULL);
2375 HF hf;
2376 bool found_cte = false;
2377
2378 /* Update/add Content-Transfer-Encoding header field. */
2379 for (hf = ct->c_first_hf; hf; hf = hf->next) {
2380 if (! strcasecmp (ENCODING_FIELD, hf->name)) {
2381 found_cte = true;
2382 free (hf->value);
2383 hf->value = cte;
2384 }
2385 }
2386 if (! found_cte) {
2387 add_header (ct, mh_xstrdup (ENCODING_FIELD), cte);
2388 }
2389 }
2390 }
2391
2392
2393 /*
2394 * Find the least restrictive encoding (7bit, 8bit, binary) of the parts
2395 * within a message.
2396 */
2397 static int
2398 least_restrictive_encoding (CT ct)
2399 {
2400 int encoding = CE_UNKNOWN;
2401
2402 switch (ct->c_type) {
2403 case CT_MULTIPART: {
2404 struct multipart *m = (struct multipart *) ct->c_ctparams;
2405 struct part *part;
2406
2407 for (part = m->mp_parts; part; part = part->mp_next) {
2408 const int part_encoding =
2409 least_restrictive_encoding (part->mp_part);
2410
2411 if (less_restrictive (encoding, part_encoding)) {
2412 encoding = part_encoding;
2413 }
2414 }
2415 break;
2416 }
2417
2418 case CT_MESSAGE:
2419 if (ct->c_subtype == MESSAGE_EXTERNAL) {
2420 struct exbody *e = (struct exbody *) ct->c_ctparams;
2421 const int part_encoding =
2422 least_restrictive_encoding (e->eb_content);
2423
2424 if (less_restrictive (encoding, part_encoding)) {
2425 encoding = part_encoding;
2426 }
2427 }
2428 break;
2429
2430 default: {
2431 if (less_restrictive (encoding, ct->c_encoding)) {
2432 encoding = ct->c_encoding;
2433 }
2434 }}
2435
2436 return encoding;
2437 }
2438
2439
2440 /*
2441 * Return whether the second encoding is less restrictive than the first, where
2442 * "less restrictive" is in the sense used by RFC 2045 Secs. 6.1 and 6.4. So,
2443 * CE_BINARY is less restrictive than CE_8BIT and
2444 * CE_8BIT is less restrictive than CE_7BIT.
2445 */
2446 static int
2447 less_restrictive (int encoding, int second_encoding)
2448 {
2449 switch (second_encoding) {
2450 case CE_BINARY:
2451 return encoding != CE_BINARY;
2452 case CE_8BIT:
2453 return encoding != CE_BINARY && encoding != CE_8BIT;
2454 case CE_7BIT:
2455 return encoding != CE_BINARY && encoding != CE_8BIT &&
2456 encoding != CE_7BIT;
2457 default :
2458 return 0;
2459 }
2460 }
2461
2462
2463 /*
2464 * Convert character set of each part.
2465 */
2466 static int
2467 convert_charsets (CT ct, char *dest_charset, int *message_mods)
2468 {
2469 int status = OK;
2470
2471 switch (ct->c_type) {
2472 case CT_TEXT:
2473 if (ct->c_subtype == TEXT_PLAIN) {
2474 status = convert_charset (ct, dest_charset, message_mods);
2475 if (status == OK) {
2476 if (verbosw) {
2477 char *ct_charset = content_charset (ct);
2478
2479 report (NULL, ct->c_partno, ct->c_file,
2480 "convert %s to %s", ct_charset, dest_charset);
2481 free (ct_charset);
2482 }
2483 } else {
2484 char *ct_charset = content_charset (ct);
2485
2486 report ("iconv", ct->c_partno, ct->c_file,
2487 "failed to convert %s to %s", ct_charset, dest_charset);
2488 free (ct_charset);
2489 }
2490 }
2491 break;
2492
2493 case CT_MULTIPART: {
2494 struct multipart *m = (struct multipart *) ct->c_ctparams;
2495 struct part *part;
2496
2497 /* Should check to see if the body for this part is encoded?
2498 For now, it gets passed along as-is by InitMultiPart(). */
2499 for (part = m->mp_parts; status == OK && part; part = part->mp_next) {
2500 status =
2501 convert_charsets (part->mp_part, dest_charset, message_mods);
2502 }
2503 break;
2504 }
2505
2506 case CT_MESSAGE:
2507 if (ct->c_subtype == MESSAGE_EXTERNAL) {
2508 struct exbody *e = (struct exbody *) ct->c_ctparams;
2509
2510 status =
2511 convert_charsets (e->eb_content, dest_charset, message_mods);
2512 }
2513 break;
2514
2515 default:
2516 break;
2517 }
2518
2519 return status;
2520 }
2521
2522
2523 /*
2524 * Fix various problems that aren't handled elsewhere. These
2525 * are fixed unconditionally: there are no switches to disable
2526 * them. Currently, "problems" are these:
2527 * 1) remove extraneous semicolon at the end of a header parameter list
2528 * 2) replace RFC 2047 encoding with RFC 2231 encoding of name and
2529 * filename parameters in Content-Type and Content-Disposition
2530 * headers, respectively.
2531 */
2532 static int
2533 fix_always (CT ct, int *message_mods)
2534 {
2535 int status = OK;
2536
2537 switch (ct->c_type) {
2538 case CT_MULTIPART: {
2539 struct multipart *m = (struct multipart *) ct->c_ctparams;
2540 struct part *part;
2541
2542 for (part = m->mp_parts; status == OK && part; part = part->mp_next) {
2543 status = fix_always (part->mp_part, message_mods);
2544 }
2545 break;
2546 }
2547
2548 case CT_MESSAGE:
2549 if (ct->c_subtype == MESSAGE_EXTERNAL) {
2550 struct exbody *e = (struct exbody *) ct->c_ctparams;
2551
2552 status = fix_always (e->eb_content, message_mods);
2553 }
2554 break;
2555
2556 default: {
2557 HF hf;
2558
2559 if (ct->c_first_hf) {
2560 fix_filename_encoding (ct);
2561 }
2562
2563 for (hf = ct->c_first_hf; hf; hf = hf->next) {
2564 size_t len = strlen (hf->value);
2565
2566 if (strcasecmp (hf->name, TYPE_FIELD) != 0 &&
2567 strcasecmp (hf->name, DISPO_FIELD) != 0) {
2568 /* Only do this for Content-Type and
2569 Content-Disposition fields because those are the
2570 only headers that parse_mime() warns about. */
2571 continue;
2572 }
2573
2574 /* whitespace following a trailing ';' will be nuked as well */
2575 if (hf->value[len - 1] == '\n') {
2576 while (isspace((unsigned char)(hf->value[len - 2]))) {
2577 if (len-- == 0) { break; }
2578 }
2579 }
2580
2581 if (hf->value[len - 2] == ';') {
2582 /* Remove trailing ';' from parameter value. */
2583 hf->value[len - 2] = '\n';
2584 hf->value[len - 1] = '\0';
2585
2586 /* Also, if Content-Type parameter, remove trailing ';'
2587 from ct->c_ctline. This probably isn't necessary
2588 but can't hurt. */
2589 if (strcasecmp(hf->name, TYPE_FIELD) == 0 && ct->c_ctline) {
2590 size_t l = strlen(ct->c_ctline) - 1;
2591 while (isspace((unsigned char)(ct->c_ctline[l])) ||
2592 ct->c_ctline[l] == ';') {
2593 ct->c_ctline[l--] = '\0';
2594 if (l == 0) { break; }
2595 }
2596 }
2597
2598 ++*message_mods;
2599 if (verbosw) {
2600 report (NULL, ct->c_partno, ct->c_file,
2601 "remove trailing ; from %s parameter value",
2602 hf->name);
2603 }
2604 }
2605 }
2606 }}
2607
2608 return status;
2609 }
2610
2611
2612 /*
2613 * Factor out common code for loops in fix_filename_encoding().
2614 */
2615 static int
2616 fix_filename_param (char *name, char *value, PM *first_pm, PM *last_pm)
2617 {
2618 bool fixed = false;
2619
2620 if (has_prefix(value, "=?") && has_suffix(value, "?=")) {
2621 /* Looks like an RFC 2047 encoded parameter. */
2622 char decoded[PATH_MAX + 1];
2623
2624 if (decode_rfc2047 (value, decoded, sizeof decoded)) {
2625 /* Encode using RFC 2231. */
2626 replace_param (first_pm, last_pm, name, decoded, 0);
2627 fixed = true;
2628 } else {
2629 inform("failed to decode %s parameter %s", name, value);
2630 }
2631 }
2632
2633 return fixed;
2634 }
2635
2636
2637 /*
2638 * Replace RFC 2047 encoding with RFC 2231 encoding of name and
2639 * filename parameters in Content-Type and Content-Disposition
2640 * headers, respectively.
2641 */
2642 static int
2643 fix_filename_encoding (CT ct)
2644 {
2645 PM pm;
2646 HF hf;
2647 int fixed = 0;
2648
2649 for (pm = ct->c_ctinfo.ci_first_pm; pm; pm = pm->pm_next) {
2650 if (pm->pm_name && pm->pm_value &&
2651 strcasecmp (pm->pm_name, "name") == 0) {
2652 fixed = fix_filename_param (pm->pm_name, pm->pm_value,
2653 &ct->c_ctinfo.ci_first_pm,
2654 &ct->c_ctinfo.ci_last_pm);
2655 }
2656 }
2657
2658 for (pm = ct->c_dispo_first; pm; pm = pm->pm_next) {
2659 if (pm->pm_name && pm->pm_value &&
2660 strcasecmp (pm->pm_name, "filename") == 0) {
2661 fixed = fix_filename_param (pm->pm_name, pm->pm_value,
2662 &ct->c_dispo_first,
2663 &ct->c_dispo_last);
2664 }
2665 }
2666
2667 /* Fix hf values to correspond. */
2668 for (hf = ct->c_first_hf; fixed && hf; hf = hf->next) {
2669 enum { OTHER, TYPE_HEADER, DISPO_HEADER } field = OTHER;
2670
2671 if (strcasecmp (hf->name, TYPE_FIELD) == 0) {
2672 field = TYPE_HEADER;
2673 } else if (strcasecmp (hf->name, DISPO_FIELD) == 0) {
2674 field = DISPO_HEADER;
2675 }
2676
2677 if (field != OTHER) {
2678 const char *const semicolon_loc = strchr (hf->value, ';');
2679
2680 if (semicolon_loc) {
2681 const size_t len =
2682 strlen (hf->name) + 1 + semicolon_loc - hf->value;
2683 const char *const params =
2684 output_params (len,
2685 field == TYPE_HEADER
2686 ? ct->c_ctinfo.ci_first_pm
2687 : ct->c_dispo_first,
2688 NULL, 0);
2689 const char *const new_params = concat (params, "\n", NULL);
2690
2691 replace_substring (&hf->value, semicolon_loc, new_params);
2692 free((void *)new_params); /* Cast away const. Sigh. */
2693 free((void *)params);
2694 } else {
2695 inform("did not find semicolon in %s:%s\n",
2696 hf->name, hf->value);
2697 }
2698 }
2699 }
2700
2701 return OK;
2702 }
2703
2704
2705 /*
2706 * Output content in input file to output file.
2707 */
2708 static int
2709 write_content (CT ct, const char *input_filename, char *outfile, FILE *outfp,
2710 int modify_inplace, int message_mods)
2711 {
2712 int status = OK;
2713
2714 if (modify_inplace) {
2715 if (message_mods > 0) {
2716 if ((status = output_message_fp (ct, outfp, outfile)) == OK) {
2717 char *infile = input_filename
2718 ? mh_xstrdup (input_filename)
2719 : mh_xstrdup (ct->c_file ? ct->c_file : "-");
2720
2721 if (remove_file (infile) == OK) {
2722 if (rename (outfile, infile)) {
2723 /* Rename didn't work, possibly because of an
2724 attempt to rename across filesystems. Try
2725 brute force copy. */
2726 int old = open (outfile, O_RDONLY);
2727 int new =
2728 open (infile, O_WRONLY | O_CREAT, m_gmprot ());
2729 int i = -1;
2730
2731 if (old != -1 && new != -1) {
2732 char buffer[NMH_BUFSIZ];
2733
2734 while ((i = read (old, buffer, sizeof buffer)) >
2735 0) {
2736 if (write (new, buffer, i) != i) {
2737 i = -1;
2738 break;
2739 }
2740 }
2741 }
2742 if (new != -1) { close (new); }
2743 if (old != -1) { close (old); }
2744 (void) m_unlink (outfile);
2745
2746 if (i < 0) {
2747 /* The -file argument processing used path() to
2748 expand filename to absolute path. */
2749 int file = ct->c_file && ct->c_file[0] == '/';
2750
2751 inform("unable to rename %s %s to %s, continuing...",
2752 file ? "file" : "message", outfile,
2753 infile);
2754 status = NOTOK;
2755 }
2756 }
2757 } else {
2758 inform("unable to remove input file %s, "
2759 "not modifying it, continuing...", infile);
2760 (void) m_unlink (outfile);
2761 status = NOTOK;
2762 }
2763
2764 free (infile);
2765 } else {
2766 status = NOTOK;
2767 }
2768 } else {
2769 /* No modifications and didn't need the tmp outfile. */
2770 (void) m_unlink (outfile);
2771 }
2772 } else {
2773 /* Output is going to some file. Produce it whether or not
2774 there were modifications. */
2775 status = output_message_fp (ct, outfp, outfile);
2776 }
2777
2778 flush_errors ();
2779 return status;
2780 }
2781
2782
2783 /*
2784 * parse_mime() does not set lf_line_endings in struct text, so use this
2785 * function to do it. It touches the parts the decodetypes identifies.
2786 */
2787 static void
2788 set_text_ctparams(CT ct, char *decodetypes, int lf_line_endings)
2789 {
2790 switch (ct->c_type) {
2791 case CT_MULTIPART: {
2792 struct multipart *m = (struct multipart *) ct->c_ctparams;
2793 struct part *part;
2794
2795 for (part = m->mp_parts; part; part = part->mp_next) {
2796 set_text_ctparams(part->mp_part, decodetypes, lf_line_endings);
2797 }
2798 break;
2799 }
2800
2801 case CT_MESSAGE:
2802 if (ct->c_subtype == MESSAGE_EXTERNAL) {
2803 struct exbody *e = (struct exbody *) ct->c_ctparams;
2804
2805 set_text_ctparams(e->eb_content, decodetypes, lf_line_endings);
2806 }
2807 break;
2808
2809 default:
2810 if (should_decode(decodetypes, ct->c_ctinfo.ci_type, ct->c_ctinfo.ci_subtype)) {
2811 if (ct->c_ctparams == NULL) {
2812 ct->c_ctparams = mh_xcalloc(1, sizeof (struct text));
2813 }
2814 ((struct text *) ct->c_ctparams)->lf_line_endings = lf_line_endings;
2815 }
2816 }
2817 }
2818
2819
2820 /*
2821 * If "rmmproc" is defined, call that to remove the file. Otherwise,
2822 * use the standard MH backup file.
2823 */
2824 static int
2825 remove_file (const char *file)
2826 {
2827 if (rmmproc) {
2828 char *rmm_command = concat (rmmproc, " ", file, NULL);
2829 int status = system (rmm_command);
2830
2831 free (rmm_command);
2832 return WIFEXITED (status) ? WEXITSTATUS (status) : NOTOK;
2833 }
2834 /* This is OK for a non-message file, it still uses the
2835 BACKUP_PREFIX form. The backup file will be in the same
2836 directory as file. */
2837 return rename (file, m_backup (file));
2838 }
2839
2840
2841 /*
2842 * Output formatted message to user.
2843 */
2844 static void
2845 report (char *what, char *partno, char *filename, char *message, ...)
2846 {
2847 va_list args;
2848 char *fmt;
2849
2850 if (verbosw) {
2851 va_start (args, message);
2852 fmt = concat (filename, partno ? " part " : ", ",
2853 FENDNULL(partno), partno ? ", " : "", message, NULL);
2854
2855 advertise (what, NULL, fmt, args);
2856
2857 free (fmt);
2858 va_end (args);
2859 }
2860 }
2861
2862
2863 static void
2864 pipeser (int i)
2865 {
2866 if (i == SIGQUIT) {
2867 fflush (stdout);
2868 fprintf (stderr, "\n");
2869 fflush (stderr);
2870 }
2871
2872 done (1);
2873 /* NOTREACHED */
2874 }