diff --git a/src/engine/api/geary-engine.vala b/src/engine/api/geary-engine.vala index 749f0ad5..69089c08 100644 --- a/src/engine/api/geary-engine.vala +++ b/src/engine/api/geary-engine.vala @@ -284,10 +284,7 @@ public class Geary.Engine : BaseObject { (security, cx) => account.untrusted_host(service, security, cx) ); - var client = new Imap.ClientSession( - endpoint, - Imap.ClientService.new_quirks_for_provider(account.service_provider) - ); + var client = new Imap.ClientSession(endpoint, new Imap.Quirks()); GLib.Error? imap_err = null; try { yield client.connect_async( diff --git a/src/engine/imap/api/imap-client-service.vala b/src/engine/imap/api/imap-client-service.vala index 21a2e4aa..044a7c05 100644 --- a/src/engine/imap/api/imap-client-service.vala +++ b/src/engine/imap/api/imap-client-service.vala @@ -39,36 +39,6 @@ public class Geary.Imap.ClientService : Geary.ClientService { private const int CHECK_NOOP_THRESHOLD_SEC = 5; - public static Quirks new_quirks_for_provider(ServiceProvider provider) { - var quirks = new Quirks(); - switch (provider) { - case GMAIL: - // As of 2020-05-02, GMail doesn't seem to quote flag - // atoms containing reserved characters, and at least one - // use of both `]` and ` ` have been found. This works - // around the former. See #746 - quirks.flag_atom_exceptions = "]"; - break; - - case ServiceProvider.OUTLOOK: - // As of June 2016, outlook.com's IMAP servers have a bug - // where a large number (~50) of pipelined STATUS commands - // on mailboxes with many messages will eventually cause - // it to break command parsing and return a BAD response, - // causing us to drop the connection. Limit the number of - // pipelined commands per batch to work around this. See - // b.g.o Bug 766552 - quirks.max_pipeline_batch_size = 25; - break; - - default: - // noop - break; - } - return quirks; - } - - /** * Set to zero or negative value if keepalives should be disabled when a connection has not * selected a mailbox. (This is not recommended.) @@ -131,7 +101,7 @@ public class Geary.Imap.ClientService : Geary.ClientService { get { return LOGGING_DOMAIN; } } - private Quirks quirks; + private Quirks quirks = new Quirks(); private Nonblocking.Mutex sessions_mutex = new Nonblocking.Mutex(); private Gee.Set all_sessions = @@ -147,7 +117,6 @@ public class Geary.Imap.ClientService : Geary.ClientService { ServiceInformation configuration, Endpoint remote) { base(account, configuration, remote); - this.quirks = new_quirks_for_provider(account.service_provider); } /** @@ -403,6 +372,7 @@ public class Geary.Imap.ClientService : Geary.ClientService { // An error was thrown, so close the pool this.close_pool.begin(true); } else { + this.quirks.update_for_server(new_session); try { yield this.sessions_mutex.execute_locked(() => { this.all_sessions.add(new_session); diff --git a/src/engine/imap/api/imap-quirks.vala b/src/engine/imap/api/imap-quirks.vala index 79ef3267..c24ae55b 100644 --- a/src/engine/imap/api/imap-quirks.vala +++ b/src/engine/imap/api/imap-quirks.vala @@ -38,5 +38,83 @@ public class Geary.Imap.Quirks : BaseObject { */ public uint max_pipeline_batch_size { get; set; default = 0; } + /** + * The value sent by the server for missing envelope mailbox local parts. + * + * IMAP FETCH ENVELOPE structures use NIL for the "mailbox name" + * part (in addition to a NIL "host name" part) as an end-of-list + * marker for RFC822 group syntax. To indicate a missing + * local-part in a non-group mailbox some mail servers use a + * string such as "MISSING_MAILBOX" rather than the empty string. + */ + public string empty_envelope_mailbox_name { get; set; default = ""; } + + /** + * The value sent by the server for missing envelope mailbox domains. + * + * IMAP FETCH ENVELOPE structures use NIL for the "host name" + * argument to indicate RFC822 group syntax. To indicate a missing + * some mail servers use a string such as "MISSING_DOMAIN" rather + * than the empty string. + */ + public string empty_envelope_host_name { get; set; default = ""; } + + + public void update_for_server(ClientSession session) { + if (session.server_greeting != null) { + var greeting = session.server_greeting.get_text() ?? ""; + if (greeting.has_prefix("Gimap")) { + update_for_gmail(); + } else if (greeting.has_prefix("The Microsoft Exchange")) { + update_for_outlook(); + } else if (greeting.has_prefix("Dovecot")) { + update_for_dovecot(); + } + } + } + + /** + * Updates this quirks object with known quirks for GMail. + * + * As of 2020-05-02, GMail doesn't seem to quote flag + * atoms containing reserved characters, and at least one + * use of both `]` and ` ` have been found. This works + * around the former. + * + * See [[https://gitlab.gnome.org/GNOME/geary/-/issues/746]] + */ + public void update_for_gmail() { + this.flag_atom_exceptions = "]"; + } + + /** + * Updates this quirks object with known quirks for Outlook.com. + * + * As of June 2016, outlook.com's IMAP servers have a bug where a + * large number (~50) of pipelined STATUS commands on mailboxes + * with many messages will eventually cause it to break command + * parsing and return a BAD response, causing us to drop the + * connection. Limit the number of pipelined commands per batch to + * work around this. + * + * See [[https://bugzilla.gnome.org/show_bug.cgi?id=766552]] + */ + public void update_for_outlook() { + this.max_pipeline_batch_size = 25; + } + + /** + * Updates this quirks object with known quirks for Dovecot + * + * Dovecot 2.3.4.1 and earlier uses "MISSING_MAILBOX" and + * "MISSING_DOMAIN" in the address structures of FETCH ENVELOPE + * replies when the mailbox or domain is missing. + * + * See [[https://dovecot.org/pipermail/dovecot/2020-August/119658.html]] + */ + public void update_for_dovecot() { + this.empty_envelope_mailbox_name = "MISSING_MAILBOX"; + this.empty_envelope_host_name = "MISSING_DOMAIN"; + } } diff --git a/src/engine/imap/message/imap-fetch-data-specifier.vala b/src/engine/imap/message/imap-fetch-data-specifier.vala index 5d05c674..7f60acc7 100644 --- a/src/engine/imap/message/imap-fetch-data-specifier.vala +++ b/src/engine/imap/message/imap-fetch-data-specifier.vala @@ -142,7 +142,7 @@ public enum Geary.Imap.FetchDataSpecifier { * * @return null if no FetchDataDecoder is associated with this value, or an invalid value. */ - public FetchDataDecoder? get_decoder() { + public FetchDataDecoder? get_decoder(Quirks quirks) { switch (this) { case UID: return new UIDDecoder(); @@ -151,7 +151,7 @@ public enum Geary.Imap.FetchDataSpecifier { return new MessageFlagsDecoder(); case ENVELOPE: - return new EnvelopeDecoder(); + return new EnvelopeDecoder(quirks); case INTERNALDATE: return new InternalDateDecoder(); diff --git a/src/engine/imap/parameter/imap-list-parameter.vala b/src/engine/imap/parameter/imap-list-parameter.vala index d580830a..777d42fc 100644 --- a/src/engine/imap/parameter/imap-list-parameter.vala +++ b/src/engine/imap/parameter/imap-list-parameter.vala @@ -29,6 +29,17 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter { private Gee.List list = new Gee.ArrayList(); + /** Constructs a new, empty list. */ + public ListParameter() { + // noop + } + + /** Constructs a new list wit a single parameter. */ + public ListParameter.single(Parameter param) { + base(); + add(param); + } + /** * Appends a parameter to the end of this list. * diff --git a/src/engine/imap/response/imap-continuation-response.vala b/src/engine/imap/response/imap-continuation-response.vala index 8fc0a7cc..d15fe45f 100644 --- a/src/engine/imap/response/imap-continuation-response.vala +++ b/src/engine/imap/response/imap-continuation-response.vala @@ -15,8 +15,10 @@ */ public class Geary.Imap.ContinuationResponse : ServerResponse { - private ContinuationResponse() { - base (Tag.get_continuation()); + + + private ContinuationResponse(Quirks quirks) { + base(Tag.get_continuation(), quirks); } /** @@ -25,9 +27,9 @@ public class Geary.Imap.ContinuationResponse : ServerResponse { * The supplied root is "stripped" of its children. This may happen even if an exception is * thrown. It's recommended to use {@link is_continuation_response} prior to this call. */ - public ContinuationResponse.migrate(RootParameters root) throws ImapError { - base.migrate(root); - + public ContinuationResponse.migrate(RootParameters root, Quirks quirks) + throws ImapError { + base.migrate(root, quirks); if (!tag.is_continuation()) throw new ImapError.INVALID("Tag %s is not a continuation", tag.to_string()); } @@ -37,8 +39,6 @@ public class Geary.Imap.ContinuationResponse : ServerResponse { */ public static bool is_continuation_response(RootParameters root) { Tag? tag = root.get_tag(); - return tag != null ? tag.is_continuation() : false; } } - diff --git a/src/engine/imap/response/imap-fetch-data-decoder.vala b/src/engine/imap/response/imap-fetch-data-decoder.vala index 1c35d99a..4701a1da 100644 --- a/src/engine/imap/response/imap-fetch-data-decoder.vala +++ b/src/engine/imap/response/imap-fetch-data-decoder.vala @@ -124,8 +124,14 @@ public class Geary.Imap.RFC822SizeDecoder : Geary.Imap.FetchDataDecoder { } public class Geary.Imap.EnvelopeDecoder : Geary.Imap.FetchDataDecoder { - public EnvelopeDecoder() { - base (FetchDataSpecifier.ENVELOPE); + + + private Quirks quirks; + + + public EnvelopeDecoder(Quirks quirks) { + base(FetchDataSpecifier.ENVELOPE); + this.quirks = quirks; } protected override MessageData decode_list(ListParameter listp) throws ImapError { @@ -150,7 +156,7 @@ public class Geary.Imap.EnvelopeDecoder : Geary.Imap.FetchDataDecoder { try { sent_date = new RFC822.Date.from_rfc822_string(sent.ascii); } catch (GLib.Error err) { - debug( + warning( "Error parsing sent date from FETCH envelope: %s", err.message ); @@ -179,14 +185,22 @@ public class Geary.Imap.EnvelopeDecoder : Geary.Imap.FetchDataDecoder { ListParameter fields = listp.get_as_empty_list(ctr); StringParameter? name = fields.get_as_nullable_string(0); StringParameter? source_route = fields.get_as_nullable_string(1); - StringParameter mailbox = fields.get_as_empty_string(2); - StringParameter domain = fields.get_as_empty_string(3); + StringParameter? mailbox = fields.get_as_empty_string(2); + StringParameter? domain = fields.get_as_empty_string(3); + + if (mailbox.ascii == this.quirks.empty_envelope_mailbox_name) { + mailbox = null; + } + if (domain.ascii == this.quirks.empty_envelope_host_name) { + domain = null; + } Geary.RFC822.MailboxAddress addr = new Geary.RFC822.MailboxAddress.imap( (name != null) ? name.nullable_ascii : null, (source_route != null) ? source_route.nullable_ascii : null, - mailbox.ascii, - domain.ascii); + mailbox != null ? mailbox.ascii : "", + domain != null ? domain.ascii : "" + ); list.add(addr); } diff --git a/src/engine/imap/response/imap-fetched-data.vala b/src/engine/imap/response/imap-fetched-data.vala index 7bc47d57..7f377396 100644 --- a/src/engine/imap/response/imap-fetched-data.vala +++ b/src/engine/imap/response/imap-fetched-data.vala @@ -73,7 +73,7 @@ public class Geary.Imap.FetchedData : Object { fetched_data.body_data_map.set(specifier, Memory.EmptyBuffer.instance); } else { FetchDataSpecifier data_item = FetchDataSpecifier.from_parameter(data_item_param); - FetchDataDecoder? decoder = data_item.get_decoder(); + FetchDataDecoder? decoder = data_item.get_decoder(server_data.quirks); if (decoder == null) { debug("Unable to decode fetch response for \"%s\": No decoder available", data_item.to_string()); diff --git a/src/engine/imap/response/imap-server-data.vala b/src/engine/imap/response/imap-server-data.vala index 29aee4b5..f9e0edc7 100644 --- a/src/engine/imap/response/imap-server-data.vala +++ b/src/engine/imap/response/imap-server-data.vala @@ -13,8 +13,8 @@ public class Geary.Imap.ServerData : ServerResponse { public ServerDataType server_data_type { get; private set; } - private ServerData(Tag tag, ServerDataType server_data_type) { - base (tag); + private ServerData(Tag tag, ServerDataType server_data_type, Quirks quirks) { + base(tag, quirks); this.server_data_type = server_data_type; } @@ -25,8 +25,9 @@ public class Geary.Imap.ServerData : ServerResponse { * The supplied root is "stripped" of its children. This may happen even if an exception is * thrown. It's recommended to use {@link is_server_data} prior to this call. */ - public ServerData.migrate(RootParameters root) throws ImapError { - base.migrate(root); + public ServerData.migrate(RootParameters root, Quirks quirks) + throws ImapError { + base.migrate(root, quirks); server_data_type = ServerDataType.from_response(this); } diff --git a/src/engine/imap/response/imap-server-response.vala b/src/engine/imap/response/imap-server-response.vala index 7d2ace51..5f33568f 100644 --- a/src/engine/imap/response/imap-server-response.vala +++ b/src/engine/imap/response/imap-server-response.vala @@ -14,10 +14,15 @@ */ public abstract class Geary.Imap.ServerResponse : RootParameters { - public Tag tag { get; private set; } - protected ServerResponse(Tag tag) { + + public Tag tag { get; private set; } + public Quirks quirks { get; private set; } + + + protected ServerResponse(Tag tag, Quirks quirks) { this.tag = tag; + this.quirks = quirks; } /** @@ -25,36 +30,16 @@ public abstract class Geary.Imap.ServerResponse : RootParameters { * * The supplied root is "stripped" of its children. */ - protected ServerResponse.migrate(RootParameters root) throws ImapError { + protected ServerResponse.migrate(RootParameters root, + Quirks quirks) + throws ImapError { base.migrate(root); + this.quirks = quirks; - if (!has_tag()) + if (!has_tag()) { throw new ImapError.INVALID("Server response does not have a tag token: %s", to_string()); - - tag = get_tag(); + } + this.tag = get_tag(); } - /** - * Migrate the contents of RootParameters into a new, properly-typed ServerResponse. - * - * The returned ServerResponse may be a {@link ContinuationResponse}, {@link ServerData}, - * or a generic {@link StatusResponse}. - * - * The RootParameters will be migrated and stripped clean upon exit. - * - * @throws ImapError.PARSE_ERROR if not a known form of ServerResponse. - */ - public static ServerResponse migrate_from_server(RootParameters root) throws ImapError { - if (ContinuationResponse.is_continuation_response(root)) - return new ContinuationResponse.migrate(root); - - if (StatusResponse.is_status_response(root)) - return new StatusResponse.migrate(root); - - if (ServerData.is_server_data(root)) - return new ServerData.migrate(root); - - throw new ImapError.PARSE_ERROR("Unknown server response: %s", root.to_string()); - } } - diff --git a/src/engine/imap/response/imap-status-response.vala b/src/engine/imap/response/imap-status-response.vala index 2b5c7336..25e2a545 100644 --- a/src/engine/imap/response/imap-status-response.vala +++ b/src/engine/imap/response/imap-status-response.vala @@ -11,8 +11,6 @@ * StatusResponses may be tagged or untagged, depending on their nature. * * See [[http://tools.ietf.org/html/rfc3501#section-7.1]] for more information. - * - * @see ServerResponse.migrate_from_server */ public class Geary.Imap.StatusResponse : ServerResponse { @@ -34,8 +32,11 @@ public class Geary.Imap.StatusResponse : ServerResponse { */ public ResponseCode? response_code { get; private set; } - private StatusResponse(Tag tag, Status status, ResponseCode? response_code) { - base (tag); + private StatusResponse(Tag tag, + Status status, + ResponseCode? response_code, + Quirks quirks) { + base(tag, quirks); this.status = status; this.response_code = response_code; @@ -48,8 +49,9 @@ public class Geary.Imap.StatusResponse : ServerResponse { * The supplied root is "stripped" of its children. This may happen even if an exception is * thrown. It's recommended to use {@link is_status_response} prior to this call. */ - public StatusResponse.migrate(RootParameters root) throws ImapError { - base.migrate(root); + public StatusResponse.migrate(RootParameters root, Quirks quirks) + throws ImapError { + base.migrate(root, quirks); status = Status.from_parameter(get_as_string(1)); response_code = get_if_list(2) as ResponseCode; diff --git a/src/engine/imap/transport/imap-client-connection.vala b/src/engine/imap/transport/imap-client-connection.vala index 756d7bb7..927ac3bd 100644 --- a/src/engine/imap/transport/imap-client-connection.vala +++ b/src/engine/imap/transport/imap-client-connection.vala @@ -467,26 +467,24 @@ public class Geary.Imap.ClientConnection : BaseObject, Logging.Source { private void on_parameters_ready(RootParameters root) { try { - ServerResponse response = ServerResponse.migrate_from_server(root); - GLib.Type type = response.get_type(); - if (type == typeof(StatusResponse)) { - on_status_response((StatusResponse) response); - } else if (type == typeof(ServerData)) { - on_server_data((ServerData) response); - } else if (type == typeof(ContinuationResponse)) { - on_continuation_response((ContinuationResponse) response); + // Important! The order of these tests matters. + if (ContinuationResponse.is_continuation_response(root)) { + on_continuation_response( + new ContinuationResponse.migrate(root, this.quirks) + ); + } else if (StatusResponse.is_status_response(root)) { + on_status_response(new StatusResponse.migrate(root, this.quirks)); + } else if (ServerData.is_server_data(root)) { + on_server_data(new ServerData.migrate(root, this.quirks)); } else { - warning( - "Unknown ServerResponse of type %s received: %s:", - response.get_type().name(), - response.to_string() + throw new ImapError.PARSE_ERROR( + "Unknown server response: %s", root.to_string() ); } } catch (ImapError err) { received_bad_response(root, err); } - if (this.pending_queue.is_empty && this.sent_queue.is_empty) { // There's nothing remaining to send, and every sent // command has been dealt with, so ready an IDLE command. diff --git a/src/engine/imap/transport/imap-client-session.vala b/src/engine/imap/transport/imap-client-session.vala index 68e6f06e..27e84837 100644 --- a/src/engine/imap/transport/imap-client-session.vala +++ b/src/engine/imap/transport/imap-client-session.vala @@ -245,6 +245,14 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { get { return this.capabilities.has_capability(Capabilities.IDLE); } } + /** + * The server's greeting, if any. + * + * This will be null up until the session has successfully + * connected and the server has responded with a greeting. + */ + public StatusResponse? server_greeting { get; private set; default = null; } + /** The currently selected mailbox, if any. */ public MailboxSpecifier? selected_mailbox = null; @@ -846,9 +854,12 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { new_state = State.LOGOUT; } + this.server_greeting = status_response; + debug("Server greeting: %s", status_response.get_text()); + try { - connect_waiter.notify(); - } catch (Error err) { + this.connect_waiter.notify(); + } catch (GLib.Error err) { warning( "Unable to notify connect_waiter of connection: %s", err.message diff --git a/src/engine/rfc822/rfc822-mailbox-address.vala b/src/engine/rfc822/rfc822-mailbox-address.vala index 173312a7..1e23fca4 100644 --- a/src/engine/rfc822/rfc822-mailbox-address.vala +++ b/src/engine/rfc822/rfc822-mailbox-address.vala @@ -22,6 +22,8 @@ public class Geary.RFC822.MailboxAddress : Gee.Hashable, DecodedMessageData { + private static Regex? email_regex = null; + private static unichar[] ATEXT = { '!', '#', '$', '%', '&', '\'', '*', '+', '-', '/', '=', '?', '^', '_', '`', '{', '|', '}', '~' @@ -29,17 +31,20 @@ public class Geary.RFC822.MailboxAddress : /** Determines if a string contains a valid RFC822 mailbox address. */ public static bool is_valid_address(string address) { - try { - // http://www.regular-expressions.info/email.html - // matches john@dep.aol.museum not john@aol...com - Regex email_regex = - new Regex("[A-Z0-9._%+-]+@((?:[A-Z0-9-]+\\.)+[A-Z]{2}|localhost)", - RegexCompileFlags.CASELESS); - return email_regex.match(address); - } catch (RegexError e) { - debug("Regex error validating email address: %s", e.message); - return false; + if (MailboxAddress.email_regex == null) { + try { + // http://www.regular-expressions.info/email.html + // matches john@dep.aol.museum not john@aol...com + MailboxAddress.email_regex = new Regex( + "[A-Z0-9._%+-]+@((?:[A-Z0-9-]+\\.)+[A-Z]{2}|localhost)", + RegexCompileFlags.CASELESS + ); + } catch (RegexError e) { + warning("Regex error validating email address: %s", e.message); + return false; + } } + return MailboxAddress.email_regex.match(address); } private static string decode_name(string name) { @@ -206,7 +211,7 @@ public class Geary.RFC822.MailboxAddress : * * The given name (if any) and address parts will be used * verbatim, and quoted or encoded if needed when serialising to - * an RFC 833 mailbox address string. + * an RFC 822 mailbox address string. */ public MailboxAddress(string? name, string address) { this.name = name; @@ -228,7 +233,18 @@ public class Geary.RFC822.MailboxAddress : this.source_route = source_route; this.mailbox = decode_address_part(mailbox); this.domain = domain; - this.address = "%s@%s".printf(mailbox, domain); + + bool empty_mailbox = String.is_empty_or_whitespace(mailbox); + bool empty_domain = String.is_empty_or_whitespace(domain); + if (!empty_mailbox && !empty_domain) { + this.address = "%s@%s".printf(mailbox, domain); + } else if (empty_mailbox) { + this.address = domain; + } else if (empty_domain) { + this.address = mailbox; + } else { + this.address = ""; + } } public MailboxAddress.from_rfc822_string(string rfc822) throws Error { @@ -261,9 +277,11 @@ public class Geary.RFC822.MailboxAddress : // GMime strips source route for us, so the address part // should only ever contain a single '@' string? name = mailbox.get_name(); - if (name != "") { - this.name = decode_name(name); - } + this.name = ( + !String.is_empty_or_whitespace(name) + ? decode_name(name) + : null + ); string address = mailbox.get_addr(); int atsign = Ascii.last_index_of(address, '@'); @@ -281,7 +299,7 @@ public class Geary.RFC822.MailboxAddress : } else { this.mailbox = ""; this.domain = ""; - this.address = address; + this.address = decode_address_part(address); } } @@ -497,15 +515,33 @@ public class Geary.RFC822.MailboxAddress : // GMime.utils_header_encode_text will use MIME encoding, // which is disallowed in mailboxes by RFC 2074 §5. So quote // manually. - string local_part = this.mailbox; - if (local_part_needs_quoting(local_part)) { - local_part = quote_string(local_part); + var address = ""; + if (this.mailbox != "") { + address = this.mailbox; + if (local_part_needs_quoting(address)) { + address = quote_string(address); + } } - return "%s@%s".printf( - local_part, - // XXX Need to punycode international domains. - this.domain - ); + if (this.domain != "") { + address = "%s@%s".printf( + address, + // XXX Need to punycode international domains. + this.domain + ); + } + if (address == "") { + // Both mailbox and domain are empty, i.e. there was no + // '@' symbol in the address, so just assume the address + // is a mailbox since this is not uncommon practice on + // UNIX systems where mail is sent from a local account, + // and it supports a greater range of characters than the + // domain component + address = this.address; + if (local_part_needs_quoting(address)) { + address = quote_string(address); + } + } + return address; } /** diff --git a/test/engine/imap/response/imap-fetch-data-decoder-test.vala b/test/engine/imap/response/imap-fetch-data-decoder-test.vala new file mode 100644 index 00000000..c80c91ee --- /dev/null +++ b/test/engine/imap/response/imap-fetch-data-decoder-test.vala @@ -0,0 +1,135 @@ +/* + * Copyright 2019 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +class Geary.Imap.FetchDataDecoderTest : TestCase { + + + public FetchDataDecoderTest() { + base("Geary.Imap.FetchDataDecoderTest"); + add_test("envelope_basic", envelope_basic); + add_test( + "envelope_mailbox_missing_mailbox_name_quirk", + envelope_mailbox_missing_mailbox_name_quirk + ); + add_test( + "envelope_mailbox_missing_host_name_quirk", + envelope_mailbox_missing_host_name_quirk + ); + } + + public void envelope_basic() throws GLib.Error { + ListParameter env = new ListParameter(); + env.add(new QuotedStringParameter("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)")); + env.add(new QuotedStringParameter("Test subject")); + + // From + env.add(new ListParameter.single(new_mailbox_structure("From", "from", "example.com"))); + + // Sender + env.add(new ListParameter.single(new_mailbox_structure("From", "from", "example.com"))); + + // Reply-To + env.add(new ListParameter.single(new_mailbox_structure("From", "from", "example.com"))); + + env.add(new ListParameter.single(new_mailbox_structure("To", "to", "example.com"))); + env.add(new ListParameter.single(new_mailbox_structure("Cc", "cc", "example.com"))); + env.add(new ListParameter.single(new_mailbox_structure("Bcc", "bcc", "example.com"))); + + // In-Reply-To + env.add(new QuotedStringParameter("<1234@example.com>")); + + // Message-Id + env.add(new QuotedStringParameter("<5678@example.com>")); + + var test_article = new EnvelopeDecoder(new Quirks()); + var decoded_generic = test_article.decode(env); + var decoded = decoded_generic as Envelope; + + assert_non_null(decoded, "decoded type"); + assert_non_null(decoded.sent, "decoded sent"); + assert_equal(decoded.subject.value, "Test subject"); + assert_equal(decoded.from.to_rfc822_string(), "From "); + assert_equal(decoded.sender.to_rfc822_string(), "From "); + assert_equal(decoded.reply_to.to_rfc822_string(), "From "); + + assert_non_null(decoded.to, "to"); + assert_equal(decoded.to.to_rfc822_string(), "To "); + + assert_non_null(decoded.cc, "cc"); + assert_equal(decoded.cc.to_rfc822_string(), "Cc "); + + assert_non_null(decoded.bcc, "bcc"); + assert_equal(decoded.bcc.to_rfc822_string(), "Bcc "); + + assert_non_null(decoded.in_reply_to, "in_reply_to"); + assert_equal(decoded.in_reply_to.to_rfc822_string(), "<1234@example.com>"); + + assert_non_null(decoded.message_id, "message_id"); + assert_equal(decoded.message_id.to_rfc822_string(), "<5678@example.com>"); + } + + public void envelope_mailbox_missing_mailbox_name_quirk() throws GLib.Error { + ListParameter env = new ListParameter(); + env.add(new QuotedStringParameter("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)")); + env.add(new QuotedStringParameter("Test subject")); + env.add(new ListParameter.single(new_mailbox_structure("From", "from", "example.com"))); + env.add(new ListParameter.single(new_mailbox_structure("From", "from", "example.com"))); + env.add(new ListParameter.single(new_mailbox_structure("From", "from", "example.com"))); + + env.add(new ListParameter.single(new_mailbox_structure("To", "BOGUS", "example.com"))); + env.add(new ListParameter.single(new_mailbox_structure("Cc", "cc", "example.com"))); + env.add(NilParameter.instance); + env.add(NilParameter.instance); + env.add(NilParameter.instance); + + var quirks = new Quirks(); + quirks.empty_envelope_mailbox_name = "BOGUS"; + + var test_article = new EnvelopeDecoder(quirks); + var decoded = test_article.decode(env) as Envelope; + + assert_non_null(decoded.to, "to"); + assert_equal(decoded.to.to_rfc822_string(), "To <@example.com>"); + assert_non_null(decoded.cc, "cc"); + assert_equal(decoded.cc.to_rfc822_string(), "Cc "); + } + + public void envelope_mailbox_missing_host_name_quirk() throws GLib.Error { + ListParameter env = new ListParameter(); + env.add(new QuotedStringParameter("Wed, 17 Jul 1996 02:23:25 -0700 (PDT)")); + env.add(new QuotedStringParameter("Test subject")); + env.add(new ListParameter.single(new_mailbox_structure("From", "from", "example.com"))); + env.add(new ListParameter.single(new_mailbox_structure("From", "from", "example.com"))); + env.add(new ListParameter.single(new_mailbox_structure("From", "from", "example.com"))); + + env.add(new ListParameter.single(new_mailbox_structure("To name", "to", "BOGUS"))); + env.add(new ListParameter.single(new_mailbox_structure("Cc", "cc", "example.com"))); + env.add(NilParameter.instance); + env.add(NilParameter.instance); + env.add(NilParameter.instance); + + var quirks = new Quirks(); + quirks.empty_envelope_host_name = "BOGUS"; + + var test_article = new EnvelopeDecoder(quirks); + var decoded = test_article.decode(env) as Envelope; + + assert_non_null(decoded.to, "to"); + assert_equal(decoded.to.to_rfc822_string(), "To name "); + assert_non_null(decoded.cc, "cc"); + assert_equal(decoded.cc.to_rfc822_string(), "Cc "); + } + + private ListParameter new_mailbox_structure(string name, string local, string domain) { + ListParameter mailbox = new ListParameter(); + mailbox.add(new QuotedStringParameter(name)); + mailbox.add(NilParameter.instance); + mailbox.add(new QuotedStringParameter(local)); + mailbox.add(new QuotedStringParameter(domain)); + return mailbox; + } +} diff --git a/test/engine/imap/response/imap-namespace-response-test.vala b/test/engine/imap/response/imap-namespace-response-test.vala index d0ea7761..2d8d18f5 100644 --- a/test/engine/imap/response/imap-namespace-response-test.vala +++ b/test/engine/imap/response/imap-namespace-response-test.vala @@ -126,7 +126,7 @@ class Geary.Imap.NamespaceResponseTest : TestCase { else root.add(shared); - return new ServerData.migrate(root); + return new ServerData.migrate(root, new Quirks()); } private ListParameter newNamespace(string prefix, string? delim) { diff --git a/test/engine/imap/transport/imap-deserializer-test.vala b/test/engine/imap/transport/imap-deserializer-test.vala index b9c456d8..e5e642df 100644 --- a/test/engine/imap/transport/imap-deserializer-test.vala +++ b/test/engine/imap/transport/imap-deserializer-test.vala @@ -202,7 +202,6 @@ class Geary.Imap.DeserializerTest : TestCase { string greeting = "* OK Gimap ready for requests from 115.187.245.46 c194mb399904375ivc"; this.stream.add_data(greeting.data); this.stream.add_data(EOL.data); - this.deser.quirks = ClientService.new_quirks_for_provider(GMAIL); this.process.begin(Expect.MESSAGE, this.async_completion); RootParameters? message = this.process.end(async_result()); @@ -249,7 +248,8 @@ class Geary.Imap.DeserializerTest : TestCase { string flags = """* FLAGS (\Answered \Flagged \Draft \Deleted \Seen $NotPhishing $Phishing)"""; this.stream.add_data(flags.data); this.stream.add_data(EOL.data); - this.deser.quirks = ClientService.new_quirks_for_provider(GMAIL); + this.deser.quirks = new Imap.Quirks(); + this.deser.quirks.update_for_gmail(); this.process.begin(Expect.MESSAGE, this.async_completion); RootParameters? message = this.process.end(async_result()); @@ -261,7 +261,8 @@ class Geary.Imap.DeserializerTest : TestCase { string flags = """* OK [PERMANENTFLAGS (\Answered \Flagged \Draft \Deleted \Seen $NotPhishing $Phishing \*)] Flags permitted."""; this.stream.add_data(flags.data); this.stream.add_data(EOL.data); - this.deser.quirks = ClientService.new_quirks_for_provider(GMAIL); + this.deser.quirks = new Imap.Quirks(); + this.deser.quirks.update_for_gmail(); this.process.begin(Expect.MESSAGE, this.async_completion); RootParameters? message = this.process.end(async_result()); @@ -275,7 +276,8 @@ class Geary.Imap.DeserializerTest : TestCase { string flags = """* FLAGS (\Answered \Flagged \Draft \Deleted \Seen $Forwarded $MDNSent $NotPhishing $Phishing Junk LoadRemoteImages NonJunk OIB-Seen-INBOX OIB-Seen-Unsubscribe OIB-Seen-[Gmail]/Important OIB-Seen-[Gmail]/Spam OIB-Seen-[Gmail]/Tous les messages)"""; this.stream.add_data(flags.data); this.stream.add_data(EOL.data); - this.deser.quirks = ClientService.new_quirks_for_provider(GMAIL); + this.deser.quirks = new Imap.Quirks(); + this.deser.quirks.update_for_gmail(); this.process.begin(Expect.MESSAGE, this.async_completion); RootParameters? message = this.process.end(async_result()); diff --git a/test/engine/rfc822/rfc822-mailbox-address-test.vala b/test/engine/rfc822/rfc822-mailbox-address-test.vala index 8718636d..fdef5b0b 100644 --- a/test/engine/rfc822/rfc822-mailbox-address-test.vala +++ b/test/engine/rfc822/rfc822-mailbox-address-test.vala @@ -9,6 +9,7 @@ class Geary.RFC822.MailboxAddressTest : TestCase { public MailboxAddressTest() { base("Geary.RFC822.MailboxAddressTest"); + add_test("imap_address", imap_address); add_test("is_valid_address", is_valid_address); add_test("unescaped_constructor", unescaped_constructor); add_test("from_rfc822_string_encoded", from_rfc822_string_encoded); @@ -24,6 +25,25 @@ class Geary.RFC822.MailboxAddressTest : TestCase { add_test("equal_to", equal_to); } + public void imap_address() throws GLib.Error { + assert_equal( + new MailboxAddress.imap(null, null, "test", "example.com").address, + "test@example.com" + ); + assert_equal( + new MailboxAddress.imap(null, null, "test", "").address, + "test" + ); + assert_equal( + new MailboxAddress.imap(null, null, "", "example.com").address, + "example.com" + ); + assert_equal( + new MailboxAddress.imap(null, null, "", "").address, + "" + ); + } + public void is_valid_address() throws GLib.Error { assert(Geary.RFC822.MailboxAddress.is_valid_address("john@dep.aol.museum") == true); assert(Geary.RFC822.MailboxAddress.is_valid_address("test@example.com") == true); @@ -73,84 +93,93 @@ class Geary.RFC822.MailboxAddressTest : TestCase { } public void from_rfc822_string_encoded() throws GLib.Error { - try { - MailboxAddress addr = new MailboxAddress.from_rfc822_string("test@example.com"); - assert(addr.name == null); - assert(addr.mailbox == "test"); - assert(addr.domain == "example.com"); + var encoded = "test@example.com"; + var addr = new MailboxAddress.from_rfc822_string(encoded); + assert_null(addr.name, encoded); + assert_equal(addr.mailbox, "test", encoded); + assert_equal(addr.domain, "example.com", encoded); - addr = new MailboxAddress.from_rfc822_string("\"test\"@example.com"); - assert(addr.name == null); - assert(addr.address == "test@example.com"); - assert(addr.mailbox == "test"); - assert(addr.domain == "example.com"); + encoded = "\"test\"@example.com"; + addr = new MailboxAddress.from_rfc822_string(encoded); + assert_null(addr.name, encoded); + assert_equal(addr.mailbox, "test", encoded); + assert_equal(addr.domain, "example.com", encoded); + assert_equal(addr.address, "test@example.com", encoded); - addr = new MailboxAddress.from_rfc822_string("=?UTF-8?b?dGVzdA==?=@example.com"); - assert(addr.name == null); - assert(addr.address == "test@example.com"); - assert(addr.mailbox == "test"); - assert(addr.domain == "example.com"); + encoded = "=?UTF-8?b?dGVzdA==?=@example.com"; + addr = new MailboxAddress.from_rfc822_string(encoded); + assert_null(addr.name, encoded); + assert_equal(addr.mailbox, "test", encoded); + assert_equal(addr.domain, "example.com", encoded); + assert_equal(addr.address, "test@example.com", encoded); - addr = new MailboxAddress.from_rfc822_string("\"=?UTF-8?b?dGVzdA==?=\"@example.com"); - assert(addr.name == null); - assert(addr.address == "test@example.com"); - assert(addr.mailbox == "test"); - assert(addr.domain == "example.com"); + encoded = "\"=?UTF-8?b?dGVzdA==?=\"@example.com"; + addr = new MailboxAddress.from_rfc822_string(encoded); + assert_null(addr.name, encoded); + assert_equal(addr.mailbox, "test", encoded); + assert_equal(addr.domain, "example.com", encoded); + assert_equal(addr.address, "test@example.com", encoded); - addr = new MailboxAddress.from_rfc822_string(""); - assert(addr.name == null); - assert(addr.address == "test@example.com"); - assert(addr.mailbox == "test"); - assert(addr.domain == "example.com"); + encoded = ""; + addr = new MailboxAddress.from_rfc822_string(encoded); + assert_null(addr.name, encoded); + assert_equal(addr.mailbox, "test"); + assert_equal(addr.domain, "example.com", encoded); + assert_equal(addr.address, "test@example.com", encoded); - addr = new MailboxAddress.from_rfc822_string("<\"test\"@example.com>"); - assert(addr.name == null); - assert(addr.address == "test@example.com"); - assert(addr.mailbox == "test"); - assert(addr.domain == "example.com"); + encoded = "<\"test\"@example.com>"; + addr = new MailboxAddress.from_rfc822_string(encoded); + assert_null(addr.name, encoded); + assert_equal(addr.mailbox, "test", encoded); + assert_equal(addr.domain, "example.com", encoded); + assert_equal(addr.address, "test@example.com", encoded); - addr = new MailboxAddress.from_rfc822_string("Test 1 "); - assert(addr.name == "Test 1"); - assert(addr.address == "test2@example.com"); - assert(addr.mailbox == "test2"); - assert(addr.domain == "example.com"); + encoded = "Test 1 "; + addr = new MailboxAddress.from_rfc822_string(encoded); + assert_equal(addr.name, "Test 1", encoded); + assert_equal(addr.mailbox, "test2", encoded); + assert_equal(addr.domain, "example.com", encoded); + assert_equal(addr.address, "test2@example.com", encoded); - addr = new MailboxAddress.from_rfc822_string("\"Test 1\" "); - assert(addr.name == "Test 1"); - assert(addr.address == "test2@example.com"); - assert(addr.mailbox == "test2"); - assert(addr.domain == "example.com"); + encoded = "\"Test 1\" "; + addr = new MailboxAddress.from_rfc822_string(encoded); + assert_equal(addr.name, "Test 1", encoded); + assert_equal(addr.mailbox, "test2", encoded); + assert_equal(addr.domain, "example.com", encoded); + assert_equal(addr.address, "test2@example.com", encoded); - addr = new MailboxAddress.from_rfc822_string("Test 1 <\"test2\"@example.com>"); - assert(addr.name == "Test 1"); - assert(addr.address == "test2@example.com"); - assert(addr.mailbox == "test2"); - assert(addr.domain == "example.com"); + encoded = "Test 1 <\"test2\"@example.com>"; + addr = new MailboxAddress.from_rfc822_string(encoded); + assert_equal(addr.name, "Test 1", encoded); + assert_equal(addr.mailbox, "test2", encoded); + assert_equal(addr.domain, "example.com", encoded); + assert_equal(addr.address, "test2@example.com", encoded); - addr = new MailboxAddress.from_rfc822_string("=?UTF-8?b?VGVzdCAx?= "); - assert(addr.name == "Test 1"); - assert(addr.address == "test2@example.com"); - assert(addr.mailbox == "test2"); - assert(addr.domain == "example.com"); + encoded = "=?UTF-8?b?VGVzdCAx?= "; + addr = new MailboxAddress.from_rfc822_string(encoded); + assert_equal(addr.name, "Test 1", encoded); + assert_equal(addr.mailbox, "test2", encoded); + assert_equal(addr.domain, "example.com", encoded); + assert_equal(addr.address, "test2@example.com", encoded); - addr = new MailboxAddress.from_rfc822_string("\"=?UTF-8?b?VGVzdCAx?=\" "); - assert(addr.name == "Test 1"); - assert(addr.address == "test2@example.com"); - assert(addr.mailbox == "test2"); - assert(addr.domain == "example.com"); + encoded = "\"=?UTF-8?b?VGVzdCAx?=\" "; + addr = new MailboxAddress.from_rfc822_string(encoded); + assert_equal(addr.name, "Test 1", encoded); + assert_equal(addr.mailbox, "test2", encoded); + assert_equal(addr.domain, "example.com", encoded); + assert_equal(addr.address, "test2@example.com", encoded); - // Courtesy Mailsploit https://www.mailsploit.com - addr = new MailboxAddress.from_rfc822_string("\"=?utf-8?b?dGVzdCIgPHBvdHVzQHdoaXRlaG91c2UuZ292Pg==?==?utf-8?Q?=00=0A?=\" "); - assert(addr.name == "test ?"); - assert(addr.address == "demo@mailsploit.com"); + // Courtesy Mailsploit https://www.mailsploit.com + encoded = "\"=?utf-8?b?dGVzdCIgPHBvdHVzQHdoaXRlaG91c2UuZ292Pg==?==?utf-8?Q?=00=0A?=\" "; + addr = new MailboxAddress.from_rfc822_string(encoded); + assert_equal(addr.name, "test ?", encoded); + assert_equal(addr.address, "demo@mailsploit.com", encoded); - // Courtesy Mailsploit https://www.mailsploit.com - addr = new MailboxAddress.from_rfc822_string("\"=?utf-8?Q?=42=45=47=49=4E=20=2F=20=28=7C=29=7C=3C=7C=3E=7C=40=7C=2C=7C=3B=7C=3A=7C=5C=7C=22=7C=2F=7C=5B=7C=5D=7C=3F=7C=2E=7C=3D=20=2F=20=00=20=50=41=53=53=45=44=20=4E=55=4C=4C=20=42=59=54=45=20=2F=20=0D=0A=20=50=41=53=53=45=44=20=43=52=4C=46=20=2F=20?==?utf-8?b?RU5E=?=\""); - assert(addr.name == null); - assert(addr.address == "BEGIN / (|)|<|>|@|,|;|:|\\|\"|/|[|]|?|.|= / ? PASSED NULL BYTE / \r\n PASSED CRLF / END"); - } catch (Error err) { - assert_not_reached(); - } + // Courtesy Mailsploit https://www.mailsploit.com + encoded = "\"=?utf-8?Q?=42=45=47=49=4E=20=2F=20=28=7C=29=7C=3C=7C=3E=7C=40=7C=2C=7C=3B=7C=3A=7C=5C=7C=22=7C=2F=7C=5B=7C=5D=7C=3F=7C=2E=7C=3D=20=2F=20=00=20=50=41=53=53=45=44=20=4E=55=4C=4C=20=42=59=54=45=20=2F=20=0D=0A=20=50=41=53=53=45=44=20=43=52=4C=46=20=2F=20?==?utf-8?b?RU5E=?=\""; + addr = new MailboxAddress.from_rfc822_string(encoded); + assert_equal(addr.name, null, encoded); + assert_equal(addr.address, "BEGIN / (|)|<|>|@|,|;|:|\\|\"|/|[|]|?|.|= / ? PASSED NULL BYTE / \r\n PASSED CRLF / END", encoded); } public void prepare_header_text_part() throws GLib.Error { @@ -286,6 +315,22 @@ class Geary.RFC822.MailboxAddressTest : TestCase { "😸@example.com" ); + assert_equal( + new MailboxAddress(null, "example1").to_rfc822_address(), + "example1" + ); + assert_equal( + new MailboxAddress.imap(null, null, "example2", "").to_rfc822_address(), + "example2" + ); + assert_equal( + new MailboxAddress.imap(null, null, "", "example3").to_rfc822_address(), + "@example3" + ); + assert_equal( + new MailboxAddress.imap(null, null, "", "").to_rfc822_address(), + "" + ); } public void to_rfc822_string() throws GLib.Error { diff --git a/test/integration/imap/client-session.vala b/test/integration/imap/client-session.vala index ad48e545..ca813b79 100644 --- a/test/integration/imap/client-session.vala +++ b/test/integration/imap/client-session.vala @@ -29,9 +29,7 @@ class Integration.Imap.ClientSession : TestCase { public override void set_up() { this.session = new Geary.Imap.ClientSession( this.config.target, - Geary.Imap.ClientService.new_quirks_for_provider( - this.config.provider - ) + new Geary.Imap.Quirks() ); } diff --git a/test/meson.build b/test/meson.build index a2fc8a62..6ea5e27a 100644 --- a/test/meson.build +++ b/test/meson.build @@ -43,6 +43,7 @@ test_engine_sources = [ 'engine/imap/message/imap-data-format-test.vala', 'engine/imap/message/imap-mailbox-specifier-test.vala', 'engine/imap/parameter/imap-list-parameter-test.vala', + 'engine/imap/response/imap-fetch-data-decoder-test.vala', 'engine/imap/response/imap-namespace-response-test.vala', 'engine/imap/transport/imap-client-connection-test.vala', 'engine/imap/transport/imap-client-session-test.vala', diff --git a/test/test-engine.vala b/test/test-engine.vala index 483163de..0260ef6a 100644 --- a/test/test-engine.vala +++ b/test/test-engine.vala @@ -56,6 +56,7 @@ int main(string[] args) { engine.add_suite(new Geary.Imap.CreateCommandTest().suite); engine.add_suite(new Geary.Imap.FetchCommandTest().suite); + engine.add_suite(new Geary.Imap.FetchDataDecoderTest().suite); engine.add_suite(new Geary.Imap.ListParameterTest().suite); engine.add_suite(new Geary.Imap.MailboxSpecifierTest().suite); engine.add_suite(new Geary.Imap.NamespaceResponseTest().suite);