Python plugin: Improve error reporting during plugin init
[claws.git] / src / plugins / python / composewindowtype.c
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 <glib.h>
24 #include <glib/gi18n.h>
25
26 #include "composewindowtype.h"
27
28 #include "clawsmailmodule.h"
29 #include "foldertype.h"
30 #include "messageinfotype.h"
31
32 #include "mainwindow.h"
33 #include "account.h"
34 #include "summaryview.h"
35
36 #include <structmember.h>
37
38 #include <string.h>
39
40 typedef struct {
41     PyObject_HEAD
42     PyObject *ui_manager;
43     PyObject *text;
44     PyObject *replyinfo;
45     Compose *compose;
46 } clawsmail_ComposeWindowObject;
47
48 static void ComposeWindow_dealloc(clawsmail_ComposeWindowObject* self)
49 {
50   Py_XDECREF(self->ui_manager);
51   Py_XDECREF(self->text);
52   Py_XDECREF(self->replyinfo);
53   self->ob_type->tp_free((PyObject*)self);
54 }
55
56 static void flush_gtk_queue(void)
57 {
58   while(gtk_events_pending())
59     gtk_main_iteration();
60 }
61
62 static void store_py_object(PyObject **target, PyObject *obj)
63 {
64   Py_XDECREF(*target);
65   if(obj)
66   {
67     Py_INCREF(obj);
68     *target = obj;
69   }
70   else {
71     Py_INCREF(Py_None);
72     *target = Py_None;
73   }
74 }
75
76 static void composewindow_set_compose(clawsmail_ComposeWindowObject *self, Compose *compose)
77 {
78   self->compose = compose;
79
80   store_py_object(&(self->ui_manager), get_gobj_from_address(compose->ui_manager));
81   store_py_object(&(self->text), get_gobj_from_address(compose->text));
82
83   store_py_object(&(self->replyinfo), clawsmail_messageinfo_new(compose->replyinfo));
84 }
85
86 static int ComposeWindow_init(clawsmail_ComposeWindowObject *self, PyObject *args, PyObject *kwds)
87 {
88   MainWindow *mainwin;
89   PrefsAccount *ac = NULL;
90   FolderItem *item;
91   GList* list;
92   GList* cur;
93   gboolean did_find_compose;
94   Compose *compose = NULL;
95   const char *ss;
96   unsigned char open_window;
97   /* if __open_window is set to 0/False,
98    * composewindow_set_compose must be called before this object is valid */
99   static char *kwlist[] = {"address", "__open_window", NULL};
100
101   ss = NULL;
102   open_window = 1;
103   PyArg_ParseTupleAndKeywords(args, kwds, "|sb", kwlist, &ss, &open_window);
104
105   if(open_window) {
106     mainwin = mainwindow_get_mainwindow();
107     item = mainwin->summaryview->folder_item;
108     did_find_compose = FALSE;
109
110     if(ss) {
111       ac = account_find_from_address(ss, FALSE);
112       if (ac && ac->protocol != A_NNTP) {
113         compose = compose_new_with_folderitem(ac, item, NULL);
114         did_find_compose = TRUE;
115       }
116     }
117     if(!did_find_compose) {
118       if (item) {
119         ac = account_find_from_item(item);
120         if (ac && ac->protocol != A_NNTP) {
121           compose = compose_new_with_folderitem(ac, item, NULL);
122           did_find_compose = TRUE;
123         }
124       }
125
126       /* use current account */
127       if (!did_find_compose && cur_account && (cur_account->protocol != A_NNTP)) {
128         compose = compose_new_with_folderitem(cur_account, item, NULL);
129         did_find_compose = TRUE;
130       }
131
132       if(!did_find_compose) {
133         /* just get the first one */
134         list = account_get_list();
135         for (cur = list ; cur != NULL ; cur = g_list_next(cur)) {
136           ac = (PrefsAccount *) cur->data;
137           if (ac->protocol != A_NNTP) {
138             compose = compose_new_with_folderitem(ac, item, NULL);
139             did_find_compose = TRUE;
140           }
141         }
142       }
143     }
144
145     if(!did_find_compose)
146       return -1;
147
148     composewindow_set_compose(self, compose);
149     gtk_widget_show_all(compose->window);
150     flush_gtk_queue();
151   }
152   return 0;
153 }
154
155 /* this is here because wrapping GTK_EDITABLEs in PyGTK is buggy */
156 static PyObject* get_python_object_from_gtk_entry(GtkWidget *entry)
157 {
158   return Py_BuildValue("s", gtk_entry_get_text(GTK_ENTRY(entry)));
159 }
160
161 static PyObject* set_gtk_entry_from_python_object(GtkWidget *entry, PyObject *args)
162 {
163   const char *ss;
164
165   if(!PyArg_ParseTuple(args, "s", &ss))
166     return NULL;
167
168   gtk_entry_set_text(GTK_ENTRY(entry), ss);
169
170   Py_INCREF(Py_None);
171   return Py_None;
172 }
173
174 static PyObject* ComposeWindow_get_subject(clawsmail_ComposeWindowObject *self, PyObject *args)
175 {
176   return get_python_object_from_gtk_entry(self->compose->subject_entry);
177 }
178
179 static PyObject* ComposeWindow_set_subject(clawsmail_ComposeWindowObject *self, PyObject *args)
180 {
181   PyObject *ret;
182   ret = set_gtk_entry_from_python_object(self->compose->subject_entry, args);
183   flush_gtk_queue();
184   return ret;
185 }
186
187 static PyObject* ComposeWindow_get_from(clawsmail_ComposeWindowObject *self, PyObject *args)
188 {
189   return get_python_object_from_gtk_entry(self->compose->from_name);
190 }
191
192 static PyObject* ComposeWindow_set_from(clawsmail_ComposeWindowObject *self, PyObject *args)
193 {
194   PyObject *ret;
195   ret = set_gtk_entry_from_python_object(self->compose->from_name, args);
196   flush_gtk_queue();
197   return ret;
198 }
199
200 static PyObject* ComposeWindow_add_To(clawsmail_ComposeWindowObject *self, PyObject *args)
201 {
202   const char *ss;
203
204   if(!PyArg_ParseTuple(args, "s", &ss))
205     return NULL;
206
207   compose_entry_append(self->compose, ss, COMPOSE_TO, PREF_NONE);
208
209   flush_gtk_queue();
210
211   Py_INCREF(Py_None);
212   return Py_None;
213 }
214
215 static PyObject* ComposeWindow_add_Cc(clawsmail_ComposeWindowObject *self, PyObject *args)
216 {
217   const char *ss;
218
219   if(!PyArg_ParseTuple(args, "s", &ss))
220     return NULL;
221
222   compose_entry_append(self->compose, ss, COMPOSE_CC, PREF_NONE);
223
224   flush_gtk_queue();
225
226   Py_INCREF(Py_None);
227   return Py_None;
228 }
229
230 static PyObject* ComposeWindow_add_Bcc(clawsmail_ComposeWindowObject *self, PyObject *args)
231 {
232   const char *ss;
233
234   if(!PyArg_ParseTuple(args, "s", &ss))
235     return NULL;
236
237   compose_entry_append(self->compose, ss, COMPOSE_BCC, PREF_NONE);
238
239   flush_gtk_queue();
240
241   Py_INCREF(Py_None);
242   return Py_None;
243 }
244
245 static PyObject* ComposeWindow_attach(clawsmail_ComposeWindowObject *self, PyObject *args)
246 {
247   PyObject *olist;
248   Py_ssize_t size, iEl;
249   GList *list = NULL;
250
251   if(!PyArg_ParseTuple(args, "O!", &PyList_Type, &olist))
252     return NULL;
253
254   size = PyList_Size(olist);
255   for(iEl = 0; iEl < size; iEl++) {
256     char *ss;
257     PyObject *element = PyList_GET_ITEM(olist, iEl);
258
259     if(!element)
260       continue;
261
262     Py_INCREF(element);
263     if(!PyArg_Parse(element, "s", &ss)) {
264       Py_DECREF(element);
265       if(list)
266         g_list_free(list);
267       return NULL;
268     }
269     list = g_list_prepend(list, ss);
270     Py_DECREF(element);
271   }
272
273   compose_attach_from_list(self->compose, list, FALSE);
274   g_list_free(list);
275
276   flush_gtk_queue();
277
278   Py_INCREF(Py_None);
279   return Py_None;
280 }
281
282 static PyObject* ComposeWindow_get_header_list(clawsmail_ComposeWindowObject *self, PyObject *args)
283 {
284   GSList *walk;
285   PyObject *retval;
286
287   retval = Py_BuildValue("[]");
288   for(walk = self->compose->header_list; walk; walk = walk->next) {
289     ComposeHeaderEntry *headerentry = walk->data;
290     const gchar *header;
291     const gchar *text;
292
293     header = gtk_editable_get_chars(GTK_EDITABLE(gtk_bin_get_child(GTK_BIN(headerentry->combo))), 0, -1);
294     text = gtk_entry_get_text(GTK_ENTRY(headerentry->entry));
295
296     if(text && strcmp("", text)) {
297       PyObject *ee;
298       int ok;
299
300       ee = Py_BuildValue("(ss)", header, text);
301       ok = PyList_Append(retval, ee);
302       Py_DECREF(ee);
303       if(ok == -1) {
304         Py_DECREF(retval);
305         return NULL;
306       }
307     }
308   }
309   return retval;
310 }
311
312 static PyObject* ComposeWindow_add_header(clawsmail_ComposeWindowObject *self, PyObject *args)
313 {
314   const char *header;
315   const char *text;
316   gint num;
317
318   if(!PyArg_ParseTuple(args, "ss", &header, &text))
319     return NULL;
320
321   /* add a dummy, and modify it then */
322   compose_entry_append(self->compose, "dummy1dummy2dummy3", COMPOSE_TO, PREF_NONE);
323   num = g_slist_length(self->compose->header_list);
324   if(num > 1) {
325     ComposeHeaderEntry *headerentry;
326     headerentry = g_slist_nth_data(self->compose->header_list, num-2);
327     if(headerentry) {
328       GtkEditable *editable;
329       gint pos;
330       gtk_entry_set_text(GTK_ENTRY(headerentry->entry), text);
331       editable = GTK_EDITABLE(gtk_bin_get_child(GTK_BIN(headerentry->combo)));
332       gtk_editable_delete_text(editable, 0, -1);
333       gtk_editable_insert_text(editable, header, -1, &pos);
334     }
335   }
336
337   flush_gtk_queue();
338
339   Py_INCREF(Py_None);
340   return Py_None;
341 }
342
343 /* this is pretty ugly, as the compose struct does not maintain a pointer to the account selection combo */
344 static PyObject* ComposeWindow_get_account_selection(clawsmail_ComposeWindowObject *self, PyObject *args)
345 {
346   GList *children, *walk;
347
348   children = gtk_container_get_children(GTK_CONTAINER(self->compose->header_table));
349   for(walk = children; walk; walk = walk->next) {
350     if(GTK_IS_HBOX(walk->data)) {
351       GList *children2, *walk2;
352       children2 = gtk_container_get_children(GTK_CONTAINER(walk->data));
353       for(walk2 = children2; walk2; walk2 = walk2->next) {
354         if(GTK_IS_EVENT_BOX(walk2->data)) {
355           return get_gobj_from_address(gtk_container_get_children(GTK_CONTAINER(walk2->data))->data);
356         }
357       }
358     }
359   }
360   Py_INCREF(Py_None);
361   return Py_None;
362 }
363
364 static PyObject* ComposeWindow_save_message_to(clawsmail_ComposeWindowObject *self, PyObject *args)
365 {
366   PyObject *arg;
367
368   if(!PyArg_ParseTuple(args, "O", &arg))
369     return NULL;
370
371   if(PyString_Check(arg)) {
372     GtkEditable *editable;
373     gint pos;
374
375     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->compose->savemsg_checkbtn), TRUE);
376
377     editable = GTK_EDITABLE(gtk_bin_get_child(GTK_BIN(self->compose->savemsg_combo)));
378     gtk_editable_delete_text(editable, 0, -1);
379     gtk_editable_insert_text(editable, PyString_AsString(arg), -1, &pos);
380   }
381   else if(clawsmail_folder_check(arg)) {
382     GtkEditable *editable;
383     gint pos;
384
385     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->compose->savemsg_checkbtn), TRUE);
386
387     editable = GTK_EDITABLE(gtk_bin_get_child(GTK_BIN(self->compose->savemsg_combo)));
388     gtk_editable_delete_text(editable, 0, -1);
389     gtk_editable_insert_text(editable, folder_item_get_identifier(clawsmail_folder_get_item(arg)), -1, &pos);
390   }
391   else if (arg == Py_None){
392     /* turn off checkbutton */
393     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->compose->savemsg_checkbtn), FALSE);
394   }
395   else {
396     PyErr_SetString(PyExc_TypeError, "function takes exactly one argument which may be a folder object, a string, or None");
397     return NULL;
398   }
399
400   flush_gtk_queue();
401
402   Py_INCREF(Py_None);
403   return Py_None;
404 }
405
406 static PyObject* ComposeWindow_set_modified(clawsmail_ComposeWindowObject *self, PyObject *args)
407 {
408   char modified = 0;
409   gboolean old_modified;
410
411   if(!PyArg_ParseTuple(args, "b", &modified))
412     return NULL;
413
414   old_modified = self->compose->modified;
415
416   self->compose->modified = (modified != 0);
417
418   /* If the modified state changed, rewrite window title.
419    * This partly duplicates functionality in compose.c::compose_set_title().
420    * While it's nice to not have to modify Claws Mail for this to work,
421    * it would be cleaner to export that function in Claws Mail. */
422   if((strcmp(gtk_window_get_title(GTK_WINDOW(self->compose->window)), _("Compose message")) != 0) &&
423       (old_modified != self->compose->modified)) {
424       gchar *str;
425       gchar *edited;
426       gchar *subject;
427
428       edited = self->compose->modified  ? _(" [Edited]") : "";
429       subject = gtk_editable_get_chars(GTK_EDITABLE(self->compose->subject_entry), 0, -1);
430       if(subject && strlen(subject))
431         str = g_strdup_printf(_("%s - Compose message%s"),
432             subject, edited);
433       else
434         str = g_strdup_printf(_("[no subject] - Compose message%s"), edited);
435       gtk_window_set_title(GTK_WINDOW(self->compose->window), str);
436       g_free(str);
437       g_free(subject);
438   }
439
440   flush_gtk_queue();
441
442   Py_INCREF(Py_None);
443   return Py_None;
444 }
445
446 static PyMethodDef ComposeWindow_methods[] = {
447     {"set_subject", (PyCFunction)ComposeWindow_set_subject, METH_VARARGS,
448      "set_subject(text) - set subject to text\n"
449      "\n"
450      "Set the subject to text. text must be a string."},
451
452     {"get_subject", (PyCFunction)ComposeWindow_get_subject, METH_NOARGS,
453      "get_subject() - get subject\n"
454      "\n"
455      "Get a string of the current subject entry."},
456
457     {"set_from", (PyCFunction)ComposeWindow_set_from, METH_VARARGS,
458      "set_from(text) - set From header entry to text\n"
459      "\n"
460      "Set the From header entry to text. text must be a string.\n"
461      "Beware: No sanity checking is performed."},
462
463     {"get_from", (PyCFunction)ComposeWindow_get_from, METH_NOARGS,
464      "get_from - get From header entry\n"
465      "\n"
466      "Get a string of the current From header entry."},
467
468     {"add_To",  (PyCFunction)ComposeWindow_add_To,  METH_VARARGS,
469      "add_To(text) - append another To header with text\n"
470      "\n"
471      "Add another header line with the combo box set to To:, and the\n"
472      "content set to text."},
473
474     {"add_Cc",  (PyCFunction)ComposeWindow_add_Cc,  METH_VARARGS,
475      "add_Cc(text) - append another Cc header with text\n"
476      "\n"
477      "Add another header line with the combo box set to Cc:, and the\n"
478      "content set to text."},
479
480     {"add_Bcc", (PyCFunction)ComposeWindow_add_Bcc, METH_VARARGS,
481      "add_Bcc(text) - append another Bcc header with text\n"
482      "\n"
483      "Add another header line with the combo box set to Bcc:, and the\n"
484      "content set to text."},
485
486     {"add_header", (PyCFunction)ComposeWindow_add_header, METH_VARARGS,
487      "add_header(headername, text) - add a custom header\n"
488      "\n"
489      "Adds a custom header with the header set to headername, and the\n"
490      "contents set to text."},
491
492     {"get_header_list", (PyCFunction)ComposeWindow_get_header_list, METH_NOARGS,
493      "get_header_list() - get list of headers\n"
494      "\n"
495      "Gets a list of headers that are currently defined in the compose window.\n"
496      "The return value is a list of tuples, where the first tuple element is\n"
497      "the header name (entry in the combo box) and the second element is the contents."},
498
499     {"attach",  (PyCFunction)ComposeWindow_attach, METH_VARARGS,
500      "attach(filenames) - attach a list of files\n"
501      "\n"
502      "Attach files to the mail. The filenames argument is a list of\n"
503      "string of the filenames that are being attached."},
504
505     {"get_account_selection", (PyCFunction)ComposeWindow_get_account_selection, METH_NOARGS,
506      "get_account_selection() - get account selection widget\n"
507      "\n"
508      "Returns the account selection combo box as a gtk.ComboBox"},
509
510     {"save_message_to", (PyCFunction)ComposeWindow_save_message_to, METH_VARARGS,
511      "save_message_to(folder) - save message to folder id\n"
512      "\n"
513      "Set the folder where the sent message will be saved to. folder may be\n"
514      "a Folder, a string of the folder identifier (e.g. #mh/foo/bar), or\n"
515      "None is which case the message will not be saved at all."},
516
517      {"set_modified", (PyCFunction)ComposeWindow_set_modified, METH_VARARGS,
518      "set_modified(bool) - set or unset modification marker of compose window\n"
519      "\n"
520      "Set or unset the modification marker of the compose window. This marker determines\n"
521      "for example whether you get a confirmation dialog when closing the compose window\n"
522      "or not.\n"
523      "In the usual case, Claws Mail keeps track of the modification status itself.\n"
524      "However, there are cases when it might be desirable to overwrite the marker,\n"
525      "for example because a compose_any script modifies the body or subject which\n"
526      "can be regarded compose window preprocessing and should not trigger a confirmation\n"
527      "dialog on close like a manual edit."},
528
529     {NULL}
530 };
531
532 static PyMemberDef ComposeWindow_members[] = {
533     {"ui_manager", T_OBJECT_EX, offsetof(clawsmail_ComposeWindowObject, ui_manager), 0,
534      "ui_manager - the gtk.UIManager of the compose window"},
535
536     {"text", T_OBJECT_EX, offsetof(clawsmail_ComposeWindowObject, text), 0,
537      "text - the gtk.TextView widget of the message body"},
538
539     {"replyinfo", T_OBJECT_EX, offsetof(clawsmail_ComposeWindowObject, replyinfo), 0,
540      "replyinfo - The MessageInfo object of the message that is being replied to, or None"},
541
542     {NULL}
543 };
544
545 static PyTypeObject clawsmail_ComposeWindowType = {
546     PyObject_HEAD_INIT(NULL)
547     0,                         /*ob_size*/
548     "clawsmail.ComposeWindow", /*tp_name*/
549     sizeof(clawsmail_ComposeWindowObject), /*tp_basicsize*/
550     0,                         /*tp_itemsize*/
551     (destructor)ComposeWindow_dealloc, /*tp_dealloc*/
552     0,                         /*tp_print*/
553     0,                         /*tp_getattr*/
554     0,                         /*tp_setattr*/
555     0,                         /*tp_compare*/
556     0,                         /*tp_repr*/
557     0,                         /*tp_as_number*/
558     0,                         /*tp_as_sequence*/
559     0,                         /*tp_as_mapping*/
560     0,                         /*tp_hash */
561     0,                         /*tp_call*/
562     0,                         /*tp_str*/
563     0,                         /*tp_getattro*/
564     0,                         /*tp_setattro*/
565     0,                         /*tp_as_buffer*/
566     Py_TPFLAGS_DEFAULT,        /*tp_flags*/
567     /* tp_doc */
568     "ComposeWindow objects. Optional argument to constructor: sender account address. ",
569     0,                         /* tp_traverse */
570     0,                         /* tp_clear */
571     0,                         /* tp_richcompare */
572     0,                         /* tp_weaklistoffset */
573     0,                         /* tp_iter */
574     0,                         /* tp_iternext */
575     ComposeWindow_methods,     /* tp_methods */
576     ComposeWindow_members,     /* tp_members */
577     0,                         /* tp_getset */
578     0,                         /* tp_base */
579     0,                         /* tp_dict */
580     0,                         /* tp_descr_get */
581     0,                         /* tp_descr_set */
582     0,                         /* tp_dictoffset */
583     (initproc)ComposeWindow_init, /* tp_init */
584     0,                         /* tp_alloc */
585     0,                         /* tp_new */
586 };
587
588 gboolean cmpy_add_composewindow(PyObject *module)
589 {
590   clawsmail_ComposeWindowType.tp_new = PyType_GenericNew;
591   if(PyType_Ready(&clawsmail_ComposeWindowType) < 0)
592     return FALSE;
593
594   Py_INCREF(&clawsmail_ComposeWindowType);
595   return (PyModule_AddObject(module, "ComposeWindow", (PyObject*)&clawsmail_ComposeWindowType) == 0);
596 }
597
598 PyObject* clawsmail_compose_new(PyObject *module, Compose *compose)
599 {
600   PyObject *class, *dict;
601   PyObject *self, *args, *kw;
602
603   if(!compose) {
604     Py_INCREF(Py_None);
605     return Py_None;
606   }
607
608   dict = PyModule_GetDict(module);
609   class = PyDict_GetItemString(dict, "ComposeWindow");
610   args = Py_BuildValue("()");
611   kw = Py_BuildValue("{s:b}", "__open_window", 0);
612   self = PyObject_Call(class, args, kw);
613   Py_DECREF(args);
614   Py_DECREF(kw);
615   composewindow_set_compose((clawsmail_ComposeWindowObject*)self, compose);
616   return self;
617 }