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