/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* * Copyright (C) 2000 Helix Code Inc. * * Authors: Michael Zucchi * * 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 #include #include #include #include #include #include #include #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:"")); 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 \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;ilen;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 */