diff --git a/bindings/vapi/gmime-2.6.vapi b/bindings/vapi/gmime-2.6.vapi index 0bb1991e..ae9458d5 100644 --- a/bindings/vapi/gmime-2.6.vapi +++ b/bindings/vapi/gmime-2.6.vapi @@ -1376,7 +1376,7 @@ namespace GMime { [CCode (cheader_filename = "gmime/gmime.h", cname = "g_mime_utils_structured_header_fold")] public static string utils_structured_header_fold (string header); [CCode (cheader_filename = "gmime/gmime.h", cname = "g_mime_utils_text_is_8bit")] - public static bool utils_text_is_8bit (uint text, size_t len); + public static bool utils_text_is_8bit (string text, size_t len); [CCode (cheader_filename = "gmime/gmime.h", cname = "g_mime_utils_unquote_string")] public static void utils_unquote_string (string str); [CCode (cheader_filename = "gmime/gmime.h", cname = "g_mime_utils_unstructured_header_fold")] diff --git a/src/client/accounts/account-dialog-edit-alternate-emails-pane.vala b/src/client/accounts/account-dialog-edit-alternate-emails-pane.vala index 2f9de01d..c5964970 100644 --- a/src/client/accounts/account-dialog-edit-alternate-emails-pane.vala +++ b/src/client/accounts/account-dialog-edit-alternate-emails-pane.vala @@ -11,7 +11,7 @@ public class AccountDialogEditAlternateEmailsPane : AccountDialogPane { public ListItem(Geary.RFC822.MailboxAddress mailbox) { this.mailbox = mailbox; - label = "%s".printf(Geary.HTML.escape_markup(mailbox.get_full_address())); + label = "%s".printf(Geary.HTML.escape_markup(mailbox.to_full_display())); use_markup = true; ellipsize = Pango.EllipsizeMode.END; set_halign(Gtk.Align.START); diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala index 62fa2a0e..09438ae6 100644 --- a/src/client/composer/composer-widget.vala +++ b/src/client/composer/composer-widget.vala @@ -1676,16 +1676,16 @@ public class ComposerWidget : Gtk.EventBox { StringBuilder tooltip = new StringBuilder(); if (to_entry.addresses != null) foreach(Geary.RFC822.MailboxAddress addr in this.to_entry.addresses) - tooltip.append(_("To: ") + addr.get_full_address() + "\n"); + tooltip.append(_("To: ") + addr.to_full_display() + "\n"); if (cc_entry.addresses != null) foreach(Geary.RFC822.MailboxAddress addr in this.cc_entry.addresses) - tooltip.append(_("Cc: ") + addr.get_full_address() + "\n"); + tooltip.append(_("Cc: ") + addr.to_full_display() + "\n"); if (bcc_entry.addresses != null) foreach(Geary.RFC822.MailboxAddress addr in this.bcc_entry.addresses) - tooltip.append(_("Bcc: ") + addr.get_full_address() + "\n"); + tooltip.append(_("Bcc: ") + addr.to_full_display() + "\n"); if (reply_to_entry.addresses != null) foreach(Geary.RFC822.MailboxAddress addr in this.reply_to_entry.addresses) - tooltip.append(_("Reply-To: ") + addr.get_full_address() + "\n"); + tooltip.append(_("Reply-To: ") + addr.to_full_display() + "\n"); this.header.set_recipients(label, tooltip.str.slice(0, -1)); // Remove trailing \n } diff --git a/src/client/conversation-list/formatted-conversation-data.vala b/src/client/conversation-list/formatted-conversation-data.vala index ae9afc65..2b428e90 100644 --- a/src/client/conversation-list/formatted-conversation-data.vala +++ b/src/client/conversation-list/formatted-conversation-data.vala @@ -27,17 +27,21 @@ public class FormattedConversationData : Geary.BaseObject { this.address = address; this.is_unread = is_unread; } - + public string get_full_markup(Gee.List account_mailboxes) { - return get_as_markup((address in account_mailboxes) ? ME : address.get_short_address()); + return get_as_markup((address in account_mailboxes) ? ME : address.to_short_display()); } - + public string get_short_markup(Gee.List account_mailboxes) { if (address in account_mailboxes) return get_as_markup(ME); - - string short_address = address.get_short_address().strip(); - + + if (address.is_spoofed()) { + return get_full_markup(account_mailboxes); + } + + string short_address = Markup.escape_text(address.to_short_display()); + if (", " in short_address) { // assume address is in Last, First format string[] tokens = short_address.split(", ", 2); @@ -57,12 +61,21 @@ public class FormattedConversationData : Geary.BaseObject { return get_as_markup(first_name); } - + private string get_as_markup(string participant) { - return "%s%s%s".printf( - is_unread ? "" : "", Geary.HTML.escape_markup(participant), is_unread ? "" : ""); + string markup = Geary.HTML.escape_markup(participant); + + if (is_unread) { + markup = "%s".printf(markup); + } + + if (this.address.is_spoofed()) { + markup = "%s".printf(markup); + } + + return markup; } - + public bool equal_to(ParticipantDisplay other) { return address.equal_to(other.address); } diff --git a/src/client/conversation-viewer/conversation-message.vala b/src/client/conversation-viewer/conversation-message.vala index 432dc284..be6fddb8 100644 --- a/src/client/conversation-viewer/conversation-message.vala +++ b/src/client/conversation-viewer/conversation-message.vala @@ -1,6 +1,6 @@ /* * Copyright 2016 Software Freedom Conservancy Inc. - * Copyright 2016 Michael Gratton + * Copyright 2016-2018 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. @@ -26,15 +26,6 @@ public class ConversationMessage : Gtk.Grid { private const int MAX_PREVIEW_BYTES = Geary.Email.MAX_PREVIEW_BYTES; - internal static inline bool has_distinct_name( - Geary.RFC822.MailboxAddress address) { - return ( - !Geary.String.is_empty(address.name) && - address.name != address.address - ); - } - - // Widget used to display sender/recipient email addresses in // message header Gtk.FlowBox instances. private class AddressFlowBoxChild : Gtk.FlowBoxChild { @@ -50,7 +41,7 @@ public class ConversationMessage : Gtk.Grid { public AddressFlowBoxChild(Geary.RFC822.MailboxAddress address, Type type = Type.OTHER) { this.address = address; - this.search_value = address.address.casefold(); + this.search_value = address.to_searchable_string().casefold(); // We use two label instances here when address has // distinct parts so we can dim the secondary part, if @@ -60,6 +51,17 @@ public class ConversationMessage : Gtk.Grid { Gtk.Grid address_parts = new Gtk.Grid(); + bool is_spoofed = address.is_spoofed(); + if (is_spoofed) { + Gtk.Image spoof_img = new Gtk.Image.from_icon_name( + "dialog-warning-symbolic", Gtk.IconSize.SMALL_TOOLBAR + ); + this.set_tooltip_text( + _("This email address may have been forged") + ); + address_parts.add(spoof_img); + } + Gtk.Label primary = new Gtk.Label(null); primary.ellipsize = Pango.EllipsizeMode.END; primary.set_halign(Gtk.Align.START); @@ -69,19 +71,21 @@ public class ConversationMessage : Gtk.Grid { } address_parts.add(primary); - if (has_distinct_name(address)) { - primary.set_text(address.name); + string display_address = address.to_address_display("", ""); + + // Don't display the name if it looks spoofed, to reduce + // chance of the user of being tricked by malware. + if (address.has_distinct_name() && !is_spoofed) { + primary.set_text(address.to_short_display()); Gtk.Label secondary = new Gtk.Label(null); secondary.ellipsize = Pango.EllipsizeMode.END; secondary.set_halign(Gtk.Align.START); secondary.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); - secondary.set_text(address.address); + secondary.set_text(display_address); address_parts.add(secondary); - - this.search_value = address.name.casefold() + this.search_value; } else { - primary.set_text(address.address); + primary.set_text(display_address); } // Update prelight state when mouse-overed. @@ -571,7 +575,7 @@ public class ConversationMessage : Gtk.Grid { Gee.List list = this.message.from.get_all(); foreach (Geary.RFC822.MailboxAddress addr in list) { - text += has_distinct_name(addr) ? addr.name : addr.address; + text += addr.to_short_display(); if (++i < list.size) // Translators: This separates multiple 'from' @@ -765,7 +769,7 @@ public class ConversationMessage : Gtk.Grid { Gee.Map values = new Gee.HashMap(); values[ACTION_OPEN_LINK] = Geary.ComposedEmail.MAILTO_SCHEME + address.address; - values[ACTION_COPY_EMAIL] = address.get_full_address(); + values[ACTION_COPY_EMAIL] = address.to_full_display(); values[ACTION_SEARCH_FROM] = address.address; Menu model = new Menu(); diff --git a/src/client/notification/libnotify.vala b/src/client/notification/libnotify.vala index c27c3f39..f6717aba 100644 --- a/src/client/notification/libnotify.vala +++ b/src/client/notification/libnotify.vala @@ -130,10 +130,10 @@ public class Libnotify : Geary.BaseObject { ins = null; } - - issue_current_notification(primary.get_short_address(), body, avatar); + + issue_current_notification(primary.to_short_display(), body, avatar); } - + private void issue_current_notification(string summary, string body, Gdk.Pixbuf? icon) { // only one outstanding notification at a time if (current_notification != null) { diff --git a/src/engine/rfc822/rfc822-mailbox-address.vala b/src/engine/rfc822/rfc822-mailbox-address.vala index 5b467928..218753df 100644 --- a/src/engine/rfc822/rfc822-mailbox-address.vala +++ b/src/engine/rfc822/rfc822-mailbox-address.vala @@ -1,102 +1,58 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. +/* + * Copyright 2016 Software Freedom Conservancy Inc. + * Copyright 2018 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. */ /** - * An immutable object containing a representation of an Internet email address. + * An immutable representation of an RFC 822 mailbox address. * - * See [[https://tools.ietf.org/html/rfc2822#section-3.4]] + * The properties of this class such as {@link name} and {@link + * address} are stores decoded UTF-8, thus they must be re-encoded + * using methods such as {@link to_rfc822_string} before being re-used + * in a message envelope. + * + * See [[https://tools.ietf.org/html/rfc5322#section-3.4]] */ +public class Geary.RFC822.MailboxAddress : + Geary.MessageData.SearchableMessageData, + Gee.Hashable, + BaseObject { -public class Geary.RFC822.MailboxAddress : Geary.MessageData.SearchableMessageData, - Gee.Hashable, BaseObject { - internal delegate string ListToStringDelegate(MailboxAddress address); - - /** - * The optional user-friendly name associated with the {@link MailboxAddress}. - * - * For "Dirk Gently ", this would be "Dirk Gently". - */ - public string? name { get; private set; } - - /** - * The routing of the message (optional, obsolete). - */ - public string? source_route { get; private set; } - - /** - * The mailbox (local-part) portion of the {@link MailboxAddress}. - * - * For "Dirk Gently ", this would be "dirk". - */ - public string mailbox { get; private set; } - - /** - * The domain portion of the {@link MailboxAddress}. - * - * For "Dirk Gently ", this would be "example.com". - */ - public string domain { get; private set; } - - /** - * The address specification of the {@link MailboxAddress}. - * - * For "Dirk Gently ", this would be "dirk@example.com". - */ - public string address { get; private set; } - - public MailboxAddress(string? name, string address) { - this.name = name; - this.address = address; - - source_route = null; - - int atsign = address.index_of_char('@'); - if (atsign > 0) { - mailbox = address.slice(0, atsign); - domain = address.slice(atsign + 1, address.length); - } else { - mailbox = ""; - domain = ""; + /** 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; } } - public MailboxAddress.imap(string? name, string? source_route, string mailbox, string domain) { - this.name = (name != null) ? decode_name(name) : null; - this.source_route = source_route; - this.mailbox = mailbox; - this.domain = domain; - - address = "%s@%s".printf(mailbox, domain); - } - - public MailboxAddress.from_rfc822_string(string rfc822) throws RFC822Error { - InternetAddressList addrlist = InternetAddressList.parse_string(rfc822); - if (addrlist == null) - return; - - int length = addrlist.length(); - for (int ctr = 0; ctr < length; ctr++) { - InternetAddress? addr = addrlist.get_address(ctr); - - // TODO: Handle group lists - InternetAddressMailbox? mbox_addr = addr as InternetAddressMailbox; - if (mbox_addr != null) { - this(mbox_addr.get_name(), mbox_addr.get_addr()); - return; - } - } - throw new RFC822Error.INVALID("Could not parse RFC822 address: %s", rfc822); - } - - // Borrowed liberally from GMime's internal _internet_address_decode_name() function. private static string decode_name(string name) { - // see if a broken mailer has sent raw 8-bit information - string text = name.validate() ? name : GMime.utils_decode_8bit(name, name.length); + return GMime.utils_header_decode_phrase(prepare_header_text_part(name)); + } - // unquote the string and decode the text + private static string decode_address_part(string mailbox) { + return GMime.utils_header_decode_text(prepare_header_text_part(mailbox)); + } + + private static string prepare_header_text_part(string part) { + // Borrowed liberally from GMime's internal + // _internet_address_decode_name() function. + + // see if a broken mailer has sent raw 8-bit information + string text = GMime.utils_text_is_8bit(part, part.length) + ? part : GMime.utils_decode_8bit(part, part.length); + + // unquote the string then decode the text GMime.utils_unquote_string(text); // Sometimes quoted printables contain unencoded spaces which trips up GMime, so we want to @@ -118,33 +74,189 @@ public class Geary.RFC822.MailboxAddress : Geary.MessageData.SearchableMessageDa offset = end; } - return GMime.utils_header_decode_text(text); + return text; + } + + + /** + * The optional human-readable part of the mailbox address. + * + * For "Dirk Gently ", this would be "Dirk Gently". + * + * The returned value has been decoded into UTF-8. + */ + public string? name { get; private set; } + + /** + * The routing of the message (optional, obsolete). + * + * The returned value has been decoded into UTF-8. + */ + public string? source_route { get; private set; } + + /** + * The mailbox (local-part) portion of the mailbox's address. + * + * For "Dirk Gently ", this would be "dirk". + * + * The returned value has been decoded into UTF-8. + */ + public string mailbox { get; private set; } + + /** + * The domain portion of the mailbox's address. + * + * For "Dirk Gently ", this would be "example.com". + * + * The returned value has been decoded into UTF-8. + */ + public string domain { get; private set; } + + /** + * The complete address part of the mailbox address. + * + * For "Dirk Gently ", this would be "dirk@example.com". + * + * The returned value has been decoded into UTF-8. + */ + public string address { get; private set; } + + + public MailboxAddress(string? name, string address) { + this.name = name; + this.source_route = null; + this.address = address; + + int atsign = address.last_index_of_char('@'); + if (atsign > 0) { + this.mailbox = address[0:atsign]; + this.domain = address[atsign + 1:address.length]; + } else { + this.mailbox = ""; + this.domain = ""; + } + } + + public MailboxAddress.imap(string? name, string? source_route, string mailbox, string domain) { + this.name = (name != null) ? decode_name(name) : null; + this.source_route = source_route; + this.mailbox = decode_address_part(mailbox); + this.domain = domain; + this.address = "%s@%s".printf(mailbox, domain); + } + + public MailboxAddress.from_rfc822_string(string rfc822) throws RFC822Error { + InternetAddressList addrlist = InternetAddressList.parse_string(rfc822); + if (addrlist == null) + return; + + int length = addrlist.length(); + for (int ctr = 0; ctr < length; ctr++) { + InternetAddress? addr = addrlist.get_address(ctr); + + // TODO: Handle group lists + InternetAddressMailbox? mbox_addr = addr as InternetAddressMailbox; + if (mbox_addr != null) { + this.gmime(mbox_addr); + return; + } + } + throw new RFC822Error.INVALID("Could not parse RFC822 address: %s", rfc822); + } + + public MailboxAddress.gmime(InternetAddressMailbox mailbox) { + // GMime strips source route for us, so the address part + // should only ever contain a single '@' + string? name = mailbox.get_name(); + if (name != null) { + this.name = decode_name(name); + } + + string address = mailbox.get_addr(); + int atsign = address.last_index_of_char('@'); + if (atsign == -1) { + // No @ detected, try decoding in case a mailer (wrongly) + // encoded the whole thing and re-try + address = decode_address_part(address); + atsign = address.last_index_of_char('@'); + } + + if (atsign >= 0) { + this.mailbox = decode_address_part(address[0:atsign]); + this.domain = address[atsign + 1:address.length]; + this.address = "%s@%s".printf(this.mailbox, this.domain); + } else { + this.mailbox = ""; + this.domain = ""; + this.address = address; + } } /** - * Returns a human-readable formatted address, showing the name (if available) and the email - * address in angled brackets. No RFC822 quoting is performed. + * Returns a full human-readable version of the mailbox address. * - * @see to_rfc822_string - */ - public string get_full_address() { - return String.is_empty(name) ? address : "%s <%s>".printf(name, address); - } - - /** - * Returns a simple address, that is, no human-readable name and the email address in angled + * This returns a formatted version of the address including + * {@link name} (if present, not a spoof, and distinct from the + * address) and {@link address} parts, suitable for display to + * people. The string will have white space reduced and + * non-printable characters removed, and the address will be + * surrounded by angle brackets if a name is present. + * + * If you need a form suitable for sending a message, see {@link + * to_rfc822_string} instead. + * + * @see has_distinct_name + * @see is_spoofed + * @param open optional string to use as the opening bracket for + * the address part, defaults to //// + * @return the cleaned //name// part if present, not spoofed and + * distinct from //address//, followed by a space then the cleaned + * //address// part, cleaned and enclosed within the specified * brackets. */ - public string get_simple_address() { - return "<%s>".printf(address); + public string to_full_display(string open = "<", string close = ">") { + string clean_name = Geary.String.reduce_whitespace(this.name); + string clean_address = Geary.String.reduce_whitespace(this.address); + return (!has_distinct_name() || is_spoofed()) + ? clean_address + : "%s %s%s%s".printf(clean_name, open, clean_address, close); } - + /** - * Returns a human-readable pretty address, showing only the name, but if unavailable, the - * mailbox name (that is, the account name without the domain). + * Returns a short human-readable version of the mailbox address. + * + * This returns a shortened version of the address suitable for + * display to people: Either the {@link name} (if present and not + * a spoof) or the {@link address} part otherwise. The string will + * have white space reduced and non-printable characters removed. + * + * @see is_spoofed + * @return the cleaned //name// part if present and not spoofed, + * or else the cleaned //address// part, cleaned but without + * brackets. */ - public string get_short_address() { - return name ?? mailbox; + public string to_short_display() { + string clean_name = Geary.String.reduce_whitespace(this.name); + string clean_address = Geary.String.reduce_whitespace(this.address); + return String.is_empty(clean_name) || is_spoofed() + ? clean_address + : clean_name; + } + + /** + * Returns a human-readable version of the address part. + * + * @param open optional string to use as the opening bracket, + * defaults to //// + * @return the {@link address} part, cleaned and enclosed within the + * specified brackets. + */ + public string to_address_display(string open = "<", string close = ">") { + return open + Geary.String.reduce_whitespace(this.address) + close; } /** @@ -153,47 +265,134 @@ public class Geary.RFC822.MailboxAddress : Geary.MessageData.SearchableMessageDa public bool is_valid() { return is_valid_address(address); } - + /** - * Returns true if the email syntax is valid. - */ - 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; - } - } - - /** - * Returns the address suitable for insertion into an RFC822 message. RFC822 quoting is - * performed if required. + * Determines if the mailbox address appears to have been spoofed. * - * @see get_full_address + * Using recipient and sender mailbox addresses where the name + * part is also actually a valid RFC822 address + * (e.g. "you@example.com ") is a common tactic + * used by spammers and malware authors to exploit MUAs that will + * display the name part only if present. It also enables more + * sophisticated attacks such as + * [[https://www.mailsploit.com/|Mailsploit]], which uses + * Quoted-Printable or Base64 encoded nulls, new lines, @'s and + * other characters to further trick MUAs into displaying a bogus + * address. + * + * This method attempts to detect such attacks by examining the + * {@link name} for non-printing characters and determining if it + * is by itself also a valid RFC822 address. + * + * @return //true// if the complete decoded address contains any + * non-printing characters, if the name part is also a valid + * RFC822 address, or if the address part is not a valid RFC822 + * address. + */ + public bool is_spoofed() { + // Empty test and regexes must apply to the raw values, not + // clean ones, otherwise any control chars present will have + // been lost + const string CONTROLS = "[[:cntrl:]]+"; + + bool is_spoof = false; + + // 1. Check the name part contains no controls and doesn't + // look like an email address + if (!Geary.String.is_empty(this.name)) { + if (Regex.match_simple(CONTROLS, this.name)) { + is_spoof = true; + } else { + // Clean up the name as usual, but remove all + // whitespace so an attack can't get away with a name + // like "potus @ whitehouse . gov" + string clean_name = Geary.String.reduce_whitespace(this.name); + clean_name = clean_name.replace(" ", ""); + if (is_valid_address(clean_name)) { + is_spoof = true; + } + } + } + + // 2. Check the mailbox part of the address doesn't contain an + // @. Is actually legal if quoted, but rarely (never?) found + // in the wild and better be safe than sorry. + if (!is_spoof && this.mailbox.contains("@")) { + is_spoof = true; + } + + // 3. Check the address doesn't contain any spaces or + // controls. Again, space in the mailbox is allowed if quoted, + // but in practice should rarely be used. + if (!is_spoof && Regex.match_simple(Geary.String.WS_OR_NP, this.address)) { + is_spoof = true; + } + + return is_spoof; + } + + /** + * Determines if the name part is different to the address part. + * + * @return //true// if {@link name} is not empty, and the cleaned + * versions of the name part and {@link address} are not equal. + */ + public bool has_distinct_name() { + string clean_name = Geary.String.reduce_whitespace(this.name); + return ( + !Geary.String.is_empty(clean_name) && + clean_name != Geary.String.reduce_whitespace(this.address) + ); + } + + /** + * Returns the complete mailbox address, armoured for RFC 822 use. + * + * This method is similar to {@link to_full_display}, but only + * checks for a distinct address (per Postel's Law) and not for + * any spoofing, and does not strip extra white space or + * non-printing characters. + * + * @return the RFC822 encoded form of the full address. */ public string to_rfc822_string() { - return String.is_empty(name) - ? address - : "%s <%s>".printf(GMime.utils_quote_string(name), address); + return has_distinct_name() + ? "%s <%s>".printf( + GMime.utils_header_encode_phrase(this.name), + to_rfc822_address() + ) + : to_rfc822_address(); } - + + /** + * Returns the address part only, armoured for RFC 822 use. + * + * @return the RFC822 encoded form of the address, without angle + * brackets. + */ + public string to_rfc822_address() { + return "%s@%s".printf( + // XXX utils_quote_string won't quote if spaces or quotes + // present, so need to do that manually + GMime.utils_quote_string(GMime.utils_header_encode_text(this.mailbox)), + // XXX Need to punycode international domains. + this.domain + ); + } + /** * See Geary.MessageData.SearchableMessageData. */ public string to_searchable_string() { - return get_full_address(); + return has_distinct_name() + ? "%s <%s>".printf(this.name, this.address) + : this.address; } - + public uint hash() { return String.stri_hash(address); } - + /** * Equality is defined as a case-insensitive comparison of the {@link address}. */ @@ -205,30 +404,13 @@ public class Geary.RFC822.MailboxAddress : Geary.MessageData.SearchableMessageDa return this.address.normalize().casefold() == address.normalize().casefold(); } + /** + * Returns the RFC822 formatted version of the address. + * + * @see to_rfc822_string + */ public string to_string() { - return get_full_address(); + return to_rfc822_string(); } - - internal static string list_to_string(Gee.List addrs, - string empty, ListToStringDelegate to_s) { - switch (addrs.size) { - case 0: - return empty; - - case 1: - return to_s(addrs[0]); - - default: - StringBuilder builder = new StringBuilder(); - foreach (MailboxAddress addr in addrs) { - if (!String.is_empty(builder.str)) - builder.append(", "); - - builder.append(to_s(addr)); - } - - return builder.str; - } - } -} +} diff --git a/src/engine/rfc822/rfc822-mailbox-addresses.vala b/src/engine/rfc822/rfc822-mailbox-addresses.vala index b5f39b1e..9d06baf5 100644 --- a/src/engine/rfc822/rfc822-mailbox-addresses.vala +++ b/src/engine/rfc822/rfc822-mailbox-addresses.vala @@ -1,24 +1,74 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. +/* + * Copyright 2016 Software Freedom Conservancy Inc. * * This software is licensed under the GNU Lesser General Public License * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.RFC822.MailboxAddresses : Geary.MessageData.AbstractMessageData, - Geary.MessageData.SearchableMessageData, Geary.RFC822.MessageData, Gee.Hashable { - - public int size { get { return addrs.size; } } - +/** + * An immutable representation an RFC 822 address list. + * + * This would typically be found as the value of the To, CC, BCC and + * other headers fields. + * + * See [[https://tools.ietf.org/html/rfc5322#section-3.4]] + */ +public class Geary.RFC822.MailboxAddresses : + Geary.MessageData.AbstractMessageData, + Geary.MessageData.SearchableMessageData, + Geary.RFC822.MessageData, Gee.Hashable { + + + /** + * Converts a list of mailbox addresses to a string. + * + * The delegate //to_s// is used for converting addresses in the + * given list. If the list is empty, the given empty string is + * returned. + */ + private static string list_to_string(Gee.List addrs, + string empty, + ListToStringDelegate to_s) { + switch (addrs.size) { + case 0: + return empty; + + case 1: + return to_s(addrs[0]); + + default: + StringBuilder builder = new StringBuilder(); + foreach (MailboxAddress addr in addrs) { + if (!String.is_empty(builder.str)) + builder.append(", "); + + builder.append(to_s(addr)); + } + + return builder.str; + } + } + + + /** Signature for "to_string" implementation for {@link list_to_string}. */ + private delegate string ListToStringDelegate(MailboxAddress address); + + /** Returns the number of addresses in this list. */ + public int size { + get { return this.addrs.size; } + } + private Gee.List addrs = new Gee.ArrayList(); - + + public MailboxAddresses(Gee.Collection addrs) { this.addrs.add_all(addrs); } - + public MailboxAddresses.single(MailboxAddress addr) { - addrs.add(addr); + this.addrs.add(addr); } - + public MailboxAddresses.from_rfc822_string(string rfc822) { InternetAddressList addrlist = InternetAddressList.parse_string(rfc822); if (addrlist == null) @@ -27,102 +77,125 @@ public class Geary.RFC822.MailboxAddresses : Geary.MessageData.AbstractMessageDa int length = addrlist.length(); for (int ctr = 0; ctr < length; ctr++) { InternetAddress? addr = addrlist.get_address(ctr); - - // TODO: Handle group lists + InternetAddressMailbox? mbox_addr = addr as InternetAddressMailbox; - if (mbox_addr == null) - continue; - - addrs.add(new MailboxAddress(mbox_addr.get_name(), mbox_addr.get_addr())); + if (mbox_addr != null) { + this.addrs.add(new MailboxAddress.gmime(mbox_addr)); + } else { + // XXX this is pretty bad - we just flatten the + // group's addresses into this list, merging lists and + // losing the group names. + InternetAddressGroup? mbox_group = addr as InternetAddressGroup; + if (mbox_group != null) { + InternetAddressList group_list = mbox_group.get_members(); + for (int i = 0; i < group_list.length(); i++) { + InternetAddressMailbox? group_addr = + addrlist.get_address(i) as InternetAddressMailbox; + if (group_addr != null) { + this.addrs.add(new MailboxAddress.gmime(group_addr)); + } + } + } + } } } - + public new MailboxAddress? get(int index) { return addrs.get(index); } - + public Gee.Iterator iterator() { return addrs.iterator(); } - + public Gee.List get_all() { return addrs.read_only_view; } - + public bool contains_normalized(string address) { if (addrs.size < 1) return false; - + string normalized_address = address.normalize().casefold(); - + foreach (MailboxAddress mailbox_address in addrs) { if (mailbox_address.address.normalize().casefold() == normalized_address) return true; } - + return false; } - + public bool contains(string address) { if (addrs.size < 1) return false; - + foreach (MailboxAddress a in addrs) if (a.address == address) return true; - + return false; } - + /** - * Returns the addresses suitable for insertion into an RFC822 message. RFC822 quoting is - * performed if required. + * Returns a new list with the given addresses appended to this list's. + */ + public MailboxAddresses append(MailboxAddresses others) { + MailboxAddresses new_addrs = new MailboxAddresses(this.addrs); + new_addrs.addrs.add_all(others.addrs); + return new_addrs; + } + + /** + * Returns the addresses suitable for insertion into an RFC822 message. + * + * RFC822 quoting is performed if required. * * @see MailboxAddress.to_rfc822_string */ public string to_rfc822_string() { - return MailboxAddress.list_to_string(addrs, "", (a) => a.to_rfc822_string()); + return list_to_string(addrs, ", ", (a) => a.to_rfc822_string()); } - + public uint hash() { // create sorted set to ensure ordering no matter the list's order Gee.TreeSet sorted_addresses = traverse(addrs) .map(m => m.address) .to_tree_set(String.stri_cmp); - + // xor all strings in sorted order uint xor = 0; foreach (string address in sorted_addresses) xor ^= address.hash(); - + return xor; } - + public bool equal_to(MailboxAddresses other) { if (this == other) return true; - + if (addrs.size != other.addrs.size) return false; - + Gee.HashSet first = new Gee.HashSet(); first.add_all(addrs); - + Gee.HashSet second = new Gee.HashSet(); second.add_all(other.addrs); - + return Collection.are_sets_equal(first, second); } - + /** * See Geary.MessageData.SearchableMessageData. */ public string to_searchable_string() { - return MailboxAddress.list_to_string(addrs, "", (a) => a.to_searchable_string()); + return list_to_string(addrs, " ", (a) => a.to_searchable_string()); } - - public override string to_string() { - return MailboxAddress.list_to_string(addrs, "(no addresses)", (a) => a.to_string()); - } -} + public override string to_string() { + return list_to_string(addrs, "(no addresses)", (a) => a.to_string()); + } + +} diff --git a/src/engine/rfc822/rfc822-message-data.vala b/src/engine/rfc822/rfc822-message-data.vala index 44593f8a..adb9da97 100644 --- a/src/engine/rfc822/rfc822-message-data.vala +++ b/src/engine/rfc822/rfc822-message-data.vala @@ -142,7 +142,17 @@ public class Geary.RFC822.MessageIDList : Geary.MessageData.AbstractMessageData, // don't assert that list.size > 0; even though this method should generated a decoded ID // from any non-empty string, an empty Message-ID (i.e. "<>") won't. } - + + /** + * Returns a new list with the given messages ids appended to this list's. + */ + public MessageIDList append(MessageIDList others) { + MessageIDList new_ids = new MessageIDList(); + new_ids.list.add_all(this.list); + new_ids.list.add_all(others.list); + return new_ids; + } + public override string to_string() { return "MessageIDList (%d)".printf(list.size); } diff --git a/src/engine/rfc822/rfc822-message.vala b/src/engine/rfc822/rfc822-message.vala index b374dc40..d11f612a 100644 --- a/src/engine/rfc822/rfc822-message.vala +++ b/src/engine/rfc822/rfc822-message.vala @@ -1,4 +1,6 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. +/* + * Copyright 2016 Software Freedom Conservancy Inc. + * Copyright 2018 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. @@ -416,106 +418,6 @@ public class Geary.RFC822.Message : BaseObject { return null; } - private void stock_from_gmime() { - // GMime calls the From address the "sender" - string? message_sender = message.get_sender(); - if (message_sender != null) { - this.from = new RFC822.MailboxAddresses.from_rfc822_string(message_sender); - } - - // And it doesn't provide a convenience method for Sender header - if (!String.is_empty(message.get_header(HEADER_SENDER))) { - string sender = GMime.utils_header_decode_text(message.get_header(HEADER_SENDER)); - try { - this.sender = new RFC822.MailboxAddress.from_rfc822_string(sender); - } catch (RFC822Error e) { - debug("Invalid RDC822 Sender address: %s", sender); - } - } - - if (!String.is_empty(message.get_reply_to())) - this.reply_to = new RFC822.MailboxAddresses.from_rfc822_string(message.get_reply_to()); - - Gee.List? converted = convert_gmime_address_list( - message.get_recipients(GMime.RecipientType.TO)); - if (converted != null && converted.size > 0) - to = new RFC822.MailboxAddresses(converted); - - converted = convert_gmime_address_list(message.get_recipients(GMime.RecipientType.CC)); - if (converted != null && converted.size > 0) - cc = new RFC822.MailboxAddresses(converted); - - converted = convert_gmime_address_list(message.get_recipients(GMime.RecipientType.BCC)); - if (converted != null && converted.size > 0) - bcc = new RFC822.MailboxAddresses(converted); - - if (!String.is_empty(message.get_header(HEADER_IN_REPLY_TO))) - in_reply_to = new RFC822.MessageIDList.from_rfc822_string(message.get_header(HEADER_IN_REPLY_TO)); - - if (!String.is_empty(message.get_header(HEADER_REFERENCES))) - references = new RFC822.MessageIDList.from_rfc822_string(message.get_header(HEADER_REFERENCES)); - - if (!String.is_empty(message.get_subject())) - subject = new RFC822.Subject.decode(message.get_subject()); - - if (!String.is_empty(message.get_header(HEADER_MAILER))) - mailer = message.get_header(HEADER_MAILER); - - if (!String.is_empty(message.get_date_as_string())) { - try { - date = new Geary.RFC822.Date(message.get_date_as_string()); - } catch (Error error) { - debug("Could not get date from message: %s", error.message); - } - } - } - - private Gee.List? convert_gmime_address_list(InternetAddressList? addrlist, - int depth = 0) { - if (addrlist == null || addrlist.length() == 0) - return null; - - Gee.List? converted = new Gee.ArrayList(); - - int length = addrlist.length(); - for (int ctr = 0; ctr < length; ctr++) { - InternetAddress addr = addrlist.get_address(ctr); - - InternetAddressMailbox? mbox_addr = addr as InternetAddressMailbox; - if (mbox_addr != null) { - converted.add(new RFC822.MailboxAddress(mbox_addr.get_name(), mbox_addr.get_addr())); - - continue; - } - - // Two problems here: - // - // First, GMime crashes when parsing a malformed group list (the case seen in the - // wild is -- weirdly enough -- a date appended to the end of a cc: list on a spam - // email. GMime interprets it as a group list but segfaults when destroying the - // InterneAddresses it generated from it. See: - // https://bugzilla.gnome.org/show_bug.cgi?id=695319 - // - // Second, RFC 822 6.2.6: "This standard does not permit recursive specification - // of groups within groups." So don't do it. - InternetAddressGroup? group = addr as InternetAddressGroup; - if (group != null) { - if (depth == 0) { - Gee.List? grouplist = convert_gmime_address_list( - group.get_members(), depth + 1); - if (grouplist != null) - converted.add_all(grouplist); - } - - continue; - } - - warning("Unknown InternetAddress in list: %s", addr.get_type().name()); - } - - return (converted.size > 0) ? converted : null; - } - public Gee.List? get_recipients() { Gee.List addrs = new Gee.ArrayList(); @@ -816,19 +718,21 @@ public class Geary.RFC822.Message : BaseObject { return body; } - + /** * Return the full list of recipients (to, cc, and bcc) as a searchable * string. Note that values that come out of this function are persisted. */ public string? get_searchable_recipients() { - Gee.List? recipients = get_recipients(); - if (recipients == null) - return null; - - return RFC822.MailboxAddress.list_to_string(recipients, "", (a) => a.to_searchable_string()); + string searchable = null; + Gee.List? recipient_list = get_recipients(); + if (recipient_list != null) { + MailboxAddresses recipients = new MailboxAddresses(recipient_list); + searchable = recipients.to_searchable_string(); + } + return searchable; } - + public Memory.Buffer get_content_by_mime_id(string mime_id) throws RFC822Error { GMime.Part? part = find_mime_part_by_mime_id(message.get_mime_part(), mime_id); if (part == null) @@ -873,7 +777,86 @@ public class Geary.RFC822.Message : BaseObject { get_attachments_recursively(attachments, message.get_mime_part(), disposition); return attachments; } - + + private void stock_from_gmime() { + this.message.get_header_list().foreach((name, value) => { + switch (name.down()) { + case "from": + this.from = append_address(this.from, value); + break; + + case "sender": + try { + this.sender = new RFC822.MailboxAddress.from_rfc822_string(value); + } catch (Error err) { + debug("Could parse subject: %s", err.message); + } + break; + + case "reply-to": + this.reply_to = append_address(this.reply_to, value); + break; + + case "to": + this.to = append_address(this.to, value); + break; + + case "cc": + this.cc = append_address(this.cc, value); + break; + + case "bcc": + this.bcc = append_address(this.bcc, value); + break; + + case "subject": + this.subject = new RFC822.Subject.decode(value); + break; + + case "date": + try { + this.date = new Geary.RFC822.Date(value); + } catch (Error err) { + debug("Could not parse date: %s", err.message); + } + break; + + case "in-reply-to": + this.in_reply_to = append_message_id(this.in_reply_to, value); + break; + + case "references": + this.references = append_message_id(this.references, value); + break; + + case "x-mailer": + this.mailer = GMime.utils_header_decode_text(value); + break; + + default: + break; + } + }); + } + + private MailboxAddresses append_address(MailboxAddresses? existing, + string header_value) { + MailboxAddresses addresses = new MailboxAddresses.from_rfc822_string(header_value); + if (existing != null) { + addresses = existing.append(addresses); + } + return addresses; + } + + private MessageIDList append_message_id(MessageIDList? existing, + string header_value) { + MessageIDList ids = new MessageIDList.from_rfc822_string(header_value); + if (existing != null) { + ids = existing.append(ids); + } + return ids; + } + private void get_attachments_recursively(Gee.List attachments, GMime.Object root, Mime.DispositionType requested_disposition) throws RFC822Error { // If this is a multipart container, dive into each of its children. diff --git a/src/engine/smtp/smtp-request.vala b/src/engine/smtp/smtp-request.vala index a98cf531..fccf4355 100644 --- a/src/engine/smtp/smtp-request.vala +++ b/src/engine/smtp/smtp-request.vala @@ -58,9 +58,9 @@ public class Geary.Smtp.EhloRequest : Geary.Smtp.Request { public class Geary.Smtp.MailRequest : Geary.Smtp.Request { public MailRequest(Geary.RFC822.MailboxAddress from) { - base (Command.MAIL, { "from:%s".printf(from.get_simple_address()) }); + base (Command.MAIL, { "from:<%s>".printf(from.to_rfc822_address()) }); } - + public MailRequest.plain(string addr) { base (Command.MAIL, { "from:<%s>".printf(addr) }); } @@ -68,7 +68,7 @@ public class Geary.Smtp.MailRequest : Geary.Smtp.Request { public class Geary.Smtp.RcptRequest : Geary.Smtp.Request { public RcptRequest(Geary.RFC822.MailboxAddress to) { - base (Command.RCPT, { "to:%s".printf(to.get_simple_address()) }); + base (Command.RCPT, { "to:%s".printf(to.to_address_display("<", ">")) }); } public RcptRequest.plain(string addr) { diff --git a/src/engine/util/util-string.vala b/src/engine/util/util-string.vala index 924ade99..0a2ef833 100644 --- a/src/engine/util/util-string.vala +++ b/src/engine/util/util-string.vala @@ -10,8 +10,13 @@ extern string glib_substring(string str, long start_pos, long end_pos); namespace Geary.String { +/** The end-of-string character, NUL. */ public const char EOS = '\0'; +/** A regex that matches one or more whitespace or non-printing chars. */ +public const string WS_OR_NP = "[[:space:][:cntrl:]]+"; + + public bool is_empty_or_whitespace(string? str) { return (str == null || str[0] == EOS || str.strip()[0] == EOS); } @@ -50,23 +55,23 @@ public int stri_cmp(string a, string b) { return strcmp(a.down(), b.down()); } -// Removes redundant spaces, tabs, and newlines. -public string reduce_whitespace(string _s) { - string s = _s; - s = s.replace("\n", " "); - s = s.replace("\r", " "); - s = s.replace("\t", " "); - s = s.strip(); - - // Condense multiple spaces to one. - for (int i = 1; i < s.length; i++) { - if (s.get_char(i) == ' ' && s.get_char(i - 1) == ' ') { - s = s.slice(0, i - 1) + s.slice(i, s.length); - i--; - } +/** + * Removes redundant white space and non-printing characters. + * + * @return the input string /str/, modified so that any non-printing + * characters are converted to spaces, all consecutive spaces are + * coalesced into a single space, and stripped of leading and trailing + * white space. If //null// is passed in, the empty string is + * returned. + */ +public string reduce_whitespace(string? str) { + string s = str ?? ""; + try { + s = new Regex(WS_OR_NP).replace(s, -1, 0, " "); + } catch (Error err) { + // Oh well } - - return s; + return s.strip(); } // Slices a string to, at most, max_length number of bytes (NOT including the null.) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9ca4140a..6f9ab8c2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -20,6 +20,7 @@ set(TEST_ENGINE_SRC engine/imap-engine/account-processor-test.vala engine/mime-content-type-test.vala engine/rfc822-mailbox-address-test.vala + engine/rfc822-mailbox-addresses-test.vala engine/rfc822-message-test.vala engine/rfc822-message-data-test.vala engine/rfc822-utils-test.vala @@ -27,6 +28,7 @@ set(TEST_ENGINE_SRC engine/util-idle-manager-test.vala engine/util-inet-test.vala engine/util-js-test.vala + engine/util-string-test.vala engine/util-timeout-manager-test.vala ) diff --git a/test/engine/rfc822-mailbox-address-test.vala b/test/engine/rfc822-mailbox-address-test.vala index b06b2638..964d977e 100644 --- a/test/engine/rfc822-mailbox-address-test.vala +++ b/test/engine/rfc822-mailbox-address-test.vala @@ -1,5 +1,5 @@ /* - * Copyright 2016 Michael Gratton + * Copyright 2016-2018 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. @@ -10,6 +10,15 @@ class Geary.RFC822.MailboxAddressTest : Gee.TestCase { public MailboxAddressTest() { base("Geary.RFC822.MailboxAddressTest"); add_test("is_valid_address", is_valid_address); + add_test("unescaped_constructor", unescaped_constructor); + add_test("from_rfc822_string_encoded", from_rfc822_string_encoded); + add_test("is_spoofed", is_spoofed); + add_test("has_distinct_name", has_distinct_name); + add_test("to_full_display", to_full_display); + add_test("to_short_display", to_short_display); + // latter depends on the former, so test that first + add_test("to_rfc822_address", to_rfc822_address); + add_test("to_rfc822_string", to_rfc822_string); } public void is_valid_address() { @@ -30,4 +39,209 @@ class Geary.RFC822.MailboxAddressTest : Gee.TestCase { assert(Geary.RFC822.MailboxAddress.is_valid_address("") == false); } + public void unescaped_constructor() { + MailboxAddress addr1 = new MailboxAddress("test1", "test2@example.com"); + assert(addr1.name == "test1"); + assert(addr1.address == "test2@example.com"); + assert(addr1.mailbox == "test2"); + assert(addr1.domain == "example.com"); + + MailboxAddress addr2 = new MailboxAddress(null, "test1@test2@example.com"); + assert(addr2.address == "test1@test2@example.com"); + assert(addr2.mailbox == "test1@test2"); + assert(addr2.domain == "example.com"); + + MailboxAddress addr3 = new MailboxAddress(null, "©@example.com"); + assert(addr3.address == "©@example.com"); + assert(addr3.mailbox == "©"); + assert(addr3.domain == "example.com"); + + MailboxAddress addr4 = new MailboxAddress(null, "😸@example.com"); + assert(addr4.address == "😸@example.com"); + assert(addr4.mailbox == "😸"); + assert(addr4.domain == "example.com"); + + MailboxAddress addr5 = new MailboxAddress(null, "example.com"); + assert(addr5.address == "example.com"); + assert(addr5.mailbox == ""); + assert(addr5.domain == ""); + } + + public void from_rfc822_string_encoded() { + try { + MailboxAddress addr = new MailboxAddress.from_rfc822_string("test@example.com"); + assert(addr.name == null); + assert(addr.mailbox == "test"); + assert(addr.domain == "example.com"); + + 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"); + + 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"); + + 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"); + + 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"); + + 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"); + + 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"); + + 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"); + + 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"); + + 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"); + + 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"); + + // 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 ?\n"); + assert(addr.address == "demo@mailsploit.com"); + + // 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(); + } + } + + public void is_spoofed() { + assert(new MailboxAddress(null, "example@example.com").is_spoofed() == false); + assert(new MailboxAddress("", "example@example.com").is_spoofed() == false); + assert(new MailboxAddress("", "example@example.com").is_spoofed() == false); + assert(new MailboxAddress("test", "example@example.com").is_spoofed() == false); + assert(new MailboxAddress("test test", "example@example.com").is_spoofed() == false); + assert(new MailboxAddress("test test", "example@example.com").is_spoofed() == false); + assert(new MailboxAddress("test?", "example@example.com").is_spoofed() == false); + + assert(new MailboxAddress("test@example.com", "example@example.com").is_spoofed() == true); + assert(new MailboxAddress("test @ example . com", "example@example.com").is_spoofed() == true); + assert(new MailboxAddress("\n", "example@example.com").is_spoofed() == true); + assert(new MailboxAddress("\n", "example@example.com").is_spoofed() == true); + assert(new MailboxAddress("test", "example@\nexample@example.com").is_spoofed() == true); + assert(new MailboxAddress("test", "example@example@example.com").is_spoofed() == true); + + try { + assert(new MailboxAddress.from_rfc822_string("\"=?utf-8?b?dGVzdCIgPHBvdHVzQHdoaXRlaG91c2UuZ292Pg==?==?utf-8?Q?=00=0A?=\" ") + .is_spoofed() == true); + } catch (Error err) { + assert_no_error(err); + } + } + + public void has_distinct_name() { + assert(new MailboxAddress("example", "example@example.com").has_distinct_name() == true); + + assert(new MailboxAddress("", "example@example.com").has_distinct_name() == false); + assert(new MailboxAddress(" ", "example@example.com").has_distinct_name() == false); + assert(new MailboxAddress("example@example.com", "example@example.com").has_distinct_name() == false); + assert(new MailboxAddress(" example@example.com ", "example@example.com").has_distinct_name() == false); + assert(new MailboxAddress(" example@example.com ", "example@example.com").has_distinct_name() == false); + } + + public void to_full_display() { + assert(new MailboxAddress("", "example@example.com").to_full_display() == + "example@example.com"); + assert(new MailboxAddress("Test", "example@example.com").to_full_display() == + "Test "); + assert(new MailboxAddress("example@example.com", "example@example.com").to_full_display() == + "example@example.com"); + assert(new MailboxAddress("Test", "example@example@example.com").to_full_display() == + "example@example@example.com"); + } + + public void to_short_display() { + assert(new MailboxAddress("", "example@example.com").to_short_display() == + "example@example.com"); + assert(new MailboxAddress("Test", "example@example.com").to_short_display() == + "Test"); + assert(new MailboxAddress("example@example.com", "example@example.com").to_short_display() == + "example@example.com"); + assert(new MailboxAddress("Test", "example@example@example.com").to_short_display() == + "example@example@example.com"); + } + + public void to_rfc822_address() { + assert(new MailboxAddress(null, "example@example.com").to_rfc822_address() == + "example@example.com"); + //assert(new MailboxAddress(null, "test test@example.com").to_rfc822_address() == + // "\"test test\"@example.com"); + //assert(new MailboxAddress(null, "test\" test@example.com").to_rfc822_address() == + // "\"test\" test\"@example.com"); + //assert(new MailboxAddress(null, "test\"test@example.com").to_rfc822_address() == + // "\"test\"test\"@example.com"); + assert(new MailboxAddress(null, "test@test@example.com").to_rfc822_address() == + "\"test@test\"@example.com"); + assert(new MailboxAddress(null, "©@example.com").to_rfc822_address() == + "\"=?iso-8859-1?b?qQ==?=\"@example.com"); + assert(new MailboxAddress(null, "😸@example.com").to_rfc822_address() == + "\"=?UTF-8?b?8J+YuA==?=\"@example.com"); + } + + public void to_rfc822_string() { + assert(new MailboxAddress("", "example@example.com").to_rfc822_string() == + "example@example.com"); + assert(new MailboxAddress(" ", "example@example.com").to_rfc822_string() == + "example@example.com"); + assert(new MailboxAddress("test", "example@example.com").to_rfc822_string() == + "test "); + assert(new MailboxAddress("test test", "example@example.com").to_rfc822_string() == + "test test "); + assert(new MailboxAddress("example@example.com", "example@example.com").to_rfc822_string() == + "example@example.com"); + assert(new MailboxAddress("test?", "example@example.com").to_rfc822_string() == + "test? "); + assert(new MailboxAddress("test@test", "example@example.com").to_rfc822_string() == + "\"test@test\" "); + assert(new MailboxAddress(";", "example@example.com").to_rfc822_string() == + "\";\" "); + assert(new MailboxAddress("©", "example@example.com").to_rfc822_string() == + "=?iso-8859-1?b?qQ==?= "); + assert(new MailboxAddress("😸", "example@example.com").to_rfc822_string() == + "=?UTF-8?b?8J+YuA==?= "); + } + } diff --git a/test/engine/rfc822-mailbox-addresses-test.vala b/test/engine/rfc822-mailbox-addresses-test.vala new file mode 100644 index 00000000..3c252d84 --- /dev/null +++ b/test/engine/rfc822-mailbox-addresses-test.vala @@ -0,0 +1,46 @@ +/* + * Copyright 2018 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.RFC822.MailboxAddressesTest : Gee.TestCase { + + public MailboxAddressesTest() { + base("Geary.RFC822.MailboxAddressesTest"); + add_test("from_rfc822_string_encoded", from_rfc822_string_encoded); + add_test("to_rfc822_string", to_rfc822_string); + } + + public void from_rfc822_string_encoded() { + MailboxAddresses addrs = new MailboxAddresses.from_rfc822_string("test@example.com"); + assert(addrs.size == 1); + + addrs = new MailboxAddresses.from_rfc822_string("test1@example.com, test2@example.com"); + assert(addrs.size == 2); + + // Courtesy Mailsploit https://www.mailsploit.com + addrs = new MailboxAddresses.from_rfc822_string("\"=?utf-8?b?dGVzdCIgPHBvdHVzQHdoaXRlaG91c2UuZ292Pg==?==?utf-8?Q?=00=0A?=\" "); + assert(addrs.size == 1); + + // Courtesy Mailsploit https://www.mailsploit.com + addrs = new MailboxAddresses.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(addrs.size == 2); + } + + public void to_rfc822_string() { + assert(new_addreses({ "test1@example.com" }).to_rfc822_string() == + "test1@example.com"); + assert(new_addreses({ "test1@example.com", "test2@example.com" }).to_rfc822_string() == + "test1@example.com, test2@example.com"); + } + + private MailboxAddresses new_addreses(string[] address_strings) { + Gee.List addresses = new Gee.LinkedList(); + foreach (string address in address_strings) { + addresses.add(new MailboxAddress(null, address)); + } + return new MailboxAddresses(addresses); + } +} diff --git a/test/engine/rfc822-message-data-test.vala b/test/engine/rfc822-message-data-test.vala index 61814f12..9d8ec34a 100644 --- a/test/engine/rfc822-message-data-test.vala +++ b/test/engine/rfc822-message-data-test.vala @@ -70,7 +70,7 @@ https://app.foobar.com/xxxxxxxxxxxxx">https://app.foobar.com/xxxxxxxxxxx

"""; - public static string HTML_BODY1_EXPECTED = "Hi Kenneth, We xxxxx xxxx xx xxx xxx xx xxxx x xxxxxxxx xxxxxxxx.  Thank you, XXXXXX XXXXXX You can reply directly to this message or click the following link: https://app.foobar.com/xxxxxxxxxxxxxxxx1641966deff6c48623aba You can change your email preferences at: https://app.foobar.com/xxxxxxxxxxx"; + public static string HTML_BODY1_EXPECTED = "Hi Kenneth, We xxxxx xxxx xx xxx xxx xx xxxx x xxxxxxxx xxxxxxxx. Thank you, XXXXXX XXXXXX You can reply directly to this message or click the following link: https://app.foobar.com/xxxxxxxxxxxxxxxx1641966deff6c48623aba You can change your email preferences at: https://app.foobar.com/xxxxxxxxxxx"; public static string HTML_BODY2_ENCODED = """ @@ -618,5 +618,5 @@ x 133, 3000 Bern 6, Switzerland """; - public static string HTML_BODY2_EXPECTED = "Buy It Now from US $1,750.00 to US $5,950.00. eBay Daccordi, Worldwide: 2 new matches today Daccordi 50th anniversary edition with... Buy it now: US $5,950.00 100% positive feedback Daccordi Griffe Campagnolo Croce D'Aune... Buy it now: US $1,750.00 100% positive feedback View all results Refine this search Disable emails for this search   Email reference id: [#d9f42b5e860b4eabb98195c2888cba9e#] We don't check this mailbox, so please don't reply to this message. If you have a question, go to Help & Contact. ©2016 eBay Inc., eBay International AG Helvetiastrasse 15/17 - P.O. Box 133, 3000 Bern 6, Switzerland"; + public static string HTML_BODY2_EXPECTED = "Buy It Now from US $1,750.00 to US $5,950.00. eBay Daccordi, Worldwide: 2 new matches today Daccordi 50th anniversary edition with... Buy it now: US $5,950.00 100% positive feedback Daccordi Griffe Campagnolo Croce D'Aune... Buy it now: US $1,750.00 100% positive feedback View all results Refine this search Disable emails for this search Email reference id: [#d9f42b5e860b4eabb98195c2888cba9e#] We don't check this mailbox, so please don't reply to this message. If you have a question, go to Help & Contact. ©2016 eBay Inc., eBay International AG Helvetiastrasse 15/17 - P.O. Box 133, 3000 Bern 6, Switzerland"; } diff --git a/test/engine/rfc822-message-test.vala b/test/engine/rfc822-message-test.vala index bf555aed..664e4b61 100644 --- a/test/engine/rfc822-message-test.vala +++ b/test/engine/rfc822-message-test.vala @@ -1,5 +1,5 @@ /* - * Copyright 2016 Michael Gratton + * Copyright 2016-2018 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. @@ -9,7 +9,72 @@ class Geary.RFC822.MessageTest : Gee.TestCase { public MessageTest() { base("Geary.RFC822.MessageTest"); - add_test("Message::get_preview", get_preview); + add_test("basic_message_from_buffer", basic_message_from_buffer); + add_test("encoded_recipient", encoded_recipient); + add_test("duplicate_mailbox", duplicate_mailbox); + add_test("duplicate_message_id", duplicate_message_id); + add_test("get_preview", get_preview); + } + + public void basic_message_from_buffer() { + Message? basic = null; + try { + basic = string_to_message(BASIC_MESSAGE); + } catch (Error err) { + assert_no_error(err); + } + assert_data(basic.subject, "Re: Saying Hello"); + assert_addresses(basic.from, "Mary Smith "); + assert_address(basic.sender, "Mary Smith Sender "); + assert_addresses(basic.reply_to, "\"Mary Smith: Personal Account\" "); + assert_addresses(basic.to, "John Doe "); + assert_addresses(basic.cc, "John Doe CC "); + assert_addresses(basic.bcc, "John Doe BCC "); + //assert_data(basic.message_id, "<3456@example.net>"); + assert_message_id_list(basic.in_reply_to, "<1234@local.machine.example>"); + assert_message_id_list(basic.references, "<1234@local.machine.example>"); + assert_data(basic.date, "Fri, 21 Nov 1997 10:01:10 -0600"); + assert(basic.mailer == "Geary Test Suite 1.0"); + } + + public void encoded_recipient() { + Message? enc = null; + try { + enc = string_to_message(ENCODED_TO); + } catch (Error err) { + assert_no_error(err); + } + + // Courtesy Mailsploit https://www.mailsploit.com + assert(enc.to[0].name == "potus@whitehouse.gov "); + } + + public void duplicate_mailbox() { + Message? dup = null; + try { + dup = string_to_message(DUPLICATE_TO); + } catch (Error err) { + assert_no_error(err); + } + + assert(dup.to.size == 2); + assert_addresses( + dup.to, "John Doe 1 , John Doe 2 " + ); + } + + public void duplicate_message_id() { + Message? dup = null; + try { + dup = string_to_message(DUPLICATE_REFERENCES); + } catch (Error err) { + assert_no_error(err); + } + + assert(dup.references.list.size == 2); + assert_message_id_list( + dup.references, "<1234@local.machine.example> <5678@local.machine.example>" + ); } public void get_preview() { @@ -27,6 +92,35 @@ class Geary.RFC822.MessageTest : Gee.TestCase { ); } + private void assert_data(Geary.MessageData.AbstractMessageData? data, string expected) { + assert(data != null); + assert(data.to_string() == expected); + } + + private void assert_address(Geary.RFC822.MailboxAddress? address, string expected) { + assert(address != null); + assert(address.to_rfc822_string() == expected); + } + + private void assert_addresses(Geary.RFC822.MailboxAddresses? addresses, string expected) { + assert(addresses != null); + assert(addresses.to_rfc822_string() == expected); + } + + private void assert_message_id_list(Geary.RFC822.MessageIDList? ids, string expected) { + assert(ids != null); + assert(ids.to_rfc822_string() == expected); + } + + private static string BASIC_MESSAGE = "From: Mary Smith \r\nSender: Mary Smith Sender \r\nTo: John Doe \r\nCC: John Doe CC \r\nBCC: John Doe BCC \r\nReply-To: \"Mary Smith: Personal Account\" \r\nSubject: Re: Saying Hello\r\nDate: Fri, 21 Nov 1997 10:01:10 -0600\r\nMessage-ID: <3456@example.net>\r\nIn-Reply-To: <1234@local.machine.example>\r\nReferences: <1234@local.machine.example>\r\nX-Mailer: Geary Test Suite 1.0\r\n\r\nThis is a reply to your hello.\r\n\r\n"; + + // Courtesy Mailsploit https://www.mailsploit.com + private static string ENCODED_TO = "From: Mary Smith \r\nTo: =?utf-8?b?cG90dXNAd2hpdGVob3VzZS5nb3YiIDx0ZXN0Pg==?= \r\nSubject: Re: Saying Hello\r\nDate: Fri, 21 Nov 1997 10:01:10 -0600\r\n\r\nThis is a reply to your hello.\r\n\r\n"; + + private static string DUPLICATE_TO = "From: Mary Smith \r\nTo: John Doe 1 \r\nTo: John Doe 2 \r\nSubject: Re: Saying Hello\r\nDate: Fri, 21 Nov 1997 10:01:10 -0600\r\n\r\nThis is a reply to your hello.\r\n\r\n"; + + private static string DUPLICATE_REFERENCES = "From: Mary Smith \r\nTo: John Doe \r\nReferences: <1234@local.machine.example>\r\nReferences: <5678@local.machine.example>\r\nSubject: Re: Saying Hello\r\nDate: Fri, 21 Nov 1997 10:01:10 -0600\r\n\r\nThis is a reply to your hello.\r\n\r\n"; + private static string MULTIPART_SIGNED_MESSAGE_TEXT = "Return-Path: \r\nReceived: from mogul.quuxo.net ([unix socket])\r\n by mogul (Cyrus v2.4.12-Debian-2.4.12-2) with LMTPA;\r\n Wed, 21 Dec 2016 06:54:03 +1030\r\nX-Sieve: CMU Sieve 2.4\r\nReceived: from huckleberry.canonical.com (huckleberry.canonical.com [91.189.94.19])\r\n by mogul.quuxo.net (8.14.4/8.14.4/Debian-2ubuntu2.1) with ESMTP id uBKKNtpt026727\r\n for ; Wed, 21 Dec 2016 06:53:57 +1030\r\nReceived: from localhost ([127.0.0.1] helo=huckleberry.canonical.com)\r\n by huckleberry.canonical.com with esmtp (Exim 4.76)\r\n (envelope-from )\r\n id 1cJQwM-0003Xk-IO; Tue, 20 Dec 2016 20:23:14 +0000\r\nReceived: from 208-151-246-43.dq1sn.easystreet.com ([208.151.246.43]\r\n helo=lizaveta.nxnw.org)\r\n by huckleberry.canonical.com with esmtps (TLS1.0:DHE_RSA_AES_256_CBC_SHA1:32)\r\n (Exim 4.76) (envelope-from )\r\n id 1cJQin-0000t2-G6\r\n for ubuntu-security-announce@lists.ubuntu.com; Tue, 20 Dec 2016 20:09:14 +0000\r\nReceived: from kryten.nxnw.org (kryten.nxnw.org [10.19.96.254])\r\n (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))\r\n (Client CN \"kryten.int.wirex.com\", Issuer \"nxnw.org\" (not verified))\r\n by lizaveta.nxnw.org (Postfix) with ESMTPS id DD8C360941\r\n for ;\r\n Tue, 20 Dec 2016 12:09:06 -0800 (PST)\r\nReceived: by kryten.nxnw.org (Postfix, from userid 1000)\r\n id 84341342F6C; Tue, 20 Dec 2016 12:09:06 -0800 (PST)\r\nDate: Tue, 20 Dec 2016 12:09:06 -0800\r\nFrom: Steve Beattie \r\nTo: ubuntu-security-announce@lists.ubuntu.com\r\nSubject: [USN-3159-1] Linux kernel vulnerability\r\nMessage-ID: <20161220200906.GF8251@nxnw.org>\r\nMail-Followup-To: Ubuntu Security \r\nMIME-Version: 1.0\r\nUser-Agent: Mutt/1.5.24 (2015-08-30)\r\nX-Mailman-Approved-At: Tue, 20 Dec 2016 20:23:12 +0000\r\nX-BeenThere: ubuntu-security-announce@lists.ubuntu.com\r\nX-Mailman-Version: 2.1.14\r\nPrecedence: list\r\nReply-To: ubuntu-users@lists.ubuntu.com, Ubuntu Security \r\nList-Id: Ubuntu Security Announcements\r\n \r\nList-Unsubscribe: , \r\n \r\nList-Archive: \r\nList-Post: \r\nList-Help: \r\nList-Subscribe: , \r\n \r\nContent-Type: multipart/mixed; boundary=\"===============7564301068935298617==\"\r\nErrors-To: ubuntu-security-announce-bounces@lists.ubuntu.com\r\nSender: ubuntu-security-announce-bounces@lists.ubuntu.com\r\nX-Greylist: Sender IP whitelisted by DNSRBL, not delayed by milter-greylist-4.3.9 (mogul.quuxo.net [203.18.245.241]); Wed, 21 Dec 2016 06:53:57 +1030 (ACDT)\r\nX-Virus-Scanned: clamav-milter 0.99.2 at mogul\r\nX-Virus-Status: Clean\r\nX-Spam-Status: No, score=-4.2 required=5.0 tests=BAYES_00,RCVD_IN_DNSWL_MED\r\n autolearn=ham version=3.3.2\r\nX-Spam-Checker-Version: SpamAssassin 3.3.2 (2011-06-06) on mogul.quuxo.net\r\n\r\n\r\n--===============7564301068935298617==\r\nContent-Type: multipart/signed; micalg=pgp-sha512;\r\n protocol=\"application/pgp-signature\"; boundary=\"O98KdSgI27dgYlM5\"\r\nContent-Disposition: inline\r\n\r\n\r\n--O98KdSgI27dgYlM5\r\nContent-Type: text/plain; charset=us-ascii\r\nContent-Disposition: inline\r\n\r\n==========================================================================\r\nUbuntu Security Notice USN-3159-1\r\nDecember 20, 2016\r\n\r\nlinux vulnerability\r\n==========================================================================\r\n\r\nA security issue affects these releases of Ubuntu and its derivatives:\r\n\r\n- Ubuntu 12.04 LTS\r\n\r\nSummary:\r\n\r\nThe system could be made to expose sensitive information.\r\n\r\nSoftware Description:\r\n- linux: Linux kernel\r\n\r\nDetails:\r\n\r\nIt was discovered that a race condition existed in the procfs\r\nenviron_read function in the Linux kernel, leading to an integer\r\nunderflow. A local attacker could use this to expose sensitive\r\ninformation (kernel memory).\r\n\r\nUpdate instructions:\r\n\r\nThe problem can be corrected by updating your system to the following\r\npackage versions:\r\n\r\nUbuntu 12.04 LTS:\r\n linux-image-3.2.0-119-generic 3.2.0-119.162\r\n linux-image-3.2.0-119-generic-pae 3.2.0-119.162\r\n linux-image-3.2.0-119-highbank 3.2.0-119.162\r\n linux-image-3.2.0-119-omap 3.2.0-119.162\r\n linux-image-3.2.0-119-powerpc-smp 3.2.0-119.162\r\n linux-image-3.2.0-119-powerpc64-smp 3.2.0-119.162\r\n linux-image-3.2.0-119-virtual 3.2.0-119.162\r\n linux-image-generic 3.2.0.119.134\r\n linux-image-generic-pae 3.2.0.119.134\r\n linux-image-highbank 3.2.0.119.134\r\n linux-image-omap 3.2.0.119.134\r\n linux-image-powerpc-smp 3.2.0.119.134\r\n linux-image-powerpc64-smp 3.2.0.119.134\r\n linux-image-virtual 3.2.0.119.134\r\n\r\nAfter a standard system update you need to reboot your computer to make\r\nall the necessary changes.\r\n\r\nATTENTION: Due to an unavoidable ABI change the kernel updates have\r\nbeen given a new version number, which requires you to recompile and\r\nreinstall all third party kernel modules you might have installed.\r\nUnless you manually uninstalled the standard kernel metapackages\r\n(e.g. linux-generic, linux-generic-lts-RELEASE, linux-virtual,\r\nlinux-powerpc), a standard system upgrade will automatically perform\r\nthis as well.\r\n\r\nReferences:\r\n http://www.ubuntu.com/usn/usn-3159-1\r\n CVE-2016-7916\r\n\r\nPackage Information:\r\n https://launchpad.net/ubuntu/+source/linux/3.2.0-119.162\r\n\r\n\r\n--O98KdSgI27dgYlM5\r\nContent-Type: application/pgp-signature; name=\"signature.asc\"\r\n\r\n-----BEGIN PGP SIGNATURE-----\r\nVersion: GnuPG v1\r\n\r\niQIcBAEBCgAGBQJYWY/iAAoJEC8Jno0AXoH0gKUQAJ7UOWV591M8K+HGXHI3BVJi\r\n75LCUSBRrV2NZTpc32ZMCsssb4TSqQinzczQfWSNtlLsgucKTLdCYGJvbXYxd32z\r\nBzHHHH9D8EDC6X4Olx0byiDBTX76kVBVUjxsKJ1zkYBFeMZ6tx9Tmgsl7Rdr26lP\r\n9oe3nBadkP0vM7j/dG1913MdzOlFc/2YOnGRK6QKzy1HhM74XMQTzvj9Nsbgs8ea\r\nZFTzWgDiUXi9SbBDLmwkY2uFJ+zreIH/vRjZHZ5ofJz9ed91HDhMB7CmRzn4JG/b\r\nSPAmTk0IRzWVBWglb0hPA8NN194ijeQFa6OJt94+EIMYuIasjsi8zGr+o1yxM5aY\r\ngTiDLzrQVWfddcZWmoCw8WWVbHAjMW60ehAs+y6ly0tBAn7wailXFRDFir1Vt4i2\r\n1WRTnJR2JebfQN4YeJ7CAiw34+PO8+vi8qHcRqMGkRu5IYdBy8AvBucVO923jIIy\r\nJBRTVkZqacRVp4PLx7vrOXX02z7y38iQcP2QSeapMoQjViYOVSMYhycO9zqGe3Tj\r\nAHMqp2HGj1uPp+3mM/yRBaE1X1j7lzjsKO1XZwjMUIYcFmAAsg2Gwi5S0FhhS+cD\r\nulCZ0A+r4wZ/1K6cZ2ZCEQoAZyMovwiVLNP+4q7pHhcQGTYAvCEgPksktQwD3YOe\r\nnSj5HG2dTMTOHDjVGSVV\r\n=qUGf\r\n-----END PGP SIGNATURE-----\r\n\r\n--O98KdSgI27dgYlM5--\r\n\r\n\r\n--===============7564301068935298617==\r\nContent-Type: text/plain; charset=\"us-ascii\"\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 7bit\r\nContent-Disposition: inline\r\n\r\n-- \r\nubuntu-security-announce mailing list\r\nubuntu-security-announce@lists.ubuntu.com\r\nModify settings or unsubscribe at: https://lists.ubuntu.com/mailman/listinfo/ubuntu-security-announce\r\n\r\n--===============7564301068935298617==--\r\n"; private static string MULTIPART_SIGNED_MESSAGE_PREVIEW = "Ubuntu Security Notice USN-3159-1 December 20, 2016 linux vulnerability A security issue affects these releases of Ubuntu and its derivatives: - Ubuntu 12.04 LTS Summary: The system could be made to expose sensitive information. Software Description: - linux: Linux kernel Details: It was discovered that a race condition existed in the procfs environ_read function in the Linux kernel, leading to an integer underflow. A local attacker could use this to expose sensitive information (kernel memory). Update instructions: The problem can be corrected by updating your system to the following package versions: Ubuntu 12.04 LTS: linux-image-3.2.0-119-generic 3.2.0-119.162 linux-image-3.2.0-119-generic-pae 3.2.0-119.162 linux-image-3.2.0-119-highbank 3.2.0-119.162 linux-image-3.2.0-119-omap 3.2.0-119.162 linux-image-3.2.0-119-powerpc-smp 3.2.0-119.162 linux-image-3.2.0-119-powerpc64-smp 3.2.0-119.162 linux-image-3.2.0-119-virtual 3.2.0-119.162 linux-image-generic 3.2.0.119.134 linux-image-generic-pae 3.2.0.119.134 linux-image-highbank 3.2.0.119.134 linux-image-omap 3.2.0.119.134 linux-image-powerpc-smp 3.2.0.119.134 linux-image-powerpc64-smp 3.2.0.119.134 linux-image-virtual 3.2.0.119.134 After a standard system update you need to reboot your computer to make all the necessary changes. ATTENTION: Due to an unavoidable ABI change the kernel updates have been given a new version number, which requires you to recompile and reinstall all third party kernel modules you might have installed. Unless you manually uninstalled the standard kernel metapackages (e.g. linux-generic, linux-generic-lts-RELEASE, linux-virtual, linux-powerpc), a standard system upgrade will automatically perform this as well. References: http://www.ubuntu.com/usn/usn-3159-1 CVE-2016-7916 Package Information: https://launchpad.net/ubuntu/+source/linux/3.2.0-119.162 ubuntu-security-announce mailing list ubuntu-security-announce@lists.ubuntu.com Modify settings or unsubscribe at: https://lists.ubuntu.com/mailman/listinfo/ubuntu-security-announce"; } diff --git a/test/engine/util-string-test.vala b/test/engine/util-string-test.vala new file mode 100644 index 00000000..64b00770 --- /dev/null +++ b/test/engine/util-string-test.vala @@ -0,0 +1,47 @@ +/* + * Copyright 2018 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.String.Test : Gee.TestCase { + + public Test() { + base("Geary.String.Test"); + add_test("test_whitespace", test_whitespace); + add_test("test_nonprinting", test_nonprinting); + } + + public void test_whitespace() { + assert(reduce_whitespace("") == ""); + assert(reduce_whitespace(" ") == ""); + assert(reduce_whitespace(" ") == ""); + assert(reduce_whitespace(" ") == ""); + assert(reduce_whitespace("test") == "test"); + assert(reduce_whitespace("test ") == "test"); + assert(reduce_whitespace("test ") == "test"); + assert(reduce_whitespace("test\n") == "test"); + assert(reduce_whitespace("test\r") == "test"); + assert(reduce_whitespace("test\t") == "test"); + assert(reduce_whitespace(" test") == "test"); + assert(reduce_whitespace(" test") == "test"); + assert(reduce_whitespace("test test") == "test test"); + assert(reduce_whitespace("test test") == "test test"); + assert(reduce_whitespace("test\ntest") == "test test"); + assert(reduce_whitespace("test\n test") == "test test"); + assert(reduce_whitespace("test \ntest") == "test test"); + assert(reduce_whitespace("test \n test") == "test test"); + assert(reduce_whitespace("test\rtest") == "test test"); + assert(reduce_whitespace("test\ttest") == "test test"); + } + + public void test_nonprinting() { + assert(reduce_whitespace("\0") == ""); // NUL + assert(reduce_whitespace("\u00A0") == ""); // ENQUIRY + assert(reduce_whitespace("\u00A0") == ""); // NO-BREAK SPACE + assert(reduce_whitespace("\u2003") == ""); // EM SPACE + assert(reduce_whitespace("test\n") == "test"); + assert(reduce_whitespace("test\ntest") == "test test"); + } +} diff --git a/test/meson.build b/test/meson.build index 2e34d186..4afad259 100644 --- a/test/meson.build +++ b/test/meson.build @@ -16,6 +16,7 @@ geary_test_engine_sources = [ 'engine/imap-engine/account-processor-test.vala', 'engine/mime-content-type-test.vala', 'engine/rfc822-mailbox-address-test.vala', + 'engine/rfc822-mailbox-addresses-test.vala', 'engine/rfc822-message-test.vala', 'engine/rfc822-message-data-test.vala', 'engine/rfc822-utils-test.vala', @@ -23,6 +24,7 @@ geary_test_engine_sources = [ 'engine/util-idle-manager-test.vala', 'engine/util-inet-test.vala', 'engine/util-js-test.vala', + 'engine/util-string-test.vala', 'engine/util-timeout-manager-test.vala' ] diff --git a/test/test-engine.vala b/test/test-engine.vala index 139308c6..d5c06bc4 100644 --- a/test/test-engine.vala +++ b/test/test-engine.vala @@ -37,9 +37,11 @@ int main(string[] args) { engine.add_suite(new Geary.JS.Test().get_suite()); engine.add_suite(new Geary.Mime.ContentTypeTest().get_suite()); engine.add_suite(new Geary.RFC822.MailboxAddressTest().get_suite()); + engine.add_suite(new Geary.RFC822.MailboxAddressesTest().get_suite()); engine.add_suite(new Geary.RFC822.MessageTest().get_suite()); engine.add_suite(new Geary.RFC822.MessageDataTest().get_suite()); engine.add_suite(new Geary.RFC822.Utils.Test().get_suite()); + engine.add_suite(new Geary.String.Test().get_suite()); /* * Run the tests