mutt stable branch with some hacks
1/*
2 * Copyright (C) 2005 Andreas Krennmair <ak@synflood.at>
3 * Copyright (C) 2005 Peter J. Holzer <hjp@hjp.net>
4 * Copyright (C) 2005-2009 Rocco Rutte <pdmef@gmx.net>
5 * Copyright (C) 2010 Michael R. Elkins <me@mutt.org>
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 *
21 */
22
23/* This file was originally part of mutt-ng */
24
25#if HAVE_CONFIG_H
26# include "config.h"
27#endif
28
29#include <stdlib.h>
30#include <string.h>
31#include <unistd.h>
32#include <ctype.h>
33#include <sys/wait.h>
34#include <sys/stat.h>
35
36#include "mutt.h"
37#include "mutt_curses.h"
38#include "ascii.h"
39#include "lib.h"
40#include "mime.h"
41
42#define FLOWED_MAX 72
43
44typedef struct flowed_state
45{
46 size_t width;
47 size_t spaces;
48 int delsp;
49} flowed_state_t;
50
51static int get_quote_level (const char *line)
52{
53 int quoted = 0;
54 char *p = (char *) line;
55
56 while (p && *p == '>')
57 {
58 quoted++;
59 p++;
60 }
61
62 return quoted;
63}
64
65/* Determines whether to add spacing between/after each quote level:
66 * >>>foo
67 * becomes
68 * > > > foo
69 */
70static int space_quotes (STATE *s)
71{
72 /* Allow quote spacing in the pager even for OPTTEXTFLOWED,
73 * but obviously not when replying.
74 */
75 if (option (OPTTEXTFLOWED) && (s->flags & MUTT_REPLYING))
76 return 0;
77
78 return option (OPTREFLOWSPACEQUOTES);
79}
80
81/* Determines whether to add a trailing space to quotes:
82 * >>> foo
83 * as opposed to
84 * >>>foo
85 */
86static int add_quote_suffix (STATE *s, int ql)
87{
88 if (s->flags & MUTT_REPLYING)
89 return 0;
90
91 if (space_quotes (s))
92 return 0;
93
94 if (!ql && !s->prefix)
95 return 0;
96
97 /* The prefix will add its own space */
98 if (!option (OPTTEXTFLOWED) && !ql && s->prefix)
99 return 0;
100
101 return 1;
102}
103
104static size_t print_indent (int ql, STATE *s, int add_suffix)
105{
106 int i;
107 size_t wid = 0;
108
109 if (s->prefix)
110 {
111 /* use given prefix only for format=fixed replies to format=flowed,
112 * for format=flowed replies to format=flowed, use '>' indentation
113 */
114 if (option (OPTTEXTFLOWED))
115 ql++;
116 else
117 {
118 state_puts (s->prefix, s);
119 wid = mutt_strwidth (s->prefix);
120 }
121 }
122 for (i = 0; i < ql; i++)
123 {
124 state_putc ('>', s);
125 if (space_quotes (s) )
126 state_putc (' ', s);
127 }
128 if (add_suffix)
129 state_putc (' ', s);
130
131 if (space_quotes (s))
132 ql *= 2;
133
134 return ql + add_suffix + wid;
135}
136
137static void flush_par (STATE *s, flowed_state_t *fst)
138{
139 if (fst->width > 0)
140 {
141 state_putc ('\n', s);
142 fst->width = 0;
143 }
144 fst->spaces = 0;
145}
146
147/* Calculate the paragraph width based upon the current quote level. The start
148 * of a quoted line will be ">>> ", so we need to subtract the space required
149 * for the prefix from the terminal width. */
150static int quote_width (STATE *s, int ql)
151{
152 int width = mutt_window_wrap_cols (MuttIndexWindow, ReflowWrap);
153 if (option(OPTTEXTFLOWED) && (s->flags & MUTT_REPLYING))
154 {
155 /* When replying, force a wrap at FLOWED_MAX to comply with RFC3676
156 * guidelines */
157 if (width > FLOWED_MAX)
158 width = FLOWED_MAX;
159 ++ql; /* When replying, we will add an additional quote level */
160 }
161 /* adjust the paragraph width subtracting the number of prefix chars */
162 width -= space_quotes (s) ? ql*2 : ql;
163 /* When displaying (not replying), there may be a space between the prefix
164 * string and the paragraph */
165 if (add_quote_suffix (s, ql))
166 --width;
167 /* failsafe for really long quotes */
168 if (width <= 0)
169 width = FLOWED_MAX; /* arbitrary, since the line will wrap */
170 return width;
171}
172
173static void print_flowed_line (char *line, STATE *s, int ql,
174 flowed_state_t *fst, int term)
175{
176 size_t width, w, words = 0;
177 char *p;
178 char last;
179
180 if (!line || !*line)
181 {
182 /* flush current paragraph (if any) first */
183 flush_par (s, fst);
184 print_indent (ql, s, 0);
185 state_putc ('\n', s);
186 return;
187 }
188
189 width = quote_width (s, ql);
190 last = line[mutt_strlen (line) - 1];
191
192 dprint (4, (debugfile, "f=f: line [%s], width = %ld, spaces = %d\n",
193 NONULL(line), (long)width, fst->spaces));
194
195 for (p = (char *)line, words = 0; (p = strsep (&line, " ")) != NULL ; )
196 {
197 dprint(4,(debugfile,"f=f: word [%s], width: %d, remaining = [%s]\n",
198 p, fst->width, line));
199
200 /* remember number of spaces */
201 if (!*p)
202 {
203 dprint(4,(debugfile,"f=f: additional space\n"));
204 fst->spaces++;
205 continue;
206 }
207 /* there's exactly one space prior to every but the first word */
208 if (words)
209 fst->spaces++;
210
211 w = mutt_strwidth (p);
212 /* see if we need to break the line but make sure the first
213 word is put on the line regardless;
214 if for DelSp=yes only one trailing space is used, we probably
215 have a long word that we should break within (we leave that
216 up to the pager or user) */
217 if (!(!fst->spaces && fst->delsp && last != ' ') &&
218 w < width && w + fst->width + fst->spaces > width)
219 {
220 dprint(4,(debugfile,"f=f: break line at %d, %d spaces left\n",
221 fst->width, fst->spaces));
222 /* only honor trailing spaces for format=flowed replies */
223 if (option(OPTTEXTFLOWED))
224 for ( ; fst->spaces; fst->spaces--)
225 state_putc (' ', s);
226 state_putc ('\n', s);
227 fst->width = 0;
228 fst->spaces = 0;
229 words = 0;
230 }
231
232 if (!words && !fst->width)
233 fst->width = print_indent (ql, s, add_quote_suffix (s, ql));
234 fst->width += w + fst->spaces;
235 for ( ; fst->spaces; fst->spaces--)
236 state_putc (' ', s);
237 state_puts (p, s);
238 words++;
239 }
240
241 if (term)
242 flush_par (s, fst);
243}
244
245static void print_fixed_line (const char *line, STATE *s, int ql,
246 flowed_state_t *fst)
247{
248 print_indent (ql, s, add_quote_suffix (s, ql));
249 if (line && *line)
250 state_puts (line, s);
251 state_putc ('\n', s);
252
253 fst->width = 0;
254 fst->spaces = 0;
255}
256
257int rfc3676_handler (BODY * a, STATE * s)
258{
259 char *buf = NULL, *t = NULL;
260 unsigned int quotelevel = 0, newql = 0, sigsep = 0;
261 int buf_off = 0, delsp = 0, fixed = 0;
262 size_t buf_len = 0, sz = 0;
263 flowed_state_t fst;
264
265 memset (&fst, 0, sizeof (fst));
266
267 /* respect DelSp of RfC3676 only with f=f parts */
268 if ((t = (char *) mutt_get_parameter ("delsp", a->parameter)))
269 {
270 delsp = mutt_strlen (t) == 3 && ascii_strncasecmp (t, "yes", 3) == 0;
271 t = NULL;
272 fst.delsp = 1;
273 }
274
275 dprint (4, (debugfile, "f=f: DelSp: %s\n", delsp ? "yes" : "no"));
276
277 while ((buf = mutt_read_line (buf, &sz, s->fpin, NULL, 0)))
278 {
279 buf_len = mutt_strlen (buf);
280 newql = get_quote_level (buf);
281
282 /* end flowed paragraph (if we're within one) if quoting level
283 * changes (should not but can happen, see RFC 3676, sec. 4.5.)
284 */
285 if (newql != quotelevel)
286 flush_par (s, &fst);
287
288 quotelevel = newql;
289 buf_off = newql;
290
291 /* respect sender's space-stuffing by removing one leading space */
292 if (buf[buf_off] == ' ')
293 buf_off++;
294
295 /* test for signature separator */
296 sigsep = ascii_strcmp (buf + buf_off, "-- ") == 0;
297
298 /* a fixed line either has no trailing space or is the
299 * signature separator */
300 fixed = buf_len == buf_off || buf[buf_len - 1] != ' ' || sigsep;
301
302 /* print fixed-and-standalone, fixed-and-empty and sigsep lines as
303 * fixed lines */
304 if ((fixed && (!fst.width || !buf_len)) || sigsep)
305 {
306 /* if we're within a flowed paragraph, terminate it */
307 flush_par (s, &fst);
308 print_fixed_line (buf + buf_off, s, quotelevel, &fst);
309 continue;
310 }
311
312 /* for DelSp=yes, we need to strip one SP prior to CRLF on flowed lines */
313 if (delsp && !fixed)
314 buf[--buf_len] = '\0';
315
316 print_flowed_line (buf + buf_off, s, quotelevel, &fst, fixed);
317 }
318
319 flush_par (s, &fst);
320
321 FREE (&buf);
322 return (0);
323}
324
325/*
326 * This routine does RfC3676 space stuffing since it's a MUST.
327 * Space stuffing means that we have to add leading spaces to
328 * certain lines:
329 * - lines starting with a space
330 * - lines starting with 'From '
331 *
332 * Care is taken to preserve the hdr->content->filename, as
333 * mutt -i -E can directly edit a passed in filename.
334 */
335static void rfc3676_space_stuff (HEADER* hdr, int unstuff)
336{
337 FILE *in = NULL, *out = NULL;
338 char *buf = NULL;
339 size_t blen = 0;
340 BUFFER *tmpfile = NULL;
341
342 tmpfile = mutt_buffer_pool_get ();
343
344 if ((in = safe_fopen (hdr->content->filename, "r")) == NULL)
345 goto bail;
346
347 mutt_buffer_mktemp (tmpfile);
348 if ((out = safe_fopen (mutt_b2s (tmpfile), "w+")) == NULL)
349 goto bail;
350
351 while ((buf = mutt_read_line (buf, &blen, in, NULL, 0)) != NULL)
352 {
353 if (unstuff)
354 {
355 if (buf[0] == ' ')
356 fputs (buf + 1, out);
357 else
358 fputs (buf, out);
359 }
360 else
361 {
362 if (ascii_strncmp ("From ", buf, 5) == 0 || buf[0] == ' ')
363 fputc (' ', out);
364 fputs (buf, out);
365 }
366 fputc ('\n', out);
367 }
368 FREE (&buf);
369 safe_fclose (&in);
370 safe_fclose (&out);
371 mutt_set_mtime (hdr->content->filename, mutt_b2s (tmpfile));
372
373 if ((in = safe_fopen (mutt_b2s (tmpfile), "r")) == NULL)
374 goto bail;
375
376 if ((truncate (hdr->content->filename, 0) == -1) ||
377 ((out = safe_fopen (hdr->content->filename, "a")) == NULL))
378 {
379 mutt_perror (hdr->content->filename);
380 goto bail;
381 }
382
383 mutt_copy_stream (in, out);
384 safe_fclose (&in);
385 safe_fclose (&out);
386 mutt_set_mtime (mutt_b2s (tmpfile), hdr->content->filename);
387 unlink (mutt_b2s (tmpfile));
388 mutt_buffer_pool_release (&tmpfile);
389 return;
390
391bail:
392 safe_fclose (&in);
393 safe_fclose (&out);
394 mutt_buffer_pool_release (&tmpfile);
395}
396
397/* Note: we don't check the option OPTTEXTFLOWED because we want to
398 * stuff based the actual content type. The option only decides
399 * whether to *set* format=flowed on new messages.
400 */
401void mutt_rfc3676_space_stuff (HEADER *hdr)
402{
403 const char *format;
404
405 if (!hdr || !hdr->content || !hdr->content->filename)
406 return;
407
408 if (hdr->content->type == TYPETEXT &&
409 !ascii_strcasecmp ("plain", hdr->content->subtype))
410 {
411 format = mutt_get_parameter ("format", hdr->content->parameter);
412 if (!ascii_strcasecmp ("flowed", format))
413 rfc3676_space_stuff (hdr, 0);
414 }
415}
416
417void mutt_rfc3676_space_unstuff (HEADER *hdr)
418{
419 const char *format;
420
421 if (!hdr || !hdr->content || !hdr->content->filename)
422 return;
423
424 if (hdr->content->type == TYPETEXT &&
425 !ascii_strcasecmp ("plain", hdr->content->subtype))
426 {
427 format = mutt_get_parameter ("format", hdr->content->parameter);
428 if (!ascii_strcasecmp ("flowed", format))
429 rfc3676_space_stuff (hdr, 1);
430 }
431}