1/*
2 * Copyright © 2016 Software Freedom Conservancy Inc.
3 * Copyright © 2020-2021 Michael Gratton <mike@vee.net>
4 *
5 * This software is licensed under the GNU Lesser General Public License
6 * (version 2.1 or later).  See the COPYING file in this distribution.
7 */
8
9/**
10 * A base interface for objects that represent decoded RFC822 headers.
11 *
12 * The value of these objects is the decoded form of the header
13 * data. Encoded forms can be obtained via {@link to_rfc822_string}.
14 */
15public interface Geary.RFC822.DecodedMessageData :
16    Geary.MessageData.AbstractMessageData {
17
18    /** Returns an RFC822-safe string representation of the data. */
19    public abstract string to_rfc822_string();
20
21}
22
23/**
24 * A base interface for objects that represent encoded RFC822 header data.
25 *
26 * The value of these objects is the RFC822 encoded form of the header
27 * data. Decoded forms can be obtained via means specific to
28 * implementations of this interface.
29 */
30public interface Geary.RFC822.EncodedMessageData :
31    Geary.MessageData.BlockMessageData {
32
33}
34
35/**
36 * A RFC822 Message-ID.
37 *
38 * The decoded form of the id is the `addr-spec` portion, that is,
39 * without the leading `<` and tailing `>`.
40 */
41public class Geary.RFC822.MessageID :
42    Geary.MessageData.StringMessageData, DecodedMessageData {
43
44    public MessageID(string value) {
45        base(value);
46    }
47
48    public MessageID.from_rfc822_string(string rfc822) throws Error {
49        int len = rfc822.length;
50        int start = 0;
51        while (start < len && rfc822[start].isspace()) {
52            start += 1;
53        }
54        char end_delim = 0;
55        bool break_on_space = false;
56        if (start < len) {
57            switch (rfc822[start]) {
58            case '<':
59                // Standard delim
60                start += 1;
61                end_delim = '>';
62                break;
63
64            case '(':
65                // Non-standard delim
66                start += 1;
67                end_delim = ')';
68                break;
69
70            default:
71                // no other supported delimiters, so just end at white
72                // space or EOS
73                break_on_space = true;
74                break;
75            }
76        }
77        int end = start + 1;
78        while (end < len &&
79               rfc822[end] != end_delim &&
80               (!break_on_space || !rfc822[end].isspace())) {
81            end += 1;
82        }
83
84        if (start + 1 >= end) {
85            throw new Error.INVALID("Empty RFC822 message id");
86        }
87        base(rfc822.slice(start, end));
88    }
89
90    /**
91     * Returns the {@link Date} in RFC 822 format.
92     */
93    public string to_rfc822_string() {
94        return "<%s>".printf(this.value);
95    }
96
97}
98
99
100/**
101 * A immutable list of RFC822 Message-ID values.
102 */
103public class Geary.RFC822.MessageIDList :
104    Geary.MessageData.AbstractMessageData,
105    DecodedMessageData {
106
107
108    /** Returns the number of ids in this list. */
109    public int size {
110        get { return this.list.size; }
111    }
112
113    /** Determines if there are no ids in the list. */
114    public bool is_empty {
115        get { return this.list.is_empty; }
116    }
117
118    private Gee.List<MessageID> list = new Gee.ArrayList<MessageID>();
119
120
121    /**
122     * Constructs a new Message-Id list.
123     *
124     * If the optional collection of ids is not given, the list
125     * is created empty. Otherwise the collection's ids are
126     * added to the list by iterating over it in natural order.
127     */
128    public MessageIDList(Gee.Collection<MessageID>? collection = null) {
129        if (collection != null) {
130            this.list.add_all(collection);
131        }
132    }
133
134    /** Constructs a new Message-Id list containing a single id. */
135    public MessageIDList.single(MessageID msg_id){
136        this();
137        list.add(msg_id);
138    }
139
140    /** Constructs a new Message-Id list by parsing a RFC822 string. */
141    public MessageIDList.from_rfc822_string(string rfc822)
142        throws Error {
143        this();
144
145        // Have seen some mailers use commas between Message-IDs and whitespace inside Message-IDs,
146        // meaning that the standard whitespace tokenizer is not sufficient.  The only guarantee
147        // made of a Message-ID is that it's surrounded by angle brackets, so save anything inside
148        // angle brackets
149        //
150        // NOTE: Seen at least one spamfilter mailer that imaginatively uses parens instead of
151        // angle brackets for its Message-IDs; accounting for that as well here.  The addt'l logic
152        // is to allow open-parens inside a Message-ID and not treat it as a delimiter; if a
153        // close-parens is found, that's a problem (but isn't expected)
154        //
155        // Also note that this parser will attempt to parse Message-IDs lacking brackets.  If one
156        // is found, then it will assume all remaining Message-IDs in the list are bracketed and
157        // be a little less liberal in its parsing.
158        StringBuilder canonicalized = new StringBuilder();
159        int index = 0;
160        char ch;
161        bool in_message_id = false;
162        bool bracketed = false;
163        while (Ascii.get_next_char(rfc822, ref index, out ch)) {
164            bool add_char = false;
165            switch (ch) {
166                case '<':
167                    in_message_id = true;
168                    bracketed = true;
169                break;
170
171                case '(':
172                    if (!in_message_id) {
173                        in_message_id = true;
174                        bracketed = true;
175                    } else {
176                        add_char = true;
177                    }
178                break;
179
180                case '>':
181                    in_message_id = false;
182                break;
183
184                case ')':
185                    if (in_message_id)
186                        in_message_id = false;
187                    else
188                        add_char = true;
189                break;
190
191                default:
192                    // deal with Message-IDs without brackets ... bracketed is set to true the
193                    // moment the first one is found, so this doesn't deal with combinations of
194                    // bracketed and unbracketed text ... MessageID's ctor will deal with adding
195                    // brackets to unbracketed id's
196                    if (!bracketed) {
197                        if (!in_message_id && !ch.isspace())
198                            in_message_id = true;
199                        else if (in_message_id && ch.isspace())
200                            in_message_id = false;
201                    }
202
203                    // only add characters inside the brackets or, if not bracketed, work around
204                    add_char = in_message_id;
205                break;
206            }
207
208            if (add_char)
209                canonicalized.append_c(ch);
210
211            if (!in_message_id && !String.is_empty(canonicalized.str)) {
212                list.add(new MessageID(canonicalized.str));
213
214                canonicalized = new StringBuilder();
215            }
216        }
217
218        // pick up anything that doesn't end with brackets
219        if (!String.is_empty(canonicalized.str))
220            list.add(new MessageID(canonicalized.str));
221
222        if (this.list.is_empty) {
223            throw new Error.INVALID("Empty RFC822 message id list: %s", rfc822);
224        }
225    }
226
227    /** Returns the id at the given index, if it exists. */
228    public new MessageID? get(int index) {
229        return this.list.get(index);
230    }
231
232    /** Returns a read-only iterator of the ids in this list. */
233    public Gee.Iterator<MessageID> iterator() {
234        return this.list.read_only_view.iterator();
235    }
236
237    /** Returns a read-only collection of the ids in this list. */
238    public Gee.List<MessageID> get_all() {
239        return this.list.read_only_view;
240    }
241
242    /**
243     * Returns a list with the given id appended if not already present.
244     *
245     * This list is returned if the given id is already present,
246     * otherwise the result of a call to {@link concatenate_id} is
247     * returned.
248     */
249    public MessageIDList merge_id(MessageID other) {
250        return this.list.contains(other) ? this : this.concatenate_id(other);
251    }
252
253    /**
254     * Returns a list with the given ids appended if not already present.
255     *
256     * This list is returned if all given ids are already present,
257     * otherwise the result of a call to {@link concatenate_id} for
258     * each not present is returned.
259     */
260    public MessageIDList merge_list(MessageIDList other) {
261        var list = this;
262        foreach (var id in other) {
263            if (!this.list.contains(id)) {
264                list = list.concatenate_id(id);
265            }
266        }
267        return list;
268    }
269
270    /**
271     * Returns a new list with the given list appended to this.
272     */
273    public MessageIDList concatenate_id(MessageID other) {
274        var new_ids = new MessageIDList(this.list);
275        new_ids.list.add(other);
276        return new_ids;
277    }
278
279    /**
280     * Returns a new list with the given list appended to this.
281     */
282    public MessageIDList concatenate_list(MessageIDList others) {
283        var new_ids = new MessageIDList(this.list);
284        new_ids.list.add_all(others.list);
285        return new_ids;
286    }
287
288    public override string to_string() {
289        return "MessageIDList (%d)".printf(list.size);
290    }
291
292    public string to_rfc822_string() {
293        string[] strings = new string[list.size];
294        for (int i = 0; i < this.list.size; ++i) {
295            strings[i] = this.list[i].to_rfc822_string();
296        }
297
298        return string.joinv(" ", strings);
299    }
300
301}
302
303public class Geary.RFC822.Date :
304    Geary.MessageData.AbstractMessageData,
305    Gee.Hashable<Geary.RFC822.Date>,
306    DecodedMessageData {
307
308
309    public GLib.DateTime value { get; private set; }
310
311    private string? rfc822;
312
313
314    public Date(GLib.DateTime datetime) {
315        this.value = datetime;
316        this.rfc822 = null;
317    }
318
319    public Date.from_rfc822_string(string rfc822) throws Error {
320        var date = GMime.utils_header_decode_date(rfc822);
321        if (date == null) {
322            throw new Error.INVALID("Not ISO-8601 date: %s", rfc822);
323        }
324        this.rfc822 = rfc822;
325        this.value = date;
326    }
327
328    /**
329     * Returns the {@link Date} in RFC 822 format.
330     */
331    public string to_rfc822_string() {
332        if (this.rfc822 == null) {
333            this.rfc822 = GMime.utils_header_format_date(this.value);
334        }
335        return this.rfc822;
336    }
337
338    public virtual bool equal_to(Geary.RFC822.Date other) {
339        return this == other || this.value.equal(other.value);
340    }
341
342    public virtual uint hash() {
343        return this.value.hash();
344    }
345
346    public override string to_string() {
347        return this.value.to_string();
348    }
349
350}
351
352public class Geary.RFC822.Subject :
353    Geary.MessageData.StringMessageData,
354    Geary.MessageData.SearchableMessageData,
355    DecodedMessageData {
356
357    public const string REPLY_PREFACE = "Re:";
358    public const string FORWARD_PREFACE = "Fwd:";
359
360
361    private string rfc822;
362
363
364    public Subject(string value) {
365        base(value);
366        this.rfc822 = null;
367    }
368
369    public Subject.from_rfc822_string(string rfc822) {
370        base(Utils.decode_rfc822_text_header_value(rfc822));
371        this.rfc822 = rfc822;
372    }
373
374    /**
375     * Returns the subject line encoded for an RFC 822 message.
376     */
377    public string to_rfc822_string() {
378        if (this.rfc822 == null) {
379            this.rfc822 = GMime.utils_header_encode_text(
380                get_format_options(), this.value, null
381            );
382        }
383        return this.rfc822;
384    }
385
386    public bool is_reply() {
387        return value.down().has_prefix(REPLY_PREFACE.down());
388    }
389
390    public Subject create_reply() {
391        return is_reply() ? new Subject(value) : new Subject("%s %s".printf(REPLY_PREFACE,
392            value));
393    }
394
395    public bool is_forward() {
396        return value.down().has_prefix(FORWARD_PREFACE.down());
397    }
398
399    public Subject create_forward() {
400        return is_forward() ? new Subject(value) : new Subject("%s %s".printf(FORWARD_PREFACE,
401            value));
402    }
403
404    /**
405     * Returns the Subject: line stripped of reply and forwarding prefixes.
406     *
407     * Strips ''all'' prefixes, meaning "Re: Fwd: Soup's on!" will return "Soup's on!"
408     *
409     * Returns an empty string if the Subject: line is empty (or is empty after stripping prefixes).
410     */
411    public string strip_prefixes() {
412        string subject_base = value;
413        bool changed = false;
414        do {
415            string stripped;
416            try {
417                Regex re_regex = new Regex("^(?i:Re:\\s*)+");
418                stripped = re_regex.replace(subject_base, -1, 0, "");
419
420                Regex fwd_regex = new Regex("^(?i:Fwd:\\s*)+");
421                stripped = fwd_regex.replace(stripped, -1, 0, "");
422            } catch (RegexError e) {
423                debug("Failed to clean up subject line \"%s\": %s", value, e.message);
424
425                break;
426            }
427
428            changed = (stripped != subject_base);
429            if (changed)
430                subject_base = stripped;
431        } while (changed);
432
433        return String.reduce_whitespace(subject_base);
434    }
435
436    /**
437     * See Geary.MessageData.SearchableMessageData.
438     */
439    public string to_searchable_string() {
440        return value;
441    }
442
443}
444
445public class Geary.RFC822.Header :
446    Geary.MessageData.BlockMessageData, EncodedMessageData {
447
448
449    private GMime.HeaderList headers;
450    private string[]? names = null;
451
452
453    // The ctors for this class seem the wrong way around, but the
454    // default accepts a memory buffer and not a GMime.HeaderList to
455    // keep it consistent with other EncodedMessageData
456    // implementations.
457
458    public Header(Memory.Buffer buffer) throws Error {
459        base("RFC822.Header", buffer);
460
461        var parser = new GMime.Parser.with_stream(
462            Utils.create_stream_mem(buffer)
463        );
464        parser.set_respect_content_length(false);
465        parser.set_format(MESSAGE);
466
467        var message = parser.construct_message(null);
468        if (message == null) {
469            throw new Error.INVALID("Unable to parse RFC 822 headers");
470        }
471
472        this.headers = message.get_header_list();
473    }
474
475    public Header.from_gmime(GMime.Object gmime) {
476        base(
477            "RFC822.Header",
478            new Memory.StringBuffer(gmime.get_headers(get_format_options()))
479        );
480        this.headers = gmime.get_header_list();
481    }
482
483    public string? get_header(string name) {
484        string? value = null;
485        var header = this.headers.get_header(name);
486        if (header != null) {
487            value = header.get_value();
488        }
489        return value;
490    }
491
492    public string? get_raw_header(string name) {
493        string? value = null;
494        var header = this.headers.get_header(name);
495        if (header != null) {
496            value = header.get_raw_value();
497        }
498        return value;
499    }
500
501    public string[] get_header_names() {
502        if (this.names == null) {
503            var names = new string[this.headers.get_count()];
504            for (int i = 0; i < names.length; i++) {
505                names[i] = this.headers.get_header_at(i).get_name();
506            }
507            this.names = names;
508        }
509        return this.names;
510    }
511
512}
513
514public class Geary.RFC822.Text :
515    Geary.MessageData.BlockMessageData, EncodedMessageData {
516
517
518    private class GMimeBuffer : Memory.Buffer, Memory.UnownedBytesBuffer {
519
520
521        public override size_t allocated_size {
522            get { return (size_t) this.stream.length; }
523        }
524
525        public override size_t size {
526            get { return (size_t) this.stream.length; }
527        }
528
529        private GMime.Stream stream;
530        private GLib.Bytes buf = null;
531
532        public GMimeBuffer(GMime.Stream stream) {
533            this.stream = stream;
534        }
535
536        public override GLib.Bytes get_bytes() {
537            if (this.buf == null) {
538                this.stream.seek(0, SET);
539                uint8[] bytes = new uint8[this.stream.length()];
540                this.stream.read(bytes);
541                this.buf = new GLib.Bytes.take(bytes);
542            }
543            return this.buf;
544        }
545
546        public unowned uint8[] to_unowned_uint8_array() {
547            return get_bytes().get_data();
548        }
549
550    }
551
552    public Text(Memory.Buffer buffer) {
553        base("RFC822.Text", buffer);
554    }
555
556    public Text.from_gmime(GMime.Stream gmime) {
557        base("RFC822.Text", new GMimeBuffer(gmime));
558    }
559
560}
561
562public class Geary.RFC822.Full :
563    Geary.MessageData.BlockMessageData, EncodedMessageData {
564
565    public Full(Memory.Buffer buffer) {
566        base("RFC822.Full", buffer);
567    }
568
569}
570
571/** Represents text providing a preview of an email's body. */
572public class Geary.RFC822.PreviewText : Geary.RFC822.Text {
573
574    public PreviewText(Memory.Buffer _buffer) {
575        base (_buffer);
576    }
577
578    public PreviewText.with_header(Memory.Buffer preview_header, Memory.Buffer preview) {
579        string preview_text = "";
580
581        // Parse the header.
582        GMime.Stream header_stream = Utils.create_stream_mem(preview_header);
583        GMime.Parser parser = new GMime.Parser.with_stream(header_stream);
584        GMime.Part? gpart = parser.construct_part(Geary.RFC822.get_parser_options()) as GMime.Part;
585        if (gpart != null) {
586            Part part = new Part(gpart);
587
588            Mime.ContentType content_type = part.content_type;
589            bool is_plain = content_type.is_type("text", "plain");
590            bool is_html = content_type.is_type("text", "html");
591
592            if (is_plain || is_html) {
593                // Parse the partial body
594                GMime.DataWrapper body = new GMime.DataWrapper.with_stream(
595                    new GMime.StreamMem.with_buffer(preview.get_uint8_array()),
596                    gpart.get_content_encoding()
597                );
598                gpart.set_content(body);
599
600                try {
601                    Memory.Buffer preview_buffer = part.write_to_buffer(
602                        Part.EncodingConversion.UTF8
603                    );
604                    preview_text = Geary.RFC822.Utils.to_preview_text(
605                        preview_buffer.get_valid_utf8(),
606                        is_html ? TextFormat.HTML : TextFormat.PLAIN
607                    );
608                } catch (Error err) {
609                    debug("Failed to parse preview body: %s", err.message);
610                }
611            }
612        }
613
614        base(new Geary.Memory.StringBuffer(preview_text));
615    }
616
617    public PreviewText.from_string(string preview) {
618        base (new Geary.Memory.StringBuffer(preview));
619    }
620
621}
622