[geary/wip/791275-mailsploit-mitigation: 3/8] Check for spoofed sender addresses, only display the address if so.



commit 71e0e6835e4c1c7c603bd76f41455f7abfdc1d52
Author: Michael James Gratton <mike vee net>
Date:   Mon Jan 29 09:57:24 2018 +1030

    Check for spoofed sender addresses, only display the address if so.
    
    This adds a check for malware spoofing of RFC 822 mailbox addresses such
    as those found in Mailsploit, and if found only displays the email
    address part and not the mailbox name part.
    
    Part 1 of Mailsploit mitigation.
    
    * src/engine/rfc822/rfc822-mailbox-address.vala (MailboxAddress): Add new
      is_spoofed method to check if the mailbox address looks like it has
      been spoofed. Add is_distinct method to determine if the name and the
      label is the same. Do whitespace and non-printing character stripping
      when generating display versions of the mailbox address, rename methods
      to make it more obvious what they do and update call sites. Add unit
      tests to cover all this.
    
    * src/client/conversation-viewer/conversation-message.vala
      (ConversationMessage): Check name is distinct and is not valid before
      displaying it. Use new MailboxAddress methods for getting display
      versions of the address, to ensure we get the stripped versions of the
      addresses.
    
    * src/client/conversation-list/formatted-conversation-data.vala
      (ParticipantDisplay): Ensure full addresses are always HTML-markup
      escaped before displaying them as markup, to avoid dropping "<address>"
      values as invalid HTML. Always show the full address if an address is
      invalid.
    
    * src/engine/util/util-string.vala (reduce_whitespace): Strip not only
      whitespace but also non-printing characters. Add unit tests.

 .../account-dialog-edit-alternate-emails-pane.vala |    2 +-
 src/client/composer/composer-widget.vala           |    8 +-
 .../formatted-conversation-data.vala               |   16 +-
 .../conversation-viewer/conversation-message.vala  |   31 +--
 src/client/notification/libnotify.vala             |    6 +-
 src/engine/rfc822/rfc822-mailbox-address.vala      |  221 +++++++++++++++----
 src/engine/smtp/smtp-request.vala                  |    6 +-
 src/engine/util/util-string.vala                   |   37 ++--
 test/CMakeLists.txt                                |    1 +
 test/engine/rfc822-mailbox-address-test.vala       |   82 +++++++
 test/engine/rfc822-message-data-test.vala          |    4 +-
 test/engine/util-string-test.vala                  |   47 ++++
 test/meson.build                                   |    1 +
 test/test-engine.vala                              |    1 +
 14 files changed, 362 insertions(+), 101 deletions(-)
---
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 2f9de01..c596497 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 = "<b>%s</b>".printf(Geary.HTML.escape_markup(mailbox.get_full_address()));
+            label = "<b>%s</b>".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 62fa2a0..09438ae 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 ae9afc6..03fdab6 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<Geary.RFC822.MailboxAddress> 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<Geary.RFC822.MailboxAddress> 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);
diff --git a/src/client/conversation-viewer/conversation-message.vala 
b/src/client/conversation-viewer/conversation-message.vala
index 432dc28..ef87805 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 <mike vee net>
+ * Copyright 2016-2018 Michael Gratton <mike vee net>
  *
  * 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
@@ -69,19 +60,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() && !address.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 +564,7 @@ public class ConversationMessage : Gtk.Grid {
             Gee.List<Geary.RFC822.MailboxAddress> 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 +758,7 @@ public class ConversationMessage : Gtk.Grid {
             Gee.Map<string,string> values = new Gee.HashMap<string,string>();
             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 c27c3f3..f6717ab 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 5b46792..c4de959 100644
--- a/src/engine/rfc822/rfc822-mailbox-address.vala
+++ b/src/engine/rfc822/rfc822-mailbox-address.vala
@@ -1,17 +1,35 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
+/*
+ * Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright 2018 Michael Gratton <mike vee net>
  *
  * 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]]
+ * See [[https://tools.ietf.org/html/rfc5322#section-3.4]]
  */
-
 public class Geary.RFC822.MailboxAddress : Geary.MessageData.SearchableMessageData,
     Gee.Hashable<MailboxAddress>, BaseObject {
+
+    /** 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;
+        }
+    }
+
+
     internal delegate string ListToStringDelegate(MailboxAddress address);
     
     /**
@@ -53,7 +71,7 @@ public class Geary.RFC822.MailboxAddress : Geary.MessageData.SearchableMessageDa
 
         source_route = null;
 
-        int atsign = address.index_of_char('@');
+        int atsign = address.last_index_of_char('@');
         if (atsign > 0) {
             mailbox = address.slice(0, atsign);
             domain = address.slice(atsign + 1, address.length);
@@ -68,8 +86,8 @@ public class Geary.RFC822.MailboxAddress : Geary.MessageData.SearchableMessageDa
         this.source_route = source_route;
         this.mailbox = mailbox;
         this.domain = domain;
-        
-        address = "%s@%s".printf(mailbox, domain);
+
+        this.address = "%s@%s".printf(mailbox, domain);
     }
 
     public MailboxAddress.from_rfc822_string(string rfc822) throws RFC822Error {
@@ -122,29 +140,70 @@ public class Geary.RFC822.MailboxAddress : Geary.MessageData.SearchableMessageDa
     }
 
     /**
-     * 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
+     * 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 //<//
+     * @param close optional string to use as the closing 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_full_address() {
-        return String.is_empty(name) ? address : "%s <%s>".printf(name, 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 simple address, that is, no human-readable name and the email address in angled
+     * 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_simple_address() {
-        return "<%s>".printf(address);
+    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 pretty address, showing only the name, but if unavailable, the
-     * mailbox name (that is, the account name without the domain).
+     * Returns a human-readable version of the address part.
+     *
+     * @param open optional string to use as the opening bracket,
+     * defaults to //<//
+     * @param close optional string to use as the closing bracket,
+     * defaults to //>//
+     * @return the {@link address} part, cleaned and enclosed within the
+     * specified brackets.
      */
-    public string get_short_address() {
-        return name ?? mailbox;
+    public string to_address_display(string open = "<", string close = ">") {
+        return open + Geary.String.reduce_whitespace(this.address) + close;
     }
 
     /**
@@ -153,47 +212,110 @@ 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.
+     * Determines if the mailbox address appears to have been spoofed.
+     *
+     * Using recipient and sender mailbox addresses where the name
+     * part is also actually a valid RFC822 address
+     * (e.g. "you example com <jerk spammer 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 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 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;
     }
-    
+
     /**
-     * Returns the address suitable for insertion into an RFC822 message.  RFC822 quoting is
-     * performed if required.
+     * Determines if the name part is different to the address part.
      *
-     * @see get_full_address
+     * @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 address suitable for insertion into an RFC822 message.
+     *
+     * @return the RFC822 quoted 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_quote_string(this.name), this.address)
+            : this.address;
     }
-    
+
     /**
      * 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,10 +327,15 @@ 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<MailboxAddress> addrs,
         string empty, ListToStringDelegate to_s) {
         switch (addrs.size) {
diff --git a/src/engine/smtp/smtp-request.vala b/src/engine/smtp/smtp-request.vala
index a98cf53..c3db013 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_address_display("<", ">")) });
     }
-    
+
     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 924ade9..0a2ef83 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 9ca4140..adae1bd 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -27,6 +27,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 b06b263..172bdda 100644
--- a/test/engine/rfc822-mailbox-address-test.vala
+++ b/test/engine/rfc822-mailbox-address-test.vala
@@ -10,6 +10,11 @@ class Geary.RFC822.MailboxAddressTest : Gee.TestCase {
     public MailboxAddressTest() {
         base("Geary.RFC822.MailboxAddressTest");
         add_test("is_valid_address", is_valid_address);
+        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);
+        add_test("to_rfc822_string", to_rfc822_string);
     }
 
     public void is_valid_address() {
@@ -30,4 +35,81 @@ class Geary.RFC822.MailboxAddressTest : Gee.TestCase {
         assert(Geary.RFC822.MailboxAddress.is_valid_address("") == false);
     }
 
+    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?=\" 
<demo mailsploit com>")
+                   .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 <example example com>");
+        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_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 <example example com>");
+        assert(new MailboxAddress("test test", "example example com").to_rfc822_string() ==
+               "test test <example example com>");
+        assert(new MailboxAddress("example example com", "example example com").to_rfc822_string() ==
+               "example example com");
+        // Technically, per
+        // https://tools.ietf.org/html/rfc5322#appendix-A.1.2 this
+        // would be fine as just "test? <example example com>",
+        // i.e. without the name being quoted, but I guess GMime is
+        // just being conservative here?
+        assert(new MailboxAddress("test?", "example example com").to_rfc822_string() ==
+               "\"test?\" <example example com>");
+        assert(new MailboxAddress(";", "example example com").to_rfc822_string() ==
+               "\";\" <example example com>");
+    }
 }
diff --git a/test/engine/rfc822-message-data-test.vala b/test/engine/rfc822-message-data-test.vala
index 61814f1..9d8ec34 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</a=
</p></td></tr>
 </table></body></html>""";
 
-    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 = """<!DOCTYPE html>
 <!--2c2a1c66-0638-7c87-5057-bff8be4291eb_v180-->
@@ -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/util-string-test.vala b/test/engine/util-string-test.vala
new file mode 100644
index 0000000..64b0077
--- /dev/null
+++ b/test/engine/util-string-test.vala
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2018 Michael Gratton <mike vee net>
+ *
+ * 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 2e34d18..d9bee04 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -23,6 +23,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 139308c..d947f4b 100644
--- a/test/test-engine.vala
+++ b/test/test-engine.vala
@@ -40,6 +40,7 @@ int main(string[] args) {
     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


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]