2 * att_remover -- for Claws Mail
4 * Copyright (C) 2005-2022 Colin Leroy <colin@colino.net>
5 * and the Claws Mail Team
7 * Claws Mail is a GTK based, lightweight, and fast e-mail client
8 * Copyright (C) 1999-2019 Hiroyuki Yamamoto and the Claws Mail Team
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 3 of the License, or
13 * (at your option) any later version.
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
20 * You should have received a copy of the GNU General Public License
21 * along with this program; if not, write to the Free Software
22 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
27 #include "claws-features.h"
31 #include <glib/gi18n.h>
35 #include <gdk/gdkkeysyms.h>
38 #include "mainwindow.h"
39 #include "summaryview.h"
43 #include "alertpanel.h"
48 #include "prefs_common.h"
50 #include "prefs_gtk.h"
52 #define PREFS_BLOCK_NAME "AttRemover"
54 static struct _AttRemover
{
62 typedef struct _AttRemover AttRemover
;
64 static PrefParam prefs
[] = {
65 {"win_width", "-1", &AttRemoverData
.win_width
, P_INT
, NULL
,
67 {"win_height", "-1", &AttRemoverData
.win_height
, P_INT
, NULL
,
78 static MimeInfo
*find_first_text_part(MimeInfo
*partinfo
)
80 while (partinfo
&& partinfo
->type
!= MIMETYPE_TEXT
) {
81 partinfo
= procmime_mimeinfo_next(partinfo
);
87 static gboolean
key_pressed_cb(GtkWidget
*widget
, GdkEventKey
*event
,
88 AttRemover
*attremover
)
90 if (event
&& event
->keyval
== GDK_KEY_Escape
)
91 gtk_widget_destroy(attremover
->window
);
96 static void cancelled_event_cb(GtkWidget
*widget
, AttRemover
*attremover
)
98 gtk_widget_destroy(attremover
->window
);
101 static void size_allocate_cb(GtkWidget
*widget
, GtkAllocation
*allocation
)
103 cm_return_if_fail(allocation
!= NULL
);
105 gtk_window_get_size(GTK_WINDOW(widget
),
106 &AttRemoverData
.win_width
, &AttRemoverData
.win_height
);
109 static gint
save_new_message(MsgInfo
*oldmsg
, MsgInfo
*newmsg
, MimeInfo
*info
,
110 gboolean has_attachment
)
113 FolderItem
*item
= oldmsg
->folder
;
114 MsgFlags flags
= {0, 0};
117 finalmsg
= procmsg_msginfo_new_from_mimeinfo(newmsg
, info
);
119 procmsg_msginfo_free(&newmsg
);
123 debug_print("finalmsg %s\n", finalmsg
->plaintext_file
);
125 flags
.tmp_flags
= oldmsg
->flags
.tmp_flags
;
126 flags
.perm_flags
= oldmsg
->flags
.perm_flags
;
129 flags
.tmp_flags
&= ~MSG_HAS_ATTACHMENT
;
131 oldmsg
->flags
.perm_flags
&= ~MSG_LOCKED
;
132 msgnum
= folder_item_add_msg(item
, finalmsg
->plaintext_file
, &flags
, TRUE
);
134 g_warning("could not add message without attachments");
135 procmsg_msginfo_free(&newmsg
);
136 procmsg_msginfo_free(&finalmsg
);
139 folder_item_remove_msg(item
, oldmsg
->msgnum
);
140 finalmsg
->msgnum
= msgnum
;
141 procmsg_msginfo_free(&newmsg
);
142 procmsg_msginfo_free(&finalmsg
);
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
);
154 #define MIMEINFO_NOT_ATTACHMENT(m) \
155 ((m)->type == MIMETYPE_MESSAGE || (m)->type == MIMETYPE_MULTIPART)
157 static void remove_attachments_cb(GtkWidget
*widget
, AttRemover
*attremover
)
159 MainWindow
*mainwin
= mainwindow_get_mainwindow();
160 SummaryView
*summaryview
= mainwin
->summaryview
;
161 GtkTreeModel
*model
= attremover
->model
;
164 MimeInfo
*info
, *parent
, *last
, *partinfo
;
166 gint att_all
= 0, att_removed
= 0, msgnum
;
167 gboolean to_removal
, iter_valid
=TRUE
;
169 newmsg
= procmsg_msginfo_copy(attremover
->msginfo
);
170 info
= procmime_scan_message(newmsg
);
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
);
180 main_window_cursor_wait(mainwin
);
181 summary_freeze(summaryview
);
182 folder_item_update_freeze();
185 while (partinfo
&& iter_valid
) {
186 if (MIMEINFO_NOT_ATTACHMENT(partinfo
)) {
188 partinfo
= procmime_mimeinfo_next(partinfo
);
193 gtk_tree_model_get(model
, &iter
, ATT_REMOVER_TOGGLE
,
197 partinfo
= procmime_mimeinfo_next(partinfo
);
198 iter_valid
= gtk_tree_model_iter_next(model
, &iter
);
203 partinfo
= procmime_mimeinfo_next(partinfo
);
204 iter_valid
= gtk_tree_model_iter_next(model
, &iter
);
206 g_node_destroy(parent
->node
);
212 if (!(parent
= procmime_mimeinfo_parent(partinfo
)))
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)
221 gint pos
= g_node_child_position(parent
->node
, partinfo
->node
);
222 g_node_unlink(partinfo
->node
);
224 if ((child
= g_node_first_child(partinfo
->node
))) {
225 g_node_unlink(child
);
226 g_node_insert(parent
->node
, pos
, child
);
228 g_node_destroy(partinfo
->node
);
230 child
= g_node_last_child(parent
->node
);
231 partinfo
= child
? child
->data
: parent
;
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
;
241 } else if (partinfo
->node
->parent
)
242 partinfo
= (MimeInfo
*) partinfo
->node
->parent
->data
;
245 msgnum
= save_new_message(attremover
->msginfo
, newmsg
, info
,
246 (att_all
- att_removed
> 0));
249 folder_item_update_thaw();
250 summary_thaw(summaryview
);
251 main_window_cursor_normal(mainwin
);
254 summary_select_by_msgnum(summaryview
, msgnum
, TRUE
);
256 gtk_widget_destroy(attremover
->window
);
259 static void remove_toggled_cb(GtkCellRendererToggle
*cell
, gchar
*path_str
,
262 GtkTreeModel
*model
= (GtkTreeModel
*)data
;
264 GtkTreePath
*path
= gtk_tree_path_new_from_string (path_str
);
267 gtk_tree_model_get_iter (model
, &iter
, path
);
268 gtk_tree_model_get (model
, &iter
, ATT_REMOVER_TOGGLE
, &fixed
, -1);
271 gtk_list_store_set (GTK_LIST_STORE (model
), &iter
, ATT_REMOVER_TOGGLE
, fixed
, -1);
273 gtk_tree_path_free (path
);
276 static void fill_attachment_store(GtkTreeView
*list_view
, MimeInfo
*partinfo
)
279 gchar
*label
, *content_type
;
281 GtkListStore
*list_store
= GTK_LIST_STORE(gtk_tree_view_get_model
282 (GTK_TREE_VIEW(list_view
)));
284 partinfo
= find_first_text_part(partinfo
);
285 partinfo
= procmime_mimeinfo_next(partinfo
);
290 if (MIMEINFO_NOT_ATTACHMENT(partinfo
)) {
291 partinfo
= procmime_mimeinfo_next(partinfo
);
295 content_type
= procmime_get_content_type_str(
296 partinfo
->type
, partinfo
->subtype
);
298 name
= g_markup_escape_text(procmime_mimeinfo_get_parameter(partinfo
, "filename"), -1);
300 name
= g_markup_escape_text(procmime_mimeinfo_get_parameter(partinfo
, "name"), -1);
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
);
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
,
314 g_free(content_type
);
315 partinfo
= procmime_mimeinfo_next(partinfo
);
319 static void remove_attachments_dialog(AttRemover
*attremover
)
323 GtkTreeView
*list_view
;
325 GtkTreeViewColumn
*column
;
326 GtkCellRenderer
*renderer
;
327 GtkWidget
*scrollwin
;
330 GtkWidget
*cancel_btn
;
331 MimeInfo
*info
= procmime_scan_message(attremover
->msginfo
);
333 static GdkGeometry geometry
;
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_type_hint(GTK_WINDOW(window
), GDK_WINDOW_TYPE_HINT_DIALOG
);
340 gtk_window_set_modal(GTK_WINDOW(window
), TRUE
);
342 g_signal_connect(G_OBJECT(window
), "delete_event",
343 G_CALLBACK(cancelled_event_cb
), attremover
);
344 g_signal_connect(G_OBJECT(window
), "key_press_event",
345 G_CALLBACK(key_pressed_cb
), attremover
);
346 g_signal_connect(G_OBJECT(window
), "size_allocate",
347 G_CALLBACK(size_allocate_cb
), NULL
);
349 vbox
= gtk_box_new(GTK_ORIENTATION_VERTICAL
, VBOX_BORDER
);
350 gtk_container_add(GTK_CONTAINER(window
), vbox
);
352 model
= GTK_TREE_MODEL(gtk_list_store_new(N_ATT_REMOVER_COLUMNS
,
356 list_view
= GTK_TREE_VIEW(gtk_tree_view_new_with_model(model
));
357 g_object_unref(model
);
358 gtk_tree_view_set_rules_hint(list_view
, prefs_common_get_prefs()->use_stripes_everywhere
);
360 renderer
= gtk_cell_renderer_toggle_new();
361 g_signal_connect(renderer
, "toggled", G_CALLBACK(remove_toggled_cb
), model
);
362 column
= gtk_tree_view_column_new_with_attributes
365 "active", ATT_REMOVER_TOGGLE
,
367 gtk_tree_view_append_column(GTK_TREE_VIEW(list_view
), column
);
369 renderer
= gtk_cell_renderer_text_new();
370 column
= gtk_tree_view_column_new_with_attributes
373 "markup", ATT_REMOVER_INFO
,
375 gtk_tree_view_append_column(GTK_TREE_VIEW(list_view
), column
);
376 fill_attachment_store(list_view
, info
);
378 scrollwin
= gtk_scrolled_window_new(NULL
, NULL
);
379 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrollwin
),
380 GTK_SHADOW_ETCHED_OUT
);
381 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrollwin
),
382 GTK_POLICY_AUTOMATIC
, GTK_POLICY_AUTOMATIC
);
383 gtk_container_add(GTK_CONTAINER(scrollwin
), GTK_WIDGET(list_view
));
384 gtk_container_set_border_width(GTK_CONTAINER(scrollwin
), 4);
385 gtk_box_pack_start(GTK_BOX(vbox
), scrollwin
, TRUE
, TRUE
, 0);
387 gtkut_stock_button_set_create(&hbbox
, &cancel_btn
, NULL
, _("_Cancel"),
388 &ok_btn
, NULL
, _("_OK"),
390 gtk_box_pack_end(GTK_BOX(vbox
), hbbox
, FALSE
, FALSE
, 0);
391 gtk_container_set_border_width(GTK_CONTAINER(hbbox
), HSPACING_NARROW
);
392 gtk_widget_grab_default(ok_btn
);
394 g_signal_connect(G_OBJECT(ok_btn
), "clicked",
395 G_CALLBACK(remove_attachments_cb
), attremover
);
396 g_signal_connect(G_OBJECT(cancel_btn
), "clicked",
397 G_CALLBACK(cancelled_event_cb
), attremover
);
399 if (!geometry
.min_height
) {
400 geometry
.min_width
= 450;
401 geometry
.min_height
= 350;
404 gtk_window_set_geometry_hints(GTK_WINDOW(window
), NULL
, &geometry
,
406 gtk_window_set_default_size(GTK_WINDOW(window
), attremover
->win_width
,
407 attremover
->win_height
);
409 attremover
->window
= window
;
410 attremover
->model
= model
;
412 gtk_widget_show_all(window
);
413 gtk_widget_grab_focus(ok_btn
);
416 static void remove_attachments(GSList
*msglist
)
418 MainWindow
*mainwin
= mainwindow_get_mainwindow();
419 SummaryView
*summaryview
= mainwin
->summaryview
;
422 gint stripped_msgs
= 0;
425 if (alertpanel_full(_("Destroy attachments"),
426 _("Do you really want to remove all attachments from "
427 "the selected messages?\n\n"
428 "The deleted data will be unrecoverable."),
429 NULL
, _("_Cancel"), "edit-delete", _("_Delete"), NULL
, NULL
,
430 ALERTFOCUS_SECOND
, FALSE
, NULL
, ALERT_QUESTION
) != G_ALERTALTERNATE
)
433 main_window_cursor_wait(summaryview
->mainwin
);
434 summary_freeze(summaryview
);
435 folder_item_update_freeze();
438 for (cur
= msglist
; cur
; cur
= cur
->next
) {
439 MsgInfo
*msginfo
= (MsgInfo
*)cur
->data
;
440 MsgInfo
*newmsg
= NULL
;
441 MimeInfo
*info
= NULL
;
442 MimeInfo
*partinfo
= NULL
;
443 MimeInfo
*nextpartinfo
= NULL
;
447 total_msgs
++; /* count all processed messages */
449 newmsg
= procmsg_msginfo_copy(msginfo
);
450 info
= procmime_scan_message(newmsg
);
452 if ( !(partinfo
= find_first_text_part(info
)) ) {
453 procmsg_msginfo_free(&newmsg
);
456 /* only strip attachments where there is at least one */
457 nextpartinfo
= procmime_mimeinfo_next(partinfo
);
459 partinfo
->node
->next
= NULL
;
460 partinfo
->node
->children
= NULL
;
461 info
->node
->children
->data
= partinfo
;
463 msgnum
= save_new_message(msginfo
, newmsg
, info
, FALSE
);
465 stripped_msgs
++; /* count messages with removed attachment(s) */
468 if (stripped_msgs
== 0) {
469 alertpanel_notice(_("The selected messages don't have any attachments."));
471 if (stripped_msgs
!= total_msgs
)
472 alertpanel_notice(_("Attachments removed from %d of the %d selected messages."),
473 stripped_msgs
, total_msgs
);
475 alertpanel_notice(_("Attachments removed from all %d selected messages."), total_msgs
);
479 folder_item_update_thaw();
480 summary_thaw(summaryview
);
481 main_window_cursor_normal(summaryview
->mainwin
);
484 summary_select_by_msgnum(summaryview
, msgnum
, TRUE
);
488 static void remove_attachments_ui(GtkAction
*action
, gpointer data
)
490 MainWindow
*mainwin
= mainwindow_get_mainwindow();
491 GSList
*msglist
= summary_get_selected_msg_list(mainwin
->summaryview
);
492 MimeInfo
*info
= NULL
, *partinfo
= NULL
;
494 if (summary_is_locked(mainwin
->summaryview
) || !msglist
)
497 if (g_slist_length(msglist
) == 1) {
498 info
= procmime_scan_message(msglist
->data
);
500 partinfo
= find_first_text_part(info
);
501 partinfo
= procmime_mimeinfo_next(partinfo
);
504 alertpanel_notice(_("This message doesn't have any attachments."));
506 AttRemoverData
.msginfo
= msglist
->data
;
507 remove_attachments_dialog(&AttRemoverData
);
510 remove_attachments(msglist
);
512 g_slist_free(msglist
);
515 static GtkActionEntry remove_att_main_menu
[] = {{
517 NULL
, N_("Remove attachments..."), NULL
, NULL
, G_CALLBACK(remove_attachments_ui
)
520 static guint context_menu_id
= 0;
521 static guint main_menu_id
= 0;
523 gint
plugin_init(gchar
**error
)
525 MainWindow
*mainwin
= mainwindow_get_mainwindow();
528 if( !check_plugin_version(MAKE_NUMERIC_VERSION(3,6,1,27),
529 VERSION_NUMERIC
, _("AttRemover"), error
) )
532 gtk_action_group_add_actions(mainwin
->action_group
, remove_att_main_menu
,
533 1, (gpointer
)mainwin
);
534 MENUITEM_ADDUI_ID_MANAGER(mainwin
->ui_manager
, "/Menu/Message", "RemoveAtt",
535 "Message/RemoveAtt", GTK_UI_MANAGER_MENUITEM
,
537 MENUITEM_ADDUI_ID_MANAGER(mainwin
->ui_manager
, "/Menus/SummaryViewPopup", "RemoveAtt",
538 "Message/RemoveAtt", GTK_UI_MANAGER_MENUITEM
,
541 prefs_set_default(prefs
);
542 rcpath
= g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S
, COMMON_RC
, NULL
);
543 prefs_read_config(prefs
, PREFS_BLOCK_NAME
, rcpath
, NULL
);
549 gboolean
plugin_done(void)
551 MainWindow
*mainwin
= mainwindow_get_mainwindow();
555 rc_file_path
= g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S
,
557 pref_file
= prefs_write_open(rc_file_path
);
558 g_free(rc_file_path
);
560 if (!pref_file
|| prefs_set_block_label(pref_file
, PREFS_BLOCK_NAME
) < 0)
563 if (prefs_write_param(prefs
, pref_file
->fp
) < 0) {
564 g_warning("failed to write AttRemover plugin configuration");
565 prefs_file_close_revert(pref_file
);
569 if (fprintf(pref_file
->fp
, "\n") < 0) {
570 FILE_OP_ERROR(rc_file_path
, "fprintf");
571 prefs_file_close_revert(pref_file
);
573 prefs_file_close(pref_file
);
578 MENUITEM_REMUI_MANAGER(mainwin
->ui_manager
,mainwin
->action_group
, "Message/RemoveAtt", main_menu_id
);
581 MENUITEM_REMUI_MANAGER(mainwin
->ui_manager
,mainwin
->action_group
, "Message/RemoveAtt", context_menu_id
);
587 const gchar
*plugin_name(void)
589 return _("AttRemover");
592 const gchar
*plugin_desc(void)
594 return _("This plugin removes attachments from mails.\n\n"
595 "Warning: this operation will be completely "
596 "un-cancellable and the deleted attachments will "
597 "be lost forever, and ever, and ever.");
600 const gchar
*plugin_type(void)
605 const gchar
*plugin_licence(void)
610 const gchar
*plugin_version(void)
615 struct PluginFeature
*plugin_provides(void)
617 static struct PluginFeature features
[] =
618 { {PLUGIN_UTILITY
, N_("Attachment handling")},
619 {PLUGIN_NOTHING
, NULL
}};