Merge branch 'wip/3.32-avatars' into 'master'

3.32 Avatars

Closes #269

See merge request GNOME/geary!154
This commit is contained in:
Michael Gratton 2019-03-09 11:25:36 +00:00
commit 791c321a00
29 changed files with 943 additions and 260 deletions

View file

@ -17,18 +17,21 @@ variables:
# Fedora packages
FEDORA_DEPS: vala
meson desktop-file-utils libcanberra-devel libgee-devel
glib2-devel gmime-devel gtk3-devel libnotify-devel sqlite-devel
webkitgtk4-devel libsecret-devel libxml2-devel vala-tools
gcr-devel enchant2-devel libunwind-devel iso-codes-devel
gnome-online-accounts-devel itstool json-glib-devel
meson desktop-file-utils libcanberra-devel
folks-devel libgee-devel glib2-devel gmime-devel
gtk3-devel libnotify-devel sqlite-devel
webkitgtk4-devel libsecret-devel libxml2-devel
vala-tools gcr-devel enchant2-devel libunwind-devel
iso-codes-devel gnome-online-accounts-devel itstool
json-glib-devel
FEDORA_TEST_DEPS: Xvfb tar xz
# Ubuntu packages
UBUNTU_DEPS: valac build-essential
meson desktop-file-utils libcanberra-dev
libgee-0.8-dev libglib2.0-dev libgmime-2.6-dev libgtk-3-dev
libsecret-1-dev libxml2-dev libnotify-dev libsqlite3-dev
libfolks-dev libgee-0.8-dev libglib2.0-dev
libgmime-2.6-dev libgtk-3-dev libsecret-1-dev
libxml2-dev libnotify-dev libsqlite3-dev
libwebkit2gtk-4.0-dev libgcr-3-dev libenchant-dev
libunwind-dev iso-codes libgoa-1.0-dev itstool gettext
libmessaging-menu-dev libunity-dev libjson-glib-dev

18
INSTALL
View file

@ -41,9 +41,9 @@ Installing dependencies on Fedora
Fedora 25 and later ships with the correct versions of the required
libraries. Install them by running this command:
sudo yum install vala meson \
desktop-file-utils iso-codes-devel libcanberra-devel libgee-devel \
glib2-devel gmime-devel gtk3-devel libnotify-devel sqlite-devel \
sudo yum install vala meson desktop-file-utils iso-codes-devel \
libcanberra-devel folks-devel libgee-devel glib2-devel \
gmime-devel gtk3-devel libnotify-devel sqlite-devel \
webkitgtk4-devel libsecret-devel libxml2-devel vala-tools \
gcr-devel enchant2-devel libunwind-devel json-glib-devel \
gnome-online-accounts-devel itstool
@ -62,12 +62,12 @@ required libraries.
Install them by running this command:
sudo apt-get install valac \
meson desktop-file-utils iso-codes libcanberra-dev \
libgee-0.8-dev libglib2.0-dev libgmime-2.6-dev libgtk-3-dev \
libsecret-1-dev libxml2-dev libnotify-dev libsqlite3-dev \
libwebkit2gtk-4.0-dev libgcr-3-dev libenchant-dev \
libunwind-dev libgoa-1.0-dev libjson-glib-dev itstool gettext
sudo apt-get install valac meson desktop-file-utils iso-codes \
libcanberra-dev libfolks-dev libgee-0.8-dev libglib2.0-dev \
libgmime-2.6-dev libgtk-3-dev libsecret-1-dev libxml2-dev \
libnotify-dev libsqlite3-dev libwebkit2gtk-4.0-dev \
libgcr-3-dev libenchant-dev libunwind-dev libgoa-1.0-dev \
libjson-glib-dev itstool gettext
And for Ubuntu Unity integration:

View file

@ -74,6 +74,8 @@
<p>Enhancements included in this release:</p>
<ul>
<li>Application menu moved to the main window</li>
<li>Desktop contacts are used for sender images</li>
<li>Unknown contacts are given personalised initials and colour</li>
<li>Updated application icons</li>
<li>Improved server compatibility</li>
<li>Custom email CSS now applied to Composer view</li>

View file

@ -123,12 +123,6 @@
<description>The last recorded size of the detached composer window.</description>
</key>
<key name="avatar-url" type="s">
<default>"https://secure.gravatar.com/avatar"</default>
<summary>Base URL to look up contact avatars</summary>
<description>A Gravatar or Libravatar compatible URL, set to the empty string to disable.</description>
</key>
<key name="migrated-config" type="b">
<default>false</default>
<summary>Whether we migrated the old settings</summary>

View file

@ -54,6 +54,7 @@ webkit2gtk = dependency('webkit2gtk-4.0', version: '>=' + target_webkit)
# Secondary deps - keep sorted alphabetically
enchant = dependency('enchant-2', version: '>=2.1', required: false) # see below
folks = dependency('folks', version: '>=0.11')
gck = dependency('gck-1')
gcr = dependency('gcr-3', version: '>= 3.10.1')
gdk = dependency('gdk-3.0', version: '>=' + target_gtk)

View file

@ -74,16 +74,6 @@
}
]
},
{
"name": "gmime",
"sources": [
{
"type": "git",
"url": "https://github.com/jstedfast/gmime.git",
"branch": "gmime-2-6"
}
]
},
{
"name": "gnome-online-accounts",
"config-opts": [
@ -105,6 +95,87 @@
}
]
},
{
"name": "libical",
"cleanup": [
"/lib/cmake"
],
"buildsystem": "cmake-ninja",
"config-opts": [
"-DCMAKE_BUILD_TYPE=Release",
"-DCMAKE_INSTALL_LIBDIR=lib",
"-DBUILD_SHARED_LIBS:BOOL=ON"
],
"sources": [
{
"type": "archive",
"url": "https://github.com/libical/libical/releases/download/v2.0.0/libical-2.0.0.tar.gz",
"sha256": "654c11f759c19237be39f6ad401d917e5a05f36f1736385ed958e60cf21456da"
}
]
},
{
"name": "evolution-data-server",
"cleanup": [
"/lib/cmake",
"/lib/evolution-data-server/*-backends",
"/libexec",
"/share/dbus-1/services"
],
"config-opts": [
"-DCMAKE_BUILD_TYPE=Release",
"-DENABLE_GTK=ON",
"-DENABLE_GOA=ON",
"-DENABLE_UOA=OFF",
"-DENABLE_GOOGLE_AUTH=OFF",
"-DENABLE_GOOGLE=OFF",
"-DENABLE_WITH_PHONENUMBER=OFF",
"-DENABLE_VALA_BINDINGS=ON",
"-DENABLE_WEATHER=OFF",
"-DWITH_OPENLDAP=OFF",
"-DWITH_LIBDB=OFF",
"-DENABLE_INTROSPECTION=ON",
"-DENABLE_INSTALLED_TESTS=OFF",
"-DENABLE_GTK_DOC=OFF",
"-DENABLE_EXAMPLES=OFF"
],
"buildsystem": "cmake-ninja",
"sources": [
{
"type": "git",
"url": "https://gitlab.gnome.org/GNOME/evolution-data-server.git"
}
]
},
{
"name": "folks",
"cleanup": [
"/bin",
"/share/GConf"
],
"config-opts": [
"--disable-telepathy-backend",
"--disable-inspect-tool",
"--disable-import-tool",
"--disable-fatal-warnings"
],
"sources": [
{
"type": "git",
"url": "https://gitlab.gnome.org/GNOME/folks.git"
}
]
},
{
"name": "gmime",
"sources": [
{
"type": "git",
"url": "https://github.com/jstedfast/gmime.git",
"branch": "gmime-2-6"
}
]
},
{
"name": "libunwind",
"sources": [
@ -121,9 +192,8 @@
"builddir": true,
"sources": [
{
"type": "git",
"url": "https://gitlab.gnome.org/GNOME/geary.git",
"branch": "master"
"type": "dir",
"path": "."
}
]
}

View file

@ -88,6 +88,7 @@ src/client/sidebar/sidebar-common.vala
src/client/sidebar/sidebar-count-cell-renderer.vala
src/client/sidebar/sidebar-entry.vala
src/client/sidebar/sidebar-tree.vala
src/client/util/util-avatar.vala
src/client/util/util-date.vala
src/client/util/util-email.vala
src/client/util/util-files.vala
@ -114,6 +115,7 @@ src/engine/api/geary-contact.vala
src/engine/api/geary-credentials-mediator.vala
src/engine/api/geary-credentials.vala
src/engine/api/geary-email-flags.vala
src/engine/api/geary-email-header-set.vala
src/engine/api/geary-email-identifier.vala
src/engine/api/geary-email-properties.vala
src/engine/api/geary-email.vala

View file

@ -12,119 +12,201 @@
public class Application.AvatarStore : Geary.BaseObject {
// Initiates and manages an avatar load using Gravatar
private class AvatarLoader : Geary.BaseObject {
// Max age is low since we really only want to cache between
// conversation loads.
private const int64 MAX_CACHE_AGE_US = 5 * 1000 * 1000;
internal Gdk.Pixbuf? avatar = null;
internal Geary.Nonblocking.Semaphore lock =
new Geary.Nonblocking.Semaphore();
private string base_url;
private Geary.RFC822.MailboxAddress address;
private int pixel_size;
// Max size is low since most conversations don't get above the
// low hundreds of messages, and those that do will likely get
// many repeated participants
private const uint MAX_CACHE_SIZE = 128;
internal AvatarLoader(Geary.RFC822.MailboxAddress address,
string base_url,
int pixel_size) {
this.address = address;
this.base_url = base_url;
this.pixel_size = pixel_size;
private class CacheEntry {
public static string to_key(Geary.RFC822.MailboxAddress mailbox) {
// Use short name as the key, since it will use the name
// first, then the email address, which is especially
// important for things like GitLab email where the
// address is always the same, but the name changes. This
// ensures that each such user gets different initials.
return mailbox.to_short_display().normalize().casefold();
}
internal async void load(Soup.Session session,
Cancellable load_cancelled)
throws GLib.Error {
Error? workaround_err = null;
if (!Geary.String.is_empty_or_whitespace(this.base_url)) {
string md5 = GLib.Checksum.compute_for_string(
GLib.ChecksumType.MD5, this.address.address.strip().down()
);
Soup.Message message = new Soup.Message(
"GET",
"%s/%s?d=%s&s=%d".printf(
this.base_url, md5, "404", this.pixel_size
)
);
public static int lru_compare(CacheEntry a, CacheEntry b) {
return (a.key == b.key)
? 0 : (int) (a.last_used - b.last_used);
}
try {
// We want to just pass load_cancelled to send_async
// here, but per Bug 778720 this is causing some
// crashy race in libsoup's cache implementation, so
// for now just let the load go through and manually
// check to see if the load has been cancelled before
// setting the avatar
InputStream data = yield session.send_async(
message,
null // should be 'load_cancelled'
);
if (message.status_code == 200 &&
data != null &&
!load_cancelled.is_cancelled()) {
this.avatar = yield new Gdk.Pixbuf.from_stream_at_scale_async(
data, pixel_size, pixel_size, true, load_cancelled
);
}
} catch (Error err) {
workaround_err = err;
public string key;
public Geary.RFC822.MailboxAddress mailbox;
// Store nulls so we can also cache avatars not found
public Folks.Individual? individual;
public int64 last_used;
private Gee.List<Gdk.Pixbuf> pixbufs = new Gee.LinkedList<Gdk.Pixbuf>();
public CacheEntry(Geary.RFC822.MailboxAddress mailbox,
Folks.Individual? individual,
int64 last_used) {
this.key = to_key(mailbox);
this.mailbox = mailbox;
this.individual = individual;
this.last_used = last_used;
}
public async Gdk.Pixbuf? load(int pixel_size,
GLib.Cancellable cancellable)
throws GLib.Error {
Gdk.Pixbuf? pixbuf = null;
foreach (Gdk.Pixbuf cached in this.pixbufs) {
if ((cached.height == pixel_size && cached.width >= pixel_size) ||
(cached.width == pixel_size && cached.height >= pixel_size)) {
pixbuf = cached;
break;
}
}
this.lock.blind_notify();
if (workaround_err != null) {
throw workaround_err;
if (pixbuf == null) {
Folks.Individual? individual = this.individual;
if (individual != null && individual.avatar != null) {
GLib.InputStream data = yield individual.avatar.load_async(
pixel_size, cancellable
);
pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async(
data, pixel_size, pixel_size, true, cancellable
);
pixbuf = Util.Avatar.round_image(pixbuf);
this.pixbufs.add(pixbuf);
}
}
if (pixbuf == null) {
string? name = null;
// XXX should really be using the folks display name
// here as below, but since we should the name from
// the email address if present in
// ConversationMessage, and since that might not match
// the folks display name, it is confusing when the
// initials are one thing and the name is
// another. Re-enable below when we start using the
// folks display name in ConversationEmail
name = this.mailbox.to_short_display();
// if (this.individual != null) {
// name = this.individual.display_name;
// } else {
// // Use short display because it will clean up the
// // string, use the name if present and fall back
// // on the address if not.
// name = this.mailbox.to_short_display();
// }
pixbuf = Util.Avatar.generate_user_picture(name, pixel_size);
pixbuf = Util.Avatar.round_image(pixbuf);
this.pixbufs.add(pixbuf);
}
return pixbuf;
}
}
private Configuration config;
private Soup.Session session;
private Soup.Cache cache;
private Gee.Map<string,AvatarLoader> loaders =
new Gee.HashMap<string,AvatarLoader>();
private Folks.IndividualAggregator individuals;
private Gee.Map<string,CacheEntry> lru_cache =
new Gee.HashMap<string,CacheEntry>();
private Gee.SortedSet<CacheEntry> lru_ordering =
new Gee.TreeSet<CacheEntry>(CacheEntry.lru_compare);
public AvatarStore(Configuration config, GLib.File cache_root) {
this.config = config;
File avatar_cache_dir = cache_root.get_child("avatars");
this.cache = new Soup.Cache(
avatar_cache_dir.get_path(),
Soup.CacheType.SINGLE_USER
);
this.cache.load();
this.cache.set_max_size(16 * 1024 * 1024); // 16MB
this.session = new Soup.Session();
this.session.add_feature(this.cache);
public AvatarStore(Folks.IndividualAggregator individuals) {
this.individuals = individuals;
}
public void close() {
this.cache.flush();
this.cache.dump();
this.lru_cache.clear();
this.lru_ordering.clear();
}
public async Gdk.Pixbuf? load(Geary.RFC822.MailboxAddress address,
int pixel_size,
Cancellable load_cancelled)
throws Error {
string key = address.to_string();
AvatarLoader loader = this.loaders.get(key);
if (loader == null) {
// Haven't started loading the avatar, so do it now
loader = new AvatarLoader(
address, this.config.avatar_url, pixel_size
);
this.loaders.set(key, loader);
yield loader.load(this.session, load_cancelled);
} else {
// Load has already started, so wait for it to finish
yield loader.lock.wait_async();
public async Gdk.Pixbuf? load(Geary.RFC822.MailboxAddress mailbox,
int pixel_size,
GLib.Cancellable cancellable)
throws GLib.Error {
// Normalise the address to improve caching
CacheEntry match = yield get_match(mailbox);
return yield match.load(pixel_size, cancellable);
}
private async CacheEntry get_match(Geary.RFC822.MailboxAddress mailbox)
throws GLib.Error {
string key = CacheEntry.to_key(mailbox);
int64 now = GLib.get_monotonic_time();
CacheEntry? entry = this.lru_cache.get(key);
if (entry != null) {
if (entry.last_used + MAX_CACHE_AGE_US >= now) {
// Need to remove the entry from the ordering before
// updating the last used time since doing so changes
// the ordering
this.lru_ordering.remove(entry);
entry.last_used = now;
this.lru_ordering.add(entry);
} else {
this.lru_cache.unset(key);
this.lru_ordering.remove(entry);
entry = null;
}
}
return loader.avatar;
if (entry == null) {
Folks.Individual? match = yield search_match(mailbox.address);
entry = new CacheEntry(mailbox, match, now);
this.lru_cache.set(key, entry);
this.lru_ordering.add(entry);
// Prune the cache if needed
if (this.lru_cache.size > MAX_CACHE_SIZE) {
CacheEntry oldest = this.lru_ordering.first();
this.lru_cache.unset(oldest.key);
this.lru_ordering.remove(oldest);
}
}
return entry;
}
private async Folks.Individual? search_match(string address)
throws GLib.Error {
Folks.SearchView view = new Folks.SearchView(
this.individuals,
new Folks.SimpleQuery(
address,
new string[] {
Folks.PersonaStore.detail_key(
Folks.PersonaDetail.EMAIL_ADDRESSES
)
}
)
);
yield view.prepare();
Folks.Individual? match = null;
if (!view.individuals.is_empty) {
match = view.individuals.first();
}
try {
yield view.unprepare();
} catch (GLib.Error err) {
warning("Error unpreparing Folks search: %s", err.message);
}
return match;
}
}

View file

@ -29,7 +29,6 @@ public class Configuration {
public const string SEARCH_STRATEGY_KEY = "search-strategy";
public const string CONVERSATION_VIEWER_ZOOM_KEY = "conversation-viewer-zoom";
public const string COMPOSER_WINDOW_SIZE_KEY = "composer-window-size";
public const string AVATAR_URL = "avatar-url";
public enum DesktopEnvironment {
@ -178,10 +177,6 @@ public class Configuration {
}
}
public string avatar_url {
owned get { return settings.get_string(AVATAR_URL); }
}
// Creates a configuration object.
public Configuration(string schema_id) {
// Start GSettings.

View file

@ -260,10 +260,16 @@ public class GearyController : Geary.BaseObject {
error("Error loading web resources: %s", err.message);
}
this.avatar_store = new Application.AvatarStore(
this.application.config,
this.application.get_user_cache_directory()
);
Folks.IndividualAggregator individuals =
Folks.IndividualAggregator.dup();
if (!individuals.is_prepared) {
try {
yield individuals.prepare();
} catch (GLib.Error err) {
error("Error preparing Folks: %s", err.message);
}
}
this.avatar_store = new Application.AvatarStore(individuals);
// Create the main window (must be done after creating actions.)
main_window = new MainWindow(this.application);
@ -2625,7 +2631,7 @@ public class GearyController : Geary.BaseObject {
// string substitution is a list of recipients of the email.
string message = _(
"Successfully sent mail to %s."
).printf(EmailUtil.to_short_recipient_display(rfc822.to));
).printf(Util.Email.to_short_recipient_display(rfc822.to));
InAppNotification notification = new InAppNotification(message);
this.main_window.add_notification(notification);
Libnotify.play_sound("message-sent-email");

View file

@ -83,7 +83,7 @@ public class ConversationListStore : Gtk.ListStore {
Geary.App.Conversation a, b;
model.get(aiter, Column.CONVERSATION_OBJECT, out a);
model.get(biter, Column.CONVERSATION_OBJECT, out b);
return compare_conversation_ascending(a, b);
return Util.Email.compare_conversation_ascending(a, b);
}
@ -225,8 +225,10 @@ public class ConversationListStore : Gtk.ListStore {
// sort the conversations so the previews are fetched from the newest to the oldest, matching
// the user experience
Gee.TreeSet<Geary.App.Conversation> sorted_conversations = new Gee.TreeSet<Geary.App.Conversation>(
compare_conversation_descending);
Gee.TreeSet<Geary.App.Conversation> sorted_conversations =
new Gee.TreeSet<Geary.App.Conversation>(
Util.Email.compare_conversation_descending
);
sorted_conversations.add_all(this.conversations.read_only_view);
foreach (Geary.App.Conversation conversation in sorted_conversations) {
// find oldest unread message for the preview

View file

@ -111,7 +111,7 @@ public class FormattedConversationData : Geary.BaseObject {
// Load preview-related data.
update_date_string();
this.subject = EmailUtil.strip_subject_prefixes(preview);
this.subject = Util.Email.strip_subject_prefixes(preview);
this.body = Geary.String.reduce_whitespace(preview.get_preview_as_string());
this.preview = preview;

View file

@ -289,6 +289,9 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface {
/** Determines if the email is a draft message. */
public bool is_draft { get; private set; }
/** The email's primary originator, if any. */
public Geary.RFC822.MailboxAddress? primary_originator { get; private set; }
/** The view displaying the email's primary message headers and body. */
public ConversationMessage primary_message { get; private set; }
@ -444,6 +447,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface {
base_ref();
this.email = email;
this.is_draft = is_draft;
this.primary_originator = Util.Email.get_primary_originator(email);
this.email_store = email_store;
this.contact_store = email_store.account.get_contact_store();
this.avatar_store = avatar_store;
@ -507,9 +511,11 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface {
// Construct the view for the primary message, hook into it
bool load_images = email.load_remote_images().is_certain();
Geary.Contact contact = this.contact_store.get_by_rfc822(
email.get_primary_originator()
);
Geary.Contact? contact = null;
if (this.primary_originator != null) {
contact = this.contact_store.get_by_rfc822(this.primary_originator);
}
if (contact != null) {
load_images |= contact.always_load_remote_images();
}
@ -577,7 +583,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface {
} catch (IOError.CANCELLED err) {
// okay
} catch (Error err) {
Geary.RFC822.MailboxAddress? from = this.email.get_primary_originator();
Geary.RFC822.MailboxAddress? from = this.primary_originator;
debug("Avatar load failed for \"%s\": %s",
from != null ? from.to_string() : "<unknown>", err.message);
}
@ -1038,7 +1044,7 @@ public class ConversationEmail : Gtk.Box, Geary.BaseInterface {
private void on_remember_remote_images(ConversationMessage view) {
Geary.RFC822.MailboxAddress? sender = this.email.get_primary_originator();
Geary.RFC822.MailboxAddress? sender = this.primary_originator;
if (sender != null) {
Geary.Contact? contact = this.contact_store.get_by_rfc822(sender);
if (contact != null) {

View file

@ -289,7 +289,7 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
bool load_remote_images,
Configuration config) {
this(
email.get_primary_originator(),
Util.Email.get_primary_originator(email),
email.from,
email.reply_to,
email.sender,
@ -315,7 +315,7 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
bool load_remote_images,
Configuration config) {
this(
message.get_primary_originator(),
Util.Email.get_primary_originator(message),
message.from,
message.reply_to,
message.sender,
@ -613,15 +613,14 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
throw new GLib.IOError.CANCELLED("Conversation load cancelled");
}
const int PIXEL_SIZE = 32;
// We occasionally get crashes calling as below
// Gtk.Image.get_pixel_size() when the image is null. There's
// perhaps some race going on there. So we need to hard-code
// the size here and keep it in sync with
// ui/conversation-message.ui. :(
const int PIXEL_SIZE = 48;
if (this.primary_originator != null) {
int window_scale = get_scale_factor();
// We occasionally get crashes calling as below
// Gtk.Image.get_pixel_size() when the image is
// null. There's perhaps some race going on there. So we
// need to hard-code the size and keep it in sync with
// ui/conversation-message.ui. :(
//
//int pixel_size = this.avatar.get_pixel_size() * window_scale;
int pixel_size = PIXEL_SIZE * window_scale;
Gdk.Pixbuf? avatar_buf = yield loader.load(

View file

@ -92,6 +92,7 @@ geary_client_vala_sources = files(
'sidebar/sidebar-entry.vala',
'sidebar/sidebar-tree.vala',
'util/util-avatar.vala',
'util/util-date.vala',
'util/util-email.vala',
'util/util-files.vala',
@ -110,6 +111,7 @@ geary_client_sources = [
geary_client_dependencies = [
libmath,
enchant,
folks,
gck,
gcr,
gee,

View file

@ -95,7 +95,8 @@ public class Libnotify : Geary.BaseObject {
return;
// possible to receive email with no originator
Geary.RFC822.MailboxAddress? primary = email.get_primary_originator();
Geary.RFC822.MailboxAddress? primary =
Util.Email.get_primary_originator(email);
if (primary == null) {
notify_new_mail(folder, 1);
@ -105,10 +106,15 @@ public class Libnotify : Geary.BaseObject {
string body;
int count = monitor.get_new_message_count(folder);
if (count <= 1) {
body = EmailUtil.strip_subject_prefixes(email);
body = Util.Email.strip_subject_prefixes(email);
} else {
body = ngettext("%s\n(%d other new message for %s)", "%s\n(%d other new messages for %s)", count - 1).printf(
EmailUtil.strip_subject_prefixes(email), count - 1, folder.account.information.display_name);
body = ngettext(
"%s\n(%d other new message for %s)",
"%s\n(%d other new messages for %s)", count - 1).printf(
Util.Email.strip_subject_prefixes(email),
count - 1,
folder.account.information.display_name
);
}
Gdk.Pixbuf? avatar = yield this.avatars.load(

View file

@ -0,0 +1,160 @@
/*
* Copyright 2019 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.
*/
namespace Util.Avatar {
// The following was based on code written by Felipe Borges for
// gnome-control-enter in panels/user-accounts/user-utils.c commit
// 02c288ab6f069a0c106323a93400f192a63cb67e. The copyright in that
// file is: "Copyright 2009-2010 Red Hat, Inc,"
public Gdk.Pixbuf generate_user_picture(string name, int size) {
Cairo.Surface surface = new Cairo.ImageSurface(
Cairo.Format.ARGB32, size, size
);
Cairo.Context cr = new Cairo.Context(surface);
cr.rectangle(0, 0, size, size);
/* Fill the background with a colour for the name */
Gdk.RGBA color = get_color_for_name(name);
cr.set_source_rgb(
color.red / 255.0, color.green / 255.0, color.blue / 255.0
);
cr.fill();
/* Draw the initials on top */
string? initials = extract_initials_from_name(name);
if (initials != null) {
string font = "Sans %d".printf((int) GLib.Math.ceil(size / 2.5));
cr.set_source_rgb(1.0, 1.0, 1.0);
Pango.Layout layout = Pango.cairo_create_layout(cr);
layout.set_text(initials, -1);
layout.set_font_description(Pango.FontDescription.from_string(font));
int width, height;
layout.get_size(out width, out height);
cr.translate(size / 2, size / 2);
cr.move_to(
-((double) width / Pango.SCALE) / 2,
-((double) height / Pango.SCALE) / 2
);
Pango.cairo_show_layout(cr, layout);
}
return Gdk.pixbuf_get_from_surface(
surface, 0, 0, size, size
);
}
public Gdk.Pixbuf round_image(Gdk.Pixbuf source) {
int size = source.width;
Cairo.Surface surface = new Cairo.ImageSurface(
Cairo.Format.ARGB32, size, size
);
Cairo.Context cr = new Cairo.Context(surface);
/* Clip a circle */
cr.arc(size / 2, size / 2, size / 2, 0, 2 * GLib.Math.PI);
cr.clip();
cr.new_path();
Gdk.cairo_set_source_pixbuf(cr, source, 0, 0);
cr.paint();
return Gdk.pixbuf_get_from_surface(
surface, 0, 0, size, size
);
}
public string? extract_initials_from_name(string name) {
string normalized = name.strip().up().normalize();
string? initials = null;
if (normalized != "") {
GLib.StringBuilder buf = new GLib.StringBuilder();
unichar c = 0;
int index = 0;
// Get the first alphanumeric char of the string
for (int i = 0; normalized.get_next_char(ref index, out c); i++) {
if (c.isalnum()) {
buf.append_unichar(c);
break;
}
}
// Get the first alphanumeric char of the last word of the string
index = normalized.last_index_of_char(' ');
for (int i = 0; normalized.get_next_char(ref index, out c); i++) {
if (c.isalnum()) {
buf.append_unichar(c);
break;
}
}
if (buf.data.length > 0) {
initials = (string) buf.data;
}
}
return initials;
}
public Gdk.RGBA get_color_for_name(string name) {
// https://gitlab.gnome.org/Community/Design/HIG-app-icons/blob/master/GNOME%20HIG.gpl
const double[,3] GNOME_COLOR_PALETTE = {
{ 98, 160, 234 },
{ 53, 132, 228 },
{ 28, 113, 216 },
{ 26, 95, 180 },
{ 87, 227, 137 },
{ 51, 209, 122 },
{ 46, 194, 126 },
{ 38, 162, 105 },
{ 248, 228, 92 },
{ 246, 211, 45 },
{ 245, 194, 17 },
{ 229, 165, 10 },
{ 255, 163, 72 },
{ 255, 120, 0 },
{ 230, 97, 0 },
{ 198, 70, 0 },
{ 237, 51, 59 },
{ 224, 27, 36 },
{ 192, 28, 40 },
{ 165, 29, 45 },
{ 192, 97, 203 },
{ 163, 71, 186 },
{ 129, 61, 156 },
{ 97, 53, 131 },
{ 181, 131, 90 },
{ 152, 106, 68 },
{ 134, 94, 60 },
{ 99, 69, 44 }
};
Gdk.RGBA color = { 255, 255, 255, 1.0 };
uint hash;
uint number_of_colors;
uint idx;
if (name == "") {
return color;
}
hash = name.hash();
number_of_colors = GNOME_COLOR_PALETTE.length[0];
idx = hash % number_of_colors;
color.red = GNOME_COLOR_PALETTE[idx,0];
color.green = GNOME_COLOR_PALETTE[idx,1];
color.blue = GNOME_COLOR_PALETTE[idx,2];
return color;
}
}

View file

@ -4,31 +4,98 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public int compare_conversation_ascending(Geary.App.Conversation a, Geary.App.Conversation b) {
Geary.Email? a_latest = a.get_latest_recv_email(Geary.App.Conversation.Location.IN_FOLDER_OUT_OF_FOLDER);
Geary.Email? b_latest = b.get_latest_recv_email(Geary.App.Conversation.Location.IN_FOLDER_OUT_OF_FOLDER);
namespace Util.Email {
if (a_latest == null)
return (b_latest == null) ? 0 : -1;
else if (b_latest == null)
return 1;
public int compare_conversation_ascending(Geary.App.Conversation a,
Geary.App.Conversation b) {
Geary.Email? a_latest = a.get_latest_recv_email(
Geary.App.Conversation.Location.IN_FOLDER_OUT_OF_FOLDER
);
Geary.Email? b_latest = b.get_latest_recv_email(
Geary.App.Conversation.Location.IN_FOLDER_OUT_OF_FOLDER
);
// use date-received so newly-arrived messages float to the top, even if they're send date
// was earlier (think of mailing lists that batch up forwarded mail)
return Geary.Email.compare_recv_date_ascending(a_latest, b_latest);
}
if (a_latest == null) {
return (b_latest == null) ? 0 : -1;
} else if (b_latest == null) {
return 1;
}
public int compare_conversation_descending(Geary.App.Conversation a, Geary.App.Conversation b) {
return compare_conversation_ascending(b, a);
}
// use date-received so newly-arrived messages float to the
// top, even if they're send date was earlier (think of
// mailing lists that batch up forwarded mail)
return Geary.Email.compare_recv_date_ascending(a_latest, b_latest);
}
namespace EmailUtil {
public int compare_conversation_descending(Geary.App.Conversation a,
Geary.App.Conversation b) {
return compare_conversation_ascending(b, a);
}
public string strip_subject_prefixes(Geary.Email email) {
string? cleaned = (email.subject != null) ? email.subject.strip_prefixes() : null;
/** Returns the stripped subject line, or a placeholder if none. */
public string strip_subject_prefixes(Geary.Email email) {
string? cleaned = (email.subject != null) ? email.subject.strip_prefixes() : null;
return !Geary.String.is_empty(cleaned) ? cleaned : _("(no subject)");
}
return !Geary.String.is_empty(cleaned) ? cleaned : _("(no subject)");
}
/**
* Returns a mailbox for the primary originator of an email.
*
* RFC 822 allows multiple and absent From header values, and
* software such as Mailman and GitLab will mangle the names in
* From mailboxes. This provides a canonical means to obtain a
* mailbox (that is, name and email address) for the first
* originator, and with the mailbox's name having been fixed up
* where possible.
*
* The first From mailbox is used and de-mangled if found, if not
* the Sender mailbox is used if present, else the first Reply-To
* mailbox is used.
*/
public Geary.RFC822.MailboxAddress?
get_primary_originator(Geary.EmailHeaderSet email) {
Geary.RFC822.MailboxAddress? primary = null;
if (email.from != null && email.from.size > 0) {
// We have a From address, so attempt to de-mangle it
Geary.RFC822.MailboxAddresses? from = email.from;
string from_name = "";
if (from != null && from.size > 0) {
primary = from[0];
from_name = primary.name ?? "";
}
Geary.RFC822.MailboxAddresses? reply_to = email.reply_to;
Geary.RFC822.MailboxAddress? primary_reply_to = null;
string reply_to_name = "";
if (reply_to != null && reply_to.size > 0) {
primary_reply_to = reply_to[0];
reply_to_name = primary_reply_to.name ?? "";
}
// Spaces are important
const string VIA = " via ";
if (reply_to_name != "" && from_name.has_prefix(reply_to_name)) {
// Mailman sometimes sends the true originator as the
// Reply-To for the email
primary = primary_reply_to;
} else if (VIA in from_name) {
// Mailman, GitLib, Discourse and others send the
// originator's name prefixing something starting with
// "via".
primary = new Geary.RFC822.MailboxAddress(
from_name.split(VIA, 2)[0], primary.address
);
}
} else if (email.sender != null) {
primary = email.sender;
} else if (email.reply_to != null && email.reply_to.size > 0) {
primary = email.reply_to[0];
}
return primary;
}
/**
* Returns a shortened recipient list suitable for display.

View file

@ -0,0 +1,46 @@
/*
* Copyright 2019 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.
*/
/**
* Denotes an object that has a set of RFC822 headers.
*/
public interface Geary.EmailHeaderSet : BaseObject {
/** Value of the RFC 822 Date header. */
public abstract RFC822.Date? date { get; protected set; }
/** Value of the RFC 822 From header, an originator field. */
public abstract RFC822.MailboxAddresses? from { get; protected set; }
/** Value of the RFC 822 Sender header, an originator field. */
public abstract RFC822.MailboxAddress? sender { get; protected set; }
/** Value of the RFC 822 Reply-To header, an originator field. */
public abstract RFC822.MailboxAddresses? reply_to { get; protected set; }
/** Value of the RFC 822 To header, a recipient field. */
public abstract RFC822.MailboxAddresses? to { get; protected set; }
/** Value of the RFC 822 Cc header, a recipient field. */
public abstract RFC822.MailboxAddresses? cc { get; protected set; }
/** Value of the RFC 822 Bcc header, a recipient field. */
public abstract RFC822.MailboxAddresses? bcc { get; protected set; }
/** Value of the RFC 822 Message-Id header, a reference field. */
public abstract RFC822.MessageID? message_id { get; protected set; }
/** Value of the RFC 822 In-Reply-To header, a reference field. */
public abstract RFC822.MessageIDList? in_reply_to { get; protected set; }
/** Value of the RFC 822 References header, a reference field. */
public abstract RFC822.MessageIDList? references { get; protected set; }
/** Value of the RFC 822 Subject header. */
public abstract RFC822.Subject? subject { get; protected set; }
}

View file

@ -26,7 +26,7 @@
* property, and if the currently loaded fields are not sufficient,
* then additional fields can be loaded via a folder.
*/
public class Geary.Email : BaseObject {
public class Geary.Email : BaseObject, EmailHeaderSet {
/**
* The maximum expected length of message body preview text.
@ -183,42 +183,124 @@ public class Geary.Email : BaseObject {
*/
public Geary.EmailIdentifier id { get; private set; }
// DATE
public Geary.RFC822.Date? date { get; private set; default = null; }
/**
* {@inheritDoc}
*
* Value will be valid if {@link Field.DATE} is set.
*/
public Geary.RFC822.Date? date { get; protected set; default = null; }
// ORIGINATORS
public Geary.RFC822.MailboxAddresses? from { get; private set; default = null; }
public Geary.RFC822.MailboxAddress? sender { get; private set; default = null; }
public Geary.RFC822.MailboxAddresses? reply_to { get; private set; default = null; }
/**
* {@inheritDoc}
*
* Value will be valid if {@link Field.ORIGINATORS} is set.
*/
public Geary.RFC822.MailboxAddresses? from { get; protected set; default = null; }
// RECEIVERS
public Geary.RFC822.MailboxAddresses? to { get; private set; default = null; }
public Geary.RFC822.MailboxAddresses? cc { get; private set; default = null; }
public Geary.RFC822.MailboxAddresses? bcc { get; private set; default = null; }
/**
* {@inheritDoc}
*
* Value will be valid if {@link Field.ORIGINATORS} is set.
*/
public Geary.RFC822.MailboxAddress? sender { get; protected set; default = null; }
// REFERENCES
public Geary.RFC822.MessageID? message_id { get; private set; default = null; }
public Geary.RFC822.MessageIDList? in_reply_to { get; private set; default = null; }
public Geary.RFC822.MessageIDList? references { get; private set; default = null; }
/**
* {@inheritDoc}
*
* Value will be valid if {@link Field.ORIGINATORS} is set.
*/
public Geary.RFC822.MailboxAddresses? reply_to { get; protected set; default = null; }
// SUBJECT
public Geary.RFC822.Subject? subject { get; private set; default = null; }
/**
* {@inheritDoc}
*
* Value will be valid if {@link Field.RECEIVERS} is set.
*/
public Geary.RFC822.MailboxAddresses? to { get; protected set; default = null; }
// HEADER
public RFC822.Header? header { get; private set; default = null; }
/**
* {@inheritDoc}
*
* Value will be valid if {@link Field.RECEIVERS} is set.
*/
public Geary.RFC822.MailboxAddresses? cc { get; protected set; default = null; }
// BODY
/**
* {@inheritDoc}
*
* Value will be valid if {@link Field.RECEIVERS} is set.
*/
public Geary.RFC822.MailboxAddresses? bcc { get; protected set; default = null; }
/**
* {@inheritDoc}
*
* Value will be valid if {@link Field.REFERENCES} is set.
*/
public Geary.RFC822.MessageID? message_id { get; protected set; default = null; }
/**
* {@inheritDoc}
*
* Value will be valid if {@link Field.REFERENCES} is set.
*/
public Geary.RFC822.MessageIDList? in_reply_to { get; protected set; default = null; }
/**
* {@inheritDoc}
*
* Value will be valid if {@link Field.REFERENCES} is set.
*/
public Geary.RFC822.MessageIDList? references { get; protected set; default = null; }
/**
* {@inheritDoc}
*
* Value will be valid if {@link Field.SUBJECT} is set.
*/
public Geary.RFC822.Subject? subject { get; protected set; default = null; }
/**
* {@inheritDoc}
*
* Value will be valid if {@link Field.HEADER} is set.
*/
public RFC822.Header? header { get; protected set; default = null; }
/**
* The complete RFC 822 message body.
*
* Value will be valid if {@link Field.BODY} is set.
*/
public RFC822.Text? body { get; private set; default = null; }
/**
* MIME multipart body parts.
*
* Value will be valid if {@link Field.BODY} is set.
*/
public Gee.List<Geary.Attachment> attachments { get; private set;
default = new Gee.ArrayList<Geary.Attachment>(); }
// PROPERTIES
public Geary.EmailProperties? properties { get; private set; default = null; }
// PREVIEW
/**
* A plain text prefix of the email's message body.
*
* Value will be valid if {@link Field.PREVIEW} is set.
*/
public RFC822.PreviewText? preview { get; private set; default = null; }
// FLAGS
/**
* Set of immutable properties for the email.
*
* Value will be valid if {@link Field.PROPERTIES} is set.
*/
public Geary.EmailProperties? properties { get; private set; default = null; }
/**
* Set of mutable flags for the email.
*
* Value will be valid if {@link Field.FLAGS} is set.
*/
public Geary.EmailFlags? email_flags { get; private set; default = null; }
/**
@ -448,25 +530,6 @@ public class Geary.Email : BaseObject {
return (preview != null) ? preview.buffer.to_string() : "";
}
/**
* Returns the primary originator of an email, which is defined as the first mailbox address
* in From:, Sender:, or Reply-To:, in that order, depending on availability.
*
* Returns null if no originators are present.
*/
public RFC822.MailboxAddress? get_primary_originator() {
if (from != null && from.size > 0)
return from[0];
if (sender != null)
return sender;
if (reply_to != null && reply_to.size > 0)
return reply_to[0];
return null;
}
public string to_string() {
return "[%s] ".printf(id.to_string());
}

View file

@ -16,6 +16,7 @@ geary_engine_vala_sources = files(
'api/geary-credentials.vala',
'api/geary-credentials-mediator.vala',
'api/geary-email-flags.vala',
'api/geary-email-header-set.vala',
'api/geary-email-identifier.vala',
'api/geary-email-properties.vala',
'api/geary-email.vala',

View file

@ -13,7 +13,7 @@
* representation of an email message, and contain no information
* other than what RFC-822 and its successor RFC documents specify.
*/
public class Geary.RFC822.Message : BaseObject {
public class Geary.RFC822.Message : BaseObject, EmailHeaderSet {
/**
* Callback for including non-text MIME entities in message bodies.
@ -35,18 +35,46 @@ public class Geary.RFC822.Message : BaseObject {
private const string HEADER_MAILER = "X-Mailer";
private const string HEADER_BCC = "Bcc";
// Internal note: If a field is added here, it *must* be set in stock_from_gmime().
public RFC822.MailboxAddress? sender { get; private set; default = null; }
public RFC822.MailboxAddresses? from { get; private set; default = null; }
public RFC822.MailboxAddresses? to { get; private set; default = null; }
public RFC822.MailboxAddresses? cc { get; private set; default = null; }
public RFC822.MailboxAddresses? bcc { get; private set; default = null; }
public RFC822.MailboxAddresses? reply_to { get; private set; default = null; }
public RFC822.MessageIDList? in_reply_to { get; private set; default = null; }
public RFC822.MessageIDList? references { get; private set; default = null; }
public RFC822.Subject? subject { get; private set; default = null; }
public string? mailer { get; private set; default = null; }
public Geary.RFC822.Date? date { get; private set; default = null; }
// Internal note: If a header field is added here, it *must* be
// set in stock_from_gmime().
/** {@inheritDoc} */
/** {@inheritDoc} */
public RFC822.MailboxAddress? sender { get; protected set; default = null; }
/** {@inheritDoc} */
public RFC822.MailboxAddresses? from { get; protected set; default = null; }
/** {@inheritDoc} */
public RFC822.MailboxAddresses? to { get; protected set; default = null; }
/** {@inheritDoc} */
public RFC822.MailboxAddresses? cc { get; protected set; default = null; }
/** {@inheritDoc} */
public RFC822.MailboxAddresses? bcc { get; protected set; default = null; }
/** {@inheritDoc} */
public RFC822.MailboxAddresses? reply_to { get; protected set; default = null; }
/** {@inheritDoc} */
public RFC822.MessageID? message_id { get; protected set; default = null; }
/** {@inheritDoc} */
public RFC822.MessageIDList? in_reply_to { get; protected set; default = null; }
/** {@inheritDoc} */
public RFC822.MessageIDList? references { get; protected set; default = null; }
/** {@inheritDoc} */
public RFC822.Subject? subject { get; protected set; default = null; }
/** {@inheritDoc} */
public Geary.RFC822.Date? date { get; protected set; default = null; }
/** Value of the X-Mailer header. */
public string? mailer { get; protected set; default = null; }
private GMime.Message message;
@ -57,6 +85,7 @@ public class Geary.RFC822.Message : BaseObject {
private Memory.Buffer? body_buffer = null;
private size_t? body_offset = null;
public Message(Full full) throws RFC822Error {
GMime.Parser parser = new GMime.Parser.with_stream(Utils.create_stream_mem(full.buffer));
@ -110,8 +139,10 @@ public class Geary.RFC822.Message : BaseObject {
// supports a list of addresses
message.set_sender(this.from.to_rfc822_string());
message.set_date_as_string(this.date.serialize());
if (message_id != null)
if (message_id != null) {
this.message_id = new MessageID(message_id);
message.set_message_id(message_id);
}
// Optional headers
if (email.to != null) {
@ -378,7 +409,7 @@ public class Geary.RFC822.Message : BaseObject {
email.set_send_date(date);
email.set_originators(from, sender, reply_to);
email.set_receivers(to, cc, bcc);
email.set_full_references(null, in_reply_to, references);
email.set_full_references(message_id, in_reply_to, references);
email.set_message_subject(subject);
email.set_message_body(new Geary.RFC822.Text(new Geary.Memory.OffsetBuffer(
body_buffer, body_offset)));
@ -410,25 +441,6 @@ public class Geary.RFC822.Message : BaseObject {
: "";
}
/**
* Returns the primary originator of an email, which is defined as the first mailbox address
* in From:, Sender:, or Reply-To:, in that order, depending on availability.
*
* Returns null if no originators are present.
*/
public RFC822.MailboxAddress? get_primary_originator() {
if (from != null && from.size > 0)
return from[0];
if (sender != null)
return sender;
if (reply_to != null && reply_to.size > 0)
return reply_to[0];
return null;
}
public Gee.List<RFC822.MailboxAddress>? get_recipients() {
Gee.List<RFC822.MailboxAddress> addrs = new Gee.ArrayList<RFC822.MailboxAddress>();
@ -778,6 +790,10 @@ public class Geary.RFC822.Message : BaseObject {
}
break;
case "message-id":
this.message_id = new MessageID(value);
break;
case "in-reply-to":
this.in_reply_to = append_message_id(this.in_reply_to, value);
break;

View file

@ -73,6 +73,7 @@ geary_bin_sources += [
geary_resources # Included here so they show up in the executable.
]
geary_bin_dependencies = [
folks,
gdk,
geary_client_dep,
geary_engine_dep,

View file

@ -0,0 +1,34 @@
/*
* Copyright 2019 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.
*/
public class Util.Avatar.Test : TestCase {
public Test() {
base("UtilAvatarTest");
add_test("extract_initials", extract_initials);
}
public void extract_initials() throws GLib.Error {
assert_string("A", extract_initials_from_name("aardvark"));
assert_string("AB", extract_initials_from_name("aardvark baardvark"));
assert_string("AB", extract_initials_from_name("aardvark baardvark"));
assert_string("AC", extract_initials_from_name("aardvark baardvark caardvark"));
assert_string("A", extract_initials_from_name("!aardvark"));
assert_string("AB", extract_initials_from_name("aardvark !baardvark"));
assert_string("AC", extract_initials_from_name("aardvark baardvark !caardvark"));
assert_true(extract_initials_from_name("") == null);
assert_true(extract_initials_from_name(" ") == null);
assert_true(extract_initials_from_name(" ") == null);
assert_true(extract_initials_from_name("!") == null);
assert_true(extract_initials_from_name("!!") == null);
assert_true(extract_initials_from_name("! !") == null);
assert_true(extract_initials_from_name("! !!") == null);
}
}

View file

@ -0,0 +1,115 @@
/*
* Copyright 2019 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.
*/
public class Util.Email.Test : TestCase {
public Test() {
base("UtilEmailTest");
add_test("null_originator", null_originator);
add_test("from_originator", from_originator);
add_test("sender_originator", sender_originator);
add_test("reply_to_originator", reply_to_originator);
add_test("reply_to_via_originator", reply_to_via_originator);
add_test("plain_via_originator", plain_via_originator);
}
public void null_originator() throws GLib.Error {
Geary.RFC822.MailboxAddress? originator = get_primary_originator(
new_email(null, null, null)
);
assert_null(originator);
}
public void from_originator() throws GLib.Error {
Geary.RFC822.MailboxAddress? originator = get_primary_originator(
new_email(
new Geary.RFC822.MailboxAddress("from", "from@example.com"),
new Geary.RFC822.MailboxAddress("sender", "sender@example.com"),
new Geary.RFC822.MailboxAddress("reply-to", "reply-to@example.com")
)
);
assert_non_null(originator);
assert_string("from", originator.name);
assert_string("from@example.com", originator.address);
}
public void sender_originator() throws GLib.Error {
Geary.RFC822.MailboxAddress? originator = get_primary_originator(
new_email(
null,
new Geary.RFC822.MailboxAddress("sender", "sender@example.com"),
new Geary.RFC822.MailboxAddress("reply-to", "reply-to@example.com")
)
);
assert_non_null(originator);
assert_string("sender", originator.name);
assert_string("sender@example.com", originator.address);
}
public void reply_to_originator() throws GLib.Error {
Geary.RFC822.MailboxAddress? originator = get_primary_originator(
new_email(
null,
null,
new Geary.RFC822.MailboxAddress("reply-to", "reply-to@example.com")
)
);
assert_non_null(originator);
assert_string("reply-to", originator.name);
assert_string("reply-to@example.com", originator.address);
}
public void reply_to_via_originator() throws GLib.Error {
Geary.RFC822.MailboxAddress? originator = get_primary_originator(
new_email(
new Geary.RFC822.MailboxAddress("test via bot", "bot@example.com"),
null,
new Geary.RFC822.MailboxAddress("test", "test@example.com")
)
);
assert_non_null(originator);
assert_string("test", originator.name);
assert_string("test@example.com", originator.address);
}
public void plain_via_originator() throws GLib.Error {
Geary.RFC822.MailboxAddress? originator = get_primary_originator(
new_email(
new Geary.RFC822.MailboxAddress("test via bot", "bot@example.com"),
null,
null
)
);
assert_non_null(originator);
assert_string("test", originator.name);
assert_string("bot@example.com", originator.address);
}
private Geary.Email new_email(Geary.RFC822.MailboxAddress? from,
Geary.RFC822.MailboxAddress? sender,
Geary.RFC822.MailboxAddress? reply_to)
throws GLib.Error {
Geary.Email email = new Geary.Email(new Geary.MockEmailIdentifer(1));
email.set_originators(
from != null
? new Geary.RFC822.MailboxAddresses(Geary.Collection.single(from))
: null,
sender,
reply_to != null
? new Geary.RFC822.MailboxAddresses(Geary.Collection.single(reply_to))
: null
);
return email;
}
}

View file

@ -68,6 +68,7 @@ geary_test_client_sources = [
# and the engine test sute needs to depend
# geary-engine_internal.vapi, which leads to duplicate symbols when
# linking
'engine/api/geary-email-identifier-mock.vala',
'engine/api/geary-credentials-mediator-mock.vala',
'client/accounts/accounts-manager-test.vala',
@ -75,6 +76,8 @@ geary_test_client_sources = [
'client/components/client-web-view-test.vala',
'client/components/client-web-view-test-case.vala',
'client/composer/composer-web-view-test.vala',
'client/util/util-avatar-test.vala',
'client/util/util-email-test.vala',
'js/client-page-state-test.vala',
'js/composer-page-state-test.vala',

View file

@ -53,11 +53,17 @@ public void assert_string(string expected, string? actual, string? context = nul
if (a.length > 32) {
a = a[0:32] + "";
}
string b = actual;
if (b.length > 32) {
b = b[0:32] + "";
string? b = actual;
if (b != null) {
if (b.length > 32) {
b = b[0:32] + "";
}
}
if (b != null) {
print_assert("Expected: \"%s\", was: \"%s\"".printf(a, b), context);
} else {
print_assert("Expected: \"%s\", was null".printf(a), context);
}
print_assert("Expected: \"%s\", was: \"%s\"".printf(a, b), context);
assert_not_reached();
}
}

View file

@ -43,6 +43,8 @@ int main(string[] args) {
client.add_suite(new ClientWebViewTest().get_suite());
client.add_suite(new ComposerWebViewTest().get_suite());
client.add_suite(new ConfigurationTest().get_suite());
client.add_suite(new Util.Avatar.Test().get_suite());
client.add_suite(new Util.Email.Test().get_suite());
TestSuite js = new TestSuite("js");

View file

@ -19,8 +19,7 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">start</property>
<property name="pixel_size">32</property>
<property name="icon_name">avatar-default-symbolic</property>
<property name="pixel_size">48</property>
</object>
<packing>
<property name="left_attach">0</property>