Autocomplete addresses in To, CC, BCC fields: Closes #4284.
This commit is contained in:
parent
bb3fc2209b
commit
f9d4c7cb9e
20 changed files with 735 additions and 42 deletions
13
sql/version-005.sql
Normal file
13
sql/version-005.sql
Normal 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
|
||||||
|
);
|
||||||
|
|
||||||
|
|
@ -19,6 +19,9 @@ engine/api/geary-account-information.vala
|
||||||
engine/api/geary-account-settings.vala
|
engine/api/geary-account-settings.vala
|
||||||
engine/api/geary-attachment.vala
|
engine/api/geary-attachment.vala
|
||||||
engine/api/geary-composed-email.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.vala
|
||||||
engine/api/geary-conversation-monitor.vala
|
engine/api/geary-conversation-monitor.vala
|
||||||
engine/api/geary-credentials.vala
|
engine/api/geary-credentials.vala
|
||||||
|
|
@ -104,8 +107,10 @@ engine/imap/transport/imap-serializable.vala
|
||||||
engine/imap/transport/imap-serializer.vala
|
engine/imap/transport/imap-serializer.vala
|
||||||
|
|
||||||
engine/imap-db/imap-db-account.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-database.vala
|
||||||
engine/imap-db/imap-db-folder.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/imap-db-message-row.vala
|
||||||
engine/imap-db/outbox/smtp-outbox-email-identifier.vala
|
engine/imap-db/outbox/smtp-outbox-email-identifier.vala
|
||||||
engine/imap-db/outbox/smtp-outbox-email-properties.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/notification/null-indicator.vala
|
||||||
|
|
||||||
client/ui/composer-window.vala
|
client/ui/composer-window.vala
|
||||||
|
client/ui/contact-entry-completion.vala
|
||||||
client/ui/geary-login.vala
|
client/ui/geary-login.vala
|
||||||
client/ui/email-entry.vala
|
client/ui/email-entry.vala
|
||||||
client/ui/icon-factory.vala
|
client/ui/icon-factory.vala
|
||||||
|
|
|
||||||
|
|
@ -1055,7 +1055,8 @@ public class GearyController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void create_compose_window(Geary.ComposedEmail? prefill = null) {
|
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.set_position(Gtk.WindowPosition.CENTER);
|
||||||
window.send.connect(on_send);
|
window.send.connect(on_send);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -132,8 +132,14 @@ public class ComposerWindow : Gtk.Window {
|
||||||
// garbage-collected.
|
// garbage-collected.
|
||||||
private WebViewEditFixer edit_fixer;
|
private WebViewEditFixer edit_fixer;
|
||||||
private Gtk.UIManager ui;
|
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);
|
setup_drag_destination(this);
|
||||||
|
|
||||||
add_events(Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK);
|
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_child = (Gtk.Widget) builder.get_object("visible_on_attachment_drag_over_child");
|
||||||
visible_on_attachment_drag_over.remove(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 = new EmailEntry();
|
||||||
|
to_entry.completion = contact_entry_completions[0];
|
||||||
(builder.get_object("to") as Gtk.EventBox).add(to_entry);
|
(builder.get_object("to") as Gtk.EventBox).add(to_entry);
|
||||||
cc_entry = new EmailEntry();
|
cc_entry = new EmailEntry();
|
||||||
|
cc_entry.completion = contact_entry_completions[1];
|
||||||
(builder.get_object("cc") as Gtk.EventBox).add(cc_entry);
|
(builder.get_object("cc") as Gtk.EventBox).add(cc_entry);
|
||||||
bcc_entry = new EmailEntry();
|
bcc_entry = new EmailEntry();
|
||||||
|
bcc_entry.completion = contact_entry_completions[2];
|
||||||
(builder.get_object("bcc") as Gtk.EventBox).add(bcc_entry);
|
(builder.get_object("bcc") as Gtk.EventBox).add(bcc_entry);
|
||||||
subject_entry = builder.get_object("subject") as Gtk.Entry;
|
subject_entry = builder.get_object("subject") as Gtk.Entry;
|
||||||
Gtk.Alignment message_area = builder.get_object("message area") as Gtk.Alignment;
|
Gtk.Alignment message_area = builder.get_object("message area") as Gtk.Alignment;
|
||||||
|
|
|
||||||
306
src/client/ui/contact-entry-completion.vala
Normal file
306
src/client/ui/contact-entry-completion.vala
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -14,7 +14,6 @@ public class EmailEntry : Gtk.Entry {
|
||||||
|
|
||||||
public EmailEntry() {
|
public EmailEntry() {
|
||||||
changed.connect(on_changed);
|
changed.connect(on_changed);
|
||||||
// TODO: Contact completion with libfolks
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void on_changed() {
|
private void on_changed() {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
public abstract async Gee.Collection<Geary.Folder> list_folders_async(Geary.FolderPath? parent,
|
||||||
Cancellable? cancellable = null) throws Error;
|
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)
|
public abstract async bool folder_exists_async(Geary.FolderPath path, Cancellable? cancellable = null)
|
||||||
throws Error;
|
throws Error;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,11 @@ public interface Geary.Account : Object {
|
||||||
public abstract async Gee.Collection<Geary.Folder> list_folders_async(Geary.FolderPath? parent,
|
public abstract async Gee.Collection<Geary.Folder> list_folders_async(Geary.FolderPath? parent,
|
||||||
Cancellable? cancellable = null) throws Error;
|
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.
|
* Returns true if the folder exists.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
38
src/engine/api/geary-contact-importance.vala
Normal file
38
src/engine/api/geary-contact-importance.vala
Normal 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;
|
||||||
|
}
|
||||||
37
src/engine/api/geary-contact-store.vala
Normal file
37
src/engine/api/geary-contact-store.vala
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/engine/api/geary-contact.vala
Normal file
27
src/engine/api/geary-contact.vala
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,24 +7,16 @@
|
||||||
public class Geary.Db.VersionedDatabase : Geary.Db.Database {
|
public class Geary.Db.VersionedDatabase : Geary.Db.Database {
|
||||||
public File schema_dir { get; private set; }
|
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) {
|
public VersionedDatabase(File db_file, File schema_dir) {
|
||||||
base (db_file);
|
base (db_file);
|
||||||
|
|
||||||
this.schema_dir = schema_dir;
|
this.schema_dir = schema_dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void notify_pre_upgrade(int version) {
|
protected virtual void pre_upgrade(int version) {
|
||||||
pre_upgrade(version);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void notify_post_upgrade(int version) {
|
protected virtual void post_upgrade(int version) {
|
||||||
post_upgrade(version);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Initialize database from version-001.sql and upgrade with version-nnn.sql
|
// 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))
|
if (!upgrade_script.query_exists(cancellable))
|
||||||
break;
|
break;
|
||||||
|
|
||||||
notify_pre_upgrade(db_version);
|
pre_upgrade(db_version);
|
||||||
|
|
||||||
check_cancelled("VersionedDatabase.open", cancellable);
|
check_cancelled("VersionedDatabase.open", cancellable);
|
||||||
|
|
||||||
|
|
@ -66,7 +58,7 @@ public class Geary.Db.VersionedDatabase : Geary.Db.Database {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
notify_post_upgrade(db_version);
|
post_upgrade(db_version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,21 @@ private class Geary.ImapDB.Account : Object {
|
||||||
// Only available when the Account is opened
|
// Only available when the Account is opened
|
||||||
public SmtpOutboxFolder? outbox { get; private set; default = null; }
|
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 string name;
|
||||||
private AccountSettings settings;
|
private AccountSettings settings;
|
||||||
private ImapDB.Database? db = null;
|
private ImapDB.Database? db = null;
|
||||||
private Gee.HashMap<Geary.FolderPath, FolderReference> folder_refs =
|
private Gee.HashMap<Geary.FolderPath, FolderReference> folder_refs =
|
||||||
new Gee.HashMap<Geary.FolderPath, FolderReference>(Hashable.hash_func, Equalable.equal_func);
|
new Gee.HashMap<Geary.FolderPath, FolderReference>(Hashable.hash_func, Equalable.equal_func);
|
||||||
|
public ContactStore contact_store { get; private set; }
|
||||||
|
|
||||||
public Account(Geary.AccountSettings settings) {
|
public Account(Geary.AccountSettings settings) {
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
|
contact_store = new ContactStore();
|
||||||
|
|
||||||
name = "IMAP database account for %s".printf(settings.credentials.user);
|
name = "IMAP database account for %s".printf(settings.credentials.user);
|
||||||
}
|
}
|
||||||
|
|
@ -40,9 +47,7 @@ private class Geary.ImapDB.Account : Object {
|
||||||
if (db != null)
|
if (db != null)
|
||||||
throw new EngineError.ALREADY_OPEN("IMAP database already open");
|
throw new EngineError.ALREADY_OPEN("IMAP database already open");
|
||||||
|
|
||||||
db = new ImapDB.Database(user_data_dir, schema_dir);
|
db = new ImapDB.Database(user_data_dir, schema_dir, account_owner_email);
|
||||||
db.pre_upgrade.connect(on_pre_upgrade);
|
|
||||||
db.post_upgrade.connect(on_post_upgrade);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.open(Db.DatabaseFlags.CREATE_DIRECTORY | Db.DatabaseFlags.CREATE_FILE, null,
|
db.open(Db.DatabaseFlags.CREATE_DIRECTORY | Db.DatabaseFlags.CREATE_FILE, null,
|
||||||
|
|
@ -56,6 +61,8 @@ private class Geary.ImapDB.Account : Object {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initialize_contacts(cancellable);
|
||||||
|
|
||||||
// ImapDB.Account holds the Outbox, which is tied to the database it maintains
|
// ImapDB.Account holds the Outbox, which is tied to the database it maintains
|
||||||
outbox = new SmtpOutboxFolder(db, settings);
|
outbox = new SmtpOutboxFolder(db, settings);
|
||||||
|
|
||||||
|
|
@ -161,6 +168,39 @@ private class Geary.ImapDB.Account : Object {
|
||||||
db_folder.set_properties(properties);
|
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,
|
public async Gee.Collection<Geary.ImapDB.Folder> list_folders_async(Geary.FolderPath? parent,
|
||||||
Cancellable? cancellable = null) throws Error {
|
Cancellable? cancellable = null) throws Error {
|
||||||
check_open();
|
check_open();
|
||||||
|
|
@ -321,7 +361,8 @@ private class Geary.ImapDB.Account : Object {
|
||||||
}
|
}
|
||||||
|
|
||||||
// create folder
|
// 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
|
// build a reference to it
|
||||||
FolderReference folder_ref = new FolderReference(folder, path);
|
FolderReference folder_ref = new FolderReference(folder, path);
|
||||||
|
|
@ -340,14 +381,6 @@ private class Geary.ImapDB.Account : Object {
|
||||||
folder_refs.unset(folder_ref.path);
|
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() {
|
private void clear_duplicate_folders() {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
|
||||||
|
|
|
||||||
25
src/engine/imap-db/imap-db-contact.vala
Normal file
25
src/engine/imap-db/imap-db-contact.vala
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -7,9 +7,11 @@
|
||||||
private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
|
private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
|
||||||
private const string DB_FILENAME = "geary.db";
|
private const string DB_FILENAME = "geary.db";
|
||||||
private const int BUSY_TIMEOUT_MSEC = Db.Connection.RECOMMENDED_BUSY_TIMEOUT_MSEC;
|
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);
|
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,
|
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);
|
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 {
|
private void on_prepare_database_connection(Db.Connection cx) throws Error {
|
||||||
cx.set_busy_timeout_msec(BUSY_TIMEOUT_MSEC);
|
cx.set_busy_timeout_msec(BUSY_TIMEOUT_MSEC);
|
||||||
cx.set_foreign_keys(true);
|
cx.set_foreign_keys(true);
|
||||||
|
|
|
||||||
|
|
@ -55,15 +55,19 @@ private class Geary.ImapDB.Folder : Object, Geary.ReferenceSemantics {
|
||||||
|
|
||||||
private ImapDB.Database db;
|
private ImapDB.Database db;
|
||||||
private Geary.FolderPath path;
|
private Geary.FolderPath path;
|
||||||
|
private ContactStore contact_store;
|
||||||
|
private string account_owner_email;
|
||||||
private int64 folder_id;
|
private int64 folder_id;
|
||||||
private Geary.Imap.FolderProperties? properties;
|
private Geary.Imap.FolderProperties? properties;
|
||||||
private Gee.HashSet<Geary.EmailIdentifier> marked_removed = new Gee.HashSet<Geary.EmailIdentifier>(
|
private Gee.HashSet<Geary.EmailIdentifier> marked_removed = new Gee.HashSet<Geary.EmailIdentifier>(
|
||||||
Hashable.hash_func, Equalable.equal_func);
|
Hashable.hash_func, Equalable.equal_func);
|
||||||
|
|
||||||
internal Folder(ImapDB.Database db, Geary.FolderPath path, int64 folder_id,
|
internal Folder(ImapDB.Database db, Geary.FolderPath path, ContactStore contact_store,
|
||||||
Geary.Imap.FolderProperties? properties) {
|
string account_owner_email, int64 folder_id, Geary.Imap.FolderProperties? properties) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.path = path;
|
this.path = path;
|
||||||
|
this.contact_store = contact_store;
|
||||||
|
this.account_owner_email = account_owner_email;
|
||||||
this.folder_id = folder_id;
|
this.folder_id = folder_id;
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
}
|
}
|
||||||
|
|
@ -202,12 +206,17 @@ private class Geary.ImapDB.Folder : Object, Geary.ReferenceSemantics {
|
||||||
check_open();
|
check_open();
|
||||||
|
|
||||||
bool created = false;
|
bool created = false;
|
||||||
yield db.exec_transaction_async(Db.TransactionType.RW, (cx) => {
|
Gee.Collection<Contact>? updated_contacts = null;
|
||||||
created = do_create_or_merge_email(cx, email, cancellable);
|
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;
|
return Db.TransactionOutcome.COMMIT;
|
||||||
}, cancellable);
|
}, cancellable);
|
||||||
|
|
||||||
|
if (outcome == Db.TransactionOutcome.COMMIT && updated_contacts != null)
|
||||||
|
contact_store.update_contacts(updated_contacts);
|
||||||
|
|
||||||
return created;
|
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,
|
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
|
// see if message already present in current folder, if not, search for duplicate throughout
|
||||||
// mailbox
|
// mailbox
|
||||||
bool associated;
|
bool associated;
|
||||||
|
|
@ -743,7 +752,7 @@ private class Geary.ImapDB.Folder : Object, Geary.ReferenceSemantics {
|
||||||
if (!associated)
|
if (!associated)
|
||||||
do_associate_with_folder(cx, message_id, email, cancellable);
|
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 to indicate a merge
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -787,6 +796,12 @@ private class Geary.ImapDB.Folder : Object, Geary.ReferenceSemantics {
|
||||||
if (email.fields.fulfills(Attachment.REQUIRED_FIELDS))
|
if (email.fields.fulfills(Attachment.REQUIRED_FIELDS))
|
||||||
do_save_attachments(cx, message_id, email.get_message().get_attachments(), cancellable);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -977,8 +992,11 @@ private class Geary.ImapDB.Folder : Object, Geary.ReferenceSemantics {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void do_merge_message_row(Db.Connection cx, MessageRow row, Cancellable? cancellable)
|
private void do_merge_message_row(Db.Connection cx, MessageRow row,
|
||||||
throws Error {
|
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;
|
Geary.Email.Field available_fields;
|
||||||
if (!do_fetch_email_fields(cx, row.id, out available_fields, cancellable))
|
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());
|
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.bind_rowid(1, row.id);
|
||||||
|
|
||||||
stmt.exec(cancellable);
|
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,
|
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);
|
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)
|
if (email.fields == Geary.Email.Field.NONE)
|
||||||
return;
|
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
|
// 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)) {
|
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
|
// Update attachments if not already in the database
|
||||||
if (!db_fields.fulfills(Attachment.REQUIRED_FIELDS)
|
if (!db_fields.fulfills(Attachment.REQUIRED_FIELDS)
|
||||||
|
|
|
||||||
134
src/engine/imap-db/imap-db-message-addresses.vala
Normal file
134
src/engine/imap-db/imap-db-message-addresses.vala
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -147,6 +147,10 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.EngineAccount {
|
||||||
return engine_list;
|
return engine_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override Geary.ContactStore get_contact_store() {
|
||||||
|
return local.contact_store;
|
||||||
|
}
|
||||||
|
|
||||||
public override async bool folder_exists_async(Geary.FolderPath path,
|
public override async bool folder_exists_async(Geary.FolderPath path,
|
||||||
Cancellable? cancellable = null) throws Error {
|
Cancellable? cancellable = null) throws Error {
|
||||||
check_open();
|
check_open();
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ public class Geary.RFC822.MailboxAddress {
|
||||||
public bool is_valid() {
|
public bool is_valid() {
|
||||||
return is_valid_address(address);
|
return is_valid_address(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the email syntax is valid.
|
* Returns true if the email syntax is valid.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,20 @@ public class Geary.RFC822.MailboxAddresses : Geary.Common.MessageData, Geary.RFC
|
||||||
return addrs.read_only_view;
|
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) {
|
public bool contains(string address) {
|
||||||
if (addrs.size < 1)
|
if (addrs.size < 1)
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue