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-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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
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() {
|
||||
changed.connect(on_changed);
|
||||
// TODO: Contact completion with libfolks
|
||||
}
|
||||
|
||||
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,
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
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 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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue