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