Commit aab2dd62 authored by Milan Crha's avatar Milan Crha

Bug 550796 - Implement free form filter expression

parent 080e9fa3
......@@ -30,9 +30,11 @@
#include <gtk/gtk.h>
#include <glib/gi18n.h>
#include <gmodule.h>
#include "e-alert.h"
#include "e-filter-input.h"
#include "e-filter-part.h"
G_DEFINE_TYPE (
EFilterInput,
......@@ -59,6 +61,7 @@ filter_input_finalize (GObject *object)
EFilterInput *input = E_FILTER_INPUT (object);
xmlFree (input->type);
g_free (input->code_gen_func);
g_list_foreach (input->values, (GFunc) g_free, NULL);
g_list_free (input->values);
......@@ -164,6 +167,9 @@ filter_input_eq (EFilterElement *element_a,
if (link_a != NULL || link_b != NULL)
return FALSE;
if (g_strcmp0 (input_a->code_gen_func, input_b->code_gen_func) != 0)
return FALSE;
return input_a->allow_empty == input_b->allow_empty;
}
......@@ -172,8 +178,12 @@ filter_input_xml_create (EFilterElement *element,
xmlNodePtr node)
{
EFilterInput *input = E_FILTER_INPUT (element);
xmlNodePtr n;
gchar *allow_empty;
g_free (input->code_gen_func);
input->code_gen_func = NULL;
/* Chain up to parent's method. */
E_FILTER_ELEMENT_CLASS (e_filter_input_parent_class)->xml_create (element, node);
......@@ -181,13 +191,29 @@ filter_input_xml_create (EFilterElement *element,
input->allow_empty = !allow_empty || g_strcmp0 (allow_empty, "true") == 0;
xmlFree (allow_empty);
for (n = node->children; n; n = n->next) {
if (g_str_equal (n->name, "code")) {
xmlChar *func = xmlGetProp (n, (xmlChar *) "func");
if (func && *func) {
if (input->code_gen_func)
g_free (input->code_gen_func);
input->code_gen_func = g_strdup ((gchar *) func);
}
if (func)
xmlFree (func);
break;
}
}
}
static xmlNodePtr
filter_input_xml_encode (EFilterElement *element)
{
EFilterInput *input = E_FILTER_INPUT (element);
xmlNodePtr value;
xmlNodePtr value, cur;
GList *link;
const gchar *type;
......@@ -200,7 +226,6 @@ filter_input_xml_encode (EFilterElement *element)
for (link = input->values; link != NULL; link = g_list_next (link)) {
xmlChar *str = link->data;
xmlNodePtr cur;
cur = xmlNewChild (value, NULL, (xmlChar *) type, NULL);
......@@ -256,6 +281,34 @@ filter_input_xml_decode (EFilterElement *element,
return 0;
}
static EFilterElement *
filter_input_clone (EFilterElement *element)
{
EFilterInput *input = E_FILTER_INPUT (element);
EFilterInput *clone_input;
EFilterElement *clone;
GList *link;
/* Chain up to parent's clone() method. */
clone = E_FILTER_ELEMENT_CLASS (e_filter_input_parent_class)->clone (element);
clone_input = E_FILTER_INPUT (clone);
if (clone_input->type)
xmlFree (clone_input->type);
clone_input->type = input->type ? (gchar *) xmlStrdup ((const xmlChar *) input->type) : NULL;
clone_input->allow_empty = input->allow_empty;
clone_input->code_gen_func = g_strdup (input->code_gen_func);
for (link = input->values; link != NULL; link = g_list_next (link)) {
clone_input->values = g_list_prepend (clone_input->values, g_strdup (link->data));
}
clone_input->values = g_list_reverse (clone_input->values);
return clone;
}
static GtkWidget *
filter_input_get_widget (EFilterElement *element)
{
......@@ -281,10 +334,38 @@ filter_input_format_sexp (EFilterElement *element,
EFilterInput *input = E_FILTER_INPUT (element);
GList *link;
if (input->code_gen_func)
return;
for (link = input->values; link != NULL; link = g_list_next (link))
camel_sexp_encode_string (out, link->data);
}
static void
filter_input_build_code (EFilterElement *element,
GString *out,
EFilterPart *part)
{
EFilterInput *input = E_FILTER_INPUT (element);
GModule *module;
void (*code_gen_func) (EFilterElement *element, GString *out, EFilterPart *part);
if (!input->code_gen_func)
return;
module = g_module_open (NULL, G_MODULE_BIND_LAZY);
if (g_module_symbol (module, input->code_gen_func, (gpointer) &code_gen_func)) {
code_gen_func (E_FILTER_ELEMENT (input), out, part);
} else {
g_warning (
"input dynamic code function '%s' not found",
input->code_gen_func);
}
g_module_close (module);
}
static void
e_filter_input_class_init (EFilterInputClass *class)
{
......@@ -300,8 +381,10 @@ e_filter_input_class_init (EFilterInputClass *class)
filter_element_class->xml_create = filter_input_xml_create;
filter_element_class->xml_encode = filter_input_xml_encode;
filter_element_class->xml_decode = filter_input_xml_decode;
filter_element_class->clone = filter_input_clone;
filter_element_class->get_widget = filter_input_get_widget;
filter_element_class->format_sexp = filter_input_format_sexp;
filter_element_class->build_code = filter_input_build_code;
}
static void
......@@ -309,6 +392,7 @@ e_filter_input_init (EFilterInput *input)
{
input->values = g_list_prepend (NULL, g_strdup (""));
input->allow_empty = TRUE;
input->code_gen_func = NULL;
}
/**
......
......@@ -61,6 +61,7 @@ struct _EFilterInput {
gchar *type; /* name of type */
GList *values; /* strings */
gboolean allow_empty; /* whether can have empty value */
gchar *code_gen_func; /* function name to build the 'code' */
};
struct _EFilterInputClass {
......
......@@ -86,6 +86,7 @@ mailinclude_HEADERS = \
e-mail-enumtypes.h \
e-mail-folder-create-dialog.h \
e-mail-folder-pane.h \
e-mail-free-form-exp.h \
e-mail-junk-options.h \
e-mail-label-action.h \
e-mail-label-dialog.h \
......@@ -163,6 +164,7 @@ libevolution_mail_la_SOURCES = \
e-mail-enumtypes.c \
e-mail-folder-create-dialog.c \
e-mail-folder-pane.c \
e-mail-free-form-exp.c \
e-mail-junk-options.c \
e-mail-label-action.c \
e-mail-label-dialog.c \
......
/*
* Copyright (C) 2014 Red Hat, Inc. (www.redhat.com)
*
* This library is free software: you can redistribute it and/or modify it
* under the terms 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/>.
*
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <glib.h>
#include <glib/gi18n-lib.h>
#include <string.h>
#include <camel/camel.h>
#include <e-util/e-util.h>
#include <libedataserver/libedataserver.h>
#include "e-mail-free-form-exp.h"
static gchar *
mail_ffe_build_header_sexp (const gchar *word,
const gchar *options,
const gchar * const *header_names)
{
GString *sexp = NULL, *encoded_word;
const gchar *compare_type = NULL;
gint ii;
g_return_val_if_fail (header_names != NULL, NULL);
g_return_val_if_fail (header_names[0] != NULL, NULL);
if (!word)
return NULL;
if (options) {
struct _KnownOptions {
const gchar *compare_type;
const gchar *alt_name;
} known_options[] = {
{ "contains", "c" },
{ "has-words", "w" },
{ "matches", "m" },
{ "starts-with", "sw" },
{ "ends-with", "ew" },
{ "soundex", "se" },
{ "regex", "r" },
{ "full-regex", "fr" }
};
for (ii = 0; ii < G_N_ELEMENTS (known_options); ii++) {
if (g_ascii_strcasecmp (options, known_options[ii].compare_type) == 0 ||
(known_options[ii].alt_name && g_ascii_strcasecmp (options, known_options[ii].alt_name) == 0)) {
compare_type = known_options[ii].compare_type;
break;
}
}
}
if (!compare_type)
compare_type = "contains";
encoded_word = g_string_new ("");
camel_sexp_encode_string (encoded_word, word);
if (!header_names[1]) {
if (!sexp)
sexp = g_string_new ("");
} else if (!sexp) {
sexp = g_string_new ("(or ");
} else {
g_string_append (sexp, "(or ");
}
for (ii = 0; header_names[ii]; ii++) {
g_string_append_printf (sexp, "(match-all (header-%s \"%s\" %s))", compare_type, header_names[ii], encoded_word->str);
}
if (header_names[1])
g_string_append (sexp, ")");
g_string_free (encoded_word, TRUE);
return sexp ? g_string_free (sexp, FALSE) : NULL;
}
static gchar *
mail_ffe_recips (const gchar *word,
const gchar *options,
const gchar *hint)
{
const gchar *header_names[] = { "To", "Cc", "Subject", NULL };
/* Include Subject only in the default expression. */
if (!hint)
header_names[2] = NULL;
return mail_ffe_build_header_sexp (word, options, header_names);
}
static gchar *
mail_ffe_from (const gchar *word,
const gchar *options,
const gchar *hint)
{
const gchar *header_names[] = { "From", NULL };
return mail_ffe_build_header_sexp (word, options, header_names);
}
static gchar *
mail_ffe_to (const gchar *word,
const gchar *options,
const gchar *hint)
{
const gchar *header_names[] = { "To", NULL };
return mail_ffe_build_header_sexp (word, options, header_names);
}
static gchar *
mail_ffe_cc (const gchar *word,
const gchar *options,
const gchar *hint)
{
const gchar *header_names[] = { "Cc", NULL };
return mail_ffe_build_header_sexp (word, options, header_names);
}
static gchar *
mail_ffe_subject (const gchar *word,
const gchar *options,
const gchar *hint)
{
const gchar *header_names[] = { "Subject", NULL };
return mail_ffe_build_header_sexp (word, options, header_names);
}
static gchar *
mail_ffe_header (const gchar *word,
const gchar *options,
const gchar *hint)
{
const gchar *header_names[] = { NULL, NULL };
const gchar *equal;
gchar *header_name, *sexp;
equal = word ? strchr (word, '=') : NULL;
if (!equal)
return NULL;
header_name = g_strndup (word, equal - word);
header_names[0] = header_name;
sexp = mail_ffe_build_header_sexp (equal + 1, options, header_names);
g_free (header_name);
return sexp;
}
static gchar *
mail_ffe_exists (const gchar *word,
const gchar *options,
const gchar *hint)
{
GString *encoded_word;
gchar *sexp;
if (!word)
return NULL;
encoded_word = g_string_new ("");
camel_sexp_encode_string (encoded_word, word);
sexp = g_strdup_printf ("(match-all (header-exists %s))", encoded_word->str);
g_string_free (encoded_word, TRUE);
return sexp;
}
static gchar *
mail_ffe_tag (const gchar *word,
const gchar *options,
const gchar *hint)
{
GString *encoded_word;
gchar *sexp;
if (!word)
return NULL;
encoded_word = g_string_new ("");
camel_sexp_encode_string (encoded_word, word);
sexp = g_strdup_printf ("(match-all (not (= (user-tag %s) \"\")))", encoded_word->str);
g_string_free (encoded_word, TRUE);
return sexp;
}
static gchar *
mail_ffe_flag (const gchar *word,
const gchar *options,
const gchar *hint)
{
const gchar *system_flags[] = {
/* Translators: This is a name of a flag, the same as all strings in the 'ffe' context.
The translated value should not contain spaces. */
NC_("ffe", "Answered"),
NC_("ffe", "Deleted"),
NC_("ffe", "Draft"),
NC_("ffe", "Flagged"),
NC_("ffe", "Seen"),
NC_("ffe", "Attachment")
};
GString *encoded_word;
gchar *sexp = NULL;
gint ii;
if (!word)
return NULL;
encoded_word = g_string_new ("");
camel_sexp_encode_string (encoded_word, word);
for (ii = 0; ii < G_N_ELEMENTS (system_flags); ii++) {
if (g_ascii_strcasecmp (word, system_flags[ii]) == 0 ||
g_ascii_strcasecmp (word, g_dpgettext2 (NULL, "ffe", system_flags[ii])) == 0) {
sexp = g_strdup_printf ("(match-all (system-flag \"%s\"))", system_flags[ii]);
break;
}
}
if (!sexp)
sexp = g_strdup_printf ("(match-all (not (= (user-tag %s) \"\")))", encoded_word->str);
g_string_free (encoded_word, TRUE);
return sexp;
}
static gchar *
mail_ffe_label (const gchar *word,
const gchar *options,
const gchar *hint)
{
GString *encoded_word;
gchar *sexp;
if (!word)
return NULL;
encoded_word = g_string_new ("");
camel_sexp_encode_string (encoded_word, word);
sexp = g_strdup_printf ("(match-all (or ((= (user-tag \"label\") %s) (user-flag (+ \"$Label\" %s)) (user-flag %s)))",
encoded_word->str, encoded_word->str, encoded_word->str);
g_string_free (encoded_word, TRUE);
return sexp;
}
static gchar *
mail_ffe_size (const gchar *word,
const gchar *options,
const gchar *hint)
{
GString *encoded_word;
gchar *sexp;
const gchar *cmp = "=";
if (!word)
return NULL;
if (options) {
if (g_ascii_strcasecmp (options, "<") == 0 ||
g_ascii_strcasecmp (options, ">") == 0)
cmp = options;
}
encoded_word = g_string_new ("");
camel_sexp_encode_string (encoded_word, word);
sexp = g_strdup_printf ("(match-all (%s (get-size) (cast-int %s)))", cmp, encoded_word->str);
g_string_free (encoded_word, TRUE);
return sexp;
}
static gchar *
mail_ffe_score (const gchar *word,
const gchar *options,
const gchar *hint)
{
GString *encoded_word;
gchar *sexp;
const gchar *cmp = "=";
if (!word)
return NULL;
if (options) {
if (g_ascii_strcasecmp (options, "<") == 0 ||
g_ascii_strcasecmp (options, ">") == 0)
cmp = options;
}
encoded_word = g_string_new ("");
camel_sexp_encode_string (encoded_word, word);
sexp = g_strdup_printf ("(match-all (%s (cast-int (user-tag \"score\")) (cast-int %s)))", cmp, encoded_word->str);
g_string_free (encoded_word, TRUE);
return sexp;
}
static gchar *
mail_ffe_body (const gchar *word,
const gchar *options,
const gchar *hint)
{
GString *encoded_word;
gchar *sexp;
const gchar *cmp = "contains";
if (!word)
return NULL;
if (options) {
if (g_ascii_strcasecmp (options, "regex") == 0 ||
g_ascii_strcasecmp (options, "re") == 0 ||
g_ascii_strcasecmp (options, "r") == 0)
cmp = "regex";
}
encoded_word = g_string_new ("");
camel_sexp_encode_string (encoded_word, word);
sexp = g_strdup_printf ("(match-all (body-%s %s))", cmp, encoded_word->str);
g_string_free (encoded_word, TRUE);
return sexp;
}
static gboolean
mail_ffe_decode_date_time (const gchar *word,
GTimeVal *tv)
{
struct tm tm;
g_return_val_if_fail (word != NULL, FALSE);
g_return_val_if_fail (tv != NULL, FALSE);
/* YYYY-MM-DD */
if (strlen (word) == 10 && word[4] == '-' && word[7] == '-') {
gint yy, mm, dd;
yy = atoi (word);
mm = atoi (word + 5);
dd = atoi (word + 8);
if (g_date_valid_dmy (dd, mm, yy)) {
GDate *date;
date = g_date_new_dmy (dd, mm, yy);
g_date_to_struct_tm (date, &tm);
g_date_free (date);
tv->tv_sec = mktime (&tm);
tv->tv_usec = 0;
return TRUE;
}
}
if (g_time_val_from_iso8601 (word, tv))
return TRUE;
if (e_time_parse_date_and_time (word, &tm) == E_TIME_PARSE_OK ||
e_time_parse_date (word, &tm) == E_TIME_PARSE_OK) {
tv->tv_sec = mktime (&tm);
tv->tv_usec = 0;
return TRUE;
}
return FALSE;
}
static gchar *
mail_ffe_process_date (const gchar *get_date_fnc,
const gchar *word,
const gchar *options)
{
gint64 rel_days;
gchar *endptr = NULL;
const gchar *op = ">";
GTimeVal tv;
g_return_val_if_fail (get_date_fnc != NULL, NULL);
if (options) {
if (g_ascii_strcasecmp (options, "<") == 0) {
op = "<";
} else if (g_ascii_strcasecmp (options, "=") == 0) {
op = "=";
} else if (g_ascii_strcasecmp (options, ">") == 0) {
op = ">";
}
}
rel_days = g_ascii_strtoll (word, &endptr, 10);
if (rel_days != 0 && endptr && !*endptr) {
return g_strdup_printf ("(match-all (%s (%s) (%s (get-current-date) %" G_GINT64_FORMAT ")))", op, get_date_fnc,
rel_days < 0 ? "+" : "-", (rel_days < 0 ? -1 : 1) * rel_days * 24 * 60 * 60);
}
if (!mail_ffe_decode_date_time (word, &tv))
return g_strdup_printf ("(match-all (%s (%s) (get-current-date)))", op, get_date_fnc);
return g_strdup_printf ("(match-all (%s (%s) %" G_GINT64_FORMAT "))", op, get_date_fnc, (gint64) tv.tv_sec);
}
static gchar *
mail_ffe_sent (const gchar *word,
const gchar *options,
const gchar *hint)
{
if (!word)
return NULL;
return mail_ffe_process_date ("get-sent-date", word, options);
}
static gchar *
mail_ffe_received (const gchar *word,
const gchar *options,
const gchar *hint)
{
if (!word)
return NULL;
return mail_ffe_process_date ("get-received-date", word, options);
}
static gchar *
mail_ffe_attachment (const gchar *word,
const gchar *options,
const gchar *hint)
{
gboolean is_neg = FALSE;
if (!word)
return NULL;
if (g_ascii_strcasecmp (word, "no") == 0 ||
g_ascii_strcasecmp (word, "false") == 0 ||
g_ascii_strcasecmp (word, C_("ffe", "no")) == 0 ||
g_ascii_strcasecmp (word, C_("ffe", "false")) == 0 ||
g_ascii_strcasecmp (word, "0") == 0) {
is_neg = TRUE;
}
return g_strdup_printf ("(match-all %s(system-flag \"Attachment\")%s)", is_neg ? "(not " : "", is_neg ? ")" : "");
}
static const EFreeFormExpSymbol mail_ffe_symbols[] = {
{ "", "1", mail_ffe_recips },
{ "from:f", NULL, mail_ffe_from },
{ "to:t", NULL, mail_ffe_to },
{ "cc:c:", NULL, mail_ffe_cc },
{ "recips:r", NULL, mail_ffe_recips },
{ "subject:s", NULL, mail_ffe_subject },
{ "header:h", NULL, mail_ffe_header },
{ "exists:e", NULL, mail_ffe_exists },
{ "tag", NULL, mail_ffe_tag },
{ "flag", NULL, mail_ffe_flag },
{ "label:l", NULL, mail_ffe_label },
{ "size:sz", NULL, mail_ffe_size },
{ "score:sc", NULL, mail_ffe_score },
{ "body:b", NULL, mail_ffe_body },
{ "sent", NULL, mail_ffe_sent },
{ "received:rcv", NULL, mail_ffe_received },
{ "attachment:a", NULL, mail_ffe_attachment },
{ NULL, NULL, NULL}
};
static gchar *
get_filter_input_value (EFilterPart *part,
const gchar *name)
{
EFilterElement *elem;
EFilterInput *input;
GString *value;
GList *link;
g_return_val_if_fail (part != NULL, NULL);
g_return_val_if_fail (name != NULL, NULL);
elem = e_filter_part_find_element (part, name);
g_return_val_if_fail (elem != NULL, NULL);
g_return_val_if_fail (E_IS_FILTER_INPUT (elem), NULL);
input = E_FILTER_INPUT (elem);
value = g_string_new ("");
for (link = input->values; link; link = g_list_next (link)) {
const gchar *val = link->data;
if (val && *val) {
if (value->len > 0)
g_string_append_c (value, ' ');
g_string_append (value, val);
}
}
return g_string_free (value, FALSE);
}
void
e_mail_free_form_exp_to_sexp (EFilterElement *element,