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