1/*
2 * Copyright © 2016 Software Freedom Conservancy Inc.
3 * Copyright © 2018, 2020 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/**
11 * An interface between the high-level engine API and an IMAP mailbox.
12 *
13 * Because of the complexities of the IMAP protocol, class takes
14 * common operations that a Geary.Folder implementation would need
15 * (in particular, {@link Geary.ImapEngine.MinimalFolder}) and makes
16 * them into simple async calls.
17 *
18 * When constructed, this class will issue an IMAP SELECT command for
19 * the mailbox represented by this folder, placing the session in the
20 * Selected state.
21 */
22private class Geary.Imap.FolderSession : Geary.Imap.SessionObject {
23
24    private const Geary.Email.Field BASIC_FETCH_FIELDS = Email.Field.ENVELOPE | Email.Field.DATE
25        | Email.Field.ORIGINATORS | Email.Field.RECEIVERS | Email.Field.REFERENCES
26        | Email.Field.SUBJECT | Email.Field.HEADER;
27
28
29    /** The folder this session operates on. */
30    public Imap.Folder folder { get; private set; }
31
32    /** Determines if this folder immutable. */
33    public Trillian readonly { get; private set; default = Trillian.UNKNOWN; }
34
35    /** This folder's set of permanent IMAP flags. */
36    public MessageFlags? permanent_flags { get; private set; default = null; }
37
38    /** Determines if this folder accepts custom IMAP flags. */
39    public Trillian accepts_user_flags { get; private set; default = Trillian.UNKNOWN; }
40
41    private MailboxSpecifier mailbox;
42
43    private Quirks quirks;
44
45    private Nonblocking.Mutex cmd_mutex = new Nonblocking.Mutex();
46    private Gee.HashMap<SequenceNumber, FetchedData>? fetch_accumulator = null;
47    private Gee.Set<Imap.UID>? search_accumulator = null;
48
49    /**
50     * A (potentially unsolicited) response from the server.
51     *
52     * See [[http://tools.ietf.org/html/rfc3501#section-7.3.1]]
53     */
54    public signal void exists(int total);
55
56    /**
57     * A (potentially unsolicited) response from the server.
58     *
59     * See [[http://tools.ietf.org/html/rfc3501#section-7.3.2]]
60     */
61    public signal void recent(int total);
62
63    /**
64     * A (potentially unsolicited) response from the server.
65     *
66     * See [[http://tools.ietf.org/html/rfc3501#section-7.4.1]]
67     */
68    public signal void expunge(SequenceNumber position);
69
70    /**
71     * Fabricated from the IMAP signals and state obtained at open_async().
72     */
73    public signal void appended(int count);
74
75    /**
76     * Fabricated from the IMAP signals and state obtained at open_async().
77     */
78    public signal void updated(SequenceNumber pos, FetchedData data);
79
80    /**
81     * Fabricated from the IMAP signals and state obtained at open_async().
82     */
83    public signal void removed(SequenceNumber pos);
84
85
86    public async FolderSession(ClientSession session,
87                               Imap.Folder folder,
88                               GLib.Cancellable? cancellable)
89        throws GLib.Error {
90        base(session);
91        this.folder = folder;
92        this.quirks = session.quirks;
93
94        if (folder.properties.attrs.is_no_select) {
95            throw new ImapError.NOT_SUPPORTED(
96                "Folder cannot be selected: %s",
97                folder.path.to_string()
98            );
99        }
100
101        // Update based on our current session
102        folder.properties.set_from_session_capabilities(session.capabilities);
103
104        // connect to interesting signals *before* selecting
105        session.exists.connect(on_exists);
106        session.expunge.connect(on_expunge);
107        session.fetch.connect(on_fetch);
108        session.recent.connect(on_recent);
109        session.search.connect(on_search);
110        session.status_response_received.connect(on_status_response);
111
112        this.mailbox = session.get_mailbox_for_path(folder.path);
113        StatusResponse? response = yield session.select_async(
114            this.mailbox, cancellable
115        );
116        throw_on_not_ok(response, "SELECT " + this.folder.path.to_string());
117
118        // if at end of SELECT command accepts_user_flags is still
119        // UNKKNOWN, treat as TRUE because, according to IMAP spec, if
120        // PERMANENTFLAGS are not returned, then assume OK
121        if (this.accepts_user_flags == Trillian.UNKNOWN)
122            this.accepts_user_flags = Trillian.TRUE;
123    }
124
125    /**
126     * Enables IMAP IDLE for the session, if supported.
127     */
128    public async void enable_idle(Cancellable? cancellable)
129        throws Error {
130        ClientSession session = get_session();
131        int token = yield this.cmd_mutex.claim_async(cancellable);
132        Error? cmd_err = null;
133        try {
134            session.enable_idle();
135        } catch (Error err) {
136            cmd_err = err;
137        }
138
139        this.cmd_mutex.release(ref token);
140
141        if (cmd_err != null) {
142            throw cmd_err;
143        }
144    }
145
146    /** {@inheritDoc} */
147    public override ClientSession? close() {
148        ClientSession? old_session = base.close();
149        if (old_session != null) {
150            old_session.exists.disconnect(on_exists);
151            old_session.expunge.disconnect(on_expunge);
152            old_session.fetch.disconnect(on_fetch);
153            old_session.recent.disconnect(on_recent);
154            old_session.search.disconnect(on_search);
155            old_session.status_response_received.disconnect(on_status_response);
156        }
157        return old_session;
158    }
159
160    /** Sends a NOOP command. */
161    public async void send_noop(GLib.Cancellable? cancellable)
162        throws GLib.Error {
163        yield exec_commands_async(
164            Collection.single(new NoopCommand(cancellable)),
165            null,
166            null,
167            cancellable
168        );
169    }
170
171    private void on_exists(int total) {
172        debug("EXISTS %d", total);
173
174        int old_total = this.folder.properties.select_examine_messages;
175        this.folder.properties.set_select_examine_message_count(total);
176
177        exists(total);
178        if (old_total >= 0 && old_total < total) {
179            appended(total - old_total);
180        }
181    }
182
183    private void on_expunge(SequenceNumber pos) {
184        debug("EXPUNGE %s", pos.to_string());
185
186        int old_total = this.folder.properties.select_examine_messages;
187        if (old_total > 0) {
188            this.folder.properties.set_select_examine_message_count(
189                old_total - 1
190            );
191        }
192
193        expunge(pos);
194        removed(pos);
195    }
196
197    private void on_fetch(FetchedData data) {
198        // add if not found, merge if already received data for this email
199        if (this.fetch_accumulator != null) {
200            FetchedData? existing = this.fetch_accumulator.get(data.seq_num);
201            this.fetch_accumulator.set(
202                data.seq_num, (existing != null) ? data.combine(existing) : data
203            );
204        } else {
205            debug("FETCH (unsolicited): %s:", data.to_string());
206            updated(data.seq_num, data);
207        }
208    }
209
210    private void on_recent(int total) {
211        debug("RECENT %d", total);
212        this.folder.properties.recent = total;
213        recent(total);
214    }
215
216    private void on_search(int64[] seq_or_uid) {
217        // All SEARCH from this class are UID SEARCH, so can reliably convert and add to
218        // accumulator
219        if (this.search_accumulator != null) {
220            foreach (int64 uid in seq_or_uid) {
221                try {
222                    this.search_accumulator.add(new UID.checked(uid));
223                } catch (ImapError imaperr) {
224                    warning("Unable to process SEARCH UID result: %s",
225                            imaperr.message);
226                }
227            }
228        } else {
229            debug("Not handling unsolicited SEARCH response");
230        }
231    }
232
233    private void on_status_response(StatusResponse status_response) {
234        // only interested in ResponseCodes here
235        ResponseCode? response_code = status_response.response_code;
236        if (response_code == null)
237            return;
238
239        try {
240            // Have to take a copy of the string property before evaluation due to this bug:
241            // https://bugzilla.gnome.org/show_bug.cgi?id=703818
242            string value = response_code.get_response_code_type().value;
243            switch (value) {
244                case ResponseCodeType.READONLY:
245                    this.readonly = Trillian.TRUE;
246                break;
247
248                case ResponseCodeType.READWRITE:
249                    this.readonly = Trillian.FALSE;
250                break;
251
252                case ResponseCodeType.UIDNEXT:
253                    try {
254                        this.folder.properties.uid_next = response_code.get_uid_next();
255                    } catch (ImapError.INVALID err) {
256                        // Some mail servers e.g hMailServer and
257                        // whatever is used by home.pl (dovecot?)
258                        // sends UIDNEXT 0. Just ignore these since
259                        // there nothing else that can be done. See
260                        // GNOME/geary#711
261                        if (response_code.get_as_string(1).as_int64() == 0) {
262                            warning("Ignoring bad UIDNEXT 0 from server");
263                        } else {
264                            throw err;
265                        }
266                    }
267                break;
268
269                case ResponseCodeType.UIDVALIDITY:
270                    this.folder.properties.uid_validity = response_code.get_uid_validity();
271                break;
272
273                case ResponseCodeType.UNSEEN:
274                    // do NOT update properties.unseen, as the UNSEEN response code (here) means
275                    // the sequence number of the first unseen message, not the total count of
276                    // unseen messages
277                break;
278
279                case ResponseCodeType.PERMANENT_FLAGS:
280                    this.permanent_flags = response_code.get_permanent_flags();
281                    this.accepts_user_flags = Trillian.from_boolean(
282                        this.permanent_flags.contains(MessageFlag.ALLOWS_NEW)
283                    );
284                break;
285
286                default:
287                    // ignored
288                break;
289            }
290        } catch (ImapError ierr) {
291            warning("Unable to parse ResponseCode %s: %s", response_code.to_string(),
292                    ierr.message);
293        }
294    }
295
296    // Executes a set of commands.
297    //
298    // All commands must executed inside the cmd_mutex. Collects
299    // results in fetch_results or store results.
300    private async Gee.Map<Command,StatusResponse>?
301        exec_commands_async(Gee.Collection<Command> cmds,
302                            Gee.HashMap<SequenceNumber, FetchedData>? fetch_results,
303                            Gee.Set<Imap.UID>? search_results,
304                            GLib.Cancellable? cancellable)
305        throws GLib.Error {
306        ClientSession session = get_session();
307        Gee.Map<Command, StatusResponse>? responses = null;
308        int token = yield this.cmd_mutex.claim_async(cancellable);
309
310        this.fetch_accumulator = fetch_results;
311        this.search_accumulator = search_results;
312
313        Error? cmd_err = null;
314        try {
315            responses = yield session.send_multiple_commands_async(
316                cmds, cancellable
317            );
318        } catch (Error err) {
319            cmd_err = err;
320        }
321
322        this.fetch_accumulator = null;
323        this.search_accumulator = null;
324
325        this.cmd_mutex.release(ref token);
326
327        if (cmd_err != null) {
328            throw cmd_err;
329        }
330
331        foreach (Command cmd in responses.keys) {
332            throw_on_not_ok(responses.get(cmd), cmd.to_string());
333        }
334
335        return responses;
336    }
337
338    // Utility method for listing UIDs on the remote within the supplied range
339    public async Gee.Set<Imap.UID>? list_uids_async(MessageSet msg_set,
340                                                    GLib.Cancellable? cancellable)
341        throws GLib.Error {
342        // Although FETCH could be used, SEARCH is more efficient in returning pure UID results,
343        // which is all we're interested in here
344        SearchCriteria criteria = new SearchCriteria(SearchCriterion.message_set(msg_set));
345        SearchCommand cmd = new SearchCommand.uid(criteria, cancellable);
346
347        Gee.Set<Imap.UID> search_results = new Gee.HashSet<Imap.UID>();
348        yield exec_commands_async(
349            Geary.iterate<Command>(cmd).to_array_list(),
350            null,
351            search_results,
352            cancellable
353        );
354
355        return (search_results.size > 0) ? search_results : null;
356    }
357
358    private Gee.Collection<FetchCommand> assemble_list_commands(
359        Imap.MessageSet msg_set,
360        Geary.Email.Field fields,
361        GLib.Cancellable? cancellable,
362        out FetchBodyDataSpecifier[]? header_specifiers,
363        out FetchBodyDataSpecifier? body_specifier,
364        out FetchBodyDataSpecifier? preview_specifier,
365        out FetchBodyDataSpecifier? preview_charset_specifier
366    ) {
367        // getting all the fields can require multiple FETCH commands (some servers don't handle
368        // well putting every required data item into single command), so aggregate FetchCommands
369        Gee.Collection<FetchCommand> cmds = new Gee.ArrayList<FetchCommand>();
370
371        // if not a UID FETCH, request UIDs for all messages so their EmailIdentifier can be
372        // created without going back to the database (assuming the messages have already been
373        // pulled down, not a guarantee); if request is for NONE, that guarantees that the
374        // EmailIdentifier will be set, and so fetch UIDs (which looks funny but works when
375        // listing a range for contents: UID FETCH x:y UID)
376        if (!msg_set.is_uid || fields == Geary.Email.Field.NONE) {
377            cmds.add(
378                new FetchCommand.data_type(
379                    msg_set, FetchDataSpecifier.UID, cancellable
380                )
381            );
382        }
383
384        // convert bulk of the "basic" fields into a one or two FETCH commands (some servers have
385        // exhibited bugs or return NO when too many FETCH data types are combined on a single
386        // command)
387        header_specifiers = null;
388        if (fields.requires_any(BASIC_FETCH_FIELDS)) {
389            Gee.List<FetchDataSpecifier> basic_types =
390                new Gee.LinkedList<FetchDataSpecifier>();
391            Gee.List<string> header_fields = new Gee.LinkedList<string>();
392
393            fields_to_fetch_data_types(fields, basic_types, header_fields);
394
395            // Add all simple data types as one FETCH command
396            if (!basic_types.is_empty) {
397                cmds.add(
398                    new FetchCommand(msg_set, basic_types, null, cancellable)
399                );
400            }
401
402            // Add all header field requests as separate FETCH
403            // command(s). If the HEADER.FIELDS hack is enabled and
404            // there is more than one header that needs fetching, we
405            // need to send multiple commands since we can't separate
406            // them by spaces in the same command.
407            //
408            // See <https://gitlab.gnome.org/GNOME/geary/issues/571>
409            if (!header_fields.is_empty) {
410                if (!this.quirks.fetch_header_part_no_space ||
411                    header_fields.size == 1) {
412                    header_specifiers = new FetchBodyDataSpecifier[1];
413                    header_specifiers[0] = new FetchBodyDataSpecifier.peek(
414                        FetchBodyDataSpecifier.SectionPart.HEADER_FIELDS,
415                        null,
416                        -1,
417                        -1,
418                        header_fields.to_array()
419                    );
420                } else {
421                    header_specifiers = new FetchBodyDataSpecifier[header_fields.size];
422                    int i = 0;
423                    foreach (string name in header_fields) {
424                        header_specifiers[i++] = new FetchBodyDataSpecifier.peek(
425                            FetchBodyDataSpecifier.SectionPart.HEADER_FIELDS,
426                            null,
427                            -1,
428                            -1,
429                            new string[] { name }
430                        );
431                    }
432                }
433
434                foreach (FetchBodyDataSpecifier header in header_specifiers) {
435                    if (this.quirks.fetch_header_part_no_space) {
436                        header.omit_request_header_fields_space();
437                    }
438                    cmds.add(
439                        new FetchCommand.body_data_type(
440                            msg_set, header, cancellable
441                        )
442                    );
443                }
444            }
445        }
446
447        // RFC822 BODY is a separate command
448        if (fields.require(Email.Field.BODY)) {
449            body_specifier = new FetchBodyDataSpecifier.peek(FetchBodyDataSpecifier.SectionPart.TEXT,
450                null, -1, -1, null);
451
452            cmds.add(
453                new FetchCommand.body_data_type(
454                    msg_set, body_specifier, cancellable
455                )
456            );
457        } else {
458            body_specifier = null;
459        }
460
461        // PREVIEW obtains the content type and a truncated version of
462        // the first part of the message, which often leads to poor
463        // results. It can also be also be synthesised from the
464        // email's RFC822 message in fetched_data_to_email, if the
465        // fields needed for reconstructing the RFC822 message are
466        // present. If so, rely on that and don't also request any
467        // additional data for the preview here.
468        if (fields.require(Email.Field.PREVIEW) &&
469            !fields.require(Email.REQUIRED_FOR_MESSAGE)) {
470            // Get the preview text (the initial MAX_PREVIEW_BYTES of
471            // the first MIME section
472
473            preview_specifier = new FetchBodyDataSpecifier.peek(FetchBodyDataSpecifier.SectionPart.NONE,
474                { 1 }, 0, Geary.Email.MAX_PREVIEW_BYTES, null);
475            cmds.add(
476                new FetchCommand.body_data_type(
477                    msg_set, preview_specifier, cancellable
478                )
479            );
480
481            // Also get the character set to properly decode it
482            preview_charset_specifier = new FetchBodyDataSpecifier.peek(
483                FetchBodyDataSpecifier.SectionPart.MIME, { 1 }, -1, -1, null);
484            cmds.add(
485                new FetchCommand.body_data_type(
486                    msg_set, preview_charset_specifier, cancellable
487                )
488            );
489        } else {
490            preview_specifier = null;
491            preview_charset_specifier = null;
492        }
493
494        // PROPERTIES and FLAGS are a separate command
495        if (fields.requires_any(Email.Field.PROPERTIES | Email.Field.FLAGS)) {
496            Gee.List<FetchDataSpecifier> data_types = new Gee.ArrayList<FetchDataSpecifier>();
497
498            if (fields.require(Geary.Email.Field.PROPERTIES)) {
499                data_types.add(FetchDataSpecifier.INTERNALDATE);
500                data_types.add(FetchDataSpecifier.RFC822_SIZE);
501            }
502
503            if (fields.require(Geary.Email.Field.FLAGS))
504                data_types.add(FetchDataSpecifier.FLAGS);
505
506            cmds.add(new FetchCommand(msg_set, data_types, null, cancellable));
507        }
508
509        return cmds;
510    }
511
512    // Returns a no-message-id ImapDB.EmailIdentifier with the UID stored in it.
513    public async Gee.List<Geary.Email>? list_email_async(MessageSet msg_set,
514                                                         Geary.Email.Field fields,
515                                                         Cancellable? cancellable)
516        throws Error {
517        Gee.HashMap<SequenceNumber, FetchedData> fetched =
518            new Gee.HashMap<SequenceNumber, FetchedData>();
519        FetchBodyDataSpecifier[]? header_specifiers = null;
520        FetchBodyDataSpecifier? body_specifier = null;
521        FetchBodyDataSpecifier? preview_specifier = null;
522        FetchBodyDataSpecifier? preview_charset_specifier = null;
523        bool success = false;
524        while (!success) {
525            Gee.Collection<FetchCommand> cmds = assemble_list_commands(
526                msg_set,
527                fields,
528                cancellable,
529                out header_specifiers,
530                out body_specifier,
531                out preview_specifier,
532                out preview_charset_specifier
533            );
534            if (cmds.size == 0) {
535                throw new ImapError.INVALID(
536                    "No FETCH commands generate for list request %s %s",
537                    msg_set.to_string(),
538                    fields.to_string()
539                );
540            }
541
542            // Commands prepped, do the fetch and accumulate all the responses
543            try {
544                yield exec_commands_async(cmds, fetched, null, cancellable);
545                success = true;
546            } catch (ImapError.SERVER_ERROR err) {
547                if (retry_bad_header_fields_response(cmds)) {
548                    // The command failed, but it wasn't using the
549                    // header field hack, so retry it.
550                    debug("Retryable server failure detected: %s", err.message);
551                } else {
552                    throw err;
553                }
554            }
555        }
556
557        if (fetched.size == 0)
558            return null;
559
560        // Convert fetched data into Geary.Email objects
561        // because this could be for a lot of email, do in a background thread
562        Gee.List<Geary.Email> email_list = new Gee.ArrayList<Geary.Email>();
563        yield Nonblocking.Concurrent.global.schedule_async(() => {
564            foreach (SequenceNumber seq_num in fetched.keys) {
565                FetchedData fetched_data = fetched.get(seq_num);
566
567                // the UID should either have been fetched (if using positional addressing) or should
568                // have come back with the response (if using UID addressing)
569                UID? uid = fetched_data.data_map.get(FetchDataSpecifier.UID) as UID;
570                if (uid == null) {
571                    message("Unable to list message #%s: No UID returned from server",
572                            seq_num.to_string());
573
574                    continue;
575                }
576
577                try {
578                    Geary.Email email = fetched_data_to_email(
579                        uid,
580                        fetched_data,
581                        fields,
582                        header_specifiers,
583                        body_specifier,
584                        preview_specifier,
585                        preview_charset_specifier
586                    );
587                    if (!email.fields.fulfills(fields)) {
588                        warning(
589                            "%s missing=%s fetched=%s",
590                            email.id.to_string(),
591                            fields.clear(email.fields).to_string(),
592                            fetched_data.to_string()
593                        );
594                        continue;
595                    }
596
597                    email_list.add(email);
598                } catch (Error err) {
599                    warning("Unable to convert email for %s %s: %s",
600                            uid.to_string(),
601                            fetched_data.to_string(),
602                            err.message);
603                }
604            }
605        }, cancellable);
606
607        return (email_list.size > 0) ? email_list : null;
608    }
609
610    /**
611     * Returns the sequence numbers for a set of UIDs.
612     *
613     * The `msg_set` parameter must be a set containing UIDs. An error
614     * is thrown if the sequence numbers cannot be determined.
615     */
616    public async Gee.Map<UID, SequenceNumber> uid_to_position_async(MessageSet msg_set,
617                                                                    Cancellable? cancellable)
618        throws Error {
619        if (!msg_set.is_uid) {
620            throw new ImapError.NOT_SUPPORTED("Message set must contain UIDs");
621        }
622
623        Gee.List<Command> cmds = new Gee.ArrayList<Command>();
624        cmds.add(
625            new FetchCommand.data_type(
626                msg_set, FetchDataSpecifier.UID, cancellable
627            )
628        );
629
630        Gee.HashMap<SequenceNumber, FetchedData> fetched =
631            new Gee.HashMap<SequenceNumber, FetchedData>();
632        yield exec_commands_async(cmds, fetched, null, cancellable);
633
634        if (fetched.is_empty) {
635            throw new ImapError.INVALID("Server returned no sequence numbers");
636        }
637
638        Gee.Map<UID,SequenceNumber> map = new Gee.HashMap<UID,SequenceNumber>();
639        foreach (SequenceNumber seq_num in fetched.keys) {
640            map.set(
641                (UID) fetched.get(seq_num).data_map.get(FetchDataSpecifier.UID),
642                seq_num
643            );
644        }
645        return map;
646    }
647
648    public async void remove_email_async(Gee.List<MessageSet> msg_sets,
649                                         GLib.Cancellable? cancellable)
650        throws GLib.Error {
651        ClientSession session = get_session();
652        Gee.List<MessageFlag> flags = new Gee.ArrayList<MessageFlag>();
653        flags.add(MessageFlag.DELETED);
654
655        Gee.List<Command> cmds = new Gee.ArrayList<Command>();
656
657        // Build STORE command for all MessageSets, see if all are UIDs so we can use UID EXPUNGE
658        bool all_uid = true;
659        foreach (MessageSet msg_set in msg_sets) {
660            if (!msg_set.is_uid)
661                all_uid = false;
662
663            cmds.add(
664                new StoreCommand(msg_set, ADD_FLAGS, SILENT, flags, cancellable)
665            );
666        }
667
668        // TODO: Only use old-school EXPUNGE when closing folder (or rely on CLOSE to do that work
669        // for us).  See:
670        // http://redmine.yorba.org/issues/7532
671        //
672        // However, current client implementation doesn't properly close INBOX when application
673        // shuts down, which means deleted messages return at application start.  See:
674        // http://redmine.yorba.org/issues/6865
675        if (all_uid && session.capabilities.supports_uidplus()) {
676            foreach (MessageSet msg_set in msg_sets) {
677                cmds.add(new ExpungeCommand.uid(msg_set, cancellable));
678            }
679        } else {
680            cmds.add(new ExpungeCommand(cancellable));
681        }
682
683        yield exec_commands_async(cmds, null, null, cancellable);
684    }
685
686    public async void mark_email_async(Gee.List<MessageSet> msg_sets, Geary.EmailFlags? flags_to_add,
687        Geary.EmailFlags? flags_to_remove, Cancellable? cancellable) throws Error {
688        Gee.List<MessageFlag> msg_flags_add = new Gee.ArrayList<MessageFlag>();
689        Gee.List<MessageFlag> msg_flags_remove = new Gee.ArrayList<MessageFlag>();
690        MessageFlag.from_email_flags(flags_to_add, flags_to_remove, out msg_flags_add,
691            out msg_flags_remove);
692
693        if (msg_flags_add.size == 0 && msg_flags_remove.size == 0)
694            return;
695
696        Gee.Collection<Command> cmds = new Gee.ArrayList<Command>();
697        foreach (MessageSet msg_set in msg_sets) {
698            if (msg_flags_add.size > 0) {
699                cmds.add(
700                    new StoreCommand(
701                        msg_set,
702                        ADD_FLAGS,
703                        SILENT,
704                        msg_flags_add,
705                        cancellable
706                    )
707                );
708            }
709
710            if (msg_flags_remove.size > 0) {
711                cmds.add(
712                    new StoreCommand(
713                        msg_set,
714                        REMOVE_FLAGS,
715                        SILENT,
716                        msg_flags_remove,
717                        cancellable
718                    )
719                );
720            }
721        }
722
723        yield exec_commands_async(cmds, null, null, cancellable);
724    }
725
726    // Returns a mapping of the source UID to the destination UID.  If the MessageSet is not for
727    // UIDs, then null is returned.  If the server doesn't support COPYUID, null is returned.
728    public async Gee.Map<UID, UID>? copy_email_async(MessageSet msg_set,
729                                                     FolderPath destination,
730                                                     GLib.Cancellable? cancellable)
731        throws GLib.Error {
732        ClientSession session = get_session();
733
734        MailboxSpecifier mailbox = session.get_mailbox_for_path(destination);
735        CopyCommand cmd = new CopyCommand(msg_set, mailbox, cancellable);
736
737        Gee.Map<Command, StatusResponse>? responses = yield exec_commands_async(
738            Geary.iterate<Command>(cmd).to_array_list(), null, null, cancellable);
739
740        if (!responses.has_key(cmd))
741            return null;
742
743        StatusResponse response = responses.get(cmd);
744        if (response.response_code != null && msg_set.is_uid) {
745            Gee.List<UID>? src_uids = null;
746            Gee.List<UID>? dst_uids = null;
747            try {
748                response.response_code.get_copyuid(null, out src_uids, out dst_uids);
749            } catch (ImapError ierr) {
750                warning("Unable to retrieve COPYUID UIDs: %s", ierr.message);
751            }
752
753            if (src_uids != null && !src_uids.is_empty &&
754                dst_uids != null && !dst_uids.is_empty) {
755                Gee.Map<UID, UID> copyuids = new Gee.HashMap<UID, UID>();
756                int ctr = 0;
757                for (;;) {
758                    UID? src_uid = (ctr < src_uids.size) ? src_uids[ctr] : null;
759                    UID? dst_uid = (ctr < dst_uids.size) ? dst_uids[ctr] : null;
760
761                    if (src_uid != null && dst_uid != null)
762                        copyuids.set(src_uid, dst_uid);
763                    else
764                        break;
765
766                    ctr++;
767                }
768
769                if (copyuids.size > 0)
770                    return copyuids;
771            }
772        }
773
774        return null;
775    }
776
777    public async Gee.SortedSet<Imap.UID>? search_async(SearchCriteria criteria,
778                                                       GLib.Cancellable? cancellable)
779        throws GLib.Error {
780        // always perform a UID SEARCH
781        Gee.Collection<Command> cmds = new Gee.ArrayList<Command>();
782        cmds.add(new SearchCommand.uid(criteria, cancellable));
783
784        Gee.Set<Imap.UID> search_results = new Gee.HashSet<Imap.UID>();
785        yield exec_commands_async(cmds, null, search_results, cancellable);
786
787        Gee.SortedSet<Imap.UID> tree = null;
788        if (search_results.size > 0) {
789            tree = new Gee.TreeSet<Imap.UID>();
790            tree.add_all(search_results);
791        }
792        return tree;
793    }
794
795    // NOTE: If fields are added or removed from this method, BASIC_FETCH_FIELDS *must* be updated
796    // as well
797    private void fields_to_fetch_data_types(Geary.Email.Field fields,
798                                            Gee.List<FetchDataSpecifier> basic_types,
799                                            Gee.List<string> header_fields) {
800        // The assumption here is that because ENVELOPE is such a common fetch command, the
801        // server will have optimizations for it, whereas if we called for each header in the
802        // envelope separately, the server has to chunk harder parsing the RFC822 header ... have
803        // to add References because IMAP ENVELOPE doesn't return them for some reason (but does
804        // return Message-ID and In-Reply-To)
805        if (fields.is_all_set(Geary.Email.Field.ENVELOPE)) {
806            basic_types.add(FetchDataSpecifier.ENVELOPE);
807            header_fields.add("References");
808
809            // remove those flags and process any remaining
810            fields = fields.clear(Geary.Email.Field.ENVELOPE);
811        }
812
813        foreach (Geary.Email.Field field in Geary.Email.Field.all()) {
814            switch (fields & field) {
815                case Geary.Email.Field.DATE:
816                    header_fields.add("Date");
817                break;
818
819                case Geary.Email.Field.ORIGINATORS:
820                    header_fields.add("From");
821                    header_fields.add("Sender");
822                    header_fields.add("Reply-To");
823                break;
824
825                case Geary.Email.Field.RECEIVERS:
826                    header_fields.add("To");
827                    header_fields.add("Cc");
828                    header_fields.add("Bcc");
829                break;
830
831                case Geary.Email.Field.REFERENCES:
832                    header_fields.add("References");
833                    header_fields.add("Message-ID");
834                    header_fields.add("In-Reply-To");
835                break;
836
837                case Geary.Email.Field.SUBJECT:
838                    header_fields.add("Subject");
839                break;
840
841                case Geary.Email.Field.HEADER:
842                    // TODO: If the entire header is being pulled, then no need to pull down partial
843                    // headers; simply get them all and decode what is needed directly
844                    basic_types.add(FetchDataSpecifier.RFC822_HEADER);
845                break;
846
847                case Geary.Email.Field.NONE:
848                case Geary.Email.Field.BODY:
849                case Geary.Email.Field.PROPERTIES:
850                case Geary.Email.Field.FLAGS:
851                case Geary.Email.Field.PREVIEW:
852                    // not set or fetched separately
853                break;
854
855                default:
856                    assert_not_reached();
857            }
858        }
859    }
860
861    private Geary.Email fetched_data_to_email(
862        UID uid,
863        FetchedData fetched_data,
864        Geary.Email.Field required_fields,
865        FetchBodyDataSpecifier[]? header_specifiers,
866        FetchBodyDataSpecifier? body_specifier,
867        FetchBodyDataSpecifier? preview_specifier,
868        FetchBodyDataSpecifier? preview_charset_specifier
869    ) throws GLib.Error {
870        // note the use of INVALID_ROWID, as the rowid for this email (if one is present in the
871        // database) is unknown at this time; this means ImapDB *must* create a new EmailIdentifier
872        // for this email after create/merge is completed
873        Geary.Email email = new Geary.Email(new ImapDB.EmailIdentifier.no_message_id(uid));
874
875        // accumulate these to submit Imap.EmailProperties all at once
876        InternalDate? internaldate = null;
877        RFC822Size? rfc822_size = null;
878
879        // accumulate these to submit References all at once
880        RFC822.MessageID? message_id = null;
881        RFC822.MessageIDList? in_reply_to = null;
882        RFC822.MessageIDList? references = null;
883
884        // loop through all available FetchDataTypes and gather converted data
885        foreach (FetchDataSpecifier data_type in fetched_data.data_map.keys) {
886            MessageData? data = fetched_data.data_map.get(data_type);
887            if (data == null)
888                continue;
889
890            switch (data_type) {
891                case FetchDataSpecifier.ENVELOPE:
892                    Envelope envelope = (Envelope) data;
893
894                    email.set_send_date(envelope.sent);
895                    email.set_message_subject(envelope.subject);
896                    email.set_originators(
897                        envelope.from,
898                        envelope.sender.equal_to(envelope.from) || envelope.sender.size == 0 ? null : envelope.sender[0],
899                        envelope.reply_to.equal_to(envelope.from) ? null : envelope.reply_to
900                    );
901                    email.set_receivers(envelope.to, envelope.cc, envelope.bcc);
902
903                    // store these to add to References all at once
904                    message_id = envelope.message_id;
905                    in_reply_to = envelope.in_reply_to;
906                break;
907
908                case FetchDataSpecifier.RFC822_HEADER:
909                    email.set_message_header((RFC822.Header) data);
910                break;
911
912                case FetchDataSpecifier.RFC822_TEXT:
913                    email.set_message_body((RFC822.Text) data);
914                break;
915
916                case FetchDataSpecifier.RFC822_SIZE:
917                    rfc822_size = (RFC822Size) data;
918                break;
919
920                case FetchDataSpecifier.FLAGS:
921                    email.set_flags(new Imap.EmailFlags((MessageFlags) data));
922                break;
923
924                case FetchDataSpecifier.INTERNALDATE:
925                    internaldate = (InternalDate) data;
926                break;
927
928                default:
929                    // everything else dropped on the floor (not applicable to Geary.Email)
930                break;
931            }
932        }
933
934        // Only set PROPERTIES if all have been found
935        if (internaldate != null && rfc822_size != null)
936            email.set_email_properties(new Geary.Imap.EmailProperties(internaldate, rfc822_size));
937
938        // if any headers were requested, convert its fields now
939        if (header_specifiers != null) {
940            // Header fields are case insensitive, so use a
941            // case-insensitive map.
942            //
943            // XXX this is bogus because it doesn't take into the
944            // presence of multiple headers. It's not common, but it's
945            // possible for there to be two To headers, for example
946            Gee.Map<string,string> headers = new Gee.HashMap<string,string>(
947                String.stri_hash, String.stri_equal
948            );
949            foreach (FetchBodyDataSpecifier header_specifier in header_specifiers) {
950                Memory.Buffer fetched_headers =
951                    fetched_data.body_data_map.get(header_specifier);
952                if (fetched_headers != null) {
953                    RFC822.Header parsed_headers = new RFC822.Header(fetched_headers);
954                    foreach (string name in parsed_headers.get_header_names()) {
955                        headers.set(name, parsed_headers.get_raw_header(name));
956                    }
957                } else {
958                    warning(
959                        "No header specifier \"%s\" found in response:",
960                        header_specifier.to_string()
961                    );
962                    foreach (FetchBodyDataSpecifier specifier in fetched_data.body_data_map.keys) {
963                        warning(" - has %s", specifier.to_string());
964                    }
965                }
966            }
967
968            // When setting email properties below, the relevant
969            // Geary.Email setter needs to be called regardless of
970            // whether the value being set is null, since the setter
971            // will update the email's flags so we know the email has
972            // the field set and it is null.
973
974            // DATE
975            if (required_but_not_set(DATE, required_fields, email)) {
976                email.set_send_date(unflatten_date(headers.get("Date")));
977            }
978
979            // ORIGINATORS
980            if (required_but_not_set(ORIGINATORS, required_fields, email)) {
981                // Allow sender to be a list (contra to the RFC), but
982                // only take the first from it
983                RFC822.MailboxAddresses? sender = unflatten_addresses(
984                    headers.get("Sender")
985                );
986                email.set_originators(
987                    unflatten_addresses(headers.get("From")),
988                    (sender != null && !sender.is_empty) ? sender.get(0) : null,
989                    unflatten_addresses(headers.get("Reply-To"))
990                );
991            }
992
993            // RECEIVERS
994            if (required_but_not_set(RECEIVERS, required_fields, email)) {
995                email.set_receivers(
996                    unflatten_addresses(headers.get("To")),
997                    unflatten_addresses(headers.get("Cc")),
998                    unflatten_addresses(headers.get("Bcc"))
999                );
1000            }
1001
1002            // REFERENCES
1003            // (Note that it's possible the request used an IMAP ENVELOPE, in which case only the
1004            // References header will be present if REFERENCES were required, which is why
1005            // REFERENCES is set at the bottom of the method, when all information has been gathered
1006            if (message_id == null) {
1007                message_id = unflatten_message_id(
1008                    headers.get("Message-ID")
1009                );
1010            }
1011            if (in_reply_to == null) {
1012                in_reply_to = unflatten_message_id_list(
1013                    headers.get("In-Reply-To")
1014                );
1015            }
1016            if (references == null) {
1017                references = unflatten_message_id_list(
1018                    headers.get("References")
1019                );
1020            }
1021
1022            // SUBJECT
1023            if (required_but_not_set(Geary.Email.Field.SUBJECT, required_fields, email)) {
1024                RFC822.Subject? subject = null;
1025                string? value = headers.get("Subject");
1026                if (value != null) {
1027                    subject = new RFC822.Subject.from_rfc822_string(value);
1028                }
1029                email.set_message_subject(subject);
1030            }
1031        }
1032
1033        // It's possible for all these fields to be null even though they were requested from
1034        // the server, so use requested fields for determination
1035        if (required_but_not_set(Geary.Email.Field.REFERENCES, required_fields, email))
1036            email.set_full_references(message_id, in_reply_to, references);
1037
1038        // if preview was requested, get it now ... both identifiers
1039        // must be supplied if one is
1040        if (preview_specifier != null || preview_charset_specifier != null) {
1041            Memory.Buffer? preview_headers = fetched_data.body_data_map.get(
1042                preview_charset_specifier
1043            );
1044            Memory.Buffer? preview_body = fetched_data.body_data_map.get(
1045                preview_specifier
1046            );
1047
1048            RFC822.PreviewText preview = new RFC822.PreviewText(new Memory.StringBuffer(""));
1049            if (preview_headers != null && preview_headers.size > 0 &&
1050                preview_body != null && preview_body.size > 0) {
1051                preview = new RFC822.PreviewText.with_header(
1052                    preview_headers, preview_body
1053                );
1054            } else {
1055                warning("No preview specifiers \"%s\" and \"%s\" found",
1056                    preview_specifier.to_string(), preview_charset_specifier.to_string());
1057                foreach (FetchBodyDataSpecifier specifier in fetched_data.body_data_map.keys)
1058                    warning(" - has %s", specifier.to_string());
1059            }
1060            email.set_message_preview(preview);
1061        }
1062
1063        // If body was requested, get it now. We also set the preview
1064        // here from the body if possible since for HTML messages at
1065        // least there's a lot of boilerplate HTML to wade through to
1066        // get some actual preview text, which usually requires more
1067        // than Geary.Email.MAX_PREVIEW_BYTES will allow for
1068        if (body_specifier != null) {
1069            if (fetched_data.body_data_map.has_key(body_specifier)) {
1070                email.set_message_body(new Geary.RFC822.Text(
1071                    fetched_data.body_data_map.get(body_specifier)));
1072
1073                // Try to set the preview
1074                Geary.RFC822.Message? message = null;
1075                try {
1076                    message = email.get_message();
1077                } catch (EngineError.INCOMPLETE_MESSAGE err) {
1078                    debug("Not enough fields to construct message for preview: %s", err.message);
1079                } catch (GLib.Error err) {
1080                    warning("Error constructing message for preview: %s", err.message);
1081                }
1082                if (message != null) {
1083                    string preview = message.get_preview();
1084                    if (preview.length > Geary.Email.MAX_PREVIEW_BYTES) {
1085                        preview = Geary.String.safe_byte_substring(
1086                            preview, Geary.Email.MAX_PREVIEW_BYTES
1087                        );
1088                    }
1089                    email.set_message_preview(
1090                        new RFC822.PreviewText.from_string(preview)
1091                    );
1092                }
1093            } else {
1094                warning("No body specifier \"%s\" found",
1095                        body_specifier.to_string());
1096                foreach (FetchBodyDataSpecifier specifier in fetched_data.body_data_map.keys)
1097                    warning(" - has %s", specifier.to_string());
1098            }
1099        }
1100
1101        return email;
1102    }
1103
1104    /**
1105     * Stores a new message in the remote mailbox.
1106     *
1107     * Returns a no-message-id ImapDB.EmailIdentifier with the UID
1108     * stored in it.
1109     *
1110     * This method does not take a cancellable; there is currently no
1111     * way to tell if an email was created or not if {@link
1112     * exec_commands_async} is cancelled during the append. For
1113     * atomicity's sake, callers need to remove the returned email ID
1114     * if a cancel occurred.
1115    */
1116    public async Geary.EmailIdentifier? create_email_async(RFC822.Message message,
1117                                                           Geary.EmailFlags? flags,
1118                                                           GLib.DateTime? date_received)
1119        throws GLib.Error {
1120        MessageFlags? msg_flags = null;
1121        if (flags != null) {
1122            Imap.EmailFlags imap_flags = Imap.EmailFlags.from_api_email_flags(flags);
1123            msg_flags = imap_flags.message_flags;
1124        } else {
1125            msg_flags = new MessageFlags(Geary.iterate<MessageFlag>(MessageFlag.SEEN).to_array_list());
1126        }
1127
1128        InternalDate? internaldate = null;
1129        if (date_received != null)
1130            internaldate = new InternalDate.from_date_time(date_received);
1131
1132        AppendCommand cmd = new AppendCommand(
1133            this.mailbox,
1134            msg_flags,
1135            internaldate,
1136            message.get_rfc822_buffer(),
1137            null
1138        );
1139
1140        Gee.Map<Command, StatusResponse> responses = yield exec_commands_async(
1141            Geary.iterate<AppendCommand>(cmd).to_array_list(), null, null, null
1142        );
1143
1144        // Grab the response and parse out the UID, if available.
1145        StatusResponse response = responses.get(cmd);
1146        if (response.status == Status.OK && response.response_code != null &&
1147            response.response_code.get_response_code_type().is_value("appenduid")) {
1148            UID new_id = new UID.checked(response.response_code.get_as_string(2).as_int64());
1149
1150            return new ImapDB.EmailIdentifier.no_message_id(new_id);
1151        }
1152
1153        // We didn't get a UID back from the server.
1154        return null;
1155    }
1156
1157    /** {@inheritDoc} */
1158    public override Logging.State to_logging_state() {
1159        return new Logging.State(
1160            this,
1161            "%s, %s, ro: %s, permanent_flags: %s, accepts_user_flags: %s",
1162            base.to_logging_state().format_message(), // XXX this is cruddy
1163            this.folder.to_string(),
1164            this.readonly.to_string(),
1165            this.permanent_flags != null
1166                ? this.permanent_flags.to_string() : "(none)",
1167            this.accepts_user_flags.to_string()
1168        );
1169    }
1170
1171    /**
1172     * Returns a valid IMAP client session for use by this object.
1173     *
1174     * In addition to the checks made by {@link
1175     * SessionObject.get_session}, this method also ensures that the
1176     * IMAP session is in the SELECTED state for the correct mailbox.
1177     */
1178    protected override ClientSession get_session()
1179        throws ImapError {
1180        var session = base.get_session();
1181        if (session.protocol_state != SELECTED &&
1182            !this.mailbox.equal_to(session.selected_mailbox)) {
1183            throw new ImapError.NOT_CONNECTED(
1184                "IMAP object no longer SELECTED for %s",
1185                this.mailbox.to_string()
1186            );
1187        }
1188        return session;
1189    }
1190
1191    // HACK: See https://bugzilla.gnome.org/show_bug.cgi?id=714902
1192    //
1193    // Detect when a server has returned a BAD response to FETCH
1194    // BODY[HEADER.FIELDS (HEADER-LIST)] due to space between
1195    // HEADER.FIELDS and (HEADER-LIST)
1196    private bool retry_bad_header_fields_response(Gee.Collection<FetchCommand> cmds) {
1197        foreach (FetchCommand fetch in cmds) {
1198            if (fetch.status.status == BAD) {
1199                foreach (FetchBodyDataSpecifier specifier in
1200                         fetch.for_body_data_specifiers) {
1201                    if (specifier.section_part == HEADER_FIELDS ||
1202                        specifier.section_part == HEADER_FIELDS_NOT) {
1203                        // Check the specifier's use of the space, not the
1204                        // folder's property, as it's possible the
1205                        // property was enabled after sending command but
1206                        // before response returned
1207                        if (specifier.request_header_fields_space) {
1208                            this.quirks.fetch_header_part_no_space = true;
1209                            return true;
1210                        }
1211                    }
1212                }
1213            }
1214        }
1215
1216        return false;
1217     }
1218
1219    private void throw_on_not_ok(StatusResponse response, string cmd)
1220        throws ImapError {
1221        switch (response.status) {
1222        case Status.OK:
1223            // All good
1224            break;
1225
1226        case Status.NO:
1227            throw new ImapError.NOT_SUPPORTED(
1228                "Request %s failed: %s", cmd.to_string(), response.to_string()
1229            );
1230
1231        default:
1232            throw new ImapError.SERVER_ERROR(
1233                "Unknown response status to %s: %s",
1234                cmd.to_string(), response.to_string()
1235            );
1236        }
1237    }
1238
1239    private static bool required_but_not_set(Geary.Email.Field check, Geary.Email.Field users_fields, Geary.Email email) {
1240        return users_fields.require(check) ? !email.fields.is_all_set(check) : false;
1241    }
1242
1243    private RFC822.Date? unflatten_date(string? str) {
1244        RFC822.Date? date = null;
1245        if (!String.is_empty_or_whitespace(str)) {
1246            try {
1247                date = new RFC822.Date.from_rfc822_string(str);
1248            } catch (RFC822.Error err) {
1249                // There's not much we can do here aside from logging
1250                // the error, since a lot of email just contain
1251                // invalid addresses
1252                debug("Invalid RFC822 date \"%s\": %s", str, err.message);
1253            }
1254        }
1255        return date;
1256    }
1257
1258    private RFC822.MailboxAddresses? unflatten_addresses(string? str) {
1259        RFC822.MailboxAddresses? addresses = null;
1260        if (!String.is_empty_or_whitespace(str)) {
1261            try {
1262                addresses = new RFC822.MailboxAddresses.from_rfc822_string(str);
1263            } catch (RFC822.Error err) {
1264                // There's not much we can do here aside from logging
1265                // the error, since a lot of email just contain
1266                // invalid addresses
1267                debug("Invalid RFC822 mailbox addresses \"%s\": %s", str, err.message);
1268            }
1269        }
1270        return addresses;
1271    }
1272
1273    private RFC822.MessageID? unflatten_message_id(string? str) {
1274        RFC822.MessageID? id = null;
1275        if (!String.is_empty_or_whitespace(str)) {
1276            try {
1277                id = new RFC822.MessageID.from_rfc822_string(str);
1278            } catch (RFC822.Error err) {
1279                // There's not much we can do here aside from logging
1280                // the error, since a lot of email just contain
1281                // invalid addresses
1282                debug("Invalid RFC822 message id \"%s\": %s", str, err.message);
1283            }
1284        }
1285        return id;
1286    }
1287
1288    private RFC822.MessageIDList? unflatten_message_id_list(string? str) {
1289        RFC822.MessageIDList? ids = null;
1290        if (!String.is_empty_or_whitespace(str)) {
1291            try {
1292                ids = new RFC822.MessageIDList.from_rfc822_string(str);
1293            } catch (RFC822.Error err) {
1294                // There's not much we can do here aside from logging
1295                // the error, since a lot of email just contain
1296                // invalid addresses
1297                debug("Invalid RFC822 message id \"%s\": %s", str, err.message);
1298            }
1299        }
1300        return ids;
1301    }
1302
1303}
1304