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