Autocomplete addresses in To, CC, BCC fields: Closes #4284.

This commit is contained in:
Matthew Pirocchi 2012-07-20 17:51:28 -07:00
parent bb3fc2209b
commit f9d4c7cb9e
20 changed files with 735 additions and 42 deletions

13
sql/version-005.sql Normal file
View file

@ -0,0 +1,13 @@
--
-- Create ContactTable for autocompletion contacts. Data is migrated in vala upgrade hooks.
--
CREATE TABLE ContactTable (
id INTEGER PRIMARY KEY,
normalized_email TEXT NOT NULL,
real_name TEXT,
email TEXT UNIQUE NOT NULL,
highest_importance INTEGER NOT NULL
);

View file

@ -19,6 +19,9 @@ engine/api/geary-account-information.vala
engine/api/geary-account-settings.vala
engine/api/geary-attachment.vala
engine/api/geary-composed-email.vala
engine/api/geary-contact.vala
engine/api/geary-contact-importance.vala
engine/api/geary-contact-store.vala
engine/api/geary-conversation.vala
engine/api/geary-conversation-monitor.vala
engine/api/geary-credentials.vala
@ -104,8 +107,10 @@ engine/imap/transport/imap-serializable.vala
engine/imap/transport/imap-serializer.vala
engine/imap-db/imap-db-account.vala
engine/imap-db/imap-db-contact.vala
engine/imap-db/imap-db-database.vala
engine/imap-db/imap-db-folder.vala
engine/imap-db/imap-db-message-addresses.vala
engine/imap-db/imap-db-message-row.vala
engine/imap-db/outbox/smtp-outbox-email-identifier.vala
engine/imap-db/outbox/smtp-outbox-email-properties.vala
@ -202,6 +207,7 @@ client/notification/notification-bubble.vala
client/notification/null-indicator.vala
client/ui/composer-window.vala
client/ui/contact-entry-completion.vala
client/ui/geary-login.vala
client/ui/email-entry.vala
client/ui/icon-factory.vala

View file

@ -1055,7 +1055,8 @@ public class GearyController {
}
private void create_compose_window(Geary.ComposedEmail? prefill = null) {
ComposerWindow window = new ComposerWindow(prefill);
Geary.ContactStore? contact_store = account == null ? null : account.get_contact_store();
ComposerWindow window = new ComposerWindow(contact_store, prefill);
window.set_position(Gtk.WindowPosition.CENTER);
window.send.connect(on_send);

View file

@ -132,8 +132,14 @@ public class ComposerWindow : Gtk.Window {
// garbage-collected.
private WebViewEditFixer edit_fixer;
private Gtk.UIManager ui;
private ContactEntryCompletion[] contact_entry_completions;
public ComposerWindow(Geary.ComposedEmail? prefill = null) {
public ComposerWindow(Geary.ContactStore? contact_store, Geary.ComposedEmail? prefill = null) {
contact_entry_completions = {
new ContactEntryCompletion(contact_store),
new ContactEntryCompletion(contact_store),
new ContactEntryCompletion(contact_store)
};
setup_drag_destination(this);
add_events(Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK);
@ -151,11 +157,16 @@ public class ComposerWindow : Gtk.Window {
visible_on_attachment_drag_over_child = (Gtk.Widget) builder.get_object("visible_on_attachment_drag_over_child");
visible_on_attachment_drag_over.remove(visible_on_attachment_drag_over_child);
// TODO: It would be nicer to set the completions inside the EmailEntry constructor. But in
// testing, this can cause non-deterministic segfaults. Investigate why, and fix if possible.
to_entry = new EmailEntry();
to_entry.completion = contact_entry_completions[0];
(builder.get_object("to") as Gtk.EventBox).add(to_entry);
cc_entry = new EmailEntry();
cc_entry.completion = contact_entry_completions[1];
(builder.get_object("cc") as Gtk.EventBox).add(cc_entry);
bcc_entry = new EmailEntry();
bcc_entry.completion = contact_entry_completions[2];
(builder.get_object("bcc") as Gtk.EventBox).add(bcc_entry);
subject_entry = builder.get_object("subject") as Gtk.Entry;
Gtk.Alignment message_area = builder.get_object("message area") as Gtk.Alignment;

View file

@ -0,0 +1,306 @@
/* Copyright 2012 Yorba Foundation
*
* 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 ContactEntryCompletion : Gtk.EntryCompletion {
// Sort column indices.
private const int SORT_COLUMN = 0;
private Gtk.ListStore list_store;
private enum Column {
CONTACT_OBJECT,
CONTACT_MARKUP_NAME,
LAST_KEY;
public static Type[] get_types() {
return {
typeof (Geary.Contact), // CONTACT_OBJECT
typeof (string), // CONTACT_MARKUP_NAME
typeof (string) // LAST_KEY
};
}
}
public ContactEntryCompletion(Geary.ContactStore? contact_store) {
list_store = new Gtk.ListStore.newv(Column.get_types());
list_store.set_sort_func(SORT_COLUMN, sort_func);
list_store.set_sort_column_id(SORT_COLUMN, Gtk.SortType.ASCENDING);
if (contact_store == null)
return;
foreach (Geary.Contact contact in contact_store.contacts)
add_contact(contact);
contact_store.contact_added.connect(on_contact_added);
contact_store.contact_updated.connect(on_contact_updated);
model = list_store;
set_match_func(completion_match_func);
Gtk.CellRendererText text_renderer = new Gtk.CellRendererText();
pack_start(text_renderer, true);
add_attribute(text_renderer, "markup", Column.CONTACT_MARKUP_NAME);
match_selected.connect(on_match_selected);
}
private void add_contact(Geary.Contact contact) {
string full_address = contact.get_rfc822_address().get_full_address();
Gtk.TreeIter iter;
list_store.append(out iter);
list_store.set(iter,
Column.CONTACT_OBJECT, contact,
Column.CONTACT_MARKUP_NAME, Markup.escape_text(full_address),
Column.LAST_KEY, "");
}
private void update_contact(Geary.Contact updated_contact) {
Gtk.TreeIter iter;
if (!list_store.get_iter_first(out iter))
return;
do {
if (get_contact(iter) != updated_contact)
continue;
Gtk.TreePath? path = list_store.get_path(iter);
if (path != null)
list_store.row_changed(path, iter);
return;
} while (list_store.iter_next(ref iter));
}
private void on_contact_added(Geary.Contact contact) {
add_contact(contact);
}
private void on_contact_updated(Geary.Contact contact) {
update_contact(contact);
}
private bool on_match_selected(Gtk.EntryCompletion sender, Gtk.TreeModel model, Gtk.TreeIter iter) {
string? full_address = get_full_address(iter);
if (full_address == null)
return false;
Gtk.Entry? entry = sender.get_entry() as Gtk.Entry;
if (entry == null)
return false;
int current_address_index;
string current_address_remainder;
Gee.List<string> addresses = get_addresses(sender, out current_address_index, null,
out current_address_remainder);
addresses[current_address_index] = full_address;
if (!Geary.String.is_null_or_whitespace(current_address_remainder))
addresses.insert(current_address_index + 1, current_address_remainder);
string delimiter = ", ";
entry.text = concat_strings(addresses, delimiter);
int characters_seen_so_far = 0;
for (int i = 0; i <= current_address_index; i++)
characters_seen_so_far += addresses[i].char_count() + delimiter.char_count();
entry.set_position(characters_seen_so_far);
return true;
}
private Geary.Contact? get_contact(Gtk.TreeIter iter) {
GLib.Value contact_value;
list_store.get_value(iter, Column.CONTACT_OBJECT, out contact_value);
return contact_value.get_object() as Geary.Contact;
}
private string? get_full_address(Gtk.TreeIter iter) {
Geary.Contact? contact = get_contact(iter);
return contact == null ? null : contact.get_rfc822_address().get_full_address();
}
private bool completion_match_func(Gtk.EntryCompletion completion, string key, Gtk.TreeIter iter) {
// We don't use the provided key, because the user can enter multiple addresses.
int current_address_index;
string current_address_key;
get_addresses(completion, out current_address_index, out current_address_key);
Geary.Contact? contact = get_contact(iter);
if (contact == null)
return false;
string highlighted_result;
if (!match_prefix_contact(current_address_key, contact, out highlighted_result))
return false;
// Changing a row in the list store causes Gtk.EntryCompletion to re-evaluate
// completion_match_func for that row. Thus we need to make sure the key has
// actually changed before settings the highlighting--otherwise we will cause
// an infinite loop.
GLib.Value last_key_value;
list_store.get_value(iter, Column.LAST_KEY, out last_key_value);
string? last_key = last_key_value.get_string();
if (current_address_key != last_key) {
list_store.set(iter,
Column.CONTACT_MARKUP_NAME, highlighted_result,
Column.LAST_KEY, current_address_key, -1);
}
return true;
}
private Gee.List<string> get_addresses(Gtk.EntryCompletion completion,
out int current_address_index = null, out string current_address_key = null,
out string current_address_remainder = null) {
current_address_index = 0;
current_address_key = "";
current_address_remainder = "";
Gtk.Entry? entry = completion.get_entry() as Gtk.Entry;
Gee.List<string> empty_addresses = new Gee.ArrayList<string>();
empty_addresses.add("");
if (entry == null)
return empty_addresses;
int cursor_position = entry.cursor_position;
if (cursor_position < 0)
return empty_addresses;
string? original_text = entry.get_text();
if (original_text == null)
return empty_addresses;
Gee.List<string> addresses = new Gee.ArrayList<string>();
string delimiter = ",";
string[] addresses_array = original_text.split(delimiter);
foreach (string address in addresses_array)
addresses.add(address);
if (addresses.size < 1)
return empty_addresses;
int characters_seen_so_far = 0;
current_address_index = addresses.size - 1;
for (int i = 0; i < addresses.size; i++) {
int token_chars = addresses[i].char_count() + delimiter.char_count();
if ((characters_seen_so_far + token_chars) > cursor_position) {
current_address_index = i;
current_address_key = addresses[i]
.substring(0, cursor_position - characters_seen_so_far)
.strip().normalize().casefold();
current_address_remainder = addresses[i]
.substring(cursor_position - characters_seen_so_far).strip();
break;
}
characters_seen_so_far += token_chars;
}
return addresses;
}
// We could only add the delimiter *between* each string (i.e., don't add it after the last
// string). But it's easier for the user if they don't have to manually type a comma after
// adding each address. So we add the delimiter after every string.
private string concat_strings(Gee.List<string> strings, string delimiter) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < strings.size; i++) {
builder.append(strings[i]);
builder.append(delimiter);
}
return builder.str;
}
private bool match_prefix_contact(string needle, Geary.Contact contact,
out string highlighted_result = null) {
string email_result;
bool email_match = match_prefix_string(needle, contact.normalized_email, out email_result);
string real_name_result;
bool real_name_match = match_prefix_string(needle, contact.real_name, out real_name_result);
// email_result and real_name_result were already escaped, then <b></b> tags were added to
// highlight matches. We don't want to escape them again.
highlighted_result = contact.real_name == null ? email_result :
real_name_result + Markup.escape_text(" <") + email_result + Markup.escape_text(">");
return email_match || real_name_match;
}
private bool match_prefix_string(string needle, string? haystack = null,
out string highlighted_result = null) {
highlighted_result = "";
if (haystack == null)
return false;
string escaped_haystack = Markup.escape_text(haystack);
// Default result if there is no match or we encounter an error.
highlighted_result = escaped_haystack;
try {
string escaped_needle = Regex.escape_string(Markup.escape_text(needle.normalize()));
Regex regex = new Regex("\\b" + escaped_needle, RegexCompileFlags.CASELESS);
if (regex.match(escaped_haystack)) {
highlighted_result = regex.replace_eval(escaped_haystack, -1, 0, 0, eval_callback);
return true;
}
} catch (RegexError err) {
debug("Error matching regex: %s", err.message);
}
return false;
}
private bool eval_callback(MatchInfo match_info, StringBuilder result) {
string? match = match_info.fetch(0);
if (match != null) {
// The target was escaped before the regex was run against it, so we don't have to
// worry about markup injections here.
result.append("<b>%s</b>".printf(match));
}
return false;
}
private int sort_func(Gtk.TreeModel model, Gtk.TreeIter aiter, Gtk.TreeIter biter) {
// Order by importance, then by real name, then by email.
GLib.Value avalue, bvalue;
model.get_value(aiter, Column.CONTACT_OBJECT, out avalue);
model.get_value(biter, Column.CONTACT_OBJECT, out bvalue);
Geary.Contact? acontact = avalue.get_object() as Geary.Contact;
Geary.Contact? bcontact = bvalue.get_object() as Geary.Contact;
// Contacts can be null if the sort func is called between TreeModel.append and
// TreeModel.set.
if (acontact == bcontact)
return 0;
if (acontact == null && bcontact != null)
return -1;
if (acontact != null && bcontact == null)
return 1;
// First order by importance.
if (acontact.highest_importance > bcontact.highest_importance)
return -1;
if (acontact.highest_importance < bcontact.highest_importance)
return 1;
// Then order by real name.
string? anormalized_real_name = acontact.real_name == null ? null :
acontact.real_name.normalize().casefold();
string? bnormalized_real_name = bcontact.real_name == null ? null :
bcontact.real_name.normalize().casefold();
// strcmp correctly marks 'null' as first in lexigraphic order, so we don't need to
// special-case it.
int result = strcmp(anormalized_real_name, bnormalized_real_name);
if (result != 0)
return result;
// Finally, order by email.
return strcmp(acontact.normalized_email, bcontact.normalized_email);
}
}

View file

@ -14,7 +14,6 @@ public class EmailEntry : Gtk.Entry {
public EmailEntry() {
changed.connect(on_changed);
// TODO: Contact completion with libfolks
}
private void on_changed() {

View file

@ -36,6 +36,8 @@ public abstract class Geary.AbstractAccount : Object, Geary.Account {
public abstract async Gee.Collection<Geary.Folder> list_folders_async(Geary.FolderPath? parent,
Cancellable? cancellable = null) throws Error;
public abstract Geary.ContactStore get_contact_store();
public abstract async bool folder_exists_async(Geary.FolderPath path, Cancellable? cancellable = null)
throws Error;

View file

@ -68,6 +68,11 @@ public interface Geary.Account : Object {
public abstract async Gee.Collection<Geary.Folder> list_folders_async(Geary.FolderPath? parent,
Cancellable? cancellable = null) throws Error;
/**
* Gets a perpetually update-to-date collection of autocompletion contacts.
*/
public abstract Geary.ContactStore get_contact_store();
/**
* Returns true if the folder exists.
*

View file

@ -0,0 +1,38 @@
/* Copyright 2012 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* Represents the relative "importance" of an occurance of a contact in a message.
*
* The first word (before the underscore) indicates where the account owner appeared in the
* message. The second word (after the underscore) indicates where the contact appeared in the
* message.
*
* || "Token" || "Definition" ||
* || FROM || appeared in the 'from' or 'sender' fields ||
* || TO || appeared in in the 'to' field ||
* || CC || appeared in the 'CC' or 'BCC' fields OR did not appear in any field (assuming BCC) ||
*
* "Examples:"
*
* || "Enum Value" || "Account Owner" || "Contact" ||
* || FROM_TO || Appeared in 'from' or 'sender' || Appeared in 'to' ||
* || CC_FROM || Appeared in 'CC', 'BCC', or did not appear || Appeared in 'from' or 'sender'. ||
*/
public enum Geary.ContactImportance {
FROM_FROM = 100,
FROM_TO = 90,
FROM_CC = 80,
TO_FROM = 70,
TO_TO = 60,
TO_CC = 50,
CC_FROM = 40,
CC_TO = 30,
CC_CC = 20,
// Minimum visibility for the contact to appear in autocompletion.
VISIBILITY_THRESHOLD = TO_TO;
}

View file

@ -0,0 +1,37 @@
/* Copyright 2012 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class Geary.ContactStore : Object {
public Gee.Collection<Contact> contacts {
owned get { return contact_map.values; }
}
private Gee.Map<string, Contact> contact_map;
public signal void contact_added(Contact contact);
public signal void contact_updated(Contact contact);
internal ContactStore() {
contact_map = new Gee.HashMap<string, Contact>();
}
public void update_contacts(Gee.Collection<Contact> new_contacts) {
foreach (Contact contact in new_contacts)
update_contact(contact);
}
private void update_contact(Contact contact) {
Contact? old_contact = contact_map[contact.normalized_email];
if (old_contact == null) {
contact_map[contact.normalized_email] = contact;
contact_added(contact);
} else if (old_contact.highest_importance < contact.highest_importance) {
old_contact.highest_importance = contact.highest_importance;
contact_updated(old_contact);
}
}
}

View file

@ -0,0 +1,27 @@
/* Copyright 2012 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class Geary.Contact : Object {
public string normalized_email { get; private set; }
public string email { get; private set; }
public string? real_name { get; private set; }
public int highest_importance { get; set; }
public Contact(string email, string? real_name, int highest_importance, string? normalized_email = null) {
this.normalized_email = normalized_email ?? email.normalize().casefold();
this.email = email;
this.real_name = real_name;
this.highest_importance = highest_importance;
}
public Contact.from_rfc822_address(RFC822.MailboxAddress address, int highest_importance) {
this(address.address, address.name, highest_importance);
}
public RFC822.MailboxAddress get_rfc822_address() {
return new RFC822.MailboxAddress(real_name, email);
}
}

View file

@ -7,24 +7,16 @@
public class Geary.Db.VersionedDatabase : Geary.Db.Database {
public File schema_dir { get; private set; }
public virtual signal void pre_upgrade(int version) {
}
public virtual signal void post_upgrade(int version) {
}
public VersionedDatabase(File db_file, File schema_dir) {
base (db_file);
this.schema_dir = schema_dir;
}
protected virtual void notify_pre_upgrade(int version) {
pre_upgrade(version);
protected virtual void pre_upgrade(int version) {
}
protected virtual void notify_post_upgrade(int version) {
post_upgrade(version);
protected virtual void post_upgrade(int version) {
}
// TODO: Initialize database from version-001.sql and upgrade with version-nnn.sql
@ -48,7 +40,7 @@ public class Geary.Db.VersionedDatabase : Geary.Db.Database {
if (!upgrade_script.query_exists(cancellable))
break;
notify_pre_upgrade(db_version);
pre_upgrade(db_version);
check_cancelled("VersionedDatabase.open", cancellable);
@ -66,7 +58,7 @@ public class Geary.Db.VersionedDatabase : Geary.Db.Database {
throw err;
}
notify_post_upgrade(db_version);
post_upgrade(db_version);
}
}
}

View file

@ -18,14 +18,21 @@ private class Geary.ImapDB.Account : Object {
// Only available when the Account is opened
public SmtpOutboxFolder? outbox { get; private set; default = null; }
// TODO: This should be updated when Geary no longer assumes username is email.
public string account_owner_email {
get { return settings.credentials.user; }
}
private string name;
private AccountSettings settings;
private ImapDB.Database? db = null;
private Gee.HashMap<Geary.FolderPath, FolderReference> folder_refs =
new Gee.HashMap<Geary.FolderPath, FolderReference>(Hashable.hash_func, Equalable.equal_func);
public ContactStore contact_store { get; private set; }
public Account(Geary.AccountSettings settings) {
this.settings = settings;
contact_store = new ContactStore();
name = "IMAP database account for %s".printf(settings.credentials.user);
}
@ -40,9 +47,7 @@ private class Geary.ImapDB.Account : Object {
if (db != null)
throw new EngineError.ALREADY_OPEN("IMAP database already open");
db = new ImapDB.Database(user_data_dir, schema_dir);
db.pre_upgrade.connect(on_pre_upgrade);
db.post_upgrade.connect(on_post_upgrade);
db = new ImapDB.Database(user_data_dir, schema_dir, account_owner_email);
try {
db.open(Db.DatabaseFlags.CREATE_DIRECTORY | Db.DatabaseFlags.CREATE_FILE, null,
@ -56,6 +61,8 @@ private class Geary.ImapDB.Account : Object {
throw err;
}
initialize_contacts(cancellable);
// ImapDB.Account holds the Outbox, which is tied to the database it maintains
outbox = new SmtpOutboxFolder(db, settings);
@ -161,6 +168,39 @@ private class Geary.ImapDB.Account : Object {
db_folder.set_properties(properties);
}
private void initialize_contacts(Cancellable? cancellable = null) throws Error {
check_open();
Gee.Collection<Contact> contacts = new Gee.LinkedList<Contact>();
Db.TransactionOutcome outcome = db.exec_transaction(Db.TransactionType.RO,
(context) => {
Db.Statement statement = context.prepare(
"SELECT email, real_name, highest_importance, normalized_email " +
"FROM ContactTable WHERE highest_importance >= ?");
statement.bind_int(0, ContactImportance.VISIBILITY_THRESHOLD);
Db.Result result = statement.exec(cancellable);
while (!result.finished) {
try {
Contact contact = new Contact(result.string_at(0), result.string_at(1),
result.int_at(2), result.string_at(3));
contacts.add(contact);
} catch (Geary.DatabaseError err) {
// We don't want to abandon loading all contacts just because there was a
// problem with one.
debug("Problem loading contact: %s", err.message);
}
result.next();
}
return Db.TransactionOutcome.DONE;
}, cancellable);
if (outcome == Db.TransactionOutcome.DONE)
contact_store.update_contacts(contacts);
}
public async Gee.Collection<Geary.ImapDB.Folder> list_folders_async(Geary.FolderPath? parent,
Cancellable? cancellable = null) throws Error {
check_open();
@ -321,7 +361,8 @@ private class Geary.ImapDB.Account : Object {
}
// create folder
folder = new Geary.ImapDB.Folder(db, path, folder_id, properties);
folder = new Geary.ImapDB.Folder(db, path, contact_store, account_owner_email, folder_id,
properties);
// build a reference to it
FolderReference folder_ref = new FolderReference(folder, path);
@ -340,14 +381,6 @@ private class Geary.ImapDB.Account : Object {
folder_refs.unset(folder_ref.path);
}
private void on_pre_upgrade(int version){
// TODO Add per-version data massaging.
}
private void on_post_upgrade(int version) {
// TODO Add per-version data massaging.
}
private void clear_duplicate_folders() {
int count = 0;

View file

@ -0,0 +1,25 @@
/* Copyright 2011-2012 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
namespace Geary.ImapDB {
private static void do_update_contact_importance(Db.Connection connection, Contact contact,
Cancellable? cancellable = null) throws Error {
// TODO: Don't overwrite a non-null real_name with a null real_name.
Db.Statement statement = connection.prepare(
"INSERT OR REPLACE INTO ContactTable(normalized_email, email, real_name, highest_importance)
VALUES(?, ?, ?, MAX(COALESCE((SELECT highest_importance FROM ContactTable
WHERE email=?1), -1), ?))");
statement.bind_string(0, contact.normalized_email);
statement.bind_string(1, contact.email);
statement.bind_string(2, contact.real_name);
statement.bind_int(3, contact.highest_importance);
statement.exec(cancellable);
}
}

View file

@ -7,9 +7,11 @@
private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
private const string DB_FILENAME = "geary.db";
private const int BUSY_TIMEOUT_MSEC = Db.Connection.RECOMMENDED_BUSY_TIMEOUT_MSEC;
private string account_owner_email;
public Database(File db_dir, File schema_dir) {
public Database(File db_dir, File schema_dir, string account_owner_email) {
base (db_dir.get_child(DB_FILENAME), schema_dir);
this.account_owner_email = account_owner_email;
}
public override void open(Db.DatabaseFlags flags, Db.PrepareConnection? prepare_cb,
@ -22,6 +24,23 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
base.open(flags, on_prepare_database_connection, cancellable);
}
protected override void post_upgrade(int version) {
if (version == 5) {
try {
Db.Result result = query("SELECT sender, from_field, to_field, cc, bcc FROM MessageTable");
while (!result.finished) {
MessageAddresses message_addresses =
new MessageAddresses.from_result(account_owner_email, result);
foreach (Contact contact in message_addresses.contacts)
do_update_contact_importance(get_master_connection(), contact);
result.next();
}
} catch (Error err) {
debug("Error population autocompletion table during upgrade to database schema 5");
}
}
}
private void on_prepare_database_connection(Db.Connection cx) throws Error {
cx.set_busy_timeout_msec(BUSY_TIMEOUT_MSEC);
cx.set_foreign_keys(true);

View file

@ -55,15 +55,19 @@ private class Geary.ImapDB.Folder : Object, Geary.ReferenceSemantics {
private ImapDB.Database db;
private Geary.FolderPath path;
private ContactStore contact_store;
private string account_owner_email;
private int64 folder_id;
private Geary.Imap.FolderProperties? properties;
private Gee.HashSet<Geary.EmailIdentifier> marked_removed = new Gee.HashSet<Geary.EmailIdentifier>(
Hashable.hash_func, Equalable.equal_func);
internal Folder(ImapDB.Database db, Geary.FolderPath path, int64 folder_id,
Geary.Imap.FolderProperties? properties) {
internal Folder(ImapDB.Database db, Geary.FolderPath path, ContactStore contact_store,
string account_owner_email, int64 folder_id, Geary.Imap.FolderProperties? properties) {
this.db = db;
this.path = path;
this.contact_store = contact_store;
this.account_owner_email = account_owner_email;
this.folder_id = folder_id;
this.properties = properties;
}
@ -202,12 +206,17 @@ private class Geary.ImapDB.Folder : Object, Geary.ReferenceSemantics {
check_open();
bool created = false;
yield db.exec_transaction_async(Db.TransactionType.RW, (cx) => {
created = do_create_or_merge_email(cx, email, cancellable);
Gee.Collection<Contact>? updated_contacts = null;
Db.TransactionOutcome outcome = yield db.exec_transaction_async(Db.TransactionType.RW,
(cx) => {
created = do_create_or_merge_email(cx, email, out updated_contacts, cancellable);
return Db.TransactionOutcome.COMMIT;
}, cancellable);
if (outcome == Db.TransactionOutcome.COMMIT && updated_contacts != null)
contact_store.update_contacts(updated_contacts);
return created;
}
@ -732,7 +741,7 @@ private class Geary.ImapDB.Folder : Object, Geary.ReferenceSemantics {
}
private bool do_create_or_merge_email(Db.Connection cx, Geary.Email email,
Cancellable? cancellable) throws Error {
out Gee.Collection<Contact> updated_contacts, Cancellable? cancellable) throws Error {
// see if message already present in current folder, if not, search for duplicate throughout
// mailbox
bool associated;
@ -743,7 +752,7 @@ private class Geary.ImapDB.Folder : Object, Geary.ReferenceSemantics {
if (!associated)
do_associate_with_folder(cx, message_id, email, cancellable);
do_merge_email(cx, message_id, email, cancellable);
do_merge_email(cx, message_id, email, out updated_contacts, cancellable);
// return false to indicate a merge
return false;
@ -787,6 +796,12 @@ private class Geary.ImapDB.Folder : Object, Geary.ReferenceSemantics {
if (email.fields.fulfills(Attachment.REQUIRED_FIELDS))
do_save_attachments(cx, message_id, email.get_message().get_attachments(), cancellable);
MessageAddresses message_addresses =
new MessageAddresses.from_email(account_owner_email, email);
foreach (Contact contact in message_addresses.contacts)
do_update_contact_importance(cx, contact, cancellable);
updated_contacts = message_addresses.contacts;
return true;
}
@ -977,8 +992,11 @@ private class Geary.ImapDB.Folder : Object, Geary.ReferenceSemantics {
return true;
}
private void do_merge_message_row(Db.Connection cx, MessageRow row, Cancellable? cancellable)
throws Error {
private void do_merge_message_row(Db.Connection cx, MessageRow row,
out Gee.Collection<Contact> updated_contacts, Cancellable? cancellable) throws Error {
// Initialize to an empty list, in case we return early.
updated_contacts = new Gee.LinkedList<Contact>();
Geary.Email.Field available_fields;
if (!do_fetch_email_fields(cx, row.id, out available_fields, cancellable))
throw new EngineError.NOT_FOUND("No message with ID %s found in database", row.id.to_string());
@ -1097,13 +1115,22 @@ private class Geary.ImapDB.Folder : Object, Geary.ReferenceSemantics {
stmt.bind_rowid(1, row.id);
stmt.exec(cancellable);
// Update the autocompletion table.
MessageAddresses message_addresses =
new MessageAddresses.from_row(account_owner_email, row);
foreach (Geary.Contact contact in message_addresses.contacts)
do_update_contact_importance(cx, contact, cancellable);
updated_contacts = message_addresses.contacts;
}
private void do_merge_email(Db.Connection cx, int64 message_id, Geary.Email email,
Cancellable? cancellable) throws Error {
out Gee.Collection<Contact> updated_contacts, Cancellable? cancellable) throws Error {
assert(message_id != Db.INVALID_ROWID);
// nothing to merge, nothing to do
// Default to an empty list, in case we never call do_merge_message_row.
updated_contacts = new Gee.LinkedList<Contact>();
if (email.fields == Geary.Email.Field.NONE)
return;
@ -1118,7 +1145,7 @@ private class Geary.ImapDB.Folder : Object, Geary.ReferenceSemantics {
// Merge in any fields in the submitted email that aren't already in the database or are mutable
if (((db_fields & email.fields) != email.fields) || email.fields.is_any_set(Geary.Email.MUTABLE_FIELDS)) {
do_merge_message_row(cx, row, cancellable);
do_merge_message_row(cx, row, out updated_contacts, cancellable);
// Update attachments if not already in the database
if (!db_fields.fulfills(Attachment.REQUIRED_FIELDS)

View file

@ -0,0 +1,134 @@
/* Copyright 2012 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
private class Geary.ImapDB.MessageAddresses : Object {
// Read-only view.
public Gee.Collection<Contact> contacts { get; private set; }
private RFC822.MailboxAddresses? sender_addresses;
private RFC822.MailboxAddresses? from_addresses;
private RFC822.MailboxAddresses? to_addresses;
private RFC822.MailboxAddresses? cc_addresses;
private RFC822.MailboxAddresses? bcc_addresses;
private int from_importance;
private int to_importance;
private int cc_importance;
private MessageAddresses(string account_owner_email, RFC822.MailboxAddresses? sender_addresses,
RFC822.MailboxAddresses? from_addresses, RFC822.MailboxAddresses? to_addresses,
RFC822.MailboxAddresses? cc_addresses, RFC822.MailboxAddresses? bcc_addresses) {
this.sender_addresses = sender_addresses;
this.from_addresses = from_addresses;
this.to_addresses = to_addresses;
this.cc_addresses = cc_addresses;
this.bcc_addresses = bcc_addresses;
calculate_importance(account_owner_email);
contacts = build_contacts();
}
private MessageAddresses.from_strings(string account_owner_email, string? sender_field,
string? from_field, string? to_field, string? cc_field, string? bcc_field) {
this(account_owner_email, get_addresses_from_string(sender_field),
get_addresses_from_string(from_field), get_addresses_from_string(to_field),
get_addresses_from_string(cc_field), get_addresses_from_string(bcc_field));
}
public MessageAddresses.from_email(string account_owner_email, Geary.Email email) {
this(account_owner_email, email.sender, email.from, email.to, email.cc, email.bcc);
}
public MessageAddresses.from_row(string account_owner_email, MessageRow row) {
this.from_strings(account_owner_email, row.sender, row.from, row.to, row.cc, row.bcc);
}
public MessageAddresses.from_result(string account_owner_email, Db.Result result) {
this.from_strings(account_owner_email, get_string_or_null(result, "sender"),
get_string_or_null(result, "from_field"), get_string_or_null(result, "to_field"),
get_string_or_null(result, "cc"), get_string_or_null(result, "bcc"));
}
private static string? get_string_or_null(Db.Result result, string column) {
try {
return result.string_for(column);
} catch (Geary.DatabaseError err) {
debug("Error fetching addresses from message row: %s", err.message);
return null;
}
}
private static RFC822.MailboxAddresses? get_addresses_from_string(string? field) {
return field == null ? null : new RFC822.MailboxAddresses.from_rfc822_string(field);
}
private void calculate_importance(string account_owner_email) {
// "Sender" is different than "from", but we give it the same importance.
bool account_owner_in_from =
(sender_addresses != null && sender_addresses.contains_normalized(account_owner_email)) ||
(from_addresses != null && from_addresses.contains_normalized(account_owner_email));
bool account_owner_in_to = to_addresses != null &&
to_addresses.contains_normalized(account_owner_email);
// If the account owner's address does not appear in any of these fields, we assume they
// were BCC'd.
bool account_owner_in_cc =
(cc_addresses != null && cc_addresses.contains_normalized(account_owner_email)) ||
(bcc_addresses != null && bcc_addresses.contains_normalized(account_owner_email)) ||
!(account_owner_in_from || account_owner_in_to);
from_importance = -1;
to_importance = -1;
cc_importance = -1;
if (account_owner_in_from) {
from_importance = int.max(from_importance, ContactImportance.FROM_FROM);
to_importance = int.max(to_importance, ContactImportance.FROM_TO);
cc_importance = int.max(cc_importance, ContactImportance.FROM_CC);
}
if (account_owner_in_to) {
from_importance = int.max(from_importance, ContactImportance.TO_FROM);
to_importance = int.max(to_importance, ContactImportance.TO_TO);
cc_importance = int.max(cc_importance, ContactImportance.TO_CC);
}
if (account_owner_in_cc) {
from_importance = int.max(from_importance, ContactImportance.CC_FROM);
to_importance = int.max(to_importance, ContactImportance.CC_TO);
cc_importance = int.max(cc_importance, ContactImportance.CC_CC);
}
}
private Gee.Collection<Contact> build_contacts() {
Gee.Map<string, Contact> contacts_map = new Gee.HashMap<string, Contact>();
add_contacts(contacts_map, sender_addresses, from_importance);
add_contacts(contacts_map, from_addresses, from_importance);
add_contacts(contacts_map, to_addresses, to_importance);
add_contacts(contacts_map, cc_addresses, cc_importance);
add_contacts(contacts_map, bcc_addresses, cc_importance);
return contacts_map.values;
}
private void add_contacts(Gee.Map<string, Contact> contacts_map, RFC822.MailboxAddresses? addresses,
int importance) {
if (addresses == null)
return;
foreach (RFC822.MailboxAddress address in addresses)
add_contact(contacts_map, address, importance);
}
private void add_contact(Gee.Map<string, Contact> contacts_map, RFC822.MailboxAddress address,
int importance) {
Contact contact = new Contact.from_rfc822_address(address, importance);
Contact? old_contact = contacts_map[contact.normalized_email];
if (old_contact == null || old_contact.highest_importance < contact.highest_importance)
contacts_map[contact.normalized_email] = contact;
}
}

View file

@ -147,6 +147,10 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.EngineAccount {
return engine_list;
}
public override Geary.ContactStore get_contact_store() {
return local.contact_store;
}
public override async bool folder_exists_async(Geary.FolderPath path,
Cancellable? cancellable = null) throws Error {
check_open();

View file

@ -93,7 +93,7 @@ public class Geary.RFC822.MailboxAddress {
public bool is_valid() {
return is_valid_address(address);
}
/**
* Returns true if the email syntax is valid.
*/

View file

@ -47,6 +47,20 @@ public class Geary.RFC822.MailboxAddresses : Geary.Common.MessageData, Geary.RFC
return addrs.read_only_view;
}
public bool contains_normalized(string address) {
if (addrs.size < 1)
return false;
string normalized_address = address.normalize().casefold();
foreach (MailboxAddress mailbox_address in addrs) {
if (mailbox_address.address.normalize().casefold() == normalized_address)
return true;
}
return false;
}
public bool contains(string address) {
if (addrs.size < 1)
return false;