Merge branch 'wip/3.32-avatars' into 'master'
3.32 Avatars Closes #269 See merge request GNOME/geary!154
This commit is contained in:
commit
791c321a00
29 changed files with 943 additions and 260 deletions
|
|
@ -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
18
INSTALL
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
160
src/client/util/util-avatar.vala
Normal file
160
src/client/util/util-avatar.vala
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
46
src/engine/api/geary-email-header-set.vala
Normal file
46
src/engine/api/geary-email-header-set.vala
Normal 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; }
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
34
test/client/util/util-avatar-test.vala
Normal file
34
test/client/util/util-avatar-test.vala
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
115
test/client/util/util-email-test.vala
Normal file
115
test/client/util/util-email-test.vala
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue