File size: 9,642 Bytes
0220cd3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
//! # format=flowed support.
//!
//! Format=flowed is defined in
//! [RFC 3676](https://tools.ietf.org/html/rfc3676).
//!
//! Older [RFC 2646](https://tools.ietf.org/html/rfc2646) is used
//! during formatting, i.e., DelSp parameter introduced in RFC 3676
//! is assumed to be set to "no".
//!
//! For received messages, DelSp parameter is honoured.
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]

/// Wraps line to 72 characters using format=flowed soft breaks.
///
/// 72 characters is the limit recommended by RFC 3676.
///
/// The function breaks line only after SP and before non-whitespace
/// characters. It also does not insert breaks before ">" to avoid the
/// need to do space stuffing (see RFC 3676) for quotes.
///
/// If there are long words, line may still exceed the limits on line
/// length. However, this should be rare and should not result in
/// immediate mail rejection: SMTP (RFC 2821) limit is 998 characters,
/// and Spam Assassin limit is 78 characters.
fn format_line_flowed(line: &str, prefix: &str) -> String {
    let mut result = String::new();
    let mut buffer = prefix.to_string();
    let mut after_space = prefix.ends_with(' ');

    for c in line.chars() {
        if c == ' ' {
            if buffer.is_empty() {
                // Space stuffing, see RFC 3676
                buffer.push(' ');
            }
            buffer.push(c);
            after_space = true;
        } else if c == '>' {
            if buffer.is_empty() {
                // Space stuffing, see RFC 3676
                buffer.push(' ');
            }
            buffer.push(c);
            after_space = false;
        } else {
            if after_space && buffer.len() >= 72 && !c.is_whitespace() {
                // Flush the buffer and insert soft break (SP CRLF).
                result += &buffer;
                result += "\r\n";
                buffer = prefix.to_string();
            }
            buffer.push(c);
            after_space = false;
        }
    }
    result + &buffer
}

/// Returns text formatted according to RFC 3676 (format=flowed).
///
/// This function accepts text separated by LF, but returns text
/// separated by CRLF.
///
/// RFC 2646 technique is used to insert soft line breaks, so DelSp
/// SHOULD be set to "no" when sending.
pub fn format_flowed(text: &str) -> String {
    let mut result = String::new();

    for line in text.split('\n') {
        if !result.is_empty() {
            result += "\r\n";
        }

        let line = line.trim_end();
        let quote_depth = line.chars().take_while(|&c| c == '>').count();
        let (prefix, mut line) = line.split_at(quote_depth);

        let mut prefix = prefix.to_string();

        if quote_depth > 0 {
            if let Some(s) = line.strip_prefix(' ') {
                line = s;
                prefix += " ";
            }
        }

        result += &format_line_flowed(line, &prefix);
    }

    result
}

/// Same as format_flowed(), but adds "> " prefix to each line.
pub fn format_flowed_quote(text: &str) -> String {
    let mut result = String::new();

    for line in text.split('\n') {
        if !result.is_empty() {
            result += "\n";
        }
        result += "> ";
        result += line;
    }

    format_flowed(&result)
}

/// Joins lines in format=flowed text.
///
/// Lines must be separated by single LF.
///
/// Signature separator line is not processed here, it is assumed to
/// be stripped beforehand.
pub fn unformat_flowed(text: &str, delsp: bool) -> String {
    let mut result = String::new();
    let mut skip_newline = true;

    for line in text.split('\n') {
        let line = if !result.is_empty() && skip_newline {
            line.trim_start_matches('>')
        } else {
            line
        };

        // Revert space-stuffing
        let line = line.strip_prefix(' ').unwrap_or(line);

        if !skip_newline {
            result.push('\n');
        }

        if let Some(line) = line.strip_suffix(' ') {
            // Flowed line
            result += line;
            if !delsp {
                result.push(' ');
            }
            skip_newline = true;
        } else {
            // Fixed line
            result += line;
            skip_newline = false;
        }
    }
    result
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_format_flowed() {
        let text = "";
        assert_eq!(format_flowed(text), "");

        let text = "Foo bar baz";
        assert_eq!(format_flowed(text), text);

        let text = ">Foo bar";
        assert_eq!(format_flowed(text), text);

        let text = "> Foo bar";
        assert_eq!(format_flowed(text), text);

        let text = ">\n\nA";
        assert_eq!(format_flowed(text), ">\r\n\r\nA");

        let text = "This is the Autocrypt Setup Message used to transfer your key between clients.\n\
                    \n\
                    To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device.";
        let expected = "This is the Autocrypt Setup Message used to transfer your key between clients.\r\n\
                        \r\n\
                        To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
                        client and enter the setup code presented on the generating device.";
        assert_eq!(format_flowed(text), expected);

        let text = "> A quote";
        assert_eq!(format_flowed(text), "> A quote");

        let text = "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A";
        assert_eq!(
            format_flowed(text),
            "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > \r\n> A"
        );

        // Test space stuffing of wrapped lines
        let text = "> This is the Autocrypt Setup Message used to transfer your key between clients.\n\
                    >                               \n\
                    > To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device.";
        let expected = "> This is the Autocrypt Setup Message used to transfer your key between \r\n\
                        > clients.\r\n\
                        >\r\n\
                        > To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
                        > client and enter the setup code presented on the generating device.";
        assert_eq!(format_flowed(text), expected);

        let text = ">> This is the Autocrypt Setup Message used to transfer your key between clients.\n\
                    >>                               \n\
                    >> To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device.";
        let expected = ">> This is the Autocrypt Setup Message used to transfer your key between \r\n\
                        >> clients.\r\n\
                        >>\r\n\
                        >> To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\
                        >> client and enter the setup code presented on the generating device.";
        assert_eq!(format_flowed(text), expected);

        // Test space stuffing of spaces.
        let text = " Foo bar baz";
        assert_eq!(format_flowed(text), "  Foo bar baz");

        let text = "   Foo bar baz";
        assert_eq!(format_flowed(text), "    Foo bar baz");

        let text =
            "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAA";
        let expected =
            "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \r\nAAAAAA";
        assert_eq!(format_flowed(text), expected);
    }

    #[test]
    fn test_unformat_flowed() {
        let text = "this is a very long message that should be wrapped using format=flowed and \n\
            unwrapped on the receiver";
        let expected =
            "this is a very long message that should be wrapped using format=flowed and \
                        unwrapped on the receiver";
        assert_eq!(unformat_flowed(text, false), expected);

        let text = "  Foo bar";
        let expected = " Foo bar";
        assert_eq!(unformat_flowed(text, false), expected);

        let text =
            "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > \n> A";
        let expected =
            "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A";
        assert_eq!(unformat_flowed(text, false), expected);
    }

    #[test]
    fn test_format_flowed_quote() {
        let quote = "this is a quoted line";
        let expected = "> this is a quoted line";
        assert_eq!(format_flowed_quote(quote), expected);

        let quote = "first quoted line\nsecond quoted line";
        let expected = "> first quoted line\r\n> second quoted line";
        assert_eq!(format_flowed_quote(quote), expected);

        let quote = "> foo bar baz";
        let expected = "> > foo bar baz";
        assert_eq!(format_flowed_quote(quote), expected);

        let quote = "this is a very long quote that should be wrapped using format=flowed and unwrapped on the receiver";
        let expected =
            "> this is a very long quote that should be wrapped using format=flowed and \r\n\
            > unwrapped on the receiver";
        assert_eq!(format_flowed_quote(quote), expected);
    }
}