Merge branch 'wip/791275-mailsploit-mitigation'. Fixes Bug 791275.

This commit is contained in:
Michael James Gratton 2018-01-31 16:41:53 +10:30
commit b7eea85725
20 changed files with 1049 additions and 372 deletions

View file

@ -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")]

View file

@ -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);

View file

@ -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
}

View file

@ -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);
@ -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 ? "<b>" : "", Geary.HTML.escape_markup(participant), is_unread ? "</b>" : "");
string markup = Geary.HTML.escape_markup(participant);
if (is_unread) {
markup = "<b>%s</b>".printf(markup);
}
if (this.address.is_spoofed()) {
markup = "<s>%s</s>".printf(markup);
}
return markup;
}
public bool equal_to(ParticipantDisplay other) {
return address.equal_to(other.address);
}

View file

@ -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
@ -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<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 +769,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();

View file

@ -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) {

View file

@ -1,102 +1,58 @@
/* 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]]
* 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<MailboxAddress>,
BaseObject {
public class Geary.RFC822.MailboxAddress : Geary.MessageData.SearchableMessageData,
Gee.Hashable<MailboxAddress>, BaseObject {
internal delegate string ListToStringDelegate(MailboxAddress address);
/**
* The optional user-friendly name associated with the {@link MailboxAddress}.
*
* For "Dirk Gently <dirk@example.com>", 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 <dirk@example.com>", this would be "dirk".
*/
public string mailbox { get; private set; }
/**
* The domain portion of the {@link MailboxAddress}.
*
* For "Dirk Gently <dirk@example.com>", this would be "example.com".
*/
public string domain { get; private set; }
/**
* The address specification of the {@link MailboxAddress}.
*
* For "Dirk Gently <dirk@example.com>", 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 <dirk@example.com>", 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 <dirk@example.com>", 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 <dirk@example.com>", 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 <dirk@example.com>", 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 <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 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<MailboxAddress> 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;
}
}
}
}

View file

@ -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<MailboxAddresses> {
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<MailboxAddresses> {
/**
* 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<MailboxAddress> 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<MailboxAddress> addrs = new Gee.ArrayList<MailboxAddress>();
public MailboxAddresses(Gee.Collection<MailboxAddress> 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<MailboxAddress> iterator() {
return addrs.iterator();
}
public Gee.List<MailboxAddress> 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<string> sorted_addresses = traverse<RFC822.MailboxAddress>(addrs)
.map<string>(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<RFC822.MailboxAddress> first = new Gee.HashSet<RFC822.MailboxAddress>();
first.add_all(addrs);
Gee.HashSet<RFC822.MailboxAddress> second = new Gee.HashSet<RFC822.MailboxAddress>();
second.add_all(other.addrs);
return Collection.are_sets_equal<RFC822.MailboxAddress>(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());
}
}

View file

@ -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);
}

View file

@ -1,4 +1,6 @@
/* 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.
@ -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<RFC822.MailboxAddress>? 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<RFC822.MailboxAddress>? convert_gmime_address_list(InternetAddressList? addrlist,
int depth = 0) {
if (addrlist == null || addrlist.length() == 0)
return null;
Gee.List<RFC822.MailboxAddress>? converted = new Gee.ArrayList<RFC822.MailboxAddress>();
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<RFC822.MailboxAddress>? 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<RFC822.MailboxAddress>? get_recipients() {
Gee.List<RFC822.MailboxAddress> addrs = new Gee.ArrayList<RFC822.MailboxAddress>();
@ -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<RFC822.MailboxAddress>? 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<RFC822.MailboxAddress>? 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<GMime.Part> attachments, GMime.Object root,
Mime.DispositionType requested_disposition) throws RFC822Error {
// If this is a multipart container, dive into each of its children.

View file

@ -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) {

View file

@ -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.)

View file

@ -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
)

View file

@ -1,5 +1,5 @@
/*
* 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.
@ -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("<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\"@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 <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("\"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("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?= <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?=\" <test2@example.com>");
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?=\" <demo@mailsploit.com>");
assert(addr.name == "test <potus@whitehouse.gov>?\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?=\" <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_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 <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");
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").to_rfc822_string() ==
"\";\" <example@example.com>");
assert(new MailboxAddress("©", "example@example.com").to_rfc822_string() ==
"=?iso-8859-1?b?qQ==?= <example@example.com>");
assert(new MailboxAddress("😸", "example@example.com").to_rfc822_string() ==
"=?UTF-8?b?8J+YuA==?= <example@example.com>");
}
}

View file

@ -0,0 +1,46 @@
/*
* 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.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?=\" <demo@mailsploit.com>");
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=?=\", <demo@mailsploit.com>");
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<MailboxAddress> addresses = new Gee.LinkedList<MailboxAddress>();
foreach (string address in address_strings) {
addresses.add(new MailboxAddress(null, address));
}
return new MailboxAddresses(addresses);
}
}

View file

@ -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";
}

File diff suppressed because one or more lines are too long

View file

@ -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");
}
}

View file

@ -16,6 +16,7 @@ geary_test_engine_sources = [
'engine/imap-engine/account-processor-test.vala',
'engine/mime-content-type-test.vala',
'engine/rfc822-mailbox-address-test.vala',
'engine/rfc822-mailbox-addresses-test.vala',
'engine/rfc822-message-test.vala',
'engine/rfc822-message-data-test.vala',
'engine/rfc822-utils-test.vala',
@ -23,6 +24,7 @@ geary_test_engine_sources = [
'engine/util-idle-manager-test.vala',
'engine/util-inet-test.vala',
'engine/util-js-test.vala',
'engine/util-string-test.vala',
'engine/util-timeout-manager-test.vala'
]

View file

@ -37,9 +37,11 @@ int main(string[] args) {
engine.add_suite(new Geary.JS.Test().get_suite());
engine.add_suite(new Geary.Mime.ContentTypeTest().get_suite());
engine.add_suite(new Geary.RFC822.MailboxAddressTest().get_suite());
engine.add_suite(new Geary.RFC822.MailboxAddressesTest().get_suite());
engine.add_suite(new Geary.RFC822.MessageTest().get_suite());
engine.add_suite(new Geary.RFC822.MessageDataTest().get_suite());
engine.add_suite(new Geary.RFC822.Utils.Test().get_suite());
engine.add_suite(new Geary.String.Test().get_suite());
/*
* Run the tests