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