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