1 /* Python plugin for Claws Mail
2  * Copyright (C) 2009 Holger Berndt
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 #ifdef HAVE_CONFIG_H
19 #  include "config.h"
20 #include "claws-features.h"
21 #endif
22 
23 #include <Python.h>
24 
25 #include <glib.h>
26 #include <glib/gi18n.h>
27 
28 #include <errno.h>
29 
30 #include "common/hooks.h"
31 #include "common/plugin.h"
32 #include "common/version.h"
33 #include "common/utils.h"
34 #include "gtk/menu.h"
35 #include "main.h"
36 #include "mainwindow.h"
37 #include "prefs_toolbar.h"
38 
39 #include "python-shell.h"
40 #include "python-hooks.h"
41 #include "clawsmailmodule.h"
42 #include "file-utils.h"
43 #include "python_prefs.h"
44 
45 #define PYTHON_SCRIPTS_BASE_DIR "python-scripts"
46 #define PYTHON_SCRIPTS_MAIN_DIR "main"
47 #define PYTHON_SCRIPTS_COMPOSE_DIR "compose"
48 #define PYTHON_SCRIPTS_AUTO_DIR "auto"
49 #define PYTHON_SCRIPTS_AUTO_STARTUP "startup"
50 #define PYTHON_SCRIPTS_AUTO_SHUTDOWN "shutdown"
51 #define PYTHON_SCRIPTS_AUTO_COMPOSE "compose_any"
52 #define PYTHON_SCRIPTS_ACTION_PREFIX "Tools/PythonScripts/"
53 
54 static GSList *menu_id_list = NULL;
55 static GSList *python_mainwin_scripts_id_list = NULL;
56 static GSList *python_mainwin_scripts_names = NULL;
57 static GSList *python_compose_scripts_names = NULL;
58 
59 static GtkWidget *python_console = NULL;
60 
61 static gulong hook_compose_create = 0;
62 
python_console_delete_event(GtkWidget * widget,GdkEvent * event,gpointer data)63 static gboolean python_console_delete_event(GtkWidget *widget, GdkEvent *event, gpointer data)
64 {
65   MainWindow *mainwin;
66   GtkToggleAction *action;
67 
68   mainwin =  mainwindow_get_mainwindow();
69   action = GTK_TOGGLE_ACTION(gtk_action_group_get_action(mainwin->action_group, "Tools/ShowPythonConsole"));
70   gtk_toggle_action_set_active(action, FALSE);
71   return TRUE;
72 }
73 
size_allocate_cb(GtkWidget * widget,GtkAllocation * allocation)74 static void size_allocate_cb(GtkWidget *widget, GtkAllocation *allocation)
75 {
76 	cm_return_if_fail(allocation != NULL);
77 
78 	python_config.console_win_width = allocation->width;
79 	python_config.console_win_height = allocation->height;
80 }
81 
setup_python_console(void)82 static void setup_python_console(void)
83 {
84   GtkWidget *vbox;
85   GtkWidget *console;
86   static GdkGeometry geometry;
87 
88   python_console = gtk_window_new(GTK_WINDOW_TOPLEVEL);
89   g_signal_connect (G_OBJECT(python_console), "size_allocate",
90 		   G_CALLBACK (size_allocate_cb), NULL);
91   if (!geometry.min_height) {
92 	  geometry.min_width = 600;
93 	  geometry.min_height = 400;
94   }
95 
96   gtk_window_set_geometry_hints(GTK_WINDOW(python_console), NULL, &geometry,
97 				    GDK_HINT_MIN_SIZE);
98   gtk_widget_set_size_request(python_console, python_config.console_win_width,
99 				  python_config.console_win_height);
100 
101   vbox = gtk_vbox_new(FALSE, 0);
102   gtk_container_add(GTK_CONTAINER(python_console), vbox);
103 
104   console = parasite_python_shell_new();
105   gtk_box_pack_start(GTK_BOX(vbox), console, TRUE, TRUE, 0);
106 
107   g_signal_connect(python_console, "delete-event", G_CALLBACK(python_console_delete_event), NULL);
108 
109   gtk_widget_show_all(python_console);
110 
111   parasite_python_shell_focus(PARASITE_PYTHON_SHELL(console));
112 }
113 
show_hide_python_console(GtkToggleAction * action,gpointer callback_data)114 static void show_hide_python_console(GtkToggleAction *action, gpointer callback_data)
115 {
116   if(gtk_toggle_action_get_active(action)) {
117     if(!python_console)
118       setup_python_console();
119     gtk_widget_show(python_console);
120   }
121   else {
122     gtk_widget_hide(python_console);
123   }
124 }
125 
remove_python_scripts_menus(void)126 static void remove_python_scripts_menus(void)
127 {
128   GSList *walk;
129   MainWindow *mainwin;
130 
131   mainwin =  mainwindow_get_mainwindow();
132 
133   /* toolbar */
134   for(walk = python_mainwin_scripts_names; walk; walk = walk->next)
135     prefs_toolbar_unregister_plugin_item(TOOLBAR_MAIN, "Python", walk->data);
136 
137   /* ui */
138   for(walk = python_mainwin_scripts_id_list; walk; walk = walk->next)
139       gtk_ui_manager_remove_ui(mainwin->ui_manager, GPOINTER_TO_UINT(walk->data));
140   g_slist_free(python_mainwin_scripts_id_list);
141   python_mainwin_scripts_id_list = NULL;
142 
143   /* actions */
144   for(walk = python_mainwin_scripts_names; walk; walk = walk->next) {
145     GtkAction *action;
146     gchar *entry;
147     entry = g_strconcat(PYTHON_SCRIPTS_ACTION_PREFIX, walk->data, NULL);
148     action = gtk_action_group_get_action(mainwin->action_group, entry);
149     g_free(entry);
150     if(action)
151       gtk_action_group_remove_action(mainwin->action_group, action);
152     g_free(walk->data);
153   }
154   g_slist_free(python_mainwin_scripts_names);
155   python_mainwin_scripts_names = NULL;
156 
157   /* compose scripts */
158   for(walk = python_compose_scripts_names; walk; walk = walk->next) {
159     prefs_toolbar_unregister_plugin_item(TOOLBAR_COMPOSE, "Python", walk->data);
160     g_free(walk->data);
161   }
162   g_slist_free(python_compose_scripts_names);
163   python_compose_scripts_names = NULL;
164 }
165 
extract_filename(const gchar * str)166 static gchar* extract_filename(const gchar *str)
167 {
168   gchar *filename;
169 
170   filename = g_strrstr(str, "/");
171   if(!filename || *(filename+1) == '\0') {
172     debug_print("Error: Could not extract filename from %s\n", str);
173     return NULL;
174   }
175   filename++;
176   return filename;
177 }
178 
run_script_file(const gchar * filename,Compose * compose)179 static void run_script_file(const gchar *filename, Compose *compose)
180 {
181   FILE *fp;
182   fp = claws_fopen(filename, "r");
183   if(!fp) {
184     debug_print("Error: Could not open file '%s'\n", filename);
185     return;
186   }
187   put_composewindow_into_module(compose);
188   if(PyRun_SimpleFile(fp, filename) == 0)
189     debug_print("Problem running script file '%s'\n", filename);
190   claws_fclose(fp);
191 }
192 
run_auto_script_file_if_it_exists(const gchar * autofilename,Compose * compose)193 static void run_auto_script_file_if_it_exists(const gchar *autofilename, Compose *compose)
194 {
195   gchar *auto_filepath;
196 
197   /* execute auto/autofilename, if it exists */
198   auto_filepath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S,
199       PYTHON_SCRIPTS_BASE_DIR, G_DIR_SEPARATOR_S,
200       PYTHON_SCRIPTS_AUTO_DIR, G_DIR_SEPARATOR_S, autofilename, NULL);
201   if(file_exist(auto_filepath, FALSE))
202     run_script_file(auto_filepath, compose);
203   g_free(auto_filepath);
204 }
205 
python_mainwin_script_callback(GtkAction * action,gpointer data)206 static void python_mainwin_script_callback(GtkAction *action, gpointer data)
207 {
208   char *filename;
209 
210   filename = extract_filename(data);
211   if(!filename)
212     return;
213   filename = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, PYTHON_SCRIPTS_BASE_DIR, G_DIR_SEPARATOR_S, PYTHON_SCRIPTS_MAIN_DIR, G_DIR_SEPARATOR_S, filename, NULL);
214   run_script_file(filename, NULL);
215   g_free(filename);
216 }
217 
218 typedef struct _ComposeActionData ComposeActionData;
219 struct _ComposeActionData {
220   gchar *name;
221   Compose *compose;
222 };
223 
python_compose_script_callback(GtkAction * action,gpointer data)224 static void python_compose_script_callback(GtkAction *action, gpointer data)
225 {
226   char *filename;
227   ComposeActionData *dat = (ComposeActionData*)data;
228 
229   filename = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, PYTHON_SCRIPTS_BASE_DIR, G_DIR_SEPARATOR_S, PYTHON_SCRIPTS_COMPOSE_DIR, G_DIR_SEPARATOR_S, dat->name, NULL);
230   run_script_file(filename, dat->compose);
231 
232   g_free(filename);
233 }
234 
mainwin_toolbar_callback(gpointer parent,const gchar * item_name,gpointer data)235 static void mainwin_toolbar_callback(gpointer parent, const gchar *item_name, gpointer data)
236 {
237 	gchar *script;
238 	script = g_strconcat(PYTHON_SCRIPTS_ACTION_PREFIX, item_name, NULL);
239 	python_mainwin_script_callback(NULL, script);
240 	g_free(script);
241 }
242 
compose_toolbar_callback(gpointer parent,const gchar * item_name,gpointer data)243 static void compose_toolbar_callback(gpointer parent, const gchar *item_name, gpointer data)
244 {
245   gchar *filename;
246 
247   filename = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S,
248       PYTHON_SCRIPTS_BASE_DIR, G_DIR_SEPARATOR_S,
249       PYTHON_SCRIPTS_COMPOSE_DIR, G_DIR_SEPARATOR_S,
250       item_name, NULL);
251   run_script_file(filename, (Compose*)parent);
252   g_free(filename);
253 }
254 
make_sure_script_directory_exists(const gchar * subdir)255 static char* make_sure_script_directory_exists(const gchar *subdir)
256 {
257   char *dir;
258   char *retval = NULL;
259   dir = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, PYTHON_SCRIPTS_BASE_DIR, G_DIR_SEPARATOR_S, subdir, NULL);
260   if(!g_file_test(dir, G_FILE_TEST_IS_DIR)) {
261     if(g_mkdir(dir, 0777) != 0)
262       retval = g_strdup_printf("Could not create directory '%s': %s", dir, g_strerror(errno));
263   }
264   g_free(dir);
265   return retval;
266 }
267 
make_sure_directories_exist(char ** error)268 static int make_sure_directories_exist(char **error)
269 {
270   const char* dirs[] = {
271       ""
272       , PYTHON_SCRIPTS_MAIN_DIR
273       , PYTHON_SCRIPTS_COMPOSE_DIR
274       , PYTHON_SCRIPTS_AUTO_DIR
275       , NULL
276   };
277   const char **dir = dirs;
278 
279   *error = NULL;
280 
281   while(*dir) {
282     *error = make_sure_script_directory_exists(*dir);
283     if(*error)
284       break;
285     dir++;
286   }
287 
288   return (*error == NULL);
289 }
290 
migrate_scripts_out_of_base_dir(void)291 static void migrate_scripts_out_of_base_dir(void)
292 {
293   char *base_dir;
294   GDir *dir;
295   const char *filename;
296   gchar *dest_dir;
297 
298   base_dir = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, PYTHON_SCRIPTS_BASE_DIR, NULL);
299   dir = g_dir_open(base_dir, 0, NULL);
300   g_free(base_dir);
301   if(!dir)
302     return;
303 
304   dest_dir = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S,
305       PYTHON_SCRIPTS_BASE_DIR, G_DIR_SEPARATOR_S,
306       PYTHON_SCRIPTS_MAIN_DIR, NULL);
307   if(!g_file_test(dest_dir, G_FILE_TEST_IS_DIR)) {
308     if(g_mkdir(dest_dir, 0777) != 0) {
309       g_free(dest_dir);
310       g_dir_close(dir);
311       return;
312     }
313   }
314 
315   while((filename = g_dir_read_name(dir)) != NULL) {
316     gchar *filepath;
317     filepath = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, PYTHON_SCRIPTS_BASE_DIR, G_DIR_SEPARATOR_S, filename, NULL);
318     if(g_file_test(filepath, G_FILE_TEST_IS_REGULAR)) {
319       gchar *dest_file;
320       dest_file = g_strconcat(dest_dir, G_DIR_SEPARATOR_S, filename, NULL);
321       if(move_file(filepath, dest_file, FALSE) == 0)
322         debug_print("Python plugin: Moved file '%s' to %s subdir\n", filename, PYTHON_SCRIPTS_MAIN_DIR);
323       else
324         debug_print("Python plugin: Warning: Could not move file '%s' to %s subdir\n", filename, PYTHON_SCRIPTS_MAIN_DIR);
325       g_free(dest_file);
326     }
327     g_free(filepath);
328   }
329   g_dir_close(dir);
330   g_free(dest_dir);
331 }
332 
333 
create_mainwindow_menus_and_items(GSList * filenames,gint num_entries)334 static void create_mainwindow_menus_and_items(GSList *filenames, gint num_entries)
335 {
336   MainWindow *mainwin;
337   gint ii;
338   GSList *walk;
339   GtkActionEntry *entries;
340 
341   /* create menu items */
342   entries = g_new0(GtkActionEntry, num_entries);
343   ii = 0;
344   mainwin =  mainwindow_get_mainwindow();
345   for(walk = filenames; walk; walk = walk->next) {
346     entries[ii].name = g_strconcat(PYTHON_SCRIPTS_ACTION_PREFIX, walk->data, NULL);
347     entries[ii].label = walk->data;
348     entries[ii].callback = G_CALLBACK(python_mainwin_script_callback);
349     gtk_action_group_add_actions(mainwin->action_group, &(entries[ii]), 1, (gpointer)entries[ii].name);
350     ii++;
351   }
352   for(ii = 0; ii < num_entries; ii++) {
353     guint id;
354 
355     python_mainwin_scripts_names = g_slist_prepend(python_mainwin_scripts_names, g_strdup(entries[ii].label));
356     MENUITEM_ADDUI_ID_MANAGER(mainwin->ui_manager, "/Menu/" PYTHON_SCRIPTS_ACTION_PREFIX, entries[ii].label,
357         entries[ii].name, GTK_UI_MANAGER_MENUITEM, id)
358     python_mainwin_scripts_id_list = g_slist_prepend(python_mainwin_scripts_id_list, GUINT_TO_POINTER(id));
359 
360     prefs_toolbar_register_plugin_item(TOOLBAR_MAIN, "Python", entries[ii].label, mainwin_toolbar_callback, NULL);
361   }
362 
363   g_free(entries);
364 }
365 
366 
367 /* this function doesn't really create menu items, but prepares a list that can be used
368  * in the compose create hook. It does however register the scripts for the toolbar editor */
create_compose_menus_and_items(GSList * filenames)369 static void create_compose_menus_and_items(GSList *filenames)
370 {
371   GSList *walk;
372   for(walk = filenames; walk; walk = walk->next) {
373     python_compose_scripts_names = g_slist_prepend(python_compose_scripts_names, g_strdup((gchar*)walk->data));
374     prefs_toolbar_register_plugin_item(TOOLBAR_COMPOSE, "Python", (gchar*)walk->data, compose_toolbar_callback, NULL);
375   }
376 }
377 
378 static GtkActionEntry compose_tools_python_actions[] = {
379     {"Tools/PythonScripts", NULL, N_("Python scripts"), NULL, NULL, NULL },
380 };
381 
ComposeActionData_destroy_cb(gpointer data)382 static void ComposeActionData_destroy_cb(gpointer data)
383 {
384   ComposeActionData *dat = (ComposeActionData*)data;
385   g_free(dat->name);
386   g_free(dat);
387 }
388 
my_compose_create_hook(gpointer cw,gpointer data)389 static gboolean my_compose_create_hook(gpointer cw, gpointer data)
390 {
391   gint ii;
392   GSList *walk;
393   GtkActionEntry *entries;
394   GtkActionGroup *action_group;
395   Compose *compose = (Compose*)cw;
396   guint num_entries = g_slist_length(python_compose_scripts_names);
397 
398   action_group = gtk_action_group_new("PythonPlugin");
399   gtk_action_group_add_actions(action_group, compose_tools_python_actions, 1, NULL);
400   entries = g_new0(GtkActionEntry, num_entries);
401   ii = 0;
402   for(walk = python_compose_scripts_names; walk; walk = walk->next) {
403     ComposeActionData *dat;
404 
405     entries[ii].name = walk->data;
406     entries[ii].label = walk->data;
407     entries[ii].callback = G_CALLBACK(python_compose_script_callback);
408 
409     dat = g_new0(ComposeActionData, 1);
410     dat->name = g_strdup(walk->data);
411     dat->compose = compose;
412 
413     gtk_action_group_add_actions_full(action_group, &(entries[ii]), 1, dat, ComposeActionData_destroy_cb);
414     ii++;
415   }
416   gtk_ui_manager_insert_action_group(compose->ui_manager, action_group, 0);
417 
418   MENUITEM_ADDUI_MANAGER(compose->ui_manager, "/Menu/Tools", "PythonScripts",
419       "Tools/PythonScripts", GTK_UI_MANAGER_MENU)
420 
421   for(ii = 0; ii < num_entries; ii++) {
422     MENUITEM_ADDUI_MANAGER(compose->ui_manager, "/Menu/" PYTHON_SCRIPTS_ACTION_PREFIX, entries[ii].label,
423         entries[ii].name, GTK_UI_MANAGER_MENUITEM)
424   }
425 
426   g_free(entries);
427 
428   run_auto_script_file_if_it_exists(PYTHON_SCRIPTS_AUTO_COMPOSE, compose);
429 
430   return FALSE;
431 }
432 
433 
refresh_scripts_in_dir(const gchar * subdir,ToolbarType toolbar_type)434 static void refresh_scripts_in_dir(const gchar *subdir, ToolbarType toolbar_type)
435 {
436   char *scripts_dir;
437   GDir *dir;
438   GError *error = NULL;
439   const char *filename;
440   GSList *filenames = NULL;
441   GSList *walk;
442   gint num_entries;
443 
444   scripts_dir = g_strconcat(get_rc_dir(),
445       G_DIR_SEPARATOR_S, PYTHON_SCRIPTS_BASE_DIR,
446       G_DIR_SEPARATOR_S, subdir,
447       NULL);
448   debug_print("Refreshing: %s\n", scripts_dir);
449 
450   dir = g_dir_open(scripts_dir, 0, &error);
451   g_free(scripts_dir);
452 
453   if(!dir) {
454     debug_print("Could not open directory '%s': %s\n", subdir, error->message);
455     g_error_free(error);
456     return;
457   }
458 
459   /* get filenames */
460   num_entries = 0;
461   while((filename = g_dir_read_name(dir)) != NULL) {
462     char *fn;
463 
464     fn = g_strdup(filename);
465     filenames = g_slist_prepend(filenames, fn);
466     num_entries++;
467   }
468   g_dir_close(dir);
469 
470   if(toolbar_type == TOOLBAR_MAIN)
471     create_mainwindow_menus_and_items(filenames, num_entries);
472   else if(toolbar_type == TOOLBAR_COMPOSE)
473     create_compose_menus_and_items(filenames);
474 
475   /* cleanup */
476   for(walk = filenames; walk; walk = walk->next)
477     g_free(walk->data);
478   g_slist_free(filenames);
479 }
480 
browse_python_scripts_dir(GtkAction * action,gpointer data)481 static void browse_python_scripts_dir(GtkAction *action, gpointer data)
482 {
483   gchar *uri;
484   GdkAppLaunchContext *launch_context;
485   GError *error = NULL;
486   MainWindow *mainwin;
487 
488   mainwin =  mainwindow_get_mainwindow();
489   if(!mainwin) {
490       debug_print("Browse Python scripts: Problems getting the mainwindow\n");
491       return;
492   }
493   launch_context = gdk_app_launch_context_new();
494   gdk_app_launch_context_set_screen(launch_context, gtk_widget_get_screen(mainwin->window));
495   uri = g_strconcat("file://", get_rc_dir(), G_DIR_SEPARATOR_S, PYTHON_SCRIPTS_BASE_DIR, G_DIR_SEPARATOR_S, NULL);
496   g_app_info_launch_default_for_uri(uri, G_APP_LAUNCH_CONTEXT(launch_context), &error);
497 
498   if(error) {
499       debug_print("Could not open scripts dir browser: '%s'\n", error->message);
500       g_error_free(error);
501   }
502 
503   g_object_unref(launch_context);
504   g_free(uri);
505 }
506 
refresh_python_scripts_menus(GtkAction * action,gpointer data)507 static void refresh_python_scripts_menus(GtkAction *action, gpointer data)
508 {
509   remove_python_scripts_menus();
510 
511   migrate_scripts_out_of_base_dir();
512 
513   refresh_scripts_in_dir(PYTHON_SCRIPTS_MAIN_DIR, TOOLBAR_MAIN);
514   refresh_scripts_in_dir(PYTHON_SCRIPTS_COMPOSE_DIR, TOOLBAR_COMPOSE);
515 }
516 
517 static GtkToggleActionEntry mainwindow_tools_python_toggle[] = {
518     {"Tools/ShowPythonConsole", NULL, N_("Show Python console..."),
519         NULL, NULL, G_CALLBACK(show_hide_python_console), FALSE},
520 };
521 
522 static GtkActionEntry mainwindow_tools_python_actions[] = {
523     {"Tools/PythonScripts", NULL, N_("Python scripts"), NULL, NULL, NULL },
524     {"Tools/PythonScripts/Refresh", NULL, N_("Refresh"),
525         NULL, NULL, G_CALLBACK(refresh_python_scripts_menus) },
526     {"Tools/PythonScripts/Browse", NULL, N_("Browse"),
527         NULL, NULL, G_CALLBACK(browse_python_scripts_dir) },
528     {"Tools/PythonScripts/---", NULL, "---", NULL, NULL, NULL },
529 };
530 
python_menu_init(char ** error)531 static int python_menu_init(char **error)
532 {
533   MainWindow *mainwin;
534   guint id;
535 
536   mainwin =  mainwindow_get_mainwindow();
537   if(!mainwin) {
538     *error = g_strdup("Could not get main window");
539     return 0;
540   }
541 
542   gtk_action_group_add_toggle_actions(mainwin->action_group, mainwindow_tools_python_toggle, 1, mainwin);
543   gtk_action_group_add_actions(mainwin->action_group, mainwindow_tools_python_actions, 3, mainwin);
544 
545   MENUITEM_ADDUI_ID_MANAGER(mainwin->ui_manager, "/Menu/Tools", "ShowPythonConsole",
546       "Tools/ShowPythonConsole", GTK_UI_MANAGER_MENUITEM, id)
547   menu_id_list = g_slist_prepend(menu_id_list, GUINT_TO_POINTER(id));
548 
549   MENUITEM_ADDUI_ID_MANAGER(mainwin->ui_manager, "/Menu/Tools", "PythonScripts",
550       "Tools/PythonScripts", GTK_UI_MANAGER_MENU, id)
551   menu_id_list = g_slist_prepend(menu_id_list, GUINT_TO_POINTER(id));
552 
553   MENUITEM_ADDUI_ID_MANAGER(mainwin->ui_manager, "/Menu/Tools/PythonScripts", "Refresh",
554       "Tools/PythonScripts/Refresh", GTK_UI_MANAGER_MENUITEM, id)
555   menu_id_list = g_slist_prepend(menu_id_list, GUINT_TO_POINTER(id));
556 
557   MENUITEM_ADDUI_ID_MANAGER(mainwin->ui_manager, "/Menu/Tools/PythonScripts", "Browse",
558       "Tools/PythonScripts/Browse", GTK_UI_MANAGER_MENUITEM, id)
559   menu_id_list = g_slist_prepend(menu_id_list, GUINT_TO_POINTER(id));
560 
561   MENUITEM_ADDUI_ID_MANAGER(mainwin->ui_manager, "/Menu/Tools/PythonScripts", "Separator1",
562       "Tools/PythonScripts/---", GTK_UI_MANAGER_SEPARATOR, id)
563   menu_id_list = g_slist_prepend(menu_id_list, GUINT_TO_POINTER(id));
564 
565   refresh_python_scripts_menus(NULL, NULL);
566 
567   return !0;
568 }
569 
python_menu_done(void)570 static void python_menu_done(void)
571 {
572   MainWindow *mainwin;
573 
574   mainwin = mainwindow_get_mainwindow();
575 
576   if(mainwin && !claws_is_exiting()) {
577     GSList *walk;
578 
579     remove_python_scripts_menus();
580 
581     for(walk = menu_id_list; walk; walk = walk->next)
582       gtk_ui_manager_remove_ui(mainwin->ui_manager, GPOINTER_TO_UINT(walk->data));
583     MENUITEM_REMUI_MANAGER(mainwin->ui_manager, mainwin->action_group, "Tools/ShowPythonConsole", 0);
584     MENUITEM_REMUI_MANAGER(mainwin->ui_manager, mainwin->action_group, "Tools/PythonScripts", 0);
585     MENUITEM_REMUI_MANAGER(mainwin->ui_manager, mainwin->action_group, "Tools/PythonScripts/Refresh", 0);
586     MENUITEM_REMUI_MANAGER(mainwin->ui_manager, mainwin->action_group, "Tools/PythonScripts/Browse", 0);
587     MENUITEM_REMUI_MANAGER(mainwin->ui_manager, mainwin->action_group, "Tools/PythonScripts/---", 0);
588   }
589 }
590 
591 
get_StringIO_instance(void)592 static PyObject *get_StringIO_instance(void)
593 {
594   PyObject *module_StringIO = NULL;
595   PyObject *class_StringIO = NULL;
596   PyObject *inst_StringIO = NULL;
597 
598   module_StringIO = PyImport_ImportModule("cStringIO");
599   if(!module_StringIO) {
600     debug_print("Error getting traceback: Could not import module cStringIO\n");
601     goto done;
602   }
603 
604   class_StringIO = PyObject_GetAttrString(module_StringIO, "StringIO");
605   if(!class_StringIO) {
606     debug_print("Error getting traceback: Could not get StringIO class\n");
607     goto done;
608   }
609 
610   inst_StringIO = PyObject_CallObject(class_StringIO, NULL);
611   if(!inst_StringIO) {
612     debug_print("Error getting traceback: Could not create an instance of the StringIO class\n");
613     goto done;
614   }
615 
616 done:
617   Py_XDECREF(module_StringIO);
618   Py_XDECREF(class_StringIO);
619 
620   return inst_StringIO;
621 }
622 
get_exception_information(PyObject * inst_StringIO)623 static char* get_exception_information(PyObject *inst_StringIO)
624 {
625   char *retval = NULL;
626   PyObject *meth_getvalue = NULL;
627   PyObject *result_getvalue = NULL;
628 
629   if(!inst_StringIO)
630     goto done;
631 
632   if(PySys_SetObject("stderr", inst_StringIO) != 0) {
633     debug_print("Error getting traceback: Could not set sys.stderr to a StringIO instance\n");
634     goto done;
635   }
636 
637   meth_getvalue = PyObject_GetAttrString(inst_StringIO, "getvalue");
638   if(!meth_getvalue) {
639     debug_print("Error getting traceback: Could not get the getvalue method of the StringIO instance\n");
640     goto done;
641   }
642 
643   PyErr_Print();
644 
645   result_getvalue = PyObject_CallObject(meth_getvalue, NULL);
646   if(!result_getvalue) {
647     debug_print("Error getting traceback: Could not call the getvalue method of the StringIO instance\n");
648     goto done;
649   }
650 
651   retval = g_strdup(PyString_AsString(result_getvalue));
652 
653 done:
654 
655   Py_XDECREF(meth_getvalue);
656   Py_XDECREF(result_getvalue);
657 
658   return retval ? retval : g_strdup("Unspecified error occurred");
659 }
660 
log_func(const gchar * log_domain,GLogLevelFlags log_level,const gchar * message,gpointer user_data)661 static void log_func(const gchar *log_domain, GLogLevelFlags log_level, const gchar *message, gpointer user_data)
662 {
663 }
664 
plugin_init(gchar ** error)665 gint plugin_init(gchar **error)
666 {
667   guint log_handler;
668   int parasite_retval;
669   PyObject *inst_StringIO = NULL;
670 
671   /* Version check */
672   if(!check_plugin_version(MAKE_NUMERIC_VERSION(3,7,6,9), VERSION_NUMERIC, _("Python"), error))
673     return -1;
674 
675   /* init/load prefs */
676   python_prefs_init();
677 
678   /* load hooks */
679   hook_compose_create = hooks_register_hook(COMPOSE_CREATED_HOOKLIST, my_compose_create_hook, NULL);
680   if(hook_compose_create == 0) {
681     *error = g_strdup(_("Failed to register \"compose create hook\" in the Python plugin"));
682     return -1;
683   }
684 
685   /* script directories */
686   if(!make_sure_directories_exist(error))
687     goto err;
688 
689   /* initialize python interpreter */
690   Py_Initialize();
691 
692   /* The Python C API only offers to print an exception to sys.stderr. In order to catch it
693    * in a string, a StringIO object is created, to which sys.stderr can be redirected in case
694    * an error occurred. */
695   inst_StringIO = get_StringIO_instance();
696 
697   /* initialize Claws Mail Python module */
698   initclawsmail();
699   if(PyErr_Occurred()) {
700     *error = get_exception_information(inst_StringIO);
701     goto err;
702   }
703 
704   if(PyRun_SimpleString("import clawsmail") == -1) {
705     *error = g_strdup("Error importing the clawsmail module");
706     goto err;
707   }
708 
709   /* initialize python interactive shell */
710   log_handler = g_log_set_handler(NULL, G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE | G_LOG_LEVEL_INFO, log_func, NULL);
711   parasite_retval = parasite_python_init(error);
712   g_log_remove_handler(NULL, log_handler);
713   if(!parasite_retval) {
714     goto err;
715   }
716 
717   /* load menu options */
718   if(!python_menu_init(error)) {
719     goto err;
720   }
721 
722   /* problems here are not fatal */
723   run_auto_script_file_if_it_exists(PYTHON_SCRIPTS_AUTO_STARTUP, NULL);
724 
725   debug_print("Python plugin loaded\n");
726 
727   return 0;
728 
729 err:
730   hooks_unregister_hook(COMPOSE_CREATED_HOOKLIST, hook_compose_create);
731   Py_XDECREF(inst_StringIO);
732   return -1;
733 }
734 
plugin_done(void)735 gboolean plugin_done(void)
736 {
737   hooks_unregister_hook(COMPOSE_CREATED_HOOKLIST, hook_compose_create);
738 
739   run_auto_script_file_if_it_exists(PYTHON_SCRIPTS_AUTO_SHUTDOWN, NULL);
740 
741   python_menu_done();
742 
743   if(python_console) {
744     gtk_widget_destroy(python_console);
745     python_console = NULL;
746   }
747 
748   /* finialize python interpreter */
749   Py_Finalize();
750 
751   parasite_python_done();
752 
753   /* save prefs */
754   python_prefs_done();
755 
756   debug_print("Python plugin done and unloaded.\n");
757   return FALSE;
758 }
759 
plugin_name(void)760 const gchar *plugin_name(void)
761 {
762   return _("Python");
763 }
764 
plugin_desc(void)765 const gchar *plugin_desc(void)
766 {
767   return _("This plugin provides Python integration features.\n"
768       "Python code can be entered interactively into an embedded Python console, "
769       "under Tools -> Show Python console, or stored in scripts.\n\n"
770       "These scripts are then available via the menu. You can assign "
771       "keyboard shortcuts to them just like it is done with other menu items. "
772       "You can also put buttons for script invocation into the toolbars "
773       "using Claws Mail's builtin toolbar editor.\n\n"
774       "You can provide scripts working on the main window by placing files "
775       "into ~/.claws-mail/python-scripts/main.\n\n"
776       "You can also provide scripts working on an open compose window "
777       "by placing files into ~/.claws-mail/python-scripts/compose.\n\n"
778       "The folder ~/.claws-mail/python-scripts/auto/ may contain some "
779       "scripts that are automatically executed when certain events "
780       "occur. Currently, the following files in this directory "
781       "are recognised:\n\n"
782       "compose_any\n"
783       "Gets executed whenever a compose window is opened, no matter "
784       "if that opening happened as a result of composing a new message, "
785       "replying or forwarding a message.\n\n"
786       "startup\n"
787       "Executed at plugin load\n\n"
788       "shutdown\n"
789       "Executed at plugin unload\n\n"
790       "\nFor the most up-to-date API documentation, type\n"
791       "\n help(clawsmail)\n"
792       "\nin the interactive Python console.\n"
793       "\nThe source distribution of this plugin comes with various example scripts "
794       "in the \"examples\" subdirectory. If you wrote a script that you would be "
795       "interested in sharing, feel free to send it to me to have it considered "
796       "for inclusion in the examples.\n"
797       "\nFeedback to <berndth@gmx.de> is welcome.");
798 }
799 
plugin_type(void)800 const gchar *plugin_type(void)
801 {
802   return "GTK2";
803 }
804 
plugin_licence(void)805 const gchar *plugin_licence(void)
806 {
807   return "GPL3+";
808 }
809 
plugin_version(void)810 const gchar *plugin_version(void)
811 {
812   return VERSION;
813 }
814 
plugin_provides(void)815 struct PluginFeature *plugin_provides(void)
816 {
817   static struct PluginFeature features[] =
818     { {PLUGIN_UTILITY, N_("Python integration")},
819       {PLUGIN_NOTHING, NULL}};
820   return features;
821 }
822