1 /**
2 * @file
3 * RFC3676 Format Flowed routines
4 *
5 * @authors
6 * Copyright (C) 2005 Andreas Krennmair <ak@synflood.at>
7 * Copyright (C) 2005 Peter J. Holzer <hjp@hjp.net>
8 * Copyright (C) 2005-2009 Rocco Rutte <pdmef@gmx.net>
9 * Copyright (C) 2010 Michael R. Elkins <me@mutt.org>
10 *
11 * @copyright
12 * This program is free software: you can redistribute it and/or modify it under
13 * the terms of the GNU General Public License as published by the Free Software
14 * Foundation, either version 2 of the License, or (at your option) any later
15 * version.
16 *
17 * This program is distributed in the hope that it will be useful, but WITHOUT
18 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
19 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
20 * details.
21 *
22 * You should have received a copy of the GNU General Public License along with
23 * this program. If not, see <http://www.gnu.org/licenses/>.
24 */
25
26 /**
27 * @page neo_rfc3676 RFC3676 Format Flowed routines
28 *
29 * RFC3676 Format Flowed routines
30 */
31
32 #include "config.h"
33 #include <stdbool.h>
34 #include <stdio.h>
35 #include <string.h>
36 #include <unistd.h>
37 #include "mutt/lib.h"
38 #include "config/lib.h"
39 #include "email/lib.h"
40 #include "core/lib.h"
41 #include "gui/lib.h"
42 #include "rfc3676.h"
43 #include "muttlib.h"
44
45 #define FLOWED_MAX 72
46
47 /**
48 * struct FlowedState - State of a Format-Flowed line of text
49 */
50 struct FlowedState
51 {
52 size_t width;
53 size_t spaces;
54 bool delsp;
55 };
56
57 /**
58 * get_quote_level - Get the quote level of a line
59 * @param line Text to examine
60 * @retval num Quote level
61 */
get_quote_level(const char * line)62 static int get_quote_level(const char *line)
63 {
64 int quoted = 0;
65 const char *p = line;
66
67 while (p && (*p == '>'))
68 {
69 quoted++;
70 p++;
71 }
72
73 return quoted;
74 }
75
76 /**
77 * space_quotes - Should we add spaces between quote levels
78 * @param s State to use
79 * @retval true Spaces should be added
80 *
81 * Determines whether to add spacing between/after each quote level:
82 * ` >>>foo`
83 * becomes
84 * ` > > > foo`
85 */
space_quotes(struct State * s)86 static int space_quotes(struct State *s)
87 {
88 /* Allow quote spacing in the pager even for `$text_flowed`,
89 * but obviously not when replying. */
90 const bool c_text_flowed = cs_subset_bool(NeoMutt->sub, "text_flowed");
91 if (c_text_flowed && (s->flags & MUTT_REPLYING))
92 return 0;
93
94 const bool c_reflow_space_quotes =
95 cs_subset_bool(NeoMutt->sub, "reflow_space_quotes");
96 return c_reflow_space_quotes;
97 }
98
99 /**
100 * add_quote_suffix - Should we add a trailing space to quotes
101 * @param s State to use
102 * @param ql Quote level
103 * @retval true Spaces should be added
104 *
105 * Determines whether to add a trailing space to quotes:
106 * ` >>> foo`
107 * as opposed to
108 * ` >>>foo`
109 */
add_quote_suffix(struct State * s,int ql)110 static bool add_quote_suffix(struct State *s, int ql)
111 {
112 if (s->flags & MUTT_REPLYING)
113 return false;
114
115 if (space_quotes(s))
116 return false;
117
118 if (!ql && !s->prefix)
119 return false;
120
121 /* The prefix will add its own space */
122 const bool c_text_flowed = cs_subset_bool(NeoMutt->sub, "text_flowed");
123 if (!c_text_flowed && !ql && s->prefix)
124 return false;
125
126 return true;
127 }
128
129 /**
130 * print_indent - Print indented text
131 * @param ql Quote level
132 * @param s State to work with
133 * @param add_suffix If true, write a trailing space character
134 * @retval num Number of characters written
135 */
print_indent(int ql,struct State * s,int add_suffix)136 static size_t print_indent(int ql, struct State *s, int add_suffix)
137 {
138 size_t wid = 0;
139
140 if (s->prefix)
141 {
142 /* use given prefix only for format=fixed replies to format=flowed,
143 * for format=flowed replies to format=flowed, use '>' indentation */
144 const bool c_text_flowed = cs_subset_bool(NeoMutt->sub, "text_flowed");
145 if (c_text_flowed)
146 ql++;
147 else
148 {
149 state_puts(s, s->prefix);
150 wid = mutt_strwidth(s->prefix);
151 }
152 }
153 for (int i = 0; i < ql; i++)
154 {
155 state_putc(s, '>');
156 if (space_quotes(s))
157 state_putc(s, ' ');
158 }
159 if (add_suffix)
160 state_putc(s, ' ');
161
162 if (space_quotes(s))
163 ql *= 2;
164
165 return ql + add_suffix + wid;
166 }
167
168 /**
169 * flush_par - Write out the paragraph
170 * @param s State to work with
171 * @param fst The state of the flowed text
172 */
flush_par(struct State * s,struct FlowedState * fst)173 static void flush_par(struct State *s, struct FlowedState *fst)
174 {
175 if (fst->width > 0)
176 {
177 state_putc(s, '\n');
178 fst->width = 0;
179 }
180 fst->spaces = 0;
181 }
182
183 /**
184 * quote_width - Calculate the paragraph width based upon the quote level
185 * @param s State to use
186 * @param ql Quote level
187 * @retval num Paragraph width
188 *
189 * The start of a quoted line will be ">>> ", so we need to subtract the space
190 * required for the prefix from the terminal width.
191 */
quote_width(struct State * s,int ql)192 static int quote_width(struct State *s, int ql)
193 {
194 const int screen_width = (s->flags & MUTT_DISPLAY) ? s->wraplen : 80;
195 const short c_reflow_wrap = cs_subset_number(NeoMutt->sub, "reflow_wrap");
196 int width = mutt_window_wrap_cols(screen_width, c_reflow_wrap);
197 const bool c_text_flowed = cs_subset_bool(NeoMutt->sub, "text_flowed");
198 if (c_text_flowed && (s->flags & MUTT_REPLYING))
199 {
200 /* When replying, force a wrap at FLOWED_MAX to comply with RFC3676
201 * guidelines */
202 if (width > FLOWED_MAX)
203 width = FLOWED_MAX;
204 ql++; /* When replying, we will add an additional quote level */
205 }
206 /* adjust the paragraph width subtracting the number of prefix chars */
207 width -= space_quotes(s) ? ql * 2 : ql;
208 /* When displaying (not replying), there may be a space between the prefix
209 * string and the paragraph */
210 if (add_quote_suffix(s, ql))
211 width--;
212 /* failsafe for really long quotes */
213 if (width <= 0)
214 width = FLOWED_MAX; /* arbitrary, since the line will wrap */
215 return width;
216 }
217
218 /**
219 * print_flowed_line - Print a format-flowed line
220 * @param line Text to print
221 * @param s State to work with
222 * @param ql Quote level
223 * @param fst The state of the flowed text
224 * @param term If true, terminate with a new line
225 */
print_flowed_line(char * line,struct State * s,int ql,struct FlowedState * fst,bool term)226 static void print_flowed_line(char *line, struct State *s, int ql,
227 struct FlowedState *fst, bool term)
228 {
229 size_t width, w, words = 0;
230 char *p = NULL;
231 char last;
232
233 if (!line || (*line == '\0'))
234 {
235 /* flush current paragraph (if any) first */
236 flush_par(s, fst);
237 print_indent(ql, s, 0);
238 state_putc(s, '\n');
239 return;
240 }
241
242 width = quote_width(s, ql);
243 last = line[mutt_str_len(line) - 1];
244
245 mutt_debug(LL_DEBUG5, "f=f: line [%s], width = %ld, spaces = %lu\n", line,
246 (long) width, fst->spaces);
247
248 for (words = 0; (p = strsep(&line, " "));)
249 {
250 mutt_debug(LL_DEBUG5, "f=f: word [%s], width: %lu, remaining = [%s]\n", p,
251 fst->width, line);
252
253 /* remember number of spaces */
254 if (*p == '\0')
255 {
256 mutt_debug(LL_DEBUG3, "f=f: additional space\n");
257 fst->spaces++;
258 continue;
259 }
260 /* there's exactly one space prior to every but the first word */
261 if (words)
262 fst->spaces++;
263
264 w = mutt_strwidth(p);
265 /* see if we need to break the line but make sure the first word is put on
266 * the line regardless; if for DelSp=yes only one trailing space is used,
267 * we probably have a long word that we should break within (we leave that
268 * up to the pager or user) */
269 if (!(!fst->spaces && fst->delsp && (last != ' ')) && (w < width) &&
270 (w + fst->width + fst->spaces > width))
271 {
272 mutt_debug(LL_DEBUG3, "f=f: break line at %lu, %lu spaces left\n",
273 fst->width, fst->spaces);
274 /* only honor trailing spaces for format=flowed replies */
275 const bool c_text_flowed = cs_subset_bool(NeoMutt->sub, "text_flowed");
276 if (c_text_flowed)
277 for (; fst->spaces; fst->spaces--)
278 state_putc(s, ' ');
279 state_putc(s, '\n');
280 fst->width = 0;
281 fst->spaces = 0;
282 words = 0;
283 }
284
285 if (!words && !fst->width)
286 fst->width = print_indent(ql, s, add_quote_suffix(s, ql));
287 fst->width += w + fst->spaces;
288 for (; fst->spaces; fst->spaces--)
289 state_putc(s, ' ');
290 state_puts(s, p);
291 words++;
292 }
293
294 if (term)
295 flush_par(s, fst);
296 }
297
298 /**
299 * print_fixed_line - Print a fixed format line
300 * @param line Text to print
301 * @param s State to work with
302 * @param ql Quote level
303 * @param fst The state of the flowed text
304 */
print_fixed_line(const char * line,struct State * s,int ql,struct FlowedState * fst)305 static void print_fixed_line(const char *line, struct State *s, int ql, struct FlowedState *fst)
306 {
307 print_indent(ql, s, add_quote_suffix(s, ql));
308 if (line && *line)
309 state_puts(s, line);
310 state_putc(s, '\n');
311
312 fst->width = 0;
313 fst->spaces = 0;
314 }
315
316 /**
317 * rfc3676_handler - Body handler implementing RFC3676 for format=flowed - Implements ::handler_t - @ingroup handler_api
318 * @retval 0 Always
319 */
rfc3676_handler(struct Body * a,struct State * s)320 int rfc3676_handler(struct Body *a, struct State *s)
321 {
322 char *buf = NULL;
323 unsigned int quotelevel = 0;
324 bool delsp = false;
325 size_t sz = 0;
326 struct FlowedState fst = { 0 };
327
328 /* respect DelSp of RFC3676 only with f=f parts */
329 char *t = mutt_param_get(&a->parameter, "delsp");
330 if (t)
331 {
332 delsp = mutt_istr_equal(t, "yes");
333 t = NULL;
334 fst.delsp = true;
335 }
336
337 mutt_debug(LL_DEBUG3, "f=f: DelSp: %s\n", delsp ? "yes" : "no");
338
339 while ((buf = mutt_file_read_line(buf, &sz, s->fp_in, NULL, MUTT_RL_NO_FLAGS)))
340 {
341 const size_t buf_len = mutt_str_len(buf);
342 const unsigned int newql = get_quote_level(buf);
343
344 /* end flowed paragraph (if we're within one) if quoting level
345 * changes (should not but can happen, see RFC3676, sec. 4.5.) */
346 if (newql != quotelevel)
347 flush_par(s, &fst);
348
349 quotelevel = newql;
350 int buf_off = newql;
351
352 /* respect sender's space-stuffing by removing one leading space */
353 if (buf[buf_off] == ' ')
354 buf_off++;
355
356 /* test for signature separator */
357 const unsigned int sigsep = mutt_str_equal(buf + buf_off, "-- ");
358
359 /* a fixed line either has no trailing space or is the
360 * signature separator */
361 const bool fixed = (buf_len == buf_off) || (buf[buf_len - 1] != ' ') || sigsep;
362
363 /* print fixed-and-standalone, fixed-and-empty and sigsep lines as
364 * fixed lines */
365 if ((fixed && ((fst.width == 0) || (buf_len == 0))) || sigsep)
366 {
367 /* if we're within a flowed paragraph, terminate it */
368 flush_par(s, &fst);
369 print_fixed_line(buf + buf_off, s, quotelevel, &fst);
370 continue;
371 }
372
373 /* for DelSp=yes, we need to strip one SP prior to CRLF on flowed lines */
374 if (delsp && !fixed)
375 buf[buf_len - 1] = '\0';
376
377 print_flowed_line(buf + buf_off, s, quotelevel, &fst, fixed);
378 }
379
380 flush_par(s, &fst);
381
382 FREE(&buf);
383 return 0;
384 }
385
386 /**
387 * mutt_rfc3676_is_format_flowed - Is the Email "format-flowed"?
388 * @param b Email Body to examine
389 * @retval true Email is "format-flowed"
390 */
mutt_rfc3676_is_format_flowed(struct Body * b)391 bool mutt_rfc3676_is_format_flowed(struct Body *b)
392 {
393 if (b && (b->type == TYPE_TEXT) && mutt_istr_equal("plain", b->subtype))
394 {
395 const char *format = mutt_param_get(&b->parameter, "format");
396 if (mutt_istr_equal("flowed", format))
397 return true;
398 }
399
400 return false;
401 }
402
403 /**
404 * rfc3676_space_stuff - Perform required RFC3676 space stuffing
405 * @param filename Attachment file
406 * @param unstuff If true, remove space stuffing
407 *
408 * Space stuffing means that we have to add leading spaces to
409 * certain lines:
410 * - lines starting with a space
411 * - lines starting with 'From '
412 *
413 * Care is taken to preserve the e->body->filename, as
414 * mutt -i -E can directly edit a passed in filename.
415 */
rfc3676_space_stuff(const char * filename,bool unstuff)416 static void rfc3676_space_stuff(const char *filename, bool unstuff)
417 {
418 FILE *fp_out = NULL;
419 char *buf = NULL;
420 size_t blen = 0;
421
422 struct Buffer *tmpfile = mutt_buffer_pool_get();
423
424 FILE *fp_in = mutt_file_fopen(filename, "r");
425 if (!fp_in)
426 goto bail;
427
428 mutt_buffer_mktemp(tmpfile);
429 fp_out = mutt_file_fopen(mutt_buffer_string(tmpfile), "w+");
430 if (!fp_out)
431 goto bail;
432
433 while ((buf = mutt_file_read_line(buf, &blen, fp_in, NULL, MUTT_RL_NO_FLAGS)) != NULL)
434 {
435 if (unstuff)
436 {
437 if (buf[0] == ' ')
438 fputs(buf + 1, fp_out);
439 else
440 fputs(buf, fp_out);
441 }
442 else
443 {
444 if ((buf[0] == ' ') || mutt_str_startswith(buf, "From "))
445 fputc(' ', fp_out);
446 fputs(buf, fp_out);
447 }
448 fputc('\n', fp_out);
449 }
450 FREE(&buf);
451 mutt_file_fclose(&fp_in);
452 mutt_file_fclose(&fp_out);
453 mutt_file_set_mtime(filename, mutt_buffer_string(tmpfile));
454
455 fp_in = mutt_file_fopen(mutt_buffer_string(tmpfile), "r");
456 if (!fp_in)
457 goto bail;
458
459 if ((truncate(filename, 0) == -1) || ((fp_out = mutt_file_fopen(filename, "a")) == NULL))
460 {
461 mutt_perror(filename);
462 goto bail;
463 }
464
465 mutt_file_copy_stream(fp_in, fp_out);
466 mutt_file_set_mtime(mutt_buffer_string(tmpfile), filename);
467 unlink(mutt_buffer_string(tmpfile));
468
469 bail:
470 mutt_file_fclose(&fp_in);
471 mutt_file_fclose(&fp_out);
472 mutt_buffer_pool_release(&tmpfile);
473 }
474
475 /**
476 * mutt_rfc3676_space_stuff - Perform RFC3676 space stuffing on an Email
477 * @param e Email
478 *
479 * @note We don't check the option `$text_flowed` because we want to stuff based
480 * the actual content type. The option only decides whether to *set*
481 * format=flowed on new messages.
482 */
mutt_rfc3676_space_stuff(struct Email * e)483 void mutt_rfc3676_space_stuff(struct Email *e)
484 {
485 if (!e || !e->body || !e->body->filename)
486 return;
487
488 if (mutt_rfc3676_is_format_flowed(e->body))
489 rfc3676_space_stuff(e->body->filename, false);
490 }
491
492 /**
493 * mutt_rfc3676_space_unstuff - Remove RFC3676 space stuffing
494 * @param e Email
495 */
mutt_rfc3676_space_unstuff(struct Email * e)496 void mutt_rfc3676_space_unstuff(struct Email *e)
497 {
498 if (!e || !e->body || !e->body->filename)
499 return;
500
501 if (mutt_rfc3676_is_format_flowed(e->body))
502 rfc3676_space_stuff(e->body->filename, true);
503 }
504
505 /**
506 * mutt_rfc3676_space_unstuff_attachment - Unstuff attachments
507 * @param b Email Body (OPTIONAL)
508 * @param filename Attachment file
509 *
510 * This routine is used when saving/piping/viewing rfc3676 attachments.
511 *
512 * If b is provided, the function will verify that the Email is format-flowed.
513 * The filename will be unstuffed, not b->filename or b->fp.
514 */
mutt_rfc3676_space_unstuff_attachment(struct Body * b,const char * filename)515 void mutt_rfc3676_space_unstuff_attachment(struct Body *b, const char *filename)
516 {
517 if (!filename)
518 return;
519
520 if (b && !mutt_rfc3676_is_format_flowed(b))
521 return;
522
523 rfc3676_space_stuff(filename, true);
524 }
525
526 /**
527 * mutt_rfc3676_space_stuff_attachment - Stuff attachments
528 * @param b Email Body (OPTIONAL)
529 * @param filename Attachment file
530 *
531 * This routine is used when filtering rfc3676 attachments.
532 *
533 * If b is provided, the function will verify that the Email is format-flowed.
534 * The filename will be unstuffed, not b->filename or b->fp.
535 */
mutt_rfc3676_space_stuff_attachment(struct Body * b,const char * filename)536 void mutt_rfc3676_space_stuff_attachment(struct Body *b, const char *filename)
537 {
538 if (!filename)
539 return;
540
541 if (b && !mutt_rfc3676_is_format_flowed(b))
542 return;
543
544 rfc3676_space_stuff(filename, false);
545 }
546