Support Outlook.com IMAP: Closes #7479

This takes advantage of Outlook.com's new IMAP support, meaning
Geary works with Outlook.com, Hotmail, and Live.com users.

Because Outlook.com doesn't support UIDPLUS, some features
(in particular, draft auto-save) is unavailable, as is the
ability to use its Archive folder directly.

This patch fixes a couple of bugs that occur when a server doesn't
support UIDPLUS.  Also fixes a use case with UID increment/decrement
that caused a flag watcher bug due to ImapDB.Folder always
returning the same email if only one was present in the folder.
This commit is contained in:
Jim Nelson 2013-09-17 16:23:48 -07:00
parent 94861a0bef
commit c8b4f9e0e2
15 changed files with 242 additions and 47 deletions

View file

@ -201,6 +201,8 @@ engine/imap-engine/gmail/imap-engine-gmail-folder.vala
engine/imap-engine/gmail/imap-engine-gmail-search-folder.vala
engine/imap-engine/other/imap-engine-other-account.vala
engine/imap-engine/other/imap-engine-other-folder.vala
engine/imap-engine/outlook/imap-engine-outlook-account.vala
engine/imap-engine/outlook/imap-engine-outlook-folder.vala
engine/imap-engine/replay-ops/imap-engine-abstract-list-email.vala
engine/imap-engine/replay-ops/imap-engine-copy-email.vala
engine/imap-engine/replay-ops/imap-engine-create-email.vala

View file

@ -497,7 +497,7 @@ public class AddEditPage : Gtk.Box {
return false;
break;
// GMAIL and YAHOO
// GMAIL, YAHOO, and OUTLOOK
default:
if (Geary.String.is_empty_or_whitespace(nickname) ||
Geary.String.is_empty_or_whitespace(email_address) ||

View file

@ -21,13 +21,21 @@ public class Geary.AccountInformation : BaseObject {
private const string IMAP_PORT = "imap_port";
private const string IMAP_SSL = "imap_ssl";
private const string IMAP_STARTTLS = "imap_starttls";
private const string IMAP_PIPELINE = "imap_pipeline";
private const string SMTP_HOST = "smtp_host";
private const string SMTP_PORT = "smtp_port";
private const string SMTP_SSL = "smtp_ssl";
private const string SMTP_STARTTLS = "smtp_starttls";
private const string SMTP_NOAUTH = "smtp_noauth";
//
// "Retired" keys
//
/*
* key: "imap_pipeline"
* value: bool
*/
public const string SETTINGS_FILENAME = "geary.ini";
public const int DEFAULT_PREFETCH_PERIOD_DAYS = 14;
@ -42,7 +50,6 @@ public class Geary.AccountInformation : BaseObject {
public string nickname { get; set; }
public string email { get; set; }
public Geary.ServiceProvider service_provider { get; set; }
public bool imap_server_pipeline { get; set; default = true; }
public int prefetch_period_days { get; set; }
// Order for display purposes.
@ -98,8 +105,6 @@ public class Geary.AccountInformation : BaseObject {
if (ordinal >= default_ordinal)
default_ordinal = ordinal + 1;
imap_server_pipeline = get_bool_value(key_file, GROUP, IMAP_PIPELINE, true);
if (service_provider == ServiceProvider.OTHER) {
default_imap_server_host = get_string_value(key_file, GROUP, IMAP_HOST);
default_imap_server_port = get_uint16_value(key_file, GROUP, IMAP_PORT,
@ -120,11 +125,6 @@ public class Geary.AccountInformation : BaseObject {
}
}
}
// currently IMAP pipelining is *always* turned off with generic servers; see
// http://redmine.yorba.org/issues/5224
if (service_provider == Geary.ServiceProvider.OTHER)
imap_server_pipeline = false;
}
// Copies all data from the "from" object into this one.
@ -133,7 +133,6 @@ public class Geary.AccountInformation : BaseObject {
nickname = from.nickname;
email = from.email;
service_provider = from.service_provider;
imap_server_pipeline = from.imap_server_pipeline;
prefetch_period_days = from.prefetch_period_days;
ordinal = from.ordinal;
default_imap_server_host = from.default_imap_server_host;
@ -313,6 +312,9 @@ public class Geary.AccountInformation : BaseObject {
case ServiceProvider.YAHOO:
return ImapEngine.YahooAccount.IMAP_ENDPOINT;
case ServiceProvider.OUTLOOK:
return ImapEngine.OutlookAccount.IMAP_ENDPOINT;
case ServiceProvider.OTHER:
Endpoint.Flags imap_flags = Endpoint.Flags.GRACEFUL_DISCONNECT;
if (default_imap_server_ssl)
@ -336,6 +338,9 @@ public class Geary.AccountInformation : BaseObject {
case ServiceProvider.YAHOO:
return ImapEngine.YahooAccount.SMTP_ENDPOINT;
case ServiceProvider.OUTLOOK:
return ImapEngine.OutlookAccount.SMTP_ENDPOINT;
case ServiceProvider.OTHER:
Endpoint.Flags smtp_flags = Endpoint.Flags.GRACEFUL_DISCONNECT;
if (default_smtp_server_ssl)
@ -421,8 +426,6 @@ public class Geary.AccountInformation : BaseObject {
key_file.set_boolean(GROUP, SMTP_REMEMBER_PASSWORD_KEY, smtp_remember_password);
key_file.set_integer(GROUP, PREFETCH_PERIOD_DAYS_KEY, prefetch_period_days);
key_file.set_boolean(GROUP, IMAP_PIPELINE, imap_server_pipeline);
if (service_provider == ServiceProvider.OTHER) {
key_file.set_value(GROUP, IMAP_HOST, default_imap_server_host);
key_file.set_integer(GROUP, IMAP_PORT, default_imap_server_port);

View file

@ -322,6 +322,11 @@ public class Geary.Engine : BaseObject {
account_information, remote_account, local_account);
break;
case ServiceProvider.OUTLOOK:
account = new ImapEngine.OutlookAccount("Outlook:%s".printf(account_information.email),
account_information, remote_account, local_account);
break;
case ServiceProvider.OTHER:
account = new ImapEngine.OtherAccount("Other:%s".printf(account_information.email),
account_information, remote_account, local_account);

View file

@ -4,15 +4,25 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* A representation of the various built-in email service providers Geary supports.
*/
public enum Geary.ServiceProvider {
GMAIL,
YAHOO,
OUTLOOK,
OTHER;
public static ServiceProvider[] get_providers() {
return { GMAIL, YAHOO, OTHER };
return { GMAIL, YAHOO, OUTLOOK, OTHER };
}
/**
* Returns the service provider in a serialized form.
*
* @see from_string
*/
public string to_string() {
switch (this) {
case GMAIL:
@ -21,6 +31,9 @@ public enum Geary.ServiceProvider {
case YAHOO:
return "YAHOO";
case OUTLOOK:
return "OUTLOOK";
case OTHER:
return "OTHER";
@ -29,6 +42,10 @@ public enum Geary.ServiceProvider {
}
}
/**
* Returns the service provider's name in a translated UTF-8 string suitable for display to the
* user.
*/
public string display_name() {
switch (this) {
case GMAIL:
@ -37,6 +54,9 @@ public enum Geary.ServiceProvider {
case YAHOO:
return _("Yahoo! Mail");
case OUTLOOK:
return _("Outlook.com");
case OTHER:
return _("Other");
@ -45,6 +65,12 @@ public enum Geary.ServiceProvider {
}
}
/**
* Converts a string form of the service provider (returned by {@link to_string} to a
* {@link ServiceProvider} value.
*
* @see to_string
*/
public static ServiceProvider from_string(string str) {
switch (str.up()) {
case "GMAIL":
@ -53,6 +79,9 @@ public enum Geary.ServiceProvider {
case "YAHOO":
return YAHOO;
case "OUTLOOK":
return OUTLOOK;
case "OTHER":
return OTHER;

View file

@ -15,7 +15,8 @@ public enum Geary.SpecialFolderType {
ALL_MAIL,
SPAM,
TRASH,
OUTBOX;
OUTBOX,
ARCHIVE;
public unowned string get_display_name() {
switch (this) {
@ -49,6 +50,9 @@ public enum Geary.SpecialFolderType {
case SEARCH:
return _("Search");
case ARCHIVE:
return _("Archive");
case NONE:
default:
return _("None");

View file

@ -251,9 +251,9 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
// deal with exclusive searches
if (!including_id) {
if (oldest_to_newest)
start_uid = start_uid.next();
start_uid = start_uid.next(false);
else
start_uid = start_uid.previous();
start_uid = start_uid.previous(false);
}
} else if (oldest_to_newest) {
start_uid = new Imap.UID(Imap.UID.MIN);
@ -261,6 +261,9 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
start_uid = new Imap.UID(Imap.UID.MAX);
}
if (!start_uid.is_valid())
return Db.TransactionOutcome.DONE;
StringBuilder sql = new StringBuilder("""
SELECT MessageLocationTable.message_id, ordering, remove_marker
FROM MessageLocationTable
@ -332,11 +335,11 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
Imap.UID end_uid = end_location.uid;
if (!including_id) {
start_uid = start_uid.next();
end_uid = end_uid.previous();
start_uid = start_uid.next(false);
end_uid = end_uid.previous(false);
}
if (start_uid.compare_to(end_uid) > 0)
if (!start_uid.is_valid() || !end_uid.is_valid() || start_uid.compare_to(end_uid) > 0)
return Db.TransactionOutcome.DONE;
Db.Statement stmt = cx.prepare("""
@ -369,11 +372,11 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
Imap.UID end_uid = end;
if (!including_id) {
start_uid = start_uid.next();
end_uid = end_uid.previous();
start_uid = start_uid.next(false);
end_uid = end_uid.previous(false);
}
if (start_uid.compare_to(end_uid) > 0)
if (!start_uid.is_valid() || !end_uid.is_valid() || start_uid.compare_to(end_uid) > 0)
return null;
// Break up work so all reading isn't done in single transaction that locks up the

View file

@ -184,7 +184,7 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
// a full normalize works from the highest possible UID on the remote and work down to the lowest UID on
// the local; this covers all messages appended since last seen as well as any removed
Imap.UID last_uid = remote_properties.uid_next.previous();
Imap.UID last_uid = remote_properties.uid_next.previous(true);
// if the difference in UIDNEXT values equals the difference in message count, then only
// an append could have happened, so only pull in the new messages ... note that this is not foolproof,
@ -195,7 +195,7 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
// situation, esp. messages being removed.)
Imap.UID first_uid;
if (!is_dirty && uidnext_diff == (remote_message_count - local_message_count)) {
first_uid = local_latest_id.uid.next();
first_uid = local_latest_id.uid.next(true);
debug("%s: Messages only appended (local/remote UIDNEXT=%s/%s total=%d/%d diff=%s), gathering mail UIDs %s:%s",
to_string(), local_properties.uid_next.to_string(), remote_properties.uid_next.to_string(),
@ -1142,7 +1142,7 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
}
criteria.and(Imap.SearchCriterion.message_set(
new Imap.MessageSet.uid_range(new Imap.UID(Imap.UID.MIN), before_uid.previous())));
new Imap.MessageSet.uid_range(new Imap.UID(Imap.UID.MIN), before_uid.previous(true))));
}
Gee.List<Geary.Email> accumulator = new Gee.ArrayList<Geary.Email>();

View file

@ -0,0 +1,68 @@
/* Copyright 2013 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
private class Geary.ImapEngine.OutlookAccount : Geary.ImapEngine.GenericAccount {
private static Geary.Endpoint? _imap_endpoint = null;
public static Geary.Endpoint IMAP_ENDPOINT { get {
if (_imap_endpoint == null) {
_imap_endpoint = new Geary.Endpoint(
"imap-mail.outlook.com",
Imap.ClientConnection.DEFAULT_PORT_SSL,
Geary.Endpoint.Flags.SSL | Geary.Endpoint.Flags.GRACEFUL_DISCONNECT,
Imap.ClientConnection.RECOMMENDED_TIMEOUT_SEC);
}
return _imap_endpoint;
} }
private static Geary.Endpoint? _smtp_endpoint = null;
public static Geary.Endpoint SMTP_ENDPOINT { get {
if (_smtp_endpoint == null) {
_smtp_endpoint = new Geary.Endpoint(
"smtp-mail.outlook.com",
Smtp.ClientConnection.DEFAULT_PORT_STARTTLS,
Geary.Endpoint.Flags.STARTTLS | Geary.Endpoint.Flags.GRACEFUL_DISCONNECT,
Smtp.ClientConnection.DEFAULT_TIMEOUT_SEC);
}
return _smtp_endpoint;
} }
public OutlookAccount(string name, AccountInformation account_information, Imap.Account remote,
ImapDB.Account local) {
base (name, account_information, remote, local);
}
protected override GenericFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
ImapDB.Account local_account, ImapDB.Folder local_folder) {
// use the Folder's attributes to determine if it's a special folder type, unless it's
// INBOX; that's determined by name
SpecialFolderType special_folder_type;
if (Imap.MailboxSpecifier.folder_path_is_inbox(path))
special_folder_type = SpecialFolderType.INBOX;
else
special_folder_type = local_folder.get_properties().attrs.get_special_folder_type();
// generate properly-interfaced Folder depending on the special type
// Proper Drafts support depends on Outlook.com supporting UIDPLUS or us devising another
// mechanism to associate new messages with drafts-in-progress; see
// http://redmine.yorba.org/issues/7495
switch (special_folder_type) {
case SpecialFolderType.SENT:
return new GenericSentMailFolder(this, remote_account, local_account, local_folder,
special_folder_type);
case SpecialFolderType.TRASH:
return new GenericTrashFolder(this, remote_account, local_account, local_folder,
special_folder_type);
default:
return new OutlookFolder(this, remote_account, local_account, local_folder,
special_folder_type);
}
}
}

View file

@ -0,0 +1,18 @@
/* Copyright 2013 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
private class Geary.ImapEngine.OutlookFolder : GenericFolder, Geary.FolderSupport.Remove {
public OutlookFolder(OutlookAccount account, Imap.Account remote, ImapDB.Account local,
ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
base (account, remote, local, local_folder, special_folder_type);
}
public async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
Cancellable? cancellable = null) throws Error {
yield expunge_email_async(email_ids, cancellable);
}
}

View file

@ -506,7 +506,7 @@ private class Geary.Imap.Folder : BaseObject {
StoreCommand store_cmd = new StoreCommand(msg_set, flags, true, false);
cmds.add(store_cmd);
if (session.capabilities.has_capability(Capabilities.UIDPLUS))
if (msg_set.is_uid && session.capabilities.supports_uidplus())
cmds.add(new ExpungeCommand.uid(msg_set));
else
cmds.add(new ExpungeCommand());
@ -564,7 +564,7 @@ private class Geary.Imap.Folder : BaseObject {
flags.add(MessageFlag.DELETED);
cmds.add(new StoreCommand(msg_set, flags, true, false));
if (msg_set.is_uid)
if (msg_set.is_uid && session.capabilities.supports_uidplus())
cmds.add(new ExpungeCommand.uid(msg_set));
else
cmds.add(new ExpungeCommand());

View file

@ -32,29 +32,33 @@ public class Geary.Imap.UID : Geary.MessageData.Int64MessageData, Geary.Imap.Mes
}
/**
* Returns a valid UID, which means returning MIN or MAX if the value is out of range (either
* direction) or MAX if this value is already MAX.
* Returns the UID logically next (or after) this one.
*
* If clamped this always returns a valid UID, which means returning MIN or MAX if
* the value is out of range (either direction) or MAX if this value is already MAX.
*
* Otherwise, it may return an invalid UID and should be verified before using.
*
* @see previous
* @see is_valid
*/
public UID next() {
if (value < MIN)
return new UID(MIN);
else if (value > MAX)
return new UID(MAX);
else
return new UID(Numeric.int64_ceiling(value + 1, MAX));
public UID next(bool clamped) {
return clamped ? new UID((value + 1).clamp(MIN, MAX)) : new UID(value + 1);
}
/**
* Returns a valid UID, which means returning MIN or MAX if the value is out of range (either
* direction) or MIN if this value is already MIN.
* Returns the UID logically previous (or before) this one.
*
* If clamped this always returns a valid UID, which means returning MIN or MAX if
* the value is out of range (either direction) or MIN if this value is already MIN.
*
* Otherwise, it may return a UID where {@link is_valid} returns false.
*
* @see next
* @see is_valid
*/
public UID previous() {
if (value < MIN)
return new UID(MIN);
else if (value > MAX)
return new UID(MAX);
else
return new UID(Numeric.int64_floor(value - 1, MIN));
public UID previous(bool clamped) {
return clamped ? new UID((value - 1).clamp(MIN, MAX)) : new UID(value - 1);
}
public virtual int compare_to(Geary.Imap.UID other) {

View file

@ -35,5 +35,23 @@ public class Geary.Imap.Capabilities : Geary.GenericCapabilities {
public override string to_string() {
return "#%d: %s".printf(revision, base.to_string());
}
/**
* Indicates the {@link ClientSession} reported support for IDLE.
*
* See [[https://tools.ietf.org/html/rfc2177]]
*/
public bool supports_idle() {
return has_capability(IDLE);
}
/**
* Indicates the {@link ClientSession} reported support for UIDPLUS.
*
* See [[https://tools.ietf.org/html/rfc4315]]
*/
public bool supports_uidplus() {
return has_capability(UIDPLUS);
}
}

View file

@ -134,6 +134,30 @@ public class Geary.Imap.MailboxAttribute : Geary.Imap.Flag {
return _xlist_important;
} }
private static MailboxAttribute? _special_use_all = null;
public static MailboxAttribute SPECIAL_FOLDER_ALL { get {
return (_special_use_all != null) ? _special_use_all
: _special_use_all = new MailboxAttribute("\\All");
} }
private static MailboxAttribute? _special_use_archive = null;
public static MailboxAttribute SPECIAL_FOLDER_ARCHIVE { get {
return (_special_use_archive != null) ? _special_use_archive
: _special_use_archive = new MailboxAttribute("\\Archive");
} }
private static MailboxAttribute? _special_use_flagged = null;
public static MailboxAttribute SPECIAL_FOLDER_FLAGGED { get {
return (_special_use_flagged != null) ? _special_use_flagged
: _special_use_flagged = new MailboxAttribute("\\Flagged");
} }
private static MailboxAttribute? _special_use_junk = null;
public static MailboxAttribute SPECIAL_FOLDER_JUNK { get {
return (_special_use_junk != null) ? _special_use_junk
: _special_use_junk = new MailboxAttribute("\\Junk");
} }
public MailboxAttribute(string value) {
base (value);
}
@ -155,6 +179,10 @@ public class Geary.Imap.MailboxAttribute : Geary.Imap.Flag {
to_init = SPECIAL_FOLDER_SPAM;
to_init = SPECIAL_FOLDER_STARRED;
to_init = SPECIAL_FOLDER_TRASH;
to_init = SPECIAL_FOLDER_ALL;
to_init = SPECIAL_FOLDER_ARCHIVE;
to_init = SPECIAL_FOLDER_FLAGGED;
to_init = SPECIAL_FOLDER_JUNK;
}
}

View file

@ -72,6 +72,19 @@ public class Geary.Imap.MailboxAttributes : Geary.Imap.Flags {
if (contains(MailboxAttribute.SPECIAL_FOLDER_IMPORTANT))
return Geary.SpecialFolderType.IMPORTANT;
if (contains(MailboxAttribute.SPECIAL_FOLDER_ALL))
return Geary.SpecialFolderType.ALL_MAIL;
// TODO: Convert into SpecialFolderType.ARCHIVE (to support services that have an Archive
// folder that isn't an All Mail folder, i.e. Outlook.com):
// http://redmine.yorba.org/issues/7492
if (contains(MailboxAttribute.SPECIAL_FOLDER_FLAGGED))
return Geary.SpecialFolderType.FLAGGED;
if (contains(MailboxAttribute.SPECIAL_FOLDER_JUNK))
return Geary.SpecialFolderType.SPAM;
return Geary.SpecialFolderType.NONE;
}
}