1 /* Claws Mail -- a GTK+ based, lightweight, and fast e-mail client
2 * Copyright (C) 2014 Charles Lehner and the Claws Mail team
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18
19 #ifdef HAVE_CONFIG_H
20 # include "config.h"
21 #include "claws-features.h"
22 #endif
23
24 #include <gdk/gdk.h>
25 #include <gdk/gdkkeysyms.h>
26 #include <glib/gi18n.h>
27 #include <string.h>
28
29 #include "defs.h"
30
31 #ifdef USE_LDAP
32 #include "ldapserver.h"
33 #include "ldapupdate.h"
34 #endif
35 #include "addrbook.h"
36 #include "addressbook.h"
37 #include "addressitem.h"
38 #include "addrmerge.h"
39 #include "alertpanel.h"
40 #include "gtkutils.h"
41 #include "file-utils.h"
42 #include "utils.h"
43 #include "prefs_common.h"
44
45 enum
46 {
47 COL_DISPLAYNAME,
48 COL_FIRSTNAME,
49 COL_LASTNAME,
50 COL_NICKNAME,
51 N_NAME_COLS
52 };
53
54 enum
55 {
56 SET_ICON,
57 SET_PERSON,
58 N_SET_COLUMNS
59 };
60
addrmerge_done(struct AddrMergePage * page)61 static void addrmerge_done(struct AddrMergePage *page)
62 {
63 g_list_free(page->emails);
64 g_list_free(page->persons);
65 gtk_widget_destroy(GTK_WIDGET(page->dialog));
66 g_free(page);
67 }
68
addrmerge_do_merge(struct AddrMergePage * page)69 static void addrmerge_do_merge(struct AddrMergePage *page)
70 {
71 GList *node;
72 ItemEMail *email;
73 ItemPerson *person;
74 ItemPerson *target = page->target;
75 ItemPerson *nameTarget = page->nameTarget;
76
77 gtk_cmclist_freeze(GTK_CMCLIST(page->clist));
78
79 /* Update target name */
80 if (nameTarget && nameTarget != target) {
81 target->status = UPDATE_ENTRY;
82 addritem_person_set_first_name( target, nameTarget->firstName );
83 addritem_person_set_last_name( target, nameTarget->lastName );
84 addritem_person_set_nick_name( target, nameTarget->nickName );
85 addritem_person_set_common_name( target, ADDRITEM_NAME(nameTarget ));
86 }
87
88 /* Merge emails into target */
89 for (node = page->emails; node; node = node->next) {
90 email = node->data;
91 person = ( ItemPerson * ) ADDRITEM_PARENT(email);
92 /* Remove the email from the person */
93 email = addrbook_person_remove_email( page->abf, person, email );
94 if( email ) {
95 addrcache_remove_email( page->abf->addressCache, email );
96 /* Add the email to the target */
97 addrcache_person_add_email( page->abf->addressCache, target, email );
98 }
99 person->status = UPDATE_ENTRY;
100 addressbook_folder_refresh_one_person( page->clist, person );
101 }
102
103 /* Merge persons into target */
104 for (node = page->persons; node; node = node->next) {
105 GList *nodeE, *nodeA;
106 person = node->data;
107
108 if (person == target) continue;
109 person->status = DELETE_ENTRY;
110
111 /* Move all emails to the target */
112 for (nodeE = person->listEMail; nodeE; nodeE = nodeE->next) {
113 email = nodeE->data;
114 addritem_person_add_email( target, email );
115 }
116 g_list_free( person->listEMail );
117 person->listEMail = NULL;
118
119 /* Move all attributes to the target */
120 for (nodeA = person->listAttrib; nodeA; nodeA = nodeA->next) {
121 UserAttribute *attrib = nodeA->data;
122 addritem_person_add_attribute( target, attrib );
123 }
124 g_list_free( person->listAttrib );
125 person->listAttrib = NULL;
126
127 /* Remove the person */
128 addrselect_list_remove( page->addressSelect, (AddrItemObject *)person );
129 addressbook_folder_remove_one_person( page->clist, person );
130 if (page->pobj->type == ADDR_ITEM_FOLDER)
131 addritem_folder_remove_person(ADAPTER_FOLDER(page->pobj)->itemFolder, person);
132 person = addrbook_remove_person( page->abf, person );
133
134 if( person ) {
135 gchar *filename = addritem_person_get_picture(person);
136 if ((g_strcmp0(person->picture, target->picture) &&
137 filename && is_file_exist(filename)))
138 claws_unlink(filename);
139 if (filename)
140 g_free(filename);
141 addritem_free_item_person( person );
142 }
143 }
144
145 addressbook_folder_refresh_one_person( page->clist, target );
146
147 addrbook_set_dirty( page->abf, TRUE );
148 addressbook_export_to_file();
149
150 #ifdef USE_LDAP
151 if (page->ds && page->ds->type == ADDR_IF_LDAP) {
152 LdapServer *server = page->ds->rawDataSource;
153 ldapsvr_set_modified(server, TRUE);
154 ldapsvr_update_book(server, NULL);
155 }
156 #endif
157 gtk_cmclist_thaw(GTK_CMCLIST(page->clist));
158
159 addrmerge_done(page);
160 }
161
addrmerge_dialog_cb(GtkWidget * widget,gint action,gpointer data)162 static void addrmerge_dialog_cb(GtkWidget* widget, gint action, gpointer data) {
163 struct AddrMergePage* page = data;
164
165 if (action != GTK_RESPONSE_ACCEPT)
166 return addrmerge_done(page);
167
168 addrmerge_do_merge(page);
169 }
170
addrmerge_update_dialog_sensitive(struct AddrMergePage * page)171 static void addrmerge_update_dialog_sensitive( struct AddrMergePage *page )
172 {
173 gboolean canMerge = (page->target && page->nameTarget);
174 gtk_dialog_set_response_sensitive( GTK_DIALOG(page->dialog),
175 GTK_RESPONSE_ACCEPT, canMerge );
176 }
177
addrmerge_name_selected(GtkCMCList * clist,gint row,gint column,GdkEvent * event,struct AddrMergePage * page)178 static void addrmerge_name_selected( GtkCMCList *clist, gint row, gint column, GdkEvent *event, struct AddrMergePage *page )
179 {
180 ItemPerson *person = gtk_cmclist_get_row_data( clist, row );
181 page->nameTarget = person;
182 addrmerge_update_dialog_sensitive(page);
183 }
184
addrmerge_picture_selected(GtkTreeView * treeview,struct AddrMergePage * page)185 static void addrmerge_picture_selected(GtkTreeView *treeview,
186 struct AddrMergePage *page)
187 {
188 GtkTreeModel *model;
189 GtkTreeIter iter;
190 GList *list;
191 ItemPerson *pictureTarget;
192
193 /* Get selected picture target */
194 model = gtk_icon_view_get_model(GTK_ICON_VIEW(page->iconView));
195 list = gtk_icon_view_get_selected_items(GTK_ICON_VIEW(page->iconView));
196 page->target = NULL;
197 if (list != NULL) {
198 if (gtk_tree_model_get_iter(model, &iter, (GtkTreePath *)list->data)) {
199 gtk_tree_model_get(model, &iter,
200 SET_PERSON, &pictureTarget,
201 -1);
202 page->target = pictureTarget;
203 }
204
205 gtk_tree_path_free(list->data);
206 g_list_free(list);
207 }
208 addrmerge_update_dialog_sensitive(page);
209 }
210
addrmerge_prompt(struct AddrMergePage * page)211 static void addrmerge_prompt( struct AddrMergePage *page )
212 {
213 GtkWidget *dialog;
214 GtkWidget *frame;
215 GtkWidget *mvbox, *vbox, *hbox;
216 GtkWidget *label;
217 GtkWidget *iconView = NULL;
218 GtkWidget *namesList = NULL;
219 MainWindow *mainwin = mainwindow_get_mainwindow();
220 GtkListStore *store = NULL;
221 GtkTreeIter iter;
222 GList *node;
223 ItemPerson *person;
224 GError *error = NULL;
225 gchar *msg, *label_msg;
226
227 dialog = page->dialog = gtk_dialog_new_with_buttons (
228 _("Merge addresses"),
229 GTK_WINDOW(mainwin->window),
230 GTK_DIALOG_DESTROY_WITH_PARENT,
231 GTK_STOCK_CANCEL,
232 GTK_RESPONSE_CANCEL,
233 "_Merge",
234 GTK_RESPONSE_ACCEPT,
235 NULL);
236
237 g_signal_connect ( dialog, "response",
238 G_CALLBACK(addrmerge_dialog_cb), page);
239
240 mvbox = gtk_vbox_new(FALSE, 4);
241 gtk_container_add(GTK_CONTAINER(
242 gtk_dialog_get_content_area(GTK_DIALOG(dialog))), mvbox);
243 gtk_container_set_border_width(GTK_CONTAINER(mvbox), 8);
244 hbox = gtk_hbox_new(FALSE, 4);
245 gtk_container_set_border_width(GTK_CONTAINER(hbox), 8);
246 gtk_box_pack_start(GTK_BOX(mvbox),
247 hbox, FALSE, FALSE, 0);
248
249 msg = page->pickPicture || page->pickName ?
250 _("Merging %u contacts." ) :
251 _("Really merge these %u contacts?" );
252 label_msg = g_strdup_printf(msg,
253 g_list_length(page->addressSelect->listSelect));
254 label = gtk_label_new( label_msg );
255 gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_LEFT);
256 gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
257 gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
258 g_free(label_msg);
259
260 if (page->pickPicture) {
261 GtkWidget *scrollwinPictures;
262
263 store = gtk_list_store_new(N_SET_COLUMNS,
264 GDK_TYPE_PIXBUF,
265 G_TYPE_POINTER,
266 -1);
267 gtk_list_store_clear(store);
268
269 vbox = gtkut_get_options_frame(mvbox, &frame,
270 _("Keep which picture?"));
271 gtk_container_set_border_width(GTK_CONTAINER(frame), 4);
272
273 scrollwinPictures = gtk_scrolled_window_new(NULL, NULL);
274 gtk_container_set_border_width(GTK_CONTAINER(scrollwinPictures), 1);
275 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrollwinPictures),
276 GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
277 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrollwinPictures),
278 GTK_SHADOW_IN);
279 gtk_box_pack_start (GTK_BOX (vbox), scrollwinPictures, FALSE, FALSE, 0);
280 gtk_widget_set_size_request(scrollwinPictures, 464, 192);
281
282 iconView = gtk_icon_view_new_with_model(GTK_TREE_MODEL(store));
283 gtk_icon_view_set_selection_mode(GTK_ICON_VIEW(iconView), GTK_SELECTION_SINGLE);
284 gtk_icon_view_set_pixbuf_column(GTK_ICON_VIEW(iconView), SET_ICON);
285 gtk_container_add(GTK_CONTAINER(scrollwinPictures), GTK_WIDGET(iconView));
286 g_signal_connect(G_OBJECT(iconView), "selection-changed",
287 G_CALLBACK(addrmerge_picture_selected), page);
288
289 /* Add pictures from persons */
290 for (node = page->persons; node; node = node->next) {
291 gchar *filename;
292 person = node->data;
293 filename = addritem_person_get_picture(person);
294 if (filename && is_file_exist(filename)) {
295 GdkPixbuf *pixbuf;
296 GtkWidget *image;
297
298 pixbuf = gdk_pixbuf_new_from_file(filename, &error);
299 if (error) {
300 debug_print("Failed to read image: \n%s",
301 error->message);
302 g_error_free(error);
303 continue;
304 }
305
306 image = gtk_image_new();
307 gtk_image_set_from_pixbuf(GTK_IMAGE(image), pixbuf);
308
309 gtk_list_store_append(store, &iter);
310 gtk_list_store_set(store, &iter,
311 SET_ICON, pixbuf,
312 SET_PERSON, person,
313 -1);
314 }
315 if (filename)
316 g_free(filename);
317 }
318 }
319
320 if (page->pickName) {
321 GtkWidget *scrollwinNames;
322 gchar *name_titles[N_NAME_COLS];
323
324 name_titles[COL_DISPLAYNAME] = _("Display Name");
325 name_titles[COL_FIRSTNAME] = _("First Name");
326 name_titles[COL_LASTNAME] = _("Last Name");
327 name_titles[COL_NICKNAME] = _("Nickname");
328
329 store = gtk_list_store_new(N_SET_COLUMNS,
330 GDK_TYPE_PIXBUF,
331 G_TYPE_POINTER,
332 -1);
333 gtk_list_store_clear(store);
334
335 vbox = gtkut_get_options_frame(mvbox, &frame,
336 _("Keep which name?"));
337 gtk_container_set_border_width(GTK_CONTAINER(frame), 4);
338
339 scrollwinNames = gtk_scrolled_window_new(NULL, NULL);
340 gtk_container_set_border_width(GTK_CONTAINER(scrollwinNames), 1);
341 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrollwinNames),
342 GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
343 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrollwinNames),
344 GTK_SHADOW_IN);
345 gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(scrollwinNames), FALSE, FALSE, 0);
346
347 namesList = gtk_cmclist_new_with_titles(N_NAME_COLS, name_titles);
348 gtk_widget_set_can_focus(GTK_CMCLIST(namesList)->column[0].button, FALSE);
349 gtk_cmclist_set_selection_mode(GTK_CMCLIST(namesList), GTK_SELECTION_BROWSE);
350 gtk_cmclist_set_column_width(GTK_CMCLIST(namesList), COL_DISPLAYNAME, 164);
351
352 gtk_container_add(GTK_CONTAINER(scrollwinNames), namesList);
353
354 /* Add names from persons */
355 for (node = page->persons; node; node = node->next) {
356 int row;
357 person = node->data;
358 gchar *text[N_NAME_COLS];
359 text[COL_DISPLAYNAME] = ADDRITEM_NAME(person);
360 text[COL_FIRSTNAME] = person->firstName;
361 text[COL_LASTNAME] = person->lastName;
362 text[COL_NICKNAME] = person->nickName;
363 row = gtk_cmclist_insert( GTK_CMCLIST(namesList), -1, text );
364 gtk_cmclist_set_row_data( GTK_CMCLIST(namesList), row, person );
365 }
366
367 g_signal_connect(G_OBJECT(namesList), "select_row",
368 G_CALLBACK(addrmerge_name_selected), page);
369 }
370
371 page->iconView = iconView;
372 page->namesList = namesList;
373
374 addrmerge_update_dialog_sensitive(page);
375 gtk_widget_show_all(dialog);
376 }
377
addrmerge_merge(GtkCMCTree * clist,AddressObject * pobj,AddressDataSource * ds,AddrSelectList * list)378 void addrmerge_merge(
379 GtkCMCTree *clist,
380 AddressObject *pobj,
381 AddressDataSource *ds,
382 AddrSelectList *list)
383 {
384 struct AddrMergePage* page;
385 AdapterDSource *ads = NULL;
386 AddressBookFile *abf;
387 gboolean procFlag;
388 GList *node;
389 AddrSelectItem *item;
390 AddrItemObject *aio;
391 ItemPerson *person, *target = NULL, *nameTarget = NULL;
392 GList *persons = NULL, *emails = NULL;
393 gboolean pickPicture = FALSE, pickName = FALSE;
394
395 /* Test for read only */
396 if( ds->interface->readOnly ) {
397 alertpanel( _("Merge addresses"),
398 _("This address data is readonly and cannot be deleted."),
399 GTK_STOCK_CLOSE, NULL, NULL, ALERTFOCUS_FIRST );
400 return;
401 }
402
403 /* Test whether Ok to proceed */
404 procFlag = FALSE;
405 if( pobj->type == ADDR_DATASOURCE ) {
406 ads = ADAPTER_DSOURCE(pobj);
407 if( ads->subType == ADDR_BOOK ) procFlag = TRUE;
408 }
409 else if( pobj->type == ADDR_ITEM_FOLDER ) {
410 procFlag = TRUE;
411 }
412 else if( pobj->type == ADDR_ITEM_GROUP ) {
413 procFlag = TRUE;
414 }
415 if( ! procFlag ) return;
416 abf = ds->rawDataSource;
417 if( abf == NULL ) return;
418
419 /* Gather selected persons and emails */
420 for (node = list->listSelect; node; node = node->next) {
421 item = node->data;
422 aio = ( AddrItemObject * ) item->addressItem;
423 if( aio->type == ITEMTYPE_EMAIL ) {
424 emails = g_list_prepend(emails, aio);
425 } else if( aio->type == ITEMTYPE_PERSON ) {
426 persons = g_list_prepend(persons, aio);
427 }
428 }
429
430 /* Check if more than one person has a picture */
431 for (node = persons; node; node = node->next) {
432 gchar *filename;
433 person = node->data;
434 filename = addritem_person_get_picture(person);
435 if (filename && is_file_exist(filename)) {
436 if (target == NULL) {
437 target = person;
438 } else {
439 pickPicture = TRUE;
440 target = NULL;
441 break;
442 }
443 }
444 if (filename)
445 g_free(filename);
446 }
447 if (pickPicture || target) {
448 /* At least one person had a picture */
449 } else if (persons && persons->data) {
450 /* No person had a picture. Use the first person as target */
451 target = persons->data;
452 } else {
453 /* No persons in list. Abort */
454 goto abort;
455 }
456
457 /* Pick which name to keep */
458 for (node = persons; node; node = node->next) {
459 person = node->data;
460 if (nameTarget == NULL) {
461 nameTarget = person;
462 } else if (nameTarget == person) {
463 continue;
464 } else if (g_strcmp0(person->firstName, nameTarget->firstName) ||
465 g_strcmp0(person->lastName, nameTarget->lastName) ||
466 g_strcmp0(person->nickName, nameTarget->nickName) ||
467 g_strcmp0(ADDRITEM_NAME(person), ADDRITEM_NAME(nameTarget))) {
468 pickName = TRUE;
469 break;
470 }
471 }
472 if (!nameTarget) {
473 /* No persons in list */
474 goto abort;
475 }
476
477 /* Create object */
478 page = g_new0(struct AddrMergePage, 1);
479 page->pickPicture = pickPicture;
480 page->pickName = pickName;
481 page->target = target;
482 page->nameTarget = nameTarget;
483 page->addressSelect = list;
484 page->persons = persons;
485 page->emails = emails;
486 page->clist = clist;
487 page->pobj = pobj;
488 page->abf = abf;
489 page->ds = ds;
490
491 addrmerge_prompt(page);
492 return;
493
494 abort:
495 g_list_free( emails );
496 g_list_free( persons );
497 }
498