list a message comes from and create the rule for it on the fly. (Many thanks to Joakim for suggesting the way to implement this.) Also, use the new shiny toolbar icons from TigerT and mark some strings for translation. svn path=/trunk/; revision=5362
703 lines
17 KiB
C
703 lines
17 KiB
C
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
|
|
/*
|
|
* Copyright (C) 2000 Helix Code Inc.
|
|
*
|
|
* Authors: Michael Zucchi <notzed@helixcode.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License as
|
|
* published by the Free Software Foundation; either version 2 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program 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 General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
|
|
* USA
|
|
*/
|
|
|
|
#include "camel/camel.h"
|
|
#include <sys/types.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <fcntl.h>
|
|
#include <glib.h>
|
|
#include <ctype.h>
|
|
|
|
#include "message-thread.h"
|
|
#include "mail-tools.h"
|
|
#include "mail-threads.h"
|
|
|
|
#define d(x)
|
|
|
|
static struct _container *thread_messages(CamelFolder *folder, GPtrArray *uids);
|
|
static void thread_messages_free(struct _container *);
|
|
|
|
/* for debug only */
|
|
int dump_tree(struct _container *c);
|
|
|
|
static void
|
|
container_add_child(struct _container *node, struct _container *child)
|
|
{
|
|
d(printf("\nAdding child %p to parent %p \n", child, node));
|
|
child->next = node->child;
|
|
node->child = child;
|
|
child->parent = node;
|
|
}
|
|
|
|
static void
|
|
container_parent_child(struct _container *parent, struct _container *child)
|
|
{
|
|
struct _container *c, *node;
|
|
|
|
/* are we already the right parent? */
|
|
if (child->parent == parent)
|
|
return;
|
|
|
|
/* are we unparented? */
|
|
if (child->parent == NULL) {
|
|
container_add_child(parent, child);
|
|
return;
|
|
}
|
|
|
|
/* else remove child from its existing parent, and reparent */
|
|
node = child->parent;
|
|
c = (struct _container *)&node->child;
|
|
d(printf("scanning children:\n"));
|
|
while (c->next) {
|
|
d(printf(" %p\n", c));
|
|
if (c->next==child) {
|
|
d(printf("found node %p\n", child));
|
|
c->next = c->next->next;
|
|
child->parent = NULL;
|
|
container_add_child(parent, child);
|
|
return;
|
|
}
|
|
c = c->next;
|
|
}
|
|
|
|
printf("DAMN, we shouldn't be here!\n");
|
|
}
|
|
|
|
static void
|
|
prune_empty(struct _container **cp)
|
|
{
|
|
struct _container *child, *next, *c, *lastc;
|
|
|
|
/* yes, this is intentional */
|
|
lastc = (struct _container *)cp;
|
|
while (lastc->next) {
|
|
c = lastc->next;
|
|
|
|
d(printf("checking message %p %p (%s)\n", c,
|
|
c->message, c->message?c->message->message_id:"<empty>"));
|
|
if (c->message == NULL) {
|
|
if (c->child == NULL) {
|
|
d(printf("removing empty node\n"));
|
|
lastc->next = c->next;
|
|
g_free(c);
|
|
continue;
|
|
}
|
|
if (c->parent || c->child->next==0) {
|
|
d(printf("promoting child\n"));
|
|
lastc->next = c->next; /* remove us */
|
|
child = c->child;
|
|
while (child) {
|
|
next = child->next;
|
|
|
|
child->parent = c->parent;
|
|
child->next = lastc->next;
|
|
lastc->next = child;
|
|
|
|
child = next;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
prune_empty(&c->child);
|
|
lastc = c;
|
|
}
|
|
}
|
|
|
|
static void
|
|
hashloop(void *key, void *value, void *data)
|
|
{
|
|
struct _container *c = value;
|
|
struct _container *tail = data;
|
|
|
|
if (c->parent == NULL) {
|
|
c->next = tail->next;
|
|
tail->next = c;
|
|
}
|
|
}
|
|
|
|
static char *
|
|
get_root_subject(struct _container *c, int *re)
|
|
{
|
|
char *s, *p;
|
|
struct _container *scan;
|
|
|
|
s = NULL;
|
|
*re = FALSE;
|
|
if (c->message)
|
|
s = c->message->subject;
|
|
else {
|
|
/* one of the children will always have a message */
|
|
scan = c->child;
|
|
while (scan) {
|
|
if (scan->message) {
|
|
s = scan->message->subject;
|
|
break;
|
|
}
|
|
scan = scan->next;
|
|
}
|
|
}
|
|
if (s != NULL) {
|
|
while (*s) {
|
|
while (isspace(*s))
|
|
s++;
|
|
if (s[0] == 0)
|
|
break;
|
|
if ((s[0] == 'r' || s[0]=='R')
|
|
&& (s[1] == 'e' || s[1]=='E')) {
|
|
p = s+2;
|
|
while (isdigit(*p) || (ispunct(*p) && (*p != ':')))
|
|
p++;
|
|
if (*p==':') {
|
|
*re = TRUE;
|
|
s = p+1;
|
|
} else
|
|
break;
|
|
} else
|
|
break;
|
|
}
|
|
if (*s)
|
|
return s;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* this can be pretty slow, but not used often */
|
|
/* clast cannot be null */
|
|
static void
|
|
remove_node(struct _container **list, struct _container *node, struct _container **clast)
|
|
{
|
|
struct _container *c;
|
|
|
|
/* this is intentional, even if it looks funny */
|
|
/* if we have a parent, then we should remove it from the parent list,
|
|
otherwise we remove it from the root list */
|
|
if (node->parent) {
|
|
c = (struct _container *)&node->parent->child;
|
|
} else {
|
|
c = (struct _container *)list;
|
|
}
|
|
while (c->next) {
|
|
if (c->next == node) {
|
|
if (*clast == c->next)
|
|
*clast = c;
|
|
c->next = c->next->next;
|
|
return;
|
|
}
|
|
c = c->next;
|
|
}
|
|
|
|
printf("ERROR: removing node %p failed\n", node);
|
|
}
|
|
|
|
static void
|
|
group_root_set(struct _container **cp)
|
|
{
|
|
GHashTable *subject_table = g_hash_table_new(g_str_hash, g_str_equal);
|
|
struct _container *c, *clast, *scan, *container;
|
|
|
|
/* gather subject lines */
|
|
d(printf("gathering subject lines\n"));
|
|
clast = (struct _container *)cp;
|
|
c = clast->next;
|
|
while (c) {
|
|
c->root_subject = get_root_subject(c, &c->re);
|
|
if (c->root_subject) {
|
|
container = g_hash_table_lookup(subject_table, c->root_subject);
|
|
if (container == NULL
|
|
|| (container->message == NULL && c->message)
|
|
|| (container->re == TRUE && !c->re)) {
|
|
g_hash_table_insert(subject_table, c->root_subject, c);
|
|
}
|
|
}
|
|
c = c->next;
|
|
}
|
|
|
|
/* merge common subjects? */
|
|
clast = (struct _container *)cp;
|
|
while (clast->next) {
|
|
c = clast->next;
|
|
d(printf("checking %p %s\n", c, c->root_subject));
|
|
if (c->root_subject
|
|
&& (container = g_hash_table_lookup(subject_table, c->root_subject))
|
|
&& (container != c)) {
|
|
d(printf(" matching %p %s\n", container, container->root_subject));
|
|
if (c->message == NULL && container->message == NULL) {
|
|
d(printf("merge containers children\n"));
|
|
/* steal the children from c onto container, and unlink c */
|
|
scan = (struct _container *)&container->child;
|
|
while (scan->next)
|
|
scan = scan->next;
|
|
scan->next = c->child;
|
|
clast->next = c->next;
|
|
g_free(c);
|
|
continue;
|
|
} if (c->message == NULL && container->message != NULL) {
|
|
d(printf("container is non-empty parent\n"));
|
|
remove_node(cp, container, &clast);
|
|
container_add_child(c, container);
|
|
} else if (c->message != NULL && container->message == NULL) {
|
|
d(printf("container is empty child\n"));
|
|
clast->next = c->next;
|
|
container_add_child(container, c);
|
|
continue;
|
|
} else if (c->re && !container->re) {
|
|
d(printf("container is re\n"));
|
|
clast->next = c->next;
|
|
container_add_child(container, c);
|
|
continue;
|
|
} else if (!c->re && container->re) {
|
|
d(printf("container is not re\n"));
|
|
remove_node(cp, container, &clast);
|
|
container_add_child(c, container);
|
|
} else if (c->re && container->re) {
|
|
d(printf("subjects are common %p and %p\n", c, container));
|
|
|
|
remove_node(cp, container, &clast);
|
|
remove_node(cp, c, &clast);
|
|
|
|
scan = g_malloc0(sizeof(*scan));
|
|
scan->root_subject = c->root_subject;
|
|
scan->re = c->re && container->re;
|
|
scan->next = c->next;
|
|
clast->next = scan;
|
|
container_add_child(scan, c);
|
|
container_add_child(scan, container);
|
|
clast = scan;
|
|
g_hash_table_insert(subject_table, scan->root_subject, scan);
|
|
continue;
|
|
}
|
|
}
|
|
clast = c;
|
|
}
|
|
g_hash_table_destroy(subject_table);
|
|
}
|
|
|
|
struct _tree_info {
|
|
GHashTable *visited;
|
|
};
|
|
|
|
static int
|
|
dump_tree_rec(struct _tree_info *info, struct _container *c, int depth)
|
|
{
|
|
char *p;
|
|
int count=0;
|
|
|
|
p = alloca(depth*2+1);
|
|
memset(p, ' ', depth*2);
|
|
p[depth*2] = 0;
|
|
|
|
while (c) {
|
|
if (g_hash_table_lookup(info->visited, c)) {
|
|
printf("WARNING: NODE REVISITED: %p\n", c);
|
|
} else {
|
|
g_hash_table_insert(info->visited, c, c);
|
|
}
|
|
if (c->message) {
|
|
printf("%s %p Subject: %s <%s>\n", p, c, c->message->subject, c->message->message_id);
|
|
count += 1;
|
|
} else {
|
|
printf("%s %p <empty>\n", p, c);
|
|
}
|
|
if (c->child)
|
|
count += dump_tree_rec(info, c->child, depth+1);
|
|
c = c->next;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
int
|
|
dump_tree(struct _container *c)
|
|
{
|
|
int count;
|
|
struct _tree_info info;
|
|
|
|
info.visited = g_hash_table_new(g_direct_hash, g_direct_equal);
|
|
count = dump_tree_rec(&info, c, 0);
|
|
g_hash_table_destroy(info.visited);
|
|
return count;
|
|
}
|
|
|
|
static void thread_messages_free(struct _container *c)
|
|
{
|
|
struct _container *n;
|
|
|
|
while (c) {
|
|
n = c->next;
|
|
if (c->child)
|
|
thread_messages_free(c->child); /* free's children first */
|
|
g_free(c);
|
|
c = n;
|
|
}
|
|
}
|
|
|
|
static int
|
|
sort_node(const void *a, const void *b)
|
|
{
|
|
const struct _container *a1 = ((struct _container **)a)[0];
|
|
const struct _container *b1 = ((struct _container **)b)[0];
|
|
|
|
/* if we have no message, it must be a dummy node, which
|
|
also means it must have a child, just use that as the
|
|
sort data (close enough?) */
|
|
if (a1->message == NULL)
|
|
a1 = a1->child;
|
|
if (b1->message == NULL)
|
|
b1 = b1->child;
|
|
if (a1->order == b1->order)
|
|
return 0;
|
|
if (a1->order < b1->order)
|
|
return 1;
|
|
else
|
|
return -1;
|
|
}
|
|
|
|
static void
|
|
sort_thread(struct _container **cp)
|
|
{
|
|
struct _container *c, *head, **carray;
|
|
int size=0;
|
|
|
|
c = *cp;
|
|
while (c) {
|
|
/* sort the children while we're at it */
|
|
if (c->child)
|
|
sort_thread(&c->child);
|
|
size++;
|
|
c = c->next;
|
|
}
|
|
if (size<2)
|
|
return;
|
|
carray = alloca(size*sizeof(struct _container *));
|
|
c = *cp;
|
|
size=0;
|
|
while (c) {
|
|
carray[size] = c;
|
|
c = c->next;
|
|
size++;
|
|
}
|
|
qsort(carray, size, sizeof(struct _container *), sort_node);
|
|
size--;
|
|
head = carray[size];
|
|
head->next = NULL;
|
|
size--;
|
|
do {
|
|
c = carray[size];
|
|
c->next = head;
|
|
head = c;
|
|
size--;
|
|
} while (size>=0);
|
|
*cp = head;
|
|
}
|
|
|
|
static struct _container *
|
|
thread_messages(CamelFolder *folder, GPtrArray *uids)
|
|
{
|
|
GHashTable *id_table, *no_id_table;
|
|
int i;
|
|
struct _container *c, *p, *child, *head, *container;
|
|
struct _header_references *ref;
|
|
|
|
id_table = g_hash_table_new(g_str_hash, g_str_equal);
|
|
no_id_table = g_hash_table_new(NULL, NULL);
|
|
for (i=0;i<uids->len;i++) {
|
|
const CamelMessageInfo *mi;
|
|
mail_tool_camel_lock_up ();
|
|
mi = camel_folder_get_message_info (folder, uids->pdata[i]);
|
|
mail_tool_camel_lock_down ();
|
|
|
|
if (mi == NULL) {
|
|
g_warning("Folder doesn't contain uid %s", (char *)uids->pdata[i]);
|
|
continue;
|
|
}
|
|
|
|
if (mi->message_id) {
|
|
d(printf("doing : %s\n", mi->message_id));
|
|
c = g_hash_table_lookup(id_table, mi->message_id);
|
|
if (!c) {
|
|
c = g_malloc0(sizeof(*c));
|
|
g_hash_table_insert(id_table, mi->message_id, c);
|
|
}
|
|
} else {
|
|
d(printf("doing : (no message id)\n"));
|
|
c = g_malloc0(sizeof(*c));
|
|
g_hash_table_insert(no_id_table, (void *)mi, c);
|
|
}
|
|
|
|
c->message = mi;
|
|
c->order = i;
|
|
container = c;
|
|
ref = mi->references;
|
|
p = NULL;
|
|
child = container;
|
|
head = NULL;
|
|
d(printf("references:\n"));
|
|
while (ref) {
|
|
if (ref->id == NULL) {
|
|
printf("ref missing id!?\n");
|
|
ref = ref->next;
|
|
continue;
|
|
}
|
|
|
|
d(printf("looking up reference: %s\n", ref->id));
|
|
c = g_hash_table_lookup(id_table, ref->id);
|
|
if (c == NULL) {
|
|
d(printf("not found\n"));
|
|
c = g_malloc0(sizeof(*c));
|
|
g_hash_table_insert(id_table, ref->id, c);
|
|
}
|
|
if (c!=child)
|
|
container_parent_child(c, child);
|
|
child = c;
|
|
if (head == NULL)
|
|
head = c;
|
|
ref = ref->next;
|
|
}
|
|
}
|
|
|
|
d(printf("\n\n"));
|
|
/* build a list of root messages (no parent) */
|
|
head = NULL;
|
|
g_hash_table_foreach(id_table, hashloop, &head);
|
|
g_hash_table_foreach(no_id_table, hashloop, &head);
|
|
|
|
g_hash_table_destroy(id_table);
|
|
g_hash_table_destroy(no_id_table);
|
|
|
|
/* remove empty parent nodes */
|
|
prune_empty(&head);
|
|
|
|
/* find any siblings which missed out */
|
|
group_root_set(&head);
|
|
|
|
#if 0
|
|
printf("finished\n");
|
|
i = dump_tree(head);
|
|
printf("%d count, %d items in tree\n", uids->len, i);
|
|
#endif
|
|
|
|
sort_thread(&head);
|
|
return head;
|
|
}
|
|
|
|
/* ** THREAD MESSAGES ***************************************************** */
|
|
|
|
typedef struct thread_messages_input_s {
|
|
MessageList *ml;
|
|
GPtrArray *uids;
|
|
gboolean use_camel_uidfree;
|
|
void (*build) (MessageList *, struct _container *);
|
|
} thread_messages_input_t;
|
|
|
|
typedef struct thread_messages_data_s {
|
|
struct _container *container;
|
|
} thread_messages_data_t;
|
|
|
|
static gchar *describe_thread_messages (gpointer in_data, gboolean gerund);
|
|
static void setup_thread_messages (gpointer in_data, gpointer op_data, CamelException *ex);
|
|
static void do_thread_messages (gpointer in_data, gpointer op_data, CamelException *ex);
|
|
static void cleanup_thread_messages (gpointer in_data, gpointer op_data, CamelException *ex);
|
|
|
|
static gchar *describe_thread_messages (gpointer in_data, gboolean gerund)
|
|
{
|
|
if (gerund)
|
|
return g_strdup ("Threading message list");
|
|
else
|
|
return g_strdup ("Thread message list");
|
|
}
|
|
|
|
static void setup_thread_messages (gpointer in_data, gpointer op_data, CamelException *ex)
|
|
{
|
|
thread_messages_input_t *input = (thread_messages_input_t *) in_data;
|
|
|
|
if (!IS_MESSAGE_LIST (input->ml)) {
|
|
camel_exception_set (ex, CAMEL_EXCEPTION_INVALID_PARAM,
|
|
"No messagelist to thread was provided to thread_messages");
|
|
return;
|
|
}
|
|
|
|
if (!input->uids) {
|
|
camel_exception_set (ex, CAMEL_EXCEPTION_INVALID_PARAM,
|
|
"No uids were provided to thread_messages");
|
|
return;
|
|
}
|
|
|
|
if (!input->build) {
|
|
camel_exception_set (ex, CAMEL_EXCEPTION_INVALID_PARAM,
|
|
"No build callback provided to thread_messages");
|
|
return;
|
|
}
|
|
|
|
gtk_object_ref (GTK_OBJECT (input->ml));
|
|
}
|
|
|
|
static void do_thread_messages (gpointer in_data, gpointer op_data, CamelException *ex)
|
|
{
|
|
thread_messages_input_t *input = (thread_messages_input_t *) in_data;
|
|
thread_messages_data_t *data = (thread_messages_data_t *) op_data;
|
|
|
|
data->container = thread_messages (input->ml->folder, input->uids);
|
|
}
|
|
|
|
static void cleanup_thread_messages (gpointer in_data, gpointer op_data, CamelException *ex)
|
|
{
|
|
thread_messages_input_t *input = (thread_messages_input_t *) in_data;
|
|
thread_messages_data_t *data = (thread_messages_data_t *) op_data;
|
|
|
|
(input->build) (input->ml, data->container);
|
|
thread_messages_free (data->container);
|
|
|
|
if (input->use_camel_uidfree) {
|
|
mail_tool_camel_lock_up ();
|
|
camel_folder_free_uids (input->ml->folder, input->uids);
|
|
mail_tool_camel_lock_down ();
|
|
} else {
|
|
g_ptr_array_add(input->uids, 0);
|
|
g_strfreev ((char **)input->uids->pdata);
|
|
g_ptr_array_free (input->uids, FALSE);
|
|
}
|
|
|
|
gtk_object_unref (GTK_OBJECT (input->ml));
|
|
}
|
|
|
|
static const mail_operation_spec op_thread_messages =
|
|
{
|
|
describe_thread_messages,
|
|
sizeof (thread_messages_data_t),
|
|
setup_thread_messages,
|
|
do_thread_messages,
|
|
cleanup_thread_messages
|
|
};
|
|
|
|
void mail_do_thread_messages (MessageList *ml, GPtrArray *uids,
|
|
gboolean use_camel_uidfree,
|
|
void (*build) (MessageList *,
|
|
struct _container *))
|
|
{
|
|
thread_messages_input_t *input;
|
|
|
|
input = g_new (thread_messages_input_t, 1);
|
|
input->ml = ml;
|
|
input->uids = uids;
|
|
input->use_camel_uidfree = use_camel_uidfree;
|
|
input->build = build;
|
|
|
|
mail_operation_queue (&op_thread_messages, input, TRUE);
|
|
}
|
|
|
|
/* ************************************************************************ */
|
|
|
|
#ifdef STANDALONE
|
|
|
|
static char *
|
|
auth_callback(char *prompt, gboolean secret,
|
|
CamelService *service, char *item,
|
|
CamelException *ex)
|
|
{
|
|
printf ("auth_callback called: %s\n", prompt);
|
|
return NULL;
|
|
}
|
|
|
|
int
|
|
main (int argc, char**argv)
|
|
{
|
|
CamelSession *session;
|
|
CamelException *ex;
|
|
CamelStore *store;
|
|
gchar *store_url = "mbox:///home/notzed/evolution/local/Inbox";
|
|
CamelFolder *folder;
|
|
CamelMimeMessage *message;
|
|
GList *uid_list;
|
|
GPtrArray *summary;
|
|
|
|
gtk_init (&argc, &argv);
|
|
camel_init ();
|
|
ex = camel_exception_new ();
|
|
|
|
session = camel_session_new (auth_callback);
|
|
store = camel_session_get_store (session, store_url, ex);
|
|
if (camel_exception_get_id (ex)) {
|
|
printf ("Exception caught in camel_session_get_store\n"
|
|
"Full description : %s\n", camel_exception_get_description (ex));
|
|
return -1;
|
|
}
|
|
|
|
folder = camel_store_get_folder (store, "mbox", TRUE, ex);
|
|
if (camel_exception_get_id (ex)) {
|
|
printf ("Exception caught in camel_store_get_folder\n"
|
|
"Full description : %s\n", camel_exception_get_description (ex));
|
|
return -1;
|
|
}
|
|
|
|
#if 0
|
|
camel_folder_open (folder, FOLDER_OPEN_RW, ex);
|
|
if (camel_exception_get_id (ex)) {
|
|
printf ("Exception caught when trying to open the folder\n"
|
|
"Full description : %s\n", camel_exception_get_description (ex));
|
|
return -1;
|
|
}
|
|
#endif
|
|
|
|
summary = camel_folder_get_summary(folder);
|
|
thread_messages((CamelMessageInfo **)summary->pdata, summary->len);
|
|
|
|
return 0;
|
|
}
|
|
|
|
#endif
|
|
|
|
/*
|
|
|
|
msgid: d
|
|
references: a b c
|
|
|
|
msgid: f
|
|
references: c d
|
|
|
|
msgid: e
|
|
references: c
|
|
|
|
a
|
|
\
|
|
b
|
|
\
|
|
c
|
|
\
|
|
d
|
|
|\
|
|
e f
|
|
*/
|
|
/*
|
|
lookup d
|
|
create new node d
|
|
child = d
|
|
loop on c b a
|
|
lookup node?
|
|
if no node, create node
|
|
add child to node
|
|
child = node
|
|
endloop
|
|
|
|
*/
|