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