Commit 71e0e683 authored by Michael Gratton's avatar Michael Gratton 🤞

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.
parent f6b4b5c9
......@@ -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);
......
......@@ -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
}
......
......@@ -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);
......
/*
* 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();
......
......@@ -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) {
......
/* 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) {
......
......@@ -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) {
......
......@@ -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.)
......
......@@ -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
)
......
......@@ -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>");
}
}
......@@ -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";
}
/*
* 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");
}
}
......@@ -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'
]
......