imap-engine-generic-account.vala 21 KB
Newer Older
1
/* Copyright 2011-2013 Yorba Foundation
2 3
 *
 * This software is licensed under the GNU Lesser General Public License
4
 * (version 2.1 or later).  See the COPYING file in this distribution.
5 6
 */

7
private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
8
    private const int REFRESH_FOLDER_LIST_SEC = 10 * 60;
9
    
10 11
    private static Geary.FolderPath? inbox_path = null;
    private static Geary.FolderPath? outbox_path = null;
Eric Gregory's avatar
Eric Gregory committed
12
    private static Geary.FolderPath? search_path = null;
13
    
14
    private Imap.Account remote;
15 16
    private ImapDB.Account local;
    private bool open = false;
17
    private Gee.HashMap<FolderPath, Imap.FolderProperties> properties_map = new Gee.HashMap<
Eric Gregory's avatar
Eric Gregory committed
18
        FolderPath, Imap.FolderProperties>();
19
    private Gee.HashMap<FolderPath, GenericFolder> existing_folders = new Gee.HashMap<
Eric Gregory's avatar
Eric Gregory committed
20 21
        FolderPath, GenericFolder>();
    private Gee.HashMap<FolderPath, Folder> local_only = new Gee.HashMap<FolderPath, Folder>();
22 23 24
    private uint refresh_folder_timeout_id = 0;
    private bool in_refresh_enumerate = false;
    private Cancellable refresh_cancellable = new Cancellable();
25
    
26
    public GenericAccount(string name, Geary.AccountInformation information, Imap.Account remote,
27
        ImapDB.Account local) {
28
        base (name, information);
Jim Nelson's avatar
Jim Nelson committed
29
        
30
        this.remote = remote;
31
        this.local = local;
32
        
33
        this.remote.login_failed.connect(on_login_failed);
34
        this.remote.email_sent.connect(on_email_sent);
35
        
36 37
        search_upgrade_monitor = local.search_index_monitor;
        
38 39 40 41 42 43 44 45
        if (inbox_path == null) {
            inbox_path = new Geary.FolderRoot(Imap.Account.INBOX_NAME, Imap.Account.ASSUMED_SEPARATOR,
                Imap.Folder.CASE_SENSITIVE);
        }
        
        if (outbox_path == null) {
            outbox_path = new SmtpOutboxFolderRoot();
        }
Eric Gregory's avatar
Eric Gregory committed
46 47 48 49
        
        if (search_path == null) {
            search_path = new SearchFolderRoot();
        }
50 51
    }
    
52 53 54 55
    internal Imap.FolderProperties? get_properties_for_folder(FolderPath path) {
        return properties_map.get(path);
    }
    
56 57 58 59 60
    private void check_open() throws EngineError {
        if (!open)
            throw new EngineError.OPEN_REQUIRED("Account %s not opened", to_string());
    }
    
61
    public override async void open_async(Cancellable? cancellable = null) throws Error {
62 63 64
        if (open)
            throw new EngineError.ALREADY_OPEN("Account %s already opened", to_string());
        
65 66 67 68 69 70
        // To prevent spurious connection failures, we make sure we have the
        // IMAP password before attempting a connection.  This might have to be
        // reworked when we allow passwordless logins.
        if (!information.imap_credentials.is_complete())
            yield information.fetch_passwords_async(Geary.CredentialsMediator.ServiceFlag.IMAP);
        
71
        yield local.open_async(information.settings_dir, Engine.instance.resource_dir.get_child("sql"), cancellable);
72 73 74
        
        // outbox is now available
        local.outbox.report_problem.connect(notify_report_problem);
75
        local_only.set(outbox_path, local.outbox);
76
        
Eric Gregory's avatar
Eric Gregory committed
77 78 79
        // Search folder.
        local_only.set(search_path, local.search_folder);
        
80 81 82 83 84 85 86 87 88 89 90 91 92 93
        // need to back out local.open_async() if remote fails
        try {
            yield remote.open_async(cancellable);
        } catch (Error err) {
            // back out
            try {
                yield local.close_async(cancellable);
            } catch (Error close_err) {
                // ignored
            }
            
            throw err;
        }
        
94
        open = true;
95 96
        
        notify_opened();
97 98

        notify_folders_available_unavailable(local_only.values, null);
99 100 101 102
        
        // schedule an immediate sweep of the folders; once this is finished, folders will be
        // regularly enumerated
        reschedule_folder_refresh(true);
103 104 105
    }
    
    public override async void close_async(Cancellable? cancellable = null) throws Error {
106 107
        if (!open)
            return;
108 109 110

        notify_folders_available_unavailable(null, local_only.values);
        notify_folders_available_unavailable(null, existing_folders.values);
111
        
112 113
        local.outbox.report_problem.disconnect(notify_report_problem);
        
114 115 116 117 118 119 120 121 122 123 124 125 126 127
        // attempt to close both regardless of errors
        Error? local_err = null;
        try {
            yield local.close_async(cancellable);
        } catch (Error lclose_err) {
            local_err = lclose_err;
        }
        
        Error? remote_err = null;
        try {
            yield remote.close_async(cancellable);
        } catch (Error rclose_err) {
            remote_err = rclose_err;
        }
128 129 130 131

        properties_map.clear();
        existing_folders.clear();
        local_only.clear();
132
        open = false;
133 134 135 136 137 138
        
        if (local_err != null)
            throw local_err;
        
        if (remote_err != null)
            throw remote_err;
139 140
    }
    
141 142 143 144
    public override bool is_open() {
        return open;
    }
    
145
    // Subclasses should implement this to return their flavor of a GenericFolder with the
146 147
    // appropriate interfaces attached.  The returned folder should have its SpecialFolderType
    // set using either the properties from the local folder or its path.
148
    //
Eric Gregory's avatar
Eric Gregory committed
149
    // This won't be called to build the Outbox or search folder, but for all others (including Inbox) it will.
150
    protected abstract GenericFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
151
        ImapDB.Account local_account, ImapDB.Folder local_folder);
152
    
153
    private GenericFolder build_folder(ImapDB.Folder local_folder) {
154
        return Geary.Collection.get_first(build_folders(new Collection.SingleItem<ImapDB.Folder>(local_folder)));
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
    }

    private Gee.Collection<GenericFolder> build_folders(Gee.Collection<ImapDB.Folder> local_folders) {
        Gee.ArrayList<ImapDB.Folder> folders_to_build = new Gee.ArrayList<ImapDB.Folder>();
        Gee.ArrayList<GenericFolder> built_folders = new Gee.ArrayList<GenericFolder>();
        Gee.ArrayList<GenericFolder> return_folders = new Gee.ArrayList<GenericFolder>();

        foreach(ImapDB.Folder local_folder in local_folders) {
            if (existing_folders.has_key(local_folder.get_path()))
                return_folders.add(existing_folders.get(local_folder.get_path()));
            else
                folders_to_build.add(local_folder);
        }

        foreach(ImapDB.Folder folder_to_build in folders_to_build) {
            GenericFolder folder = new_folder(folder_to_build.get_path(), remote, local, folder_to_build);
            existing_folders.set(folder.get_path(), folder);
            built_folders.add(folder);
            return_folders.add(folder);
        }

        if (built_folders.size > 0)
            notify_folders_available_unavailable(built_folders, null);
178
        
179
        return return_folders;
180 181
    }
    
182 183 184 185
    public override Gee.Collection<Geary.Folder> list_matching_folders(
        Geary.FolderPath? parent) throws Error {
        check_open();
        
186 187 188 189 190
        Gee.ArrayList<Geary.Folder> matches = new Gee.ArrayList<Geary.Folder>();

        foreach(FolderPath path in existing_folders.keys) {
            FolderPath? path_parent = path.get_parent();
            if ((parent == null && path_parent == null) ||
Eric Gregory's avatar
Eric Gregory committed
191
                (parent != null && path_parent != null && path_parent.equal_to(parent))) {
192 193 194 195 196 197
                matches.add(existing_folders.get(path));
            }
        }
        return matches;
    }

198 199
    public override Gee.Collection<Geary.Folder> list_folders() throws Error {
        check_open();
Eric Gregory's avatar
Eric Gregory committed
200 201 202
        Gee.HashSet<Geary.Folder> all_folders = new Gee.HashSet<Geary.Folder>();
        all_folders.add_all(existing_folders.values);
        all_folders.add_all(local_only.values);
203
        
Eric Gregory's avatar
Eric Gregory committed
204
        return all_folders;
205 206
    }
    
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
    private void reschedule_folder_refresh(bool immediate) {
        if (in_refresh_enumerate)
            return;
        
        cancel_folder_refresh();
        
        refresh_folder_timeout_id = immediate
            ? Idle.add(on_refresh_folders)
            : Timeout.add_seconds(REFRESH_FOLDER_LIST_SEC, on_refresh_folders);
    }
    
    private void cancel_folder_refresh() {
        if (refresh_folder_timeout_id != 0) {
            Source.remove(refresh_folder_timeout_id);
            refresh_folder_timeout_id = 0;
        }
    }
    
    private bool on_refresh_folders() {
        in_refresh_enumerate = true;
        enumerate_folders_async.begin(null, refresh_cancellable, on_refresh_completed);
        
        refresh_folder_timeout_id = 0;
        
        return false;
    }
    
    private void on_refresh_completed(Object? source, AsyncResult result) {
        try {
            enumerate_folders_async.end(result);
        } catch (Error err) {
            if (!(err is IOError.CANCELLED))
                debug("Refresh of account %s folders did not complete: %s", to_string(), err.message);
        }
        
        in_refresh_enumerate = false;
        reschedule_folder_refresh(false);
    }
    
246
    private async void enumerate_folders_async(Geary.FolderPath? parent, Cancellable? cancellable = null)
247
        throws Error {
248 249 250
        check_open();
        
        Gee.Collection<ImapDB.Folder>? local_list = null;
Jim Nelson's avatar
Jim Nelson committed
251 252 253 254 255 256 257
        try {
            local_list = yield local.list_folders_async(parent, cancellable);
        } catch (EngineError err) {
            // don't pass on NOT_FOUND's, that means we need to go to the server for more info
            if (!(err is EngineError.NOT_FOUND))
                throw err;
        }
258 259
        
        Gee.Collection<Geary.Folder> engine_list = new Gee.ArrayList<Geary.Folder>();
Jim Nelson's avatar
Jim Nelson committed
260
        if (local_list != null && local_list.size > 0) {
261
            engine_list.add_all(build_folders(local_list));
Jim Nelson's avatar
Jim Nelson committed
262
        }
263
        
264
        // Add local folders (assume that local-only folders always go in root)
265
        if (parent == null)
266
            engine_list.add_all(local_only.values);
267
        
268
        background_update_folders.begin(parent, engine_list, cancellable);
269 270
    }
    
271 272 273 274
    public override Geary.ContactStore get_contact_store() {
        return local.contact_store;
    }
    
275 276
    public override async bool folder_exists_async(Geary.FolderPath path,
        Cancellable? cancellable = null) throws Error {
277 278
        check_open();
        
279 280 281 282 283 284
        if (yield local.folder_exists_async(path, cancellable))
            return true;
        
        return yield remote.folder_exists_async(path, cancellable);
    }
    
285
    // TODO: This needs to be made into a single transaction
Jim Nelson's avatar
Jim Nelson committed
286
    public override async Geary.Folder fetch_folder_async(Geary.FolderPath path,
287
        Cancellable? cancellable = null) throws Error {
288
        check_open();
289
        
290 291
        if (local_only.has_key(path))
            return local_only.get(path);
292
        
293
        try {
294
            return build_folder((ImapDB.Folder) yield local.fetch_folder_async(path, cancellable));
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
        } catch (EngineError err) {
            // don't thrown NOT_FOUND's, that means we need to fall through and clone from the
            // server
            if (!(err is EngineError.NOT_FOUND))
                throw err;
        }
        
        // clone the entire path
        int length = path.get_path_length();
        for (int ctr = 0; ctr < length; ctr++) {
            Geary.FolderPath folder = path.get_folder_at(ctr);
            
            if (yield local.folder_exists_async(folder))
                continue;
            
310
            Imap.Folder remote_folder = (Imap.Folder) yield remote.fetch_folder_async(folder,
311 312 313 314 315
                cancellable);
            
            yield local.clone_folder_async(remote_folder, cancellable);
        }
        
316
        // Fetch the local account's version of the folder for the GenericFolder
317
        return build_folder((ImapDB.Folder) yield local.fetch_folder_async(path, cancellable));
318 319
    }
    
Jim Nelson's avatar
Jim Nelson committed
320
    private async void background_update_folders(Geary.FolderPath? parent,
321
        Gee.Collection<Geary.Folder> engine_folders, Cancellable? cancellable) {
322
        Gee.Collection<Geary.Imap.Folder> remote_folders;
323
        try {
324
            remote_folders = yield remote.list_folders_async(parent, cancellable);
325
        } catch (Error remote_error) {
326 327 328
            debug("Unable to retrieve folder list from server: %s", remote_error.message);
            
            return;
329 330
        }
        
331
        // update all remote folders properties in the local store and active in the system
Eric Gregory's avatar
Eric Gregory committed
332
        Gee.HashSet<Geary.FolderPath> altered_paths = new Gee.HashSet<Geary.FolderPath>();
333
        foreach (Imap.Folder remote_folder in remote_folders) {
334 335 336 337 338 339
            // only worry about alterations if the remote is openable
            if (remote_folder.get_properties().is_openable.is_possible()) {
                ImapDB.Folder? local_folder = null;
                try {
                    local_folder = yield local.fetch_folder_async(remote_folder.get_path(), cancellable);
                } catch (Error err) {
340 341 342 343
                    if (!(err is EngineError.NOT_FOUND)) {
                        debug("Unable to fetch local folder for remote %s: %s", remote_folder.get_path().to_string(),
                            err.message);
                    }
344 345 346 347 348 349 350 351
                }
                
                if (local_folder != null) {
                    if (remote_folder.get_properties().have_contents_changed(local_folder.get_properties()).is_possible())
                        altered_paths.add(remote_folder.get_path());
                }
            }
            
352
            try {
353
                yield local.update_folder_status_async(remote_folder, cancellable);
354 355 356 357 358
            } catch (Error update_error) {
                debug("Unable to update local folder %s with remote properties: %s",
                    remote_folder.to_string(), update_error.message);
            }
        }
359
        
360
        // Get local paths of all engine (local) folders
Eric Gregory's avatar
Eric Gregory committed
361
        Gee.Set<Geary.FolderPath> local_paths = new Gee.HashSet<Geary.FolderPath>();
362 363 364 365
        foreach (Geary.Folder local_folder in engine_folders)
            local_paths.add(local_folder.get_path());
        
        // Get remote paths of all remote folders
Eric Gregory's avatar
Eric Gregory committed
366
        Gee.Set<Geary.FolderPath> remote_paths = new Gee.HashSet<Geary.FolderPath>();
367 368
        foreach (Geary.Imap.Folder remote_folder in remote_folders) {
            remote_paths.add(remote_folder.get_path());
369 370
            
            // use this iteration to add discovered properties to map
371 372 373
            properties_map.set(remote_folder.get_path(), remote_folder.get_properties());
            
            // also use this iteration to set the local folder's special type
374 375
            // (but only promote, not demote, since getting the special folder type via its
            // properties relies on the optional XLIST extension)
376
            GenericFolder? local_folder = existing_folders.get(remote_folder.get_path());
377
            if (local_folder != null && local_folder.get_special_folder_type() == SpecialFolderType.NONE)
378
                local_folder.set_special_folder_type(remote_folder.get_properties().attrs.get_special_folder_type());
379
        }
380
        
381
        // If path in remote but not local, need to add it
382 383
        Gee.List<Geary.Imap.Folder> to_add = new Gee.ArrayList<Geary.Imap.Folder>();
        foreach (Geary.Imap.Folder folder in remote_folders) {
384
            if (!local_paths.contains(folder.get_path()))
385 386 387
                to_add.add(folder);
        }
        
388 389
        // If path in local but not remote (and isn't local-only, i.e. the Outbox), need to remove
        // it
390 391
        Gee.List<Geary.Folder>? to_remove = new Gee.ArrayList<Geary.Imap.Folder>();
        foreach (Geary.Folder folder in engine_folders) {
392
            if (!remote_paths.contains(folder.get_path()) && !local_only.keys.contains(folder.get_path()))
393 394
                to_remove.add(folder);
        }
395 396 397 398 399 400 401
        
        if (to_add.size == 0)
            to_add = null;
        
        if (to_remove.size == 0)
            to_remove = null;
        
402
        // For folders to add, clone them and their properties locally
403
        if (to_add != null) {
404
            foreach (Geary.Imap.Folder folder in to_add) {
405
                try {
406
                    yield local.clone_folder_async(folder, cancellable);
407 408 409 410 411
                } catch (Error err) {
                    debug("Unable to add/remove folder %s: %s", folder.get_path().to_string(),
                        err.message);
                }
            }
412 413
        }
        
414
        // Create Geary.Folder objects for all added folders
415 416 417
        Gee.Collection<Geary.Folder> engine_added = null;
        if (to_add != null) {
            engine_added = new Gee.ArrayList<Geary.Folder>();
418 419

            Gee.ArrayList<ImapDB.Folder> folders_to_build = new Gee.ArrayList<ImapDB.Folder>();
420
            foreach (Geary.Imap.Folder remote_folder in to_add) {
421
                try {
422 423
                    folders_to_build.add((ImapDB.Folder) yield local.fetch_folder_async(
                        remote_folder.get_path(), cancellable));
424
                } catch (Error convert_err) {
425 426 427 428
                    // This isn't fatal, but irksome ... in the future, when local folders are
                    // removed, it's possible for one to disappear between cloning it and fetching
                    // it
                    debug("Unable to fetch local folder after cloning: %s", convert_err.message);
429 430
                }
            }
431 432

            engine_added.add_all(build_folders(folders_to_build));
433 434
        }
        
435 436 437 438 439 440 441
        // TODO: Remove local folders no longer available remotely.
        if (to_remove != null) {
            foreach (Geary.Folder folder in to_remove) {
                debug(@"Need to remove folder $folder");
            }
        }
        
442 443
        if (engine_added != null)
            notify_folders_added_removed(engine_added, null);
444 445 446 447 448 449 450 451 452 453 454 455 456 457 458
        
        // report all altered folders
        if (altered_paths.size > 0) {
            Gee.ArrayList<Geary.Folder> altered = new Gee.ArrayList<Geary.Folder>();
            foreach (Geary.FolderPath path in altered_paths) {
                if (existing_folders.has_key(path))
                    altered.add(existing_folders.get(path));
                else
                    debug("Unable to report %s altered: no local representation", path.to_string());
            }
            
            if (altered.size > 0)
                notify_folders_contents_altered(altered);
        }
        
459
        // enumerate children of each remote folder
460 461 462 463 464 465 466 467 468 469
        foreach (Imap.Folder remote_folder in remote_folders) {
            if (remote_folder.get_properties().has_children.is_possible()) {
                try {
                    yield enumerate_folders_async(remote_folder.get_path(), cancellable);
                } catch (Error err) {
                    debug("Unable to enumerate children of %s: %s", remote_folder.get_path().to_string(),
                        err.message);
                }
            }
        }
470
    }
471
    
472 473
    public override async void send_email_async(Geary.ComposedEmail composed,
        Cancellable? cancellable = null) throws Error {
474 475
        check_open();
        
476
        Geary.RFC822.Message rfc822 = new Geary.RFC822.Message.from_composed_email(composed);
477 478 479
        
        // don't use create_email_async() as that requires the folder be open to use
        yield local.outbox.enqueue_email_async(rfc822, cancellable);
480
    }
481 482 483 484

    private void on_email_sent(Geary.RFC822.Message rfc822) {
        notify_email_sent(rfc822);
    }
485
    
486 487
    public override async Gee.MultiMap<Geary.Email, Geary.FolderPath?>? local_search_message_id_async(
        Geary.RFC822.MessageID message_id, Geary.Email.Field requested_fields, bool partial_ok,
488
        Gee.Collection<Geary.FolderPath?>? folder_blacklist, Cancellable? cancellable = null) throws Error {
489 490 491 492
        return yield local.search_message_id_async(
            message_id, requested_fields, partial_ok, folder_blacklist, cancellable);
    }
    
493 494 495 496 497
    public override async Geary.Email local_fetch_email_async(Geary.EmailIdentifier email_id,
        Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error {
        return yield local.fetch_email_async(email_id, required_fields, cancellable);
    }
    
498 499
    public override async Gee.Collection<Geary.Email>? local_search_async(string keywords,
        Geary.Email.Field requested_fields, bool partial_ok,
Eric Gregory's avatar
Eric Gregory committed
500 501
        Gee.Collection<Geary.FolderPath?>? folder_blacklist = null,
        Gee.Collection<Geary.EmailIdentifier>? search_ids = null, Cancellable? cancellable = null) throws Error {
502
        return yield local.search_async(local.prepare_search_query(keywords),
503
            requested_fields, partial_ok, folder_blacklist, search_ids, cancellable);
Eric Gregory's avatar
Eric Gregory committed
504 505
    }
    
506
    private void on_login_failed(Geary.Credentials? credentials) {
507 508 509 510 511 512 513 514 515 516 517 518
        do_login_failed_async.begin(credentials);
    }
    
    private async void do_login_failed_async(Geary.Credentials? credentials) {
        try {
            if (yield information.fetch_passwords_async(CredentialsMediator.ServiceFlag.IMAP))
                return;
        } catch (Error e) {
            debug("Error prompting for IMAP password: %s", e.message);
        }
        
        notify_report_problem(Geary.Account.Problem.RECV_EMAIL_LOGIN_FAILED, null);
519
    }
520 521
}