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