/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
* Copyright (C) 2015 Red Hat Inc. (www.redhat.com)
*
* This library is free software: you can redistribute it and/or modify it
* under the terms of version 2.1. of the GNU Lesser General Public License
* as published by the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#include "evolution-config.h"
#include <stdio.h>
#include <string.h>
#include <camel/camel.h>
#include <sqlite3.h>
#include "e-mail-remote-content.h"
#define CURRENT_VERSION 1
#define RECENT_CACHE_SIZE 10
typedef struct _RecentData {
gchar *value;
gboolean result;
} RecentData;
struct _EMailRemoteContentPrivate {
CamelDB *db;
GMutex recent_lock;
RecentData recent_mails[RECENT_CACHE_SIZE];
RecentData recent_sites[RECENT_CACHE_SIZE];
guint recent_last_mails;
guint recent_last_sites;
};
G_DEFINE_TYPE (EMailRemoteContent, e_mail_remote_content, G_TYPE_OBJECT)
static void
e_mail_remote_content_add_to_recent_cache (EMailRemoteContent *content,
const gchar *value,
gboolean result,
RecentData *recent_cache,
guint *recent_last)
{
gint ii, first_free = -1, index;
g_return_if_fail (E_IS_MAIL_REMOTE_CONTENT (content));
g_return_if_fail (value != NULL);
g_return_if_fail (recent_cache != NULL);
g_return_if_fail (recent_last != NULL);
g_mutex_lock (&content->priv->recent_lock);
for (ii = 0; ii < RECENT_CACHE_SIZE; ii++) {
index = (*recent_last + ii) % RECENT_CACHE_SIZE;
if (!recent_cache[index].value) {
if (first_free == -1)
first_free = index;
} else if (g_ascii_strcasecmp (recent_cache[index].value, value) == 0) {
recent_cache[index].result = result;
break;
}
}
if (ii == RECENT_CACHE_SIZE) {
if (first_free != -1) {
g_warn_if_fail (recent_cache[first_free].value == NULL);
recent_cache[first_free].value = g_strdup (value);
recent_cache[first_free].result = result;
if (first_free == (*recent_last + 1) % RECENT_CACHE_SIZE)
*recent_last = first_free;
} else {
index = (*recent_last + 1) % RECENT_CACHE_SIZE;
g_free (recent_cache[index].value);
recent_cache[index].value = g_strdup (value);
recent_cache[index].result = result;
*recent_last = index;
}
}
g_mutex_unlock (&content->priv->recent_lock);
}
static void
e_mail_remote_content_add (EMailRemoteContent *content,
const gchar *table,
const gchar *value,
RecentData *recent_cache,
guint *recent_last)
{
gchar *stmt;
GError *error = NULL;
g_return_if_fail (E_IS_MAIL_REMOTE_CONTENT (content));
g_return_if_fail (table != NULL);
g_return_if_fail (value != NULL);
g_return_if_fail (recent_cache != NULL);
g_return_if_fail (recent_last != NULL);
e_mail_remote_content_add_to_recent_cache (content, value, TRUE, recent_cache, recent_last);
if (!content->priv->db)
return;
stmt = sqlite3_mprintf ("INSERT OR IGNORE INTO %Q ('value') VALUES (lower(%Q))", table, value);
camel_db_command (content->priv->db, stmt, &error);
sqlite3_free (stmt);
if (error) {
g_warning ("%s: Failed to add to '%s' value '%s': %s", G_STRFUNC, table, value, error->message);
g_clear_error (&error);
}
}
static void
e_mail_remote_content_remove (EMailRemoteContent *content,
const gchar *table,
const gchar *value,
RecentData *recent_cache,
guint *recent_last)
{
gchar *stmt;
gint ii;
GError *error = NULL;
g_return_if_fail (E_IS_MAIL_REMOTE_CONTENT (content));
g_return_if_fail (table != NULL);
g_return_if_fail (value != NULL);
g_return_if_fail (recent_cache != NULL);
g_return_if_fail (recent_last != NULL);
g_mutex_lock (&content->priv->recent_lock);
for (ii = 0; ii < RECENT_CACHE_SIZE; ii++) {
gint index = (*recent_last + ii) % RECENT_CACHE_SIZE;
if (recent_cache[index].value && g_ascii_strcasecmp (recent_cache[index].value, value) == 0) {
g_free (recent_cache[index].value);
recent_cache[index].value = NULL;
break;
}
}
g_mutex_unlock (&content->priv->recent_lock);
if (!content->priv->db)
return;
stmt = sqlite3_mprintf ("DELETE FROM %Q WHERE value=lower(%Q)", table, value);
camel_db_command (content->priv->db, stmt, &error);
sqlite3_free (stmt);
if (error) {
g_warning ("%s: Failed to remove from '%s' value '%s': %s", G_STRFUNC, table, value, error->message);
g_clear_error (&error);
}
}
typedef struct _CheckFoundData {
gboolean found;
gboolean added_generic;
gboolean check_for_generic;
EMailRemoteContent *content;
RecentData *recent_cache;
guint *recent_last;
} CheckFoundData;
static gint
e_mail_remote_content_check_found_cb (gpointer data,
gint ncol,
gchar **colvalues,
gchar **colnames)
{
CheckFoundData *cfd = data;
if (cfd) {
cfd->found = TRUE;
if (colvalues && *colvalues && **colvalues) {
if (cfd->check_for_generic && *colvalues[0] == '@')
cfd->added_generic = TRUE;
e_mail_remote_content_add_to_recent_cache (cfd->content, colvalues[0], TRUE, cfd->recent_cache, cfd->recent_last);
}
}
return 0;
}
static gboolean
e_mail_remote_content_has (EMailRemoteContent *content,
const gchar *table,
const GSList *values,
RecentData *recent_cache,
guint *recent_last)
{
GString *stmt;
gint ii;
gchar *tmp;
const GSList *link;
gboolean found = FALSE, recent_cache_found = FALSE, added_generic = FALSE;
g_return_val_if_fail (E_IS_MAIL_REMOTE_CONTENT (content), FALSE);
g_return_val_if_fail (table != NULL, FALSE);
g_return_val_if_fail (values != NULL, FALSE);
g_return_val_if_fail (recent_cache != NULL, FALSE);
g_return_val_if_fail (recent_last != NULL, FALSE);
g_mutex_lock (&content->priv->recent_lock);
for (link = values; link && !found; link = g_slist_next (link)) {
const gchar *value = link->data;
for (ii = 0; ii < RECENT_CACHE_SIZE; ii++) {
gint index = (*recent_last + ii) % RECENT_CACHE_SIZE;
if (recent_cache[index].value && g_ascii_strcasecmp (recent_cache[index].value, value) == 0) {
recent_cache_found = TRUE;
found = recent_cache[index].result;
if (found)
break;
}
}
}
g_mutex_unlock (&content->priv->recent_lock);
if (recent_cache_found)
return found;
if (!content->priv->db)
return FALSE;
stmt = g_string_new ("");
for (link = values; link; link = g_slist_next (link)) {
const gchar *value = link->data;
if (!value || !*value)
continue;
if (stmt->len)
g_string_append (stmt, " OR ");
tmp = sqlite3_mprintf ("value=lower(%Q)", value);
g_string_append (stmt, tmp);
sqlite3_free (tmp);
}
if (stmt->len) {
CheckFoundData cfd;
cfd.found = FALSE;
cfd.added_generic = FALSE;
cfd.check_for_generic = g_strcmp0 (table, "mail");
cfd.content = content;
cfd.recent_cache = recent_cache;
cfd.recent_last = recent_last;
tmp = sqlite3_mprintf ("SELECT value FROM %Q WHERE ", table);
g_string_prepend (stmt, tmp);
sqlite3_free (tmp);
camel_db_select (content->priv->db, stmt->str, e_mail_remote_content_check_found_cb, &cfd, NULL);
found = cfd.found;
added_generic = cfd.added_generic;
}
g_string_free (stmt, TRUE);
if (!added_generic)
e_mail_remote_content_add_to_recent_cache (content, values->data, found, recent_cache, recent_last);
return found;
}
static gint
e_mail_remote_content_get_values_cb (gpointer data,
gint ncol,
gchar **colvalues,
gchar **colnames)
{
GHashTable *values_hash = data;
if (values_hash && colvalues && colvalues[0])
g_hash_table_insert (values_hash, g_strdup (colvalues[0]), NULL);
return 0;
}
static GSList *
e_mail_remote_content_get (EMailRemoteContent *content,
const gchar *table,
RecentData *recent_cache,
guint *recent_last)
{
GHashTable *values_hash;
GHashTableIter iter;
GSList *values = NULL;
gpointer itr_key, itr_value;
gint ii;
g_return_val_if_fail (E_IS_MAIL_REMOTE_CONTENT (content), NULL);
g_return_val_if_fail (table != NULL, NULL);
g_return_val_if_fail (recent_cache != NULL, NULL);
g_return_val_if_fail (recent_last != NULL, NULL);
values_hash = g_hash_table_new_full (camel_strcase_hash, camel_strcase_equal, g_free, NULL);
g_mutex_lock (&content->priv->recent_lock);
for (ii = 0; ii < RECENT_CACHE_SIZE; ii++) {
gint index = (*recent_last + ii) % RECENT_CACHE_SIZE;
if (recent_cache[index].value && recent_cache[index].result) {
g_hash_table_insert (values_hash, g_strdup (recent_cache[index].value), NULL);
}
}
g_mutex_unlock (&content->priv->recent_lock);
if (content->priv->db) {
gchar *stmt;
stmt = sqlite3_mprintf ("SELECT value FROM %Q ORDER BY value", table);
camel_db_select (content->priv->db, stmt, e_mail_remote_content_get_values_cb, values_hash, NULL);
sqlite3_free (stmt);
}
g_hash_table_iter_init (&iter, values_hash);
while (g_hash_table_iter_next (&iter, &itr_key, &itr_value)) {
const gchar *value = itr_key;
if (value && *value)
values = g_slist_prepend (values, g_strdup (value));
}
g_hash_table_destroy (values_hash);
return g_slist_reverse (values);
}
static gint
e_mail_remote_content_get_version_cb (gpointer data,
gint ncol,
gchar **colvalues,
gchar **colnames)
{
gint *pversion = data;
if (pversion && ncol == 1 && colvalues && colvalues[0])
*pversion = (gint) g_ascii_strtoll (colvalues[0], NULL, 10);
return 0;
}
static void
e_mail_remote_content_set_config_filename (EMailRemoteContent *content,
const gchar *config_filename)
{
GError *error = NULL;
g_return_if_fail (E_IS_MAIL_REMOTE_CONTENT (content));
g_return_if_fail (config_filename != NULL);
g_return_if_fail (content->priv->db == NULL);
content->priv->db = camel_db_new (config_filename, &error);
if (error) {
g_warning ("%s: Failed to open '%s': %s", G_STRFUNC, config_filename, error->message);
g_clear_error (&error);
}
if (content->priv->db) {
#define ctb(stmt) G_STMT_START { \
if (content->priv->db) { \
camel_db_command (content->priv->db, stmt, &error); \
if (error) { \
g_warning ("%s: Failed to execute '%s' on '%s': %s", \
G_STRFUNC, stmt, config_filename, error->message); \
g_clear_error (&error); \
} \
} \
} G_STMT_END
ctb ("CREATE TABLE IF NOT EXISTS version (current INT)");
ctb ("CREATE TABLE IF NOT EXISTS sites (value TEXT PRIMARY KEY)");
ctb ("CREATE TABLE IF NOT EXISTS mails (value TEXT PRIMARY KEY)");
#undef ctb
}
if (content->priv->db) {
gint version = -1;
gchar *stmt;
camel_db_select (content->priv->db, "SELECT 'current' FROM 'version'", e_mail_remote_content_get_version_cb, &version, NULL);
if (version != -1 && version < CURRENT_VERSION) {
/* Here will be added migration code, if needed in the future */
}
stmt = sqlite3_mprintf ("DELETE FROM %Q", "version");
camel_db_command (content->priv->db, stmt, NULL);
sqlite3_free (stmt);
stmt = sqlite3_mprintf ("INSERT INTO %Q ('current') VALUES (%d);", "version", CURRENT_VERSION);
camel_db_command (content->priv->db, stmt, NULL);
sqlite3_free (stmt);
}
}
static void
mail_remote_content_finalize (GObject *object)
{
EMailRemoteContent *content;
gint ii;
content = E_MAIL_REMOTE_CONTENT (object);
if (content->priv->db) {
GError *error = NULL;
camel_db_maybe_run_maintenance (content->priv->db, &error);
if (error) {
g_warning ("%s: Failed to run maintenance: %s", G_STRFUNC, error->message);
g_clear_error (&error);
}
g_clear_object (&content->priv->db);
}
g_mutex_lock (&content->priv->recent_lock);
for (ii = 0; ii < RECENT_CACHE_SIZE; ii++) {
g_free (content->priv->recent_sites[ii].value);
g_free (content->priv->recent_mails[ii].value);
content->priv->recent_sites[ii].value = NULL;
content->priv->recent_mails[ii].value = NULL;
}
g_mutex_unlock (&content->priv->recent_lock);
g_mutex_clear (&content->priv->recent_lock);
/* Chain up to parent's finalize() method. */
G_OBJECT_CLASS (e_mail_remote_content_parent_class)->finalize (object);
}
static void
e_mail_remote_content_class_init (EMailRemoteContentClass *class)
{
GObjectClass *object_class;
g_type_class_add_private (class, sizeof (EMailRemoteContentPrivate));
object_class = G_OBJECT_CLASS (class);
object_class->finalize = mail_remote_content_finalize;
}
static void
e_mail_remote_content_init (EMailRemoteContent *content)
{
content->priv = G_TYPE_INSTANCE_GET_PRIVATE (content, E_TYPE_MAIL_REMOTE_CONTENT, EMailRemoteContentPrivate);
g_mutex_init (&content->priv->recent_lock);
}
EMailRemoteContent *
e_mail_remote_content_new (const gchar *config_filename)
{
EMailRemoteContent *content;
content = g_object_new (E_TYPE_MAIL_REMOTE_CONTENT, NULL);
if (config_filename != NULL)
e_mail_remote_content_set_config_filename (content, config_filename);
return content;
}
void
e_mail_remote_content_add_site (EMailRemoteContent *content,
const gchar *site)
{
g_return_if_fail (E_IS_MAIL_REMOTE_CONTENT (content));
g_return_if_fail (site != NULL);
e_mail_remote_content_add (content, "sites", site, content->priv->recent_sites, &content->priv->recent_last_sites);
}
void
e_mail_remote_content_remove_site (EMailRemoteContent *content,
const gchar *site)
{
g_return_if_fail (E_IS_MAIL_REMOTE_CONTENT (content));
g_return_if_fail (site != NULL);
e_mail_remote_content_remove (content, "sites", site, content->priv->recent_sites, &content->priv->recent_last_sites);
}
gboolean
e_mail_remote_content_has_site (EMailRemoteContent *content,
const gchar *site)
{
GSList *values = NULL;
gboolean result;
g_return_val_if_fail (E_IS_MAIL_REMOTE_CONTENT (content), FALSE);
g_return_val_if_fail (site != NULL, FALSE);
values = g_slist_prepend (values, (gpointer) site);
result = e_mail_remote_content_has (content, "sites", values, content->priv->recent_sites, &content->priv->recent_last_sites);
g_slist_free (values);
return result;
}
/* Free the result with g_slist_free_full (values, g_free); */
GSList *
e_mail_remote_content_get_sites (EMailRemoteContent *content)
{
g_return_val_if_fail (E_IS_MAIL_REMOTE_CONTENT (content), NULL);
return e_mail_remote_content_get (content, "sites", content->priv->recent_sites, &content->priv->recent_last_sites);
}
void
e_mail_remote_content_add_mail (EMailRemoteContent *content,
const gchar *mail)
{
g_return_if_fail (E_IS_MAIL_REMOTE_CONTENT (content));
g_return_if_fail (mail != NULL);
e_mail_remote_content_add (content, "mails", mail, content->priv->recent_mails, &content->priv->recent_last_mails);
}
void
e_mail_remote_content_remove_mail (EMailRemoteContent *content,
const gchar *mail)
{
g_return_if_fail (E_IS_MAIL_REMOTE_CONTENT (content));
g_return_if_fail (mail != NULL);
e_mail_remote_content_remove (content, "mails", mail, content->priv->recent_mails, &content->priv->recent_last_mails);
}
gboolean
e_mail_remote_content_has_mail (EMailRemoteContent *content,
const gchar *mail)
{
GSList *values = NULL;
const gchar *at;
gboolean result;
g_return_val_if_fail (E_IS_MAIL_REMOTE_CONTENT (content), FALSE);
g_return_val_if_fail (mail != NULL, FALSE);
at = strchr (mail, '@');
if (at)
values = g_slist_prepend (values, (gpointer) at);
values = g_slist_prepend (values, (gpointer) mail);
result = e_mail_remote_content_has (content, "mails", values, content->priv->recent_mails, &content->priv->recent_last_mails);
g_slist_free (values);
return result;
}
/* Free the result with g_slist_free_full (values, g_free); */
GSList *
e_mail_remote_content_get_mails (EMailRemoteContent *content)
{
g_return_val_if_fail (E_IS_MAIL_REMOTE_CONTENT (content), NULL);
return e_mail_remote_content_get (content, "mails", content->priv->recent_mails, &content->priv->recent_last_mails);
}