2007-10-07 [colin] 3.0.2cvs31
[claws.git] / src / addrduplicates.c
1 /* Claws Mail -- a GTK+ based, lightweight, and fast e-mail client
2  * Copyright (C) 2007 Holger Berndt <hb@claws-mail.org> 
3  * and the Claws Mail team
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program. If not, see <http://www.gnu.org/licenses/>. 
17  */
18
19 #ifdef HAVE_CONFIG_H
20 #  include "config.h"
21 #endif
22
23 #include <glib/gi18n.h>
24 #include <string.h>
25
26 #include "defs.h"
27
28 #ifdef USE_LDAP
29 #include "ldapserver.h"
30 #include "ldapupdate.h"
31 #endif
32 #include "addrduplicates.h"
33 #include "addrindex.h"
34 #include "addrbook.h"
35 #include "addressbook.h"
36 #include "editaddress.h"
37 #include "alertpanel.h"
38 #include "gtkutils.h"
39 #include "inc.h"
40 #include "utils.h"
41
42 typedef struct
43 {
44         ItemPerson        *person;
45         AddressDataSource *ds;
46 }
47 AddrDupListEntry;
48
49 enum {
50     COL_BOOK = 0,
51     COL_NAME,
52     COL_ITEM,
53     COL_DS,
54     NUM_COLS
55 };
56
57 static gboolean create_dialog();
58 static void refresh_addr_hash(void);
59 static void refresh_stores(void);
60 static void present_finder_results(GtkWindow*);
61 static void cb_finder_results_dialog_destroy(GtkWindow*, gpointer);
62 static void destroy_addr_hash_val(gpointer);
63 static GSList* deep_copy_hash_val(GSList*);
64 static void fill_hash_table();
65 static gint collect_emails(ItemPerson*, AddressDataSource*);
66 static gboolean is_not_duplicate(gpointer, gpointer, gpointer);
67 static gint books_compare(gconstpointer, gconstpointer);
68 static GtkWidget* create_email_view(GtkListStore*);
69 static GtkWidget* create_detail_view(GtkListStore*);
70 static void append_to_email_store(gpointer,gpointer,gpointer);
71 static void email_selection_changed(GtkTreeSelection*,gpointer);
72 static void detail_selection_changed(GtkTreeSelection*,gpointer);
73 static void detail_row_activated(GtkTreeView*,GtkTreePath*,
74                                  GtkTreeViewColumn*,
75                                  gpointer);
76 static void cb_del_btn_clicked(GtkButton *, gpointer);
77 static gboolean delete_item(ItemPerson*, AddressDataSource*);
78
79 static GHashTable *addr_hash;
80 static gboolean include_same_book = TRUE;
81 static gboolean include_other_books = TRUE;
82
83 static GtkListStore *email_store;
84 static GtkListStore *detail_store;
85
86 static GtkWidget *del_btn;
87
88 void addrduplicates_find(GtkWindow *parent)
89 {
90         if(create_dialog()) {
91                 refresh_addr_hash();
92                 present_finder_results(parent);
93         }
94 }
95
96 static gboolean create_dialog()
97 {
98         gboolean want_search;
99         GtkWidget *vbox;
100         GtkWidget *check_same_book;
101         GtkWidget *check_other_book;
102         AlertValue val;
103
104         want_search = FALSE;
105
106         vbox = gtk_vbox_new(FALSE, 0);
107         check_same_book = gtk_check_button_new_with_label(_("Show duplicates in "
108                           "the same book"));
109         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check_same_book),
110                                      include_same_book);
111         gtk_box_pack_start(GTK_BOX(vbox), check_same_book, FALSE, FALSE, 0);
112         gtk_widget_show(check_same_book);
113         check_other_book = gtk_check_button_new_with_label(_("Show duplicates in "
114                            "different books"));
115         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check_other_book),
116                                      include_other_books);
117         gtk_box_pack_start(GTK_BOX(vbox), check_other_book, FALSE, FALSE, 0);
118         gtk_widget_show(check_other_book);
119
120         /* prevent checkboxes from being destroyed on dialog close */
121         g_object_ref(check_same_book);
122         g_object_ref(check_other_book);
123
124         val = alertpanel_full(_("Find address book email duplicates"),
125                               _("Claws-Mail will now search for duplicate email "
126                                 "addresses in the addressbook."),
127                               _("Cancel"),_("Search"),NULL, FALSE, vbox, ALERT_NOTICE,
128                               G_ALERTALTERNATE);
129         if(val == G_ALERTALTERNATE) {
130                 want_search = TRUE;
131
132                 /* save options */
133                 include_same_book =
134                     gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(check_same_book));
135                 include_other_books =
136                     gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(check_other_book));
137
138         }
139
140         g_object_unref(check_same_book);
141         g_object_unref(check_other_book);
142         return want_search;
143 }
144
145 static void refresh_addr_hash(void)
146 {
147         if(addr_hash)
148                 g_hash_table_destroy(addr_hash);
149         addr_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
150                                           g_free, destroy_addr_hash_val);
151         fill_hash_table();
152 }
153
154 static void destroy_addr_hash_val(gpointer value)
155 {
156         GSList *list = (GSList*) value;
157         GSList *walk;
158
159         for(walk = list; walk; walk = walk->next) {
160                 AddrDupListEntry *entry = (AddrDupListEntry*) walk->data;
161                 if(entry)
162                         g_free(entry);
163         }
164         if(list)
165                 g_slist_free(list);
166 }
167
168 static GSList* deep_copy_hash_val(GSList *in)
169 {
170         GSList *walk;
171         GSList *out = NULL;
172
173         out = g_slist_copy(in);
174         for(walk = out; walk; walk = walk->next) {
175                 AddrDupListEntry *out_entry;
176                 AddrDupListEntry *in_entry = walk->data;
177
178                 out_entry = g_new0(AddrDupListEntry,1);
179                 out_entry->person = in_entry->person;
180                 out_entry->ds     = in_entry->ds;
181                 walk->data = out_entry;
182         }
183
184         return out;
185 }
186
187 static void fill_hash_table()
188 {
189         addrindex_load_person_ds(collect_emails);
190         g_hash_table_foreach_remove(addr_hash,is_not_duplicate, NULL);
191 }
192
193 static gboolean is_not_duplicate(gpointer key, gpointer value,
194                                  gpointer user_data)
195 {
196         gboolean is_in_same_book;
197         gboolean is_in_other_books;
198         GSList *books;
199         GSList *walk;
200         gboolean retval;
201         GSList *list = value;
202
203         /* remove everything that is just in one book */
204         if(g_slist_length(list) <= 1)
205                 return TRUE;
206
207         /* work on a shallow copy */
208         books = g_slist_copy(list);
209
210         /* sorting the list makes it easier to check for books */
211         books = g_slist_sort(books, books_compare);
212
213         /* check if a book appears twice */
214         is_in_same_book = FALSE;
215         for(walk = books; walk && walk->next; walk = walk->next) {
216                 if(books_compare(walk->data, walk->next->data) == 0) {
217                         is_in_same_book = TRUE;
218                         break;
219                 }
220         }
221
222         /* check is at least two different books appear in the list */
223         is_in_other_books = FALSE;
224         if(books && books->next) {
225                 for(walk = books->next; walk; walk = walk->next) {
226                         if(books_compare(walk->data, books->data) != 0) {
227                                 is_in_other_books = TRUE;
228                                 break;
229                         }
230                 }
231         }
232
233         /* delete the shallow copy */
234         g_slist_free(books);
235
236         retval = FALSE;
237         if(is_in_same_book && include_same_book)
238                 retval = TRUE;
239         if(is_in_other_books && include_other_books)
240                 retval = TRUE;
241         retval = !retval;
242
243         return retval;
244 }
245
246 static gint collect_emails(ItemPerson *itemperson, AddressDataSource *ds)
247 {
248         gchar *addr;
249         GList *nodeM;
250         GSList *old_val;
251         GSList *new_val;
252         AddrDupListEntry *entry;
253
254         /* Process each E-Mail address */
255         nodeM = itemperson->listEMail;
256         while(nodeM) {
257                 ItemEMail *email = nodeM->data;
258
259                 addr = g_strdup(email->address);
260                 g_strdown(addr);
261                 old_val = g_hash_table_lookup(addr_hash, addr);
262                 if(old_val)
263                         new_val = deep_copy_hash_val(old_val);
264                 else
265                         new_val = NULL;
266
267                 entry = g_new0(AddrDupListEntry,1);
268                 entry->person = itemperson;
269                 entry->ds     = ds;
270
271                 new_val = g_slist_prepend(new_val, entry);
272                 g_hash_table_insert(addr_hash, addr, new_val);
273
274                 nodeM = g_list_next(nodeM);
275         }
276         return 0;
277 }
278
279 static gint books_compare(gconstpointer a, gconstpointer b)
280 {
281         const AddrDupListEntry *entry1;
282         const AddrDupListEntry *entry2;
283         entry1 = a;
284         entry2 = b;
285         return strcmp(addrindex_ds_get_name(entry1->ds),
286                       addrindex_ds_get_name(entry2->ds));
287 }
288
289 static void present_finder_results(GtkWindow *parent)
290 {
291         GtkWidget *email_view;
292         GtkWidget *detail_view;
293         GtkWidget *dialog = NULL;
294         GtkWidget *vbox;
295         GtkWidget *hbox;
296         GtkWidget *hpaned;
297         GtkWidget *close;
298         GtkTreeSelection *email_select;
299         GtkTreeSelection *detail_select;
300         static GdkGeometry geometry;
301
302         if(g_hash_table_size(addr_hash) == 0) {
303                 alertpanel_notice(_("No duplicate email addresses in the addressbook found"));
304                 return;
305         }
306
307         email_store = gtk_list_store_new(1, G_TYPE_STRING);
308         refresh_stores();
309         email_view = create_email_view(email_store);
310         email_select = gtk_tree_view_get_selection(GTK_TREE_VIEW(email_view));
311         gtk_tree_selection_set_mode(email_select,GTK_SELECTION_SINGLE);
312
313         g_signal_connect(email_select, "changed",
314                          (GCallback)email_selection_changed, NULL);
315
316         detail_store = gtk_list_store_new(NUM_COLS, G_TYPE_STRING, G_TYPE_STRING,
317                                           G_TYPE_POINTER, G_TYPE_POINTER);
318         detail_view = create_detail_view(detail_store);
319         detail_select = gtk_tree_view_get_selection(GTK_TREE_VIEW(detail_view));
320         gtk_tree_selection_set_mode(detail_select,GTK_SELECTION_MULTIPLE);
321
322         g_signal_connect(detail_select, "changed",
323                          (GCallback)detail_selection_changed, NULL);
324
325         dialog = gtkut_window_new(GTK_WINDOW_TOPLEVEL, "address_dupes_finder");
326         gtk_window_set_transient_for(GTK_WINDOW(dialog),parent);
327         gtk_window_set_modal(GTK_WINDOW(dialog),TRUE);
328         if(!geometry.min_height) {
329                 geometry.min_width = 600;
330                 geometry.min_height = 400;
331         }
332         gtk_window_set_geometry_hints(GTK_WINDOW(dialog), NULL, &geometry,
333                                       GDK_HINT_MIN_SIZE);
334         gtk_window_set_title(GTK_WINDOW(dialog), _("Duplicate email addresses"));
335
336
337         vbox = gtk_vbox_new(FALSE, 0);
338         gtk_container_add(GTK_CONTAINER(dialog), vbox);
339
340         hpaned = gtk_hpaned_new();
341         gtk_box_pack_start(GTK_BOX(vbox), hpaned, TRUE, TRUE, 0);
342
343         gtk_paned_add1(GTK_PANED(hpaned), email_view);
344         gtk_paned_add2(GTK_PANED(hpaned), detail_view);
345
346         hbox = gtk_hbox_new (FALSE, 0);
347         gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
348
349         close = gtk_button_new_from_stock(GTK_STOCK_CLOSE);
350         gtk_box_pack_end(GTK_BOX(hbox), close, FALSE, FALSE, 5);
351
352         del_btn = gtk_button_new_from_stock(GTK_STOCK_DELETE);
353         gtk_box_pack_end(GTK_BOX(hbox), del_btn, FALSE, FALSE, 5);
354         gtk_widget_set_sensitive(del_btn, FALSE);
355
356         g_signal_connect(dialog, "destroy",
357                          G_CALLBACK(cb_finder_results_dialog_destroy), NULL);
358         g_signal_connect_swapped(close, "clicked",
359                                  G_CALLBACK(gtk_widget_destroy), dialog);
360         g_signal_connect(del_btn, "clicked",
361                          G_CALLBACK(cb_del_btn_clicked), detail_view);
362         inc_lock();
363         gtk_widget_show_all(dialog);
364 }
365
366 static void cb_finder_results_dialog_destroy(GtkWindow *win, gpointer data)
367 {
368         email_store = NULL;
369         detail_store = NULL;
370
371         if(addr_hash) {
372                 g_hash_table_destroy(addr_hash);
373                 addr_hash = NULL;
374         }
375         addressbook_refresh();
376         inc_unlock();
377 }
378
379 static GtkWidget* create_email_view(GtkListStore *store)
380 {
381         GtkWidget *view;
382         GtkCellRenderer *renderer;
383
384         view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
385         renderer = gtk_cell_renderer_text_new();
386         gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(view),
387                 -1,
388                 _("Address"),
389                 renderer,
390                 "text", 0,
391                 NULL);
392         g_object_unref(store);
393         return view;
394 }
395
396 static GtkWidget* create_detail_view(GtkListStore *store)
397 {
398         GtkWidget *view;
399         GtkCellRenderer *renderer;
400
401         view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
402         renderer = gtk_cell_renderer_text_new();
403
404         /* col 1 */
405         gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(view),
406                 -1,
407                 _("Bookname"),
408                 renderer,
409                 "text", COL_BOOK,
410                 NULL);
411         /* col 2 */
412         gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(view),
413                 -1,
414                 _("Name"),
415                 renderer,
416                 "text", COL_NAME,
417                 NULL);
418
419         g_signal_connect(view, "row-activated",
420                          (GCallback)detail_row_activated, NULL);
421
422         return view;
423 }
424
425 static void append_to_email_store(gpointer key,gpointer value,gpointer data)
426 {
427         GtkTreeIter iter;
428         GtkListStore *store = (GtkListStore*) data;
429
430         gtk_list_store_append(store, &iter);
431         gtk_list_store_set(store, &iter, 0, (gchar*) key, -1);
432 }
433
434 static void detail_selection_changed(GtkTreeSelection *selection, gpointer data)
435 {
436         if(gtk_tree_selection_count_selected_rows(selection) > 0)
437                 gtk_widget_set_sensitive(del_btn,TRUE);
438         else
439                 gtk_widget_set_sensitive(del_btn,FALSE);
440 }
441
442 static void email_selection_changed(GtkTreeSelection *selection, gpointer data)
443 {
444         GtkTreeIter iter;
445         GtkTreeModel *model;
446         gchar *email;
447
448         if(gtk_tree_selection_get_selected(selection, &model, &iter)) {
449                 GSList *hashval;
450                 GSList *walk;
451
452                 gtk_tree_model_get(model, &iter, 0, &email, -1);
453
454                 hashval = g_hash_table_lookup(addr_hash, email);
455                 gtk_list_store_clear(detail_store);
456                 for(walk = hashval; walk; walk = walk->next) {
457                         AddrDupListEntry *entry = walk->data;
458                         if(!entry)
459                                 continue;
460                         gtk_list_store_append(detail_store, &iter);
461                         gtk_list_store_set(detail_store, &iter,
462                                            COL_BOOK, addrindex_ds_get_name(entry->ds),
463                                            COL_NAME, ADDRITEM_NAME(entry->person),
464                                            COL_ITEM, entry->person,
465                                            COL_DS, entry->ds,
466                                            -1);
467                 }
468                 g_free(email);
469         }
470 }
471
472 static void refresh_stores(void)
473 {
474         refresh_addr_hash();
475         if(email_store)
476                 gtk_list_store_clear(email_store);
477         if(detail_store)
478                 gtk_list_store_clear(detail_store);
479         g_hash_table_foreach(addr_hash,append_to_email_store,email_store);
480 }
481
482 static void detail_row_activated(GtkTreeView       *tree_view,
483                                  GtkTreePath       *path,
484                                  GtkTreeViewColumn *column,
485                                  gpointer           user_data)
486 {
487         GtkTreeIter iter;
488         ItemPerson *person;
489         AddressDataSource *ds;
490         GtkTreeModel *model;
491         AddressBookFile *abf;
492
493         model = gtk_tree_view_get_model(tree_view);
494
495         if(!gtk_tree_model_get_iter(model,&iter,path))
496                 return;
497
498         gtk_tree_model_get(model, &iter, COL_ITEM, &person, COL_DS, &ds, -1);
499
500
501         if(!((ds->type == ADDR_IF_BOOK) || ds->type == ADDR_IF_LDAP)) {
502                 debug_print("Unsupported address datasource type for editing\n");
503                 return;
504         }
505
506         abf = ds->rawDataSource;
507         if(addressbook_edit_person(abf,NULL,person,FALSE,NULL,NULL,TRUE))
508                 refresh_stores();
509 }
510
511 void cb_del_btn_clicked(GtkButton *button, gpointer data)
512 {
513         GtkTreeView *detail_view;
514         GtkTreeIter iter;
515         GtkTreeModel *model;
516         GtkTreeSelection *selection;
517         ItemPerson *item;
518         AddressDataSource *ds;
519         GList *list;
520         GList *ref_list;
521         GList *walk;
522         GtkTreeRowReference *ref;
523         AlertValue aval;
524
525         detail_view = data;
526
527         selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(detail_view));
528
529         list = gtk_tree_selection_get_selected_rows(selection, &model);
530
531         if(!list)
532                 return;
533
534         aval = alertpanel(_("Delete address(es)"),
535                           _("Really delete the address(es)?"),
536                           GTK_STOCK_CANCEL, "+"GTK_STOCK_DELETE, NULL);
537         if(aval != G_ALERTALTERNATE)
538                 return;
539
540         ref_list = NULL;
541         for(walk = list; walk; walk = walk->next) {
542                 ref = gtk_tree_row_reference_new(model,(GtkTreePath*)(walk->data));
543                 ref_list = g_list_prepend(ref_list, ref);
544         }
545         g_list_foreach(list, (GFunc)gtk_tree_path_free, NULL);
546         g_list_free(list);
547
548         for(walk = ref_list; walk; walk = walk->next) {
549                 GtkTreePath *path;
550                 ref = walk->data;
551                 if(!gtk_tree_row_reference_valid(ref))
552                         continue;
553                 path = gtk_tree_row_reference_get_path(ref);
554                 if(gtk_tree_model_get_iter(model, &iter, path)) {
555                         gtk_tree_model_get(model, &iter, COL_ITEM, &item, COL_DS, &ds, -1);
556                         delete_item(item,ds);
557                 }
558                 gtk_tree_path_free(path);
559         }
560
561         g_list_foreach(ref_list, (GFunc)gtk_tree_row_reference_free, NULL);
562         g_list_free(ref_list);
563
564         refresh_stores();
565 }
566
567 static gboolean delete_item(ItemPerson *item, AddressDataSource *ds)
568 {
569         AddressBookFile *abf;
570         AddressInterface *iface;
571
572         /* Test for read only */
573         iface = ds->interface;
574         if( iface->readOnly ) {
575                 alertpanel( _("Delete address"),
576                             _("This address data is readonly and cannot be deleted."),
577                             GTK_STOCK_CLOSE, NULL, NULL );
578                 return FALSE;
579         }
580
581         if(!(abf = ds->rawDataSource))
582                 return FALSE;
583
584         item->status = DELETE_ENTRY;
585         item = addrbook_remove_person(abf, item);
586
587 #ifdef USE_LDAP
588
589         if (ds && ds->type == ADDR_IF_LDAP) {
590                 LdapServer *server = ds->rawDataSource;
591                 ldapsvr_set_modified(server, TRUE);
592                 ldapsvr_update_book(server, item);
593         }
594
595 #endif
596
597         if(item)
598                 addritem_free_item_person(item);
599
600         return TRUE;
601 }