Attachment remover: notify the user about what has been done when processing
[claws.git] / src / plugins / att_remover / att_remover.c
1 /*
2  * att_remover -- for Claws Mail
3  *
4  * Copyright (C) 2005 Colin Leroy <colin@colino.net>
5  *
6  * Sylpheed is a GTK+ based, lightweight, and fast e-mail client
7  * Copyright (C) 1999-2005 Hiroyuki Yamamoto and the Claws Mail Team
8  *
9  * This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation; either version 3 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License
20  * along with this program; if not, write to the Free Software
21  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
22  */
23
24 #ifdef HAVE_CONFIG_H
25 #  include "config.h"
26 #include "claws-features.h"
27 #endif
28
29 #include <glib.h>
30 #include <glib/gi18n.h>
31
32 #include <string.h>
33
34 #include <gdk/gdkkeysyms.h>
35 #include <gtk/gtk.h>
36
37 #include "mainwindow.h"
38 #include "summaryview.h"
39 #include "folder.h"
40 #include "version.h"
41 #include "summaryview.h"
42 #include "procmime.h"
43 #include "alertpanel.h"
44 #include "inc.h"
45 #include "menu.h"
46 #include "claws.h"
47 #include "plugin.h"
48 #include "prefs_common.h"
49 #include "defs.h"
50 #include "prefs_gtk.h"
51
52 #define PREFS_BLOCK_NAME "AttRemover"
53
54 static struct _AttRemover {
55         GtkWidget       *window;
56         MsgInfo         *msginfo;
57         GtkTreeModel    *model;
58         gint            win_width;
59         gint            win_height;
60 } AttRemoverData;
61
62 typedef struct _AttRemover AttRemover;
63
64 static PrefParam prefs[] = {
65         {"win_width", "-1", &AttRemoverData.win_width, P_INT, NULL,
66         NULL, NULL},
67         {"win_height", "-1", &AttRemoverData.win_height, P_INT, NULL, 
68          NULL, NULL},
69         {0,0,0,0}
70 };
71
72 enum {
73         ATT_REMOVER_INFO,
74         ATT_REMOVER_TOGGLE,
75         N_ATT_REMOVER_COLUMNS
76 };
77
78 static MimeInfo *find_first_text_part(MimeInfo *partinfo)
79 {
80         while (partinfo && partinfo->type != MIMETYPE_TEXT) {
81                 partinfo = procmime_mimeinfo_next(partinfo);
82         }
83         
84         return partinfo;
85 }
86
87 static gboolean key_pressed_cb(GtkWidget *widget, GdkEventKey *event,
88                                 AttRemover *attremover)
89 {
90         if (event && event->keyval == GDK_KEY_Escape)
91                 gtk_widget_destroy(attremover->window);
92
93         return FALSE;
94 }
95
96 static void cancelled_event_cb(GtkWidget *widget, AttRemover *attremover)
97 {
98         gtk_widget_destroy(attremover->window);
99 }
100
101 static void size_allocate_cb(GtkWidget *widget, GtkAllocation *allocation)
102 {
103         cm_return_if_fail(allocation != NULL);
104
105         AttRemoverData.win_width = allocation->width;
106         AttRemoverData.win_height = allocation->height;
107 }
108
109 static gint save_new_message(MsgInfo *oldmsg, MsgInfo *newmsg, MimeInfo *info,
110                                 gboolean has_attachment)
111 {
112         MsgInfo *finalmsg;
113         FolderItem *item = oldmsg->folder;
114         MsgFlags flags = {0, 0};
115         gint msgnum = -1;
116         
117         finalmsg = procmsg_msginfo_new_from_mimeinfo(newmsg, info);
118         if (!finalmsg) {
119                 procmsg_msginfo_free(&newmsg);
120                 return -1;
121         }
122
123         debug_print("finalmsg %s\n", finalmsg->plaintext_file);
124                 
125         flags.tmp_flags = oldmsg->flags.tmp_flags;
126         flags.perm_flags = oldmsg->flags.perm_flags;
127         
128         if (!has_attachment)
129                 flags.tmp_flags &= ~MSG_HAS_ATTACHMENT;
130
131         oldmsg->flags.perm_flags &= ~MSG_LOCKED;
132         msgnum = folder_item_add_msg(item, finalmsg->plaintext_file, &flags, TRUE);
133         if (msgnum < 0) {
134                 g_warning("could not add message without attachments");
135                 procmsg_msginfo_free(&newmsg);
136                 procmsg_msginfo_free(&finalmsg);
137                 return msgnum;
138         }
139         folder_item_remove_msg(item, oldmsg->msgnum);
140         finalmsg->msgnum = msgnum;
141         procmsg_msginfo_free(&newmsg);
142         procmsg_msginfo_free(&finalmsg);
143                 
144         newmsg = folder_item_get_msginfo(item, msgnum);
145         if (newmsg && item) {
146                 procmsg_msginfo_unset_flags(newmsg, ~0, ~0);
147                 procmsg_msginfo_set_flags(newmsg, flags.perm_flags, flags.tmp_flags);
148                 procmsg_msginfo_free(&newmsg);
149         }
150         
151         return msgnum;
152 }
153
154 #define MIMEINFO_NOT_ATTACHMENT(m) \
155         ((m)->type == MIMETYPE_MESSAGE || (m)->type == MIMETYPE_MULTIPART)
156
157 static void remove_attachments_cb(GtkWidget *widget, AttRemover *attremover)
158 {
159         MainWindow *mainwin = mainwindow_get_mainwindow();
160         SummaryView *summaryview = mainwin->summaryview;
161         GtkTreeModel *model = attremover->model;
162         GtkTreeIter iter;
163         MsgInfo *newmsg;
164         MimeInfo *info, *parent, *last, *partinfo;
165         GNode *child;
166         gint att_all = 0, att_removed = 0, msgnum;
167         gboolean to_removal, iter_valid=TRUE;
168         
169         newmsg = procmsg_msginfo_copy(attremover->msginfo);
170         info = procmime_scan_message(newmsg);
171         
172         last = partinfo = find_first_text_part(info);
173         partinfo = procmime_mimeinfo_next(partinfo);
174         if (!partinfo || !gtk_tree_model_get_iter_first(model, &iter)) {
175                 gtk_widget_destroy(attremover->window);
176                 procmsg_msginfo_free(&newmsg);
177                 return;
178         }
179
180         main_window_cursor_wait(mainwin);
181         summary_freeze(summaryview);
182         folder_item_update_freeze();
183         inc_lock();
184         
185         while (partinfo && iter_valid) {
186                 if (MIMEINFO_NOT_ATTACHMENT(partinfo)) {
187                         last = partinfo;
188                         partinfo = procmime_mimeinfo_next(partinfo);
189                         continue;
190                 }
191                         
192                 att_all++;
193                 gtk_tree_model_get(model, &iter, ATT_REMOVER_TOGGLE,
194                                    &to_removal, -1);
195                 if (!to_removal) {
196                         last = partinfo;
197                         partinfo = procmime_mimeinfo_next(partinfo);
198                         iter_valid = gtk_tree_model_iter_next(model, &iter);
199                         continue;
200                 }
201
202                 parent = partinfo;
203                 partinfo = procmime_mimeinfo_next(partinfo);
204                 iter_valid = gtk_tree_model_iter_next(model, &iter);
205
206                 g_node_destroy(parent->node);
207                 att_removed++;
208         }
209
210         partinfo = last;
211         while (partinfo) {
212                 if (!(parent = procmime_mimeinfo_parent(partinfo)))
213                         break;
214                 
215                 /* multipart/{alternative,mixed,related} make sense
216                    only when they have at least 2 nodes, remove last
217                    one and move it one level up if otherwise  */
218                 if (MIMEINFO_NOT_ATTACHMENT(partinfo) &&
219                         g_node_n_children(partinfo->node) < 2)
220                 {
221                         gint pos = g_node_child_position(parent->node, partinfo->node);         
222                         g_node_unlink(partinfo->node);
223                         
224                         if ((child = g_node_first_child(partinfo->node))) {
225                                 g_node_unlink(child);
226                                 g_node_insert(parent->node, pos, child);
227                         }
228                         g_node_destroy(partinfo->node);
229                         
230                         child = g_node_last_child(parent->node);
231                         partinfo = child ? child->data : parent;
232                         continue;
233                 }
234
235                 if (partinfo->node->prev) {
236                         partinfo = (MimeInfo *) partinfo->node->prev->data;
237                         if (partinfo->node->children) {
238                                 child = g_node_last_child(partinfo->node);
239                                 partinfo = (MimeInfo *) child->data;                    
240                         }
241                 } else if (partinfo->node->parent)
242                         partinfo = (MimeInfo *) partinfo->node->parent->data;           
243         }
244
245         msgnum = save_new_message(attremover->msginfo, newmsg, info,
246                          (att_all - att_removed > 0));
247                          
248         inc_unlock();
249         folder_item_update_thaw();
250         summary_thaw(summaryview);
251         main_window_cursor_normal(mainwin);
252
253         if (msgnum > 0)
254                 summary_select_by_msgnum(summaryview, msgnum, TRUE);
255
256         gtk_widget_destroy(attremover->window);
257 }
258
259 static void remove_toggled_cb(GtkCellRendererToggle *cell, gchar *path_str,
260                                 gpointer data)
261 {
262         GtkTreeModel *model = (GtkTreeModel *)data;
263         GtkTreeIter  iter;
264         GtkTreePath *path = gtk_tree_path_new_from_string (path_str);
265         gboolean fixed;
266                                
267         gtk_tree_model_get_iter (model, &iter, path);
268         gtk_tree_model_get (model, &iter, ATT_REMOVER_TOGGLE, &fixed, -1);
269                                      
270         fixed ^= 1;
271         gtk_list_store_set (GTK_LIST_STORE (model), &iter, ATT_REMOVER_TOGGLE, fixed, -1);
272                                              
273         gtk_tree_path_free (path);
274 }
275
276 static void fill_attachment_store(GtkTreeView *list_view, MimeInfo *partinfo)
277 {
278         const gchar *name;
279         gchar *label, *content_type;
280         GtkTreeIter iter;
281         GtkListStore *list_store = GTK_LIST_STORE(gtk_tree_view_get_model
282                                         (GTK_TREE_VIEW(list_view)));
283
284         partinfo = find_first_text_part(partinfo);
285         partinfo = procmime_mimeinfo_next(partinfo);
286         if (!partinfo)
287                 return;
288                 
289         while (partinfo) {
290                 if (MIMEINFO_NOT_ATTACHMENT(partinfo)) {
291                         partinfo = procmime_mimeinfo_next(partinfo);
292                         continue;
293                 }
294                         
295                 content_type = procmime_get_content_type_str(
296                                         partinfo->type, partinfo->subtype);
297                 
298                 name = procmime_mimeinfo_get_parameter(partinfo, "filename");
299                 if (!name)
300                         name = procmime_mimeinfo_get_parameter(partinfo, "name");
301                 if (!name)
302                         name = _("unknown");
303                 
304                 label = g_strconcat("<b>",_("Type:"), "</b> ", content_type, "   <b>",
305                          _("Size:"), "</b> ", to_human_readable((goffset)partinfo->length),
306                         "\n", "<b>", _("Filename:"), "</b> ", name, NULL);
307
308                 gtk_list_store_append(list_store, &iter);
309                 gtk_list_store_set(list_store, &iter,
310                                    ATT_REMOVER_INFO, label,
311                                    ATT_REMOVER_TOGGLE, TRUE,
312                                    -1);
313                 g_free(label);
314                 g_free(content_type);
315                 partinfo = procmime_mimeinfo_next(partinfo);
316         }
317 }
318
319 static void remove_attachments_dialog(AttRemover *attremover)
320 {
321         GtkWidget *window;
322         GtkWidget *vbox;
323         GtkTreeView *list_view;
324         GtkTreeModel *model;
325         GtkTreeViewColumn *column;
326         GtkCellRenderer *renderer;
327         GtkWidget *scrollwin;
328         GtkWidget *hbbox;
329         GtkWidget *ok_btn;
330         GtkWidget *cancel_btn;
331         MimeInfo *info = procmime_scan_message(attremover->msginfo);
332
333         static GdkGeometry geometry;
334
335         window = gtkut_window_new(GTK_WINDOW_TOPLEVEL, "AttRemover");
336         gtk_container_set_border_width( GTK_CONTAINER(window), VBOX_BORDER);
337         gtk_window_set_title(GTK_WINDOW(window), _("Remove attachments"));
338         gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
339         gtk_window_set_modal(GTK_WINDOW(window), TRUE);
340
341         g_signal_connect(G_OBJECT(window), "delete_event",
342                           G_CALLBACK(cancelled_event_cb), attremover);
343         g_signal_connect(G_OBJECT(window), "key_press_event",
344                           G_CALLBACK(key_pressed_cb), attremover);
345         g_signal_connect(G_OBJECT(window), "size_allocate",
346                          G_CALLBACK(size_allocate_cb), NULL);
347
348         vbox = gtk_vbox_new(FALSE, VBOX_BORDER);
349         gtk_container_add(GTK_CONTAINER(window), vbox);
350
351         model = GTK_TREE_MODEL(gtk_list_store_new(N_ATT_REMOVER_COLUMNS,
352                                   G_TYPE_STRING,
353                                   G_TYPE_BOOLEAN,
354                                   -1));
355         list_view = GTK_TREE_VIEW(gtk_tree_view_new_with_model(model));
356         g_object_unref(model);  
357         gtk_tree_view_set_rules_hint(list_view, prefs_common_get_prefs()->use_stripes_everywhere);
358         
359         renderer = gtk_cell_renderer_toggle_new();
360         g_signal_connect(renderer, "toggled", G_CALLBACK(remove_toggled_cb), model);
361         column = gtk_tree_view_column_new_with_attributes
362                 (_("Remove"),
363                 renderer,
364                 "active", ATT_REMOVER_TOGGLE,
365                 NULL);
366         gtk_tree_view_append_column(GTK_TREE_VIEW(list_view), column);
367
368         renderer = gtk_cell_renderer_text_new();
369         column = gtk_tree_view_column_new_with_attributes
370                 (_("Attachment"),
371                  renderer,
372                  "markup", ATT_REMOVER_INFO,
373                  NULL);
374         gtk_tree_view_append_column(GTK_TREE_VIEW(list_view), column);
375         fill_attachment_store(list_view, info);
376
377         scrollwin = gtk_scrolled_window_new(NULL, NULL);
378         gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrollwin),
379                                 GTK_SHADOW_ETCHED_OUT);
380         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrollwin),
381                                 GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
382         gtk_container_add(GTK_CONTAINER(scrollwin), GTK_WIDGET(list_view));
383         gtk_container_set_border_width(GTK_CONTAINER(scrollwin), 4);
384         gtk_box_pack_start(GTK_BOX(vbox), scrollwin, TRUE, TRUE, 0); 
385
386         gtkut_stock_button_set_create(&hbbox, &cancel_btn, GTK_STOCK_CANCEL,
387                                       &ok_btn, GTK_STOCK_OK,
388                                       NULL, NULL);
389         gtk_box_pack_end(GTK_BOX(vbox), hbbox, FALSE, FALSE, 0);
390         gtk_container_set_border_width(GTK_CONTAINER(hbbox), HSPACING_NARROW);
391         gtk_widget_grab_default(ok_btn);
392
393         g_signal_connect(G_OBJECT(ok_btn), "clicked",
394                          G_CALLBACK(remove_attachments_cb), attremover);
395         g_signal_connect(G_OBJECT(cancel_btn), "clicked",
396                          G_CALLBACK(cancelled_event_cb), attremover);
397                          
398         if (!geometry.min_height) {
399                 geometry.min_width = 450;
400                 geometry.min_height = 350;
401         }
402
403         gtk_window_set_geometry_hints(GTK_WINDOW(window), NULL, &geometry,
404                                       GDK_HINT_MIN_SIZE);
405         gtk_widget_set_size_request(window, attremover->win_width,
406                                         attremover->win_height);
407
408         attremover->window = window;
409         attremover->model  = model;
410
411         gtk_widget_show_all(window);
412         gtk_widget_grab_focus(ok_btn);
413 }
414
415 static void remove_attachments(GSList *msglist)
416 {
417         MainWindow *mainwin = mainwindow_get_mainwindow();
418         SummaryView *summaryview = mainwin->summaryview;
419         GSList *cur;
420         gint msgnum = -1;
421         gint stripped_msgs = 0;
422         gint total_msgs = 0;
423         
424         if (alertpanel_full(_("Destroy attachments"),
425                   _("Do you really want to remove all attachments from "
426                   "the selected messages?\n\n"
427                   "The deleted data will be unrecoverable."), 
428                   GTK_STOCK_CANCEL, GTK_STOCK_DELETE, NULL, ALERTFOCUS_SECOND,
429                   FALSE, NULL, ALERT_QUESTION) != G_ALERTALTERNATE)
430                 return;
431
432         main_window_cursor_wait(summaryview->mainwin);
433         summary_freeze(summaryview);
434         folder_item_update_freeze();
435         inc_lock();
436
437         for (cur = msglist; cur; cur = cur->next) {
438                 MsgInfo *msginfo = (MsgInfo *)cur->data;
439                 MsgInfo *newmsg = NULL;
440                 MimeInfo *info = NULL;
441                 MimeInfo *partinfo = NULL;
442                 MimeInfo *nextpartinfo = NULL;
443
444                 if (!msginfo)
445                         continue;
446                 total_msgs++;                   /* count all processed messages */
447
448                 newmsg = procmsg_msginfo_copy(msginfo);
449                 info = procmime_scan_message(newmsg);
450         
451                 if ( !(partinfo = find_first_text_part(info)) ) {
452                         procmsg_msginfo_free(&newmsg);
453                         continue;
454                 }
455                 /* only strip attachments where there is at least one */
456                 nextpartinfo = procmime_mimeinfo_next(partinfo);
457                 if (nextpartinfo) {
458                         partinfo->node->next = NULL;
459                         partinfo->node->children = NULL;
460                         info->node->children->data = partinfo;
461
462                         msgnum = save_new_message(msginfo, newmsg, info, FALSE);
463
464                         stripped_msgs++;        /* count messages with removed attachment(s) */
465                 }
466         }
467         if (stripped_msgs == 0) {
468                 alertpanel_notice(_("The selected messages don't have any attachments."));
469         } else {
470                 if (stripped_msgs != total_msgs)
471                         alertpanel_notice(_("Attachments removed from %d of the %d selected messages."),
472                                                         stripped_msgs, total_msgs);
473                 else
474                         alertpanel_notice(_("Attachments removed from all %d selected messages."), total_msgs);
475         }       
476
477         inc_unlock();
478         folder_item_update_thaw();
479         summary_thaw(summaryview);
480         main_window_cursor_normal(summaryview->mainwin);
481
482         if (msgnum > 0) {
483                 summary_select_by_msgnum(summaryview, msgnum, TRUE);
484         }
485 }
486
487 static void remove_attachments_ui(GtkAction *action, gpointer data)
488 {
489         MainWindow *mainwin = mainwindow_get_mainwindow();
490         GSList *msglist = summary_get_selected_msg_list(mainwin->summaryview);
491         MimeInfo *info = NULL, *partinfo = NULL;
492         
493         if (summary_is_locked(mainwin->summaryview) || !msglist) 
494                 return;
495
496         if (g_slist_length(msglist) == 1) {
497                 info = procmime_scan_message(msglist->data);
498         
499                 partinfo = find_first_text_part(info);
500                 partinfo = procmime_mimeinfo_next(partinfo);
501                 
502                 if (!partinfo) {
503                         alertpanel_notice(_("This message doesn't have any attachments."));
504                 } else {
505                         AttRemoverData.msginfo = msglist->data;
506                         remove_attachments_dialog(&AttRemoverData);
507                 }
508         } else
509                 remove_attachments(msglist);
510
511         g_slist_free(msglist);
512 }
513
514 static GtkActionEntry remove_att_main_menu[] = {{
515         "Message/RemoveAtt",
516         NULL, N_("Remove attachments..."), NULL, NULL, G_CALLBACK(remove_attachments_ui)
517 }};
518
519 static guint context_menu_id = 0;
520 static guint main_menu_id = 0;
521
522 gint plugin_init(gchar **error)
523 {
524         MainWindow *mainwin = mainwindow_get_mainwindow();
525         gchar *rcpath;
526         
527         if( !check_plugin_version(MAKE_NUMERIC_VERSION(3,6,1,27),
528                                 VERSION_NUMERIC, _("AttRemover"), error) )
529                 return -1;
530
531         gtk_action_group_add_actions(mainwin->action_group, remove_att_main_menu,
532                         1, (gpointer)mainwin);
533         MENUITEM_ADDUI_ID_MANAGER(mainwin->ui_manager, "/Menu/Message", "RemoveAtt", 
534                           "Message/RemoveAtt", GTK_UI_MANAGER_MENUITEM,
535                           main_menu_id)
536         MENUITEM_ADDUI_ID_MANAGER(mainwin->ui_manager, "/Menus/SummaryViewPopup", "RemoveAtt", 
537                           "Message/RemoveAtt", GTK_UI_MANAGER_MENUITEM,
538                           context_menu_id)
539
540         prefs_set_default(prefs);
541         rcpath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, COMMON_RC, NULL);
542         prefs_read_config(prefs, PREFS_BLOCK_NAME, rcpath, NULL);
543         g_free(rcpath);
544
545         return 0;
546 }
547
548 gboolean plugin_done(void)
549 {
550         MainWindow *mainwin = mainwindow_get_mainwindow();
551         PrefFile *pref_file;
552         gchar *rc_file_path;
553
554         rc_file_path = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S,
555                                    COMMON_RC, NULL);
556         pref_file = prefs_write_open(rc_file_path);
557         g_free(rc_file_path);
558         
559         if (!pref_file || prefs_set_block_label(pref_file, PREFS_BLOCK_NAME) < 0)
560                 return TRUE;
561         
562         if (prefs_write_param(prefs, pref_file->fp) < 0) {
563                 g_warning("failed to write AttRemover Plugin configuration");
564                 prefs_file_close_revert(pref_file);
565                 return TRUE;
566         }
567
568         if (fprintf(pref_file->fp, "\n") < 0) {
569                 FILE_OP_ERROR(rc_file_path, "fprintf");
570                 prefs_file_close_revert(pref_file);
571         } else
572                 prefs_file_close(pref_file);
573
574         if (mainwin == NULL)
575                 return TRUE;
576
577         MENUITEM_REMUI_MANAGER(mainwin->ui_manager,mainwin->action_group, "Message/RemoveAtt", main_menu_id);
578         main_menu_id = 0;
579
580         MENUITEM_REMUI_MANAGER(mainwin->ui_manager,mainwin->action_group, "Message/RemoveAtt", context_menu_id);
581         context_menu_id = 0;
582
583         return TRUE;
584 }
585
586 const gchar *plugin_name(void)
587 {
588         return _("AttRemover");
589 }
590
591 const gchar *plugin_desc(void)
592 {
593         return _("This plugin removes attachments from mails.\n\n"
594                  "Warning: this operation will be completely "
595                  "un-cancellable and the deleted attachments will "
596                  "be lost forever, and ever, and ever.");
597 }
598
599 const gchar *plugin_type(void)
600 {
601         return "GTK2";
602 }
603
604 const gchar *plugin_licence(void)
605 {
606                 return "GPL3+";
607 }
608
609 const gchar *plugin_version(void)
610 {
611         return VERSION;
612 }
613
614 struct PluginFeature *plugin_provides(void)
615 {
616         static struct PluginFeature features[] = 
617                 { {PLUGIN_UTILITY, N_("Attachment handling")},
618                   {PLUGIN_NOTHING, NULL}};
619         return features;
620 }