1/*
2 * Copyright 2016 Software Freedom Conservancy Inc.
3 * Copyright 2018 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 * An Email represents a single RFC 822 style email message.
11 *
12 * This class provides a common abstraction over different
13 * representations of email messages, allowing messages from different
14 * mail systems, from both local and remote sources, and locally
15 * composed email messages to all be represented by a single
16 * object. While this object represents a RFC 822 message, it also
17 * holds additional metadata about an email not specified by that
18 * format, such as its unique {@link id}, and unread state and other
19 * {@link email_flags}.
20 *
21 * Email objects may by constructed in many ways, but are usually
22 * obtained via a {@link Folder}. Email objects may be partial
23 * representations of messages, in cases where a remote message has
24 * not been fully downloaded, or a local message not fully loaded from
25 * a database. This can be checked via an email's {@link fields}
26 * property, and if the currently loaded fields are not sufficient,
27 * then additional fields can be loaded via a folder.
28 */
29public class Geary.Email : BaseObject, EmailHeaderSet {
30
31    /**
32     * The maximum expected length of message body preview text.
33     */
34    public const int MAX_PREVIEW_BYTES = 256;
35
36    /**
37     * Indicates email fields that may change over time.
38     *
39     * The mutable fields are: FLAGS -- since these change as for
40     * example messages are marked as read, and PREVIEW -- since the
41     * preview is updated when the full message body is
42     * available. All others never change once stored in the
43     * database.
44     */
45    public const Field MUTABLE_FIELDS = (
46        Geary.Email.Field.FLAGS | Geary.Email.Field.PREVIEW
47    );
48
49    /**
50     * Indicates the email fields required to build an RFC822.Message.
51     *
52     * @see get_message
53     */
54    public const Field REQUIRED_FOR_MESSAGE = (
55        Geary.Email.Field.HEADER | Geary.Email.Field.BODY
56    );
57
58    /**
59     * Specifies specific parts of an email message.
60     *
61     * See the {@link Email.fields} property to determine which parts
62     * an email object currently contains.
63     */
64    public enum Field {
65        // THESE VALUES ARE PERSISTED.  Change them only if you know what you're doing.
66
67        /** Denotes no fields. */
68        NONE =              0,
69
70        /** The RFC 822 Date header. */
71        DATE =              1 << 0,
72
73        /** The RFC 822 From, Sender, and Reply-To headers. */
74        ORIGINATORS =       1 << 1,
75
76        /** The RFC 822 To, Cc, and Bcc headers. */
77        RECEIVERS =         1 << 2,
78
79        /** The RFC 822 Message-Id, In-Reply-To, and References headers. */
80        REFERENCES =        1 << 3,
81
82        /** The RFC 822 Subject header. */
83        SUBJECT =           1 << 4,
84
85        /** The list of all RFC 822 headers. */
86        HEADER =            1 << 5,
87
88        /** The RFC 822 message body and attachments. */
89        BODY =              1 << 6,
90
91        /** The {@link Email.properties} object. */
92        PROPERTIES =        1 << 7,
93
94        /** The plain text preview. */
95        PREVIEW =           1 << 8,
96
97        /** The {@link Email.email_flags} object. */
98        FLAGS =             1 << 9,
99
100        /**
101         * The union of the primary headers of a message.
102         *
103         * The envelope includes the {@link DATE}, {@link
104         * ORIGINATORS}, {@link RECEIVERS}, {@link REFERENCES}, and
105         * {@link SUBJECT} fields.
106         */
107        ENVELOPE = DATE | ORIGINATORS | RECEIVERS | REFERENCES | SUBJECT,
108
109        /** The union of all email fields. */
110        ALL =      DATE | ORIGINATORS | RECEIVERS | REFERENCES | SUBJECT |
111                   HEADER | BODY | PROPERTIES | PREVIEW | FLAGS;
112
113        public static Field[] all() {
114            return {
115                DATE,
116                ORIGINATORS,
117                RECEIVERS,
118                REFERENCES,
119                SUBJECT,
120                HEADER,
121                BODY,
122                PROPERTIES,
123                PREVIEW,
124                FLAGS
125            };
126        }
127
128        public inline bool is_all_set(Field required_fields) {
129            return (this & required_fields) == required_fields;
130        }
131
132        public inline bool is_any_set(Field required_fields) {
133            return (this & required_fields) != 0;
134        }
135
136        public inline Field set(Field field) {
137            return (this | field);
138        }
139
140        public inline Field clear(Field field) {
141            return (this & ~(field));
142        }
143
144        public inline bool fulfills(Field required_fields) {
145            return is_all_set(required_fields);
146        }
147
148        public inline bool fulfills_any(Field required_fields) {
149            return is_any_set(required_fields);
150        }
151
152        public inline bool require(Field required_fields) {
153            return is_all_set(required_fields);
154        }
155
156        public inline bool requires_any(Field required_fields) {
157            return is_any_set(required_fields);
158        }
159
160        public string to_string() {
161            string value = "NONE";
162            if (this == ALL) {
163                value = "ALL";
164            } else if (this > 0) {
165                StringBuilder builder = new StringBuilder();
166                foreach (Field f in all()) {
167                    if (is_all_set(f)) {
168                        if (!String.is_empty(builder.str)) {
169                            builder.append(",");
170                        }
171                        builder.append(
172                            ObjectUtils.to_enum_nick(typeof(Field), f).up()
173                        );
174                    }
175                }
176                value = builder.str;
177            }
178            return value;
179        }
180    }
181
182    /**
183     * A unique identifier for the Email in the Folder.
184     *
185     * This is is guaranteed to be unique for as long as the Folder is
186     * open. Once closed, guarantees are no longer made.
187     *
188     * This field is always returned, no matter what Fields are used
189     * to retrieve the Email.
190     */
191    public Geary.EmailIdentifier id { get; private set; }
192
193    /**
194     * {@inheritDoc}
195     *
196     * Value will be valid if {@link Field.ORIGINATORS} is set.
197     */
198    public RFC822.MailboxAddresses? from { get { return this._from; } }
199    private RFC822.MailboxAddresses? _from  = null;
200
201    /**
202     * {@inheritDoc}
203     *
204     * Value will be valid if {@link Field.ORIGINATORS} is set.
205     */
206    public RFC822.MailboxAddress? sender { get { return this._sender; } }
207    private RFC822.MailboxAddress? _sender = null;
208
209    /**
210     * {@inheritDoc}
211     *
212     * Value will be valid if {@link Field.ORIGINATORS} is set.
213     */
214    public RFC822.MailboxAddresses? reply_to { get { return this._reply_to; } }
215    private RFC822.MailboxAddresses? _reply_to = null;
216
217    /**
218     * {@inheritDoc}
219     *
220     * Value will be valid if {@link Field.RECEIVERS} is set.
221     */
222    public RFC822.MailboxAddresses? to { get { return this._to; } }
223    private RFC822.MailboxAddresses? _to = null;
224
225    /**
226     * {@inheritDoc}
227     *
228     * Value will be valid if {@link Field.RECEIVERS} is set.
229     */
230    public RFC822.MailboxAddresses? cc { get { return this._cc; } }
231    private RFC822.MailboxAddresses? _cc = null;
232
233    /**
234     * {@inheritDoc}
235     *
236     * Value will be valid if {@link Field.RECEIVERS} is set.
237     */
238    public RFC822.MailboxAddresses? bcc { get { return this._bcc; } }
239    private RFC822.MailboxAddresses? _bcc = null;
240
241    /**
242     * {@inheritDoc}
243     *
244     * Value will be valid if {@link Field.REFERENCES} is set.
245     */
246    public RFC822.MessageID? message_id { get { return this._message_id; } }
247    private RFC822.MessageID? _message_id = null;
248
249    /**
250     * {@inheritDoc}
251     *
252     * Value will be valid if {@link Field.REFERENCES} is set.
253     */
254    public RFC822.MessageIDList? in_reply_to { get { return this._in_reply_to; } }
255    private RFC822.MessageIDList? _in_reply_to = null;
256
257    /**
258     * {@inheritDoc}
259     *
260     * Value will be valid if {@link Field.REFERENCES} is set.
261     */
262    public RFC822.MessageIDList? references { get { return this._references; } }
263    private RFC822.MessageIDList? _references = null;
264
265    /**
266     * {@inheritDoc}
267     *
268     * Value will be valid if {@link Field.SUBJECT} is set.
269     */
270    public RFC822.Subject? subject { get { return this._subject; } }
271    private RFC822.Subject? _subject = null;
272
273    /**
274     * {@inheritDoc}
275     *
276     * Value will be valid if {@link Field.DATE} is set.
277     */
278    public RFC822.Date? date { get { return this._date; } }
279    private RFC822.Date? _date = null;
280
281    /**
282     * {@inheritDoc}
283     *
284     * Value will be valid if {@link Field.HEADER} is set.
285     */
286    public RFC822.Header? header { get; protected set; default = null; }
287
288    /**
289     * The complete RFC 822 message body.
290     *
291     * Value will be valid if {@link Field.BODY} is set.
292     */
293    public RFC822.Text? body { get; private set; default = null; }
294
295    /**
296     * MIME multipart body parts.
297     *
298     * Value will be valid if {@link Field.BODY} is set.
299     */
300    public Gee.List<Geary.Attachment> attachments { get; private set;
301        default = new Gee.ArrayList<Geary.Attachment>(); }
302
303    /**
304     * A plain text prefix of the email's message body.
305     *
306     * Value will be valid if {@link Field.PREVIEW} is set.
307     */
308    public RFC822.PreviewText? preview { get; private set; default = null; }
309
310    /**
311     * Set of immutable properties for the email.
312     *
313     * Value will be valid if {@link Field.PROPERTIES} is set.
314     */
315    public Geary.EmailProperties? properties { get; private set; default = null; }
316
317    /**
318     * Set of mutable flags for the email.
319     *
320     * Value will be valid if {@link Field.FLAGS} is set.
321     */
322    public Geary.EmailFlags? email_flags { get; private set; default = null; }
323
324    /**
325     * Specifies the properties that have been populated for this email.
326     *
327     * Since this email object may be a partial representation of a
328     * complete email message, this property lists all parts of the
329     * object that have actually been loaded, as opposed to parts that
330     * are simply missing from the email it represents.
331     *
332     * For example, if this property includes the {@link
333     * Field.SUBJECT} flag, then the {@link subject} property has been
334     * set to reflect the Subject header of the message. Of course,
335     * the subject may then still may be null or empty, if the email
336     * did not specify a subject header.
337     */
338    public Geary.Email.Field fields { get; private set; default = Field.NONE; }
339
340
341    private Geary.RFC822.Message? message = null;
342
343
344    /** Constructs a new, empty email with the given id. */
345    public Email(Geary.EmailIdentifier id) {
346        this.id = id;
347    }
348
349    /**
350     * Construct a Geary.Email from a complete RFC822 message.
351     */
352    public Email.from_message(EmailIdentifier id,
353                              RFC822.Message message) throws GLib.Error {
354        this(id);
355        set_send_date(message.date);
356        set_originators(message.from, message.sender, message.reply_to);
357        set_receivers(message.to, message.cc, message.bcc);
358        set_full_references(
359            message.message_id, message.in_reply_to, message.references
360        );
361        set_message_subject(message.subject);
362        set_message_header(message.get_header());
363        set_message_body(message.get_body());
364        string preview = message.get_preview();
365        if (!String.is_empty_or_whitespace(preview)) {
366            set_message_preview(new RFC822.PreviewText.from_string(preview));
367        }
368
369        // Set this last as the methods above would reset it otherwise
370        this.message = message;
371    }
372
373    /**
374     * Determines if this message is unread from its flags.
375     *
376     * If {@link email_flags} is not null, returns the value of {@link
377     * EmailFlags.is_unread}, otherwise returns {@link
378     * Trillian.UNKNOWN}.
379     */
380    public inline Trillian is_unread() {
381        return email_flags != null ? Trillian.from_boolean(email_flags.is_unread()) : Trillian.UNKNOWN;
382    }
383
384    /**
385     * Determines if this message is flagged from its flags.
386     *
387     * If {@link email_flags} is not null, returns the value of {@link
388     * EmailFlags.is_flagged}, otherwise returns {@link
389     * Trillian.UNKNOWN}.
390     */
391    public inline Trillian is_flagged() {
392        return email_flags != null ? Trillian.from_boolean(email_flags.is_flagged()) : Trillian.UNKNOWN;
393    }
394
395    /**
396     * Determines if this message is flagged from its flags.
397     *
398     * If {@link email_flags} is not null, returns the value of {@link
399     * EmailFlags.load_remote_images}, otherwise returns {@link
400     * Trillian.UNKNOWN}.
401     */
402    public inline Trillian load_remote_images() {
403        return email_flags != null ? Trillian.from_boolean(email_flags.load_remote_images()) : Trillian.UNKNOWN;
404    }
405
406    public void set_send_date(RFC822.Date? date) {
407        this._date = date;
408
409        this.message = null;
410        this.fields |= Field.DATE;
411    }
412
413    /**
414     * Sets the RFC822 originators for the message.
415     *
416     * RFC 2822 requires at least one From address, that the Sender
417     * and From not be identical, and that both From and ReplyTo are
418     * optional.
419     */
420    public void set_originators(RFC822.MailboxAddresses? from,
421                                RFC822.MailboxAddress? sender,
422                                RFC822.MailboxAddresses? reply_to)
423        throws Error {
424        // XXX Should be throwing an error here if from is empty or
425        // sender is same as from
426        this._from = from;
427        this._sender = sender;
428        this._reply_to = reply_to;
429
430        this.message = null;
431        this.fields |= Field.ORIGINATORS;
432    }
433
434    public void set_receivers(RFC822.MailboxAddresses? to,
435                              RFC822.MailboxAddresses? cc,
436                              RFC822.MailboxAddresses? bcc) {
437        this._to = to;
438        this._cc = cc;
439        this._bcc = bcc;
440
441        this.message = null;
442        this.fields |= Field.RECEIVERS;
443    }
444
445    public void set_full_references(RFC822.MessageID? message_id,
446                                    RFC822.MessageIDList? in_reply_to,
447        Geary.RFC822.MessageIDList? references) {
448        this._message_id = message_id;
449        this._in_reply_to = in_reply_to;
450        this._references = references;
451
452        this.message = null;
453        this.fields |= Field.REFERENCES;
454    }
455
456    public void set_message_subject(Geary.RFC822.Subject? subject) {
457        this._subject = subject;
458
459        this.message = null;
460        this.fields |= Field.SUBJECT;
461    }
462
463    public void set_message_header(Geary.RFC822.Header header) {
464        this.header = header;
465
466        this.message = null;
467        fields |= Field.HEADER;
468    }
469
470    public void set_message_body(Geary.RFC822.Text body) {
471        this.body = body;
472
473        this.message = null;
474        fields |= Field.BODY;
475    }
476
477    public void set_email_properties(Geary.EmailProperties properties) {
478        this.properties = properties;
479
480        fields |= Field.PROPERTIES;
481    }
482
483    public void set_message_preview(Geary.RFC822.PreviewText preview) {
484        this.preview = preview;
485
486        fields |= Field.PREVIEW;
487    }
488
489    public void set_flags(Geary.EmailFlags email_flags) {
490        this.email_flags = email_flags;
491
492        fields |= Field.FLAGS;
493    }
494
495    public void add_attachment(Geary.Attachment attachment) {
496        attachments.add(attachment);
497    }
498
499    public void add_attachments(Gee.Collection<Geary.Attachment> attachments) {
500        this.attachments.add_all(attachments);
501    }
502
503    public string get_searchable_attachment_list() {
504        StringBuilder search = new StringBuilder();
505        foreach (Geary.Attachment attachment in attachments) {
506            if (attachment.has_content_filename) {
507                search.append(attachment.content_filename);
508                search.append("\n");
509            }
510        }
511        return search.str;
512    }
513
514    /**
515     * Constructs a new RFC 822 message from this email.
516     *
517     * This method requires the {@link REQUIRED_FOR_MESSAGE} fields be
518     * present. If not, {@link EngineError.INCOMPLETE_MESSAGE} is
519     * thrown.
520     */
521    public Geary.RFC822.Message get_message() throws EngineError, Error {
522        if (this.message == null) {
523            if (!fields.fulfills(REQUIRED_FOR_MESSAGE)) {
524                throw new EngineError.INCOMPLETE_MESSAGE(
525                    "Parsed email requires HEADER and BODY"
526                );
527            }
528            this.message = new Geary.RFC822.Message.from_parts(header, body);
529        }
530        return this.message;
531    }
532
533    /**
534     * Returns the attachment with the given MIME Content ID.
535     *
536     * Requires the REQUIRED_FOR_MESSAGE fields be present; else
537     * EngineError.INCOMPLETE_MESSAGE is thrown.
538     */
539    public Geary.Attachment? get_attachment_by_content_id(string cid)
540    throws EngineError {
541        if (!fields.fulfills(REQUIRED_FOR_MESSAGE))
542            throw new EngineError.INCOMPLETE_MESSAGE("Parsed email requires HEADER and BODY");
543
544        foreach (Geary.Attachment attachment in attachments) {
545            if (attachment.content_id == cid) {
546                return attachment;
547            }
548        }
549        return null;
550    }
551
552    /**
553     * Returns a list of this email's ancestry by Message-ID.  IDs are not returned in any
554     * particular order.  The ancestry is made up from this email's Message-ID, its References,
555     * and its In-Reply-To.  Thus, this email must have been fetched with Field.REFERENCES for
556     * this method to return a complete list.
557     */
558    public Gee.Set<RFC822.MessageID>? get_ancestors() {
559        Gee.Set<RFC822.MessageID> ancestors = new Gee.HashSet<RFC822.MessageID>();
560
561        // the email's Message-ID counts as its lineage
562        if (message_id != null)
563            ancestors.add(message_id);
564
565        // References list the email trail back to its source
566        if (references != null)
567            ancestors.add_all(references.get_all());
568
569        // RFC822 requires the In-Reply-To Message-ID be prepended to the References list, but
570        // this ensures that's the case
571        if (in_reply_to != null)
572           ancestors.add_all(in_reply_to.get_all());
573
574       return (ancestors.size > 0) ? ancestors : null;
575    }
576
577    public string get_preview_as_string() {
578        return (preview != null) ? preview.buffer.to_string() : "";
579    }
580
581    public string to_string() {
582        return "[%s] ".printf(id.to_string());
583    }
584
585    /**
586     * Converts a Collection of {@link Email}s to a Map of Emails keyed by {@link EmailIdentifier}s.
587     *
588     * @return null if emails is empty or null.
589     */
590    public static Gee.Map<Geary.EmailIdentifier, Geary.Email>? emails_to_map(Gee.Collection<Geary.Email>? emails) {
591        if (emails == null || emails.size == 0)
592            return null;
593
594        Gee.Map<Geary.EmailIdentifier, Geary.Email> map = new Gee.HashMap<Geary.EmailIdentifier,
595            Geary.Email>();
596        foreach (Email email in emails)
597            map.set(email.id, email);
598
599        return map;
600    }
601
602    /**
603     * CompareFunc to sort {@link Email} by {@link date} ascending.
604     *
605     * If the date field is unavailable on either Email, their identifiers are compared to
606     * stabilize the sort.
607     */
608    public static int compare_sent_date_ascending(Geary.Email aemail, Geary.Email bemail) {
609        if (aemail.date == null || bemail.date == null) {
610            GLib.message("Warning: comparing email for sent date but no Date: field loaded");
611
612            return compare_id_ascending(aemail, bemail);
613        }
614
615        int compare = aemail.date.value.compare(bemail.date.value);
616
617        // stabilize sort by using the mail identifier's stable sort ordering
618        return (compare != 0) ? compare : compare_id_ascending(aemail, bemail);
619    }
620
621    /**
622     * CompareFunc to sort {@link Email} by {@link date} descending.
623     *
624     * If the date field is unavailable on either Email, their identifiers are compared to
625     * stabilize the sort.
626     */
627    public static int compare_sent_date_descending(Geary.Email aemail, Geary.Email bemail) {
628        return compare_sent_date_ascending(bemail, aemail);
629    }
630
631    /**
632     * CompareFunc to sort {@link Email} by {@link EmailProperties.date_received} ascending.
633     *
634     * If {@link properties} is unavailable on either Email, their identifiers are compared to
635     * stabilize the sort.
636     */
637    public static int compare_recv_date_ascending(Geary.Email aemail, Geary.Email bemail) {
638        if (aemail.properties == null || bemail.properties == null) {
639            GLib.message("Warning: comparing email for received date but email properties not loaded");
640
641            return compare_id_ascending(aemail, bemail);
642        }
643
644        int compare = aemail.properties.date_received.compare(bemail.properties.date_received);
645
646        // stabilize sort with identifiers
647        return (compare != 0) ? compare : compare_id_ascending(aemail, bemail);
648    }
649
650    /**
651     * CompareFunc to sort {@link Email} by {@link EmailProperties.date_received} descending.
652     *
653     * If {@link properties} is unavailable on either Email, their identifiers are compared to
654     * stabilize the sort.
655     */
656    public static int compare_recv_date_descending(Geary.Email aemail, Geary.Email bemail) {
657        return compare_recv_date_ascending(bemail, aemail);
658    }
659
660    // only used to stabilize a sort
661    private static int compare_id_ascending(Geary.Email aemail, Geary.Email bemail) {
662        return aemail.id.stable_sort_comparator(bemail.id);
663    }
664
665    /**
666     * CompareFunc to sort Email by EmailProperties.total_bytes.  If not available, emails are
667     * compared by EmailIdentifier.
668     */
669    public static int compare_size_ascending(Geary.Email aemail, Geary.Email bemail) {
670        Geary.EmailProperties? aprop = (Geary.EmailProperties) aemail.properties;
671        Geary.EmailProperties? bprop = (Geary.EmailProperties) bemail.properties;
672
673        if (aprop == null || bprop == null) {
674            GLib.message("Warning: comparing email by size but email properties not loaded");
675
676            return compare_id_ascending(aemail, bemail);
677        }
678
679        int cmp = (int) (aprop.total_bytes - bprop.total_bytes).clamp(-1, 1);
680
681        return (cmp != 0) ? cmp : compare_id_ascending(aemail, bemail);
682    }
683
684    /**
685     * CompareFunc to sort Email by EmailProperties.total_bytes.  If not available, emails are
686     * compared by EmailIdentifier.
687     */
688    public static int compare_size_descending(Geary.Email aemail, Geary.Email bemail) {
689        return compare_size_ascending(bemail, aemail);
690    }
691}
692
693