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 ///
+ * @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_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 ///
+ * @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 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