1 /* Python plugin for Claws Mail
2 * Copyright (C) 2009 Holger Berndt
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.
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.
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/>.
20 # include "claws-features.h"
23 #include "composewindowtype.h"
24 #include "accounttype.h"
26 #include "clawsmailmodule.h"
27 #include "foldertype.h"
28 #include "messageinfotype.h"
30 #include "mainwindow.h"
32 #include "summaryview.h"
33 #include "gtk/combobox.h"
36 #include <glib/gi18n.h>
38 #include <structmember.h>
49 } clawsmail_ComposeWindowObject
;
51 static void ComposeWindow_dealloc(clawsmail_ComposeWindowObject
* self
)
53 Py_XDECREF(self
->ui_manager
);
54 Py_XDECREF(self
->text
);
55 Py_XDECREF(self
->replyinfo
);
56 Py_XDECREF(self
->fwdinfo
);
57 Py_TYPE(self
)->tp_free((PyObject
*)self
);
60 static void flush_gtk_queue(void)
62 while(gtk_events_pending())
66 static void store_py_object(PyObject
**target
, PyObject
*obj
)
80 static void composewindow_set_compose(clawsmail_ComposeWindowObject
*self
, Compose
*compose
)
82 self
->compose
= compose
;
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
));
87 store_py_object(&(self
->replyinfo
), clawsmail_messageinfo_new(compose
->replyinfo
));
88 store_py_object(&(self
->fwdinfo
), clawsmail_messageinfo_new(compose
->fwdinfo
));
91 static int ComposeWindow_init(clawsmail_ComposeWindowObject
*self
, PyObject
*args
, PyObject
*kwds
)
94 PrefsAccount
*ac
= NULL
;
98 gboolean did_find_compose
;
99 Compose
*compose
= NULL
;
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
};
108 PyArg_ParseTupleAndKeywords(args
, kwds
, "|sb", kwlist
, &ss
, &open_window
);
111 mainwin
= mainwindow_get_mainwindow();
112 item
= mainwin
->summaryview
->folder_item
;
113 did_find_compose
= FALSE
;
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
;
122 if(!did_find_compose
) {
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
;
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
;
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
;
150 if(!did_find_compose
)
153 composewindow_set_compose(self
, compose
);
154 gtk_widget_show_all(compose
->window
);
160 /* this is here because wrapping GTK_EDITABLEs in PyGTK is buggy */
161 static PyObject
* get_python_object_from_gtk_entry(GtkWidget
*entry
)
163 return Py_BuildValue("s", gtk_entry_get_text(GTK_ENTRY(entry
)));
166 static PyObject
* set_gtk_entry_from_python_object(GtkWidget
*entry
, PyObject
*args
)
170 if(!PyArg_ParseTuple(args
, "s", &ss
))
173 gtk_entry_set_text(GTK_ENTRY(entry
), ss
);
179 static PyObject
* ComposeWindow_get_subject(clawsmail_ComposeWindowObject
*self
, PyObject
*args
)
181 return get_python_object_from_gtk_entry(self
->compose
->subject_entry
);
184 static PyObject
* ComposeWindow_set_subject(clawsmail_ComposeWindowObject
*self
, PyObject
*args
)
187 ret
= set_gtk_entry_from_python_object(self
->compose
->subject_entry
, args
);
192 static PyObject
* ComposeWindow_get_from(clawsmail_ComposeWindowObject
*self
, PyObject
*args
)
194 return get_python_object_from_gtk_entry(self
->compose
->from_name
);
197 static PyObject
* ComposeWindow_set_from(clawsmail_ComposeWindowObject
*self
, PyObject
*args
)
200 ret
= set_gtk_entry_from_python_object(self
->compose
->from_name
, args
);
205 static PyObject
* ComposeWindow_add_To(clawsmail_ComposeWindowObject
*self
, PyObject
*args
)
209 if(!PyArg_ParseTuple(args
, "s", &ss
))
212 compose_entry_append(self
->compose
, ss
, COMPOSE_TO
, PREF_NONE
);
220 static PyObject
* ComposeWindow_add_Cc(clawsmail_ComposeWindowObject
*self
, PyObject
*args
)
224 if(!PyArg_ParseTuple(args
, "s", &ss
))
227 compose_entry_append(self
->compose
, ss
, COMPOSE_CC
, PREF_NONE
);
235 static PyObject
* ComposeWindow_add_Bcc(clawsmail_ComposeWindowObject
*self
, PyObject
*args
)
239 if(!PyArg_ParseTuple(args
, "s", &ss
))
242 compose_entry_append(self
->compose
, ss
, COMPOSE_BCC
, PREF_NONE
);
250 static PyObject
* ComposeWindow_attach(clawsmail_ComposeWindowObject
*self
, PyObject
*args
)
253 Py_ssize_t size
, iEl
;
256 if(!PyArg_ParseTuple(args
, "O!", &PyList_Type
, &olist
))
259 size
= PyList_Size(olist
);
260 for(iEl
= 0; iEl
< size
; iEl
++) {
262 PyObject
*element
= PyList_GET_ITEM(olist
, iEl
);
268 if(!PyArg_Parse(element
, "s", &ss
)) {
274 list
= g_list_prepend(list
, ss
);
278 compose_attach_from_list(self
->compose
, list
, FALSE
);
287 static PyObject
* ComposeWindow_get_header_list(clawsmail_ComposeWindowObject
*self
, PyObject
*args
)
292 retval
= Py_BuildValue("[]");
293 for(walk
= self
->compose
->header_list
; walk
; walk
= walk
->next
) {
294 ComposeHeaderEntry
*headerentry
= walk
->data
;
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
));
301 if(text
&& strcmp("", text
)) {
305 ee
= Py_BuildValue("(ss)", header
, text
);
306 ok
= PyList_Append(retval
, ee
);
317 static PyObject
* ComposeWindow_set_header_list(clawsmail_ComposeWindowObject
*self
, PyObject
*args
)
321 PyObject
*headerlist
;
322 Py_ssize_t headerlistsize
;
325 if(!PyArg_ParseTuple(args
, "O!", &PyList_Type
, &headerlist
))
328 headerlistsize
= PyList_Size(headerlist
);
329 num
= g_slist_length(self
->compose
->header_list
);
331 /* check correctness of argument before deleting old content */
332 for(iEl
= 0; iEl
< headerlistsize
; iEl
++) {
334 PyObject
*headerfield
;
335 PyObject
*headercontent
;
337 /* check that we got a list of tuples with two elements */
338 element
= PyList_GET_ITEM(headerlist
, iEl
);
339 if(!element
|| !PyObject_TypeCheck(element
, &PyTuple_Type
) || (PyTuple_Size(element
) != 2)) {
340 PyErr_SetString(PyExc_LookupError
, "Argument to set_header_list() must be a list of tuples with two bytestrings");
344 /* check that the two tuple elements are strings */
345 headerfield
= PyTuple_GetItem(element
, 0);
346 headercontent
= PyTuple_GetItem(element
, 1);
347 if(!headerfield
|| !headercontent
348 || !PyObject_TypeCheck(headerfield
, &PyBytes_Type
) || !PyObject_TypeCheck(headercontent
, &PyBytes_Type
)) {
349 PyErr_SetString(PyExc_LookupError
, "Argument to set_header_list() must be a list of tuples with two bytestrings");
354 /* delete old headers */
355 for(walk
= self
->compose
->header_list
; walk
; walk
= walk
->next
) {
356 ComposeHeaderEntry
*headerentry
= walk
->data
;
357 gtk_entry_set_text(GTK_ENTRY(headerentry
->entry
), "");
360 /* if given header list is bigger than current header list, add dummy values */
361 while(num
< headerlistsize
) {
362 compose_entry_append(self
->compose
, "dummy1dummy2dummy3", COMPOSE_TO
, PREF_NONE
);
366 /* set headers to new values */
367 for(iEl
= 0; iEl
< headerlistsize
; iEl
++) {
369 PyObject
*headerfield
;
370 PyObject
*headercontent
;
371 ComposeHeaderEntry
*headerentry
;
372 GtkEditable
*editable
;
375 element
= PyList_GET_ITEM(headerlist
, iEl
);
376 headerfield
= PyTuple_GetItem(element
, 0);
377 headercontent
= PyTuple_GetItem(element
, 1);
379 headerentry
= g_slist_nth_data(self
->compose
->header_list
, iEl
);
381 /* set header field */
382 editable
= GTK_EDITABLE(gtk_bin_get_child(GTK_BIN(headerentry
->combo
)));
383 gtk_editable_delete_text(editable
, 0, -1);
384 gtk_editable_insert_text(editable
, PyBytes_AsString(headerfield
), -1, &pos
);
386 /* set header content */
387 gtk_entry_set_text(GTK_ENTRY(headerentry
->entry
), PyBytes_AsString(headercontent
));
394 static PyObject
* ComposeWindow_add_header(clawsmail_ComposeWindowObject
*self
, PyObject
*args
)
400 if(!PyArg_ParseTuple(args
, "ss", &header
, &text
))
403 /* add a dummy, and modify it then */
404 compose_entry_append(self
->compose
, "dummy1dummy2dummy3", COMPOSE_TO
, PREF_NONE
);
405 num
= g_slist_length(self
->compose
->header_list
);
407 ComposeHeaderEntry
*headerentry
;
408 headerentry
= g_slist_nth_data(self
->compose
->header_list
, num
-2);
410 GtkEditable
*editable
;
412 gtk_entry_set_text(GTK_ENTRY(headerentry
->entry
), text
);
413 editable
= GTK_EDITABLE(gtk_bin_get_child(GTK_BIN(headerentry
->combo
)));
414 gtk_editable_delete_text(editable
, 0, -1);
415 gtk_editable_insert_text(editable
, header
, -1, &pos
);
425 static PyObject
* ComposeWindow_get_account_selection(clawsmail_ComposeWindowObject
*self
, PyObject
*args
)
427 if(GTK_IS_COMBO_BOX(self
->compose
->account_combo
))
428 return get_gobj_from_address(self
->compose
->account_combo
);
434 static PyObject
* ComposeWindow_save_message_to(clawsmail_ComposeWindowObject
*self
, PyObject
*args
)
438 if(!PyArg_ParseTuple(args
, "O", &arg
))
441 if(PyBytes_Check(arg
)) {
442 GtkEditable
*editable
;
445 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self
->compose
->savemsg_checkbtn
), TRUE
);
447 editable
= GTK_EDITABLE(gtk_bin_get_child(GTK_BIN(self
->compose
->savemsg_combo
)));
448 gtk_editable_delete_text(editable
, 0, -1);
449 gtk_editable_insert_text(editable
, PyBytes_AsString(arg
), -1, &pos
);
451 else if(clawsmail_folder_check(arg
)) {
452 GtkEditable
*editable
;
455 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self
->compose
->savemsg_checkbtn
), TRUE
);
457 editable
= GTK_EDITABLE(gtk_bin_get_child(GTK_BIN(self
->compose
->savemsg_combo
)));
458 gtk_editable_delete_text(editable
, 0, -1);
459 gtk_editable_insert_text(editable
, folder_item_get_identifier(clawsmail_folder_get_item(arg
)), -1, &pos
);
461 else if (arg
== Py_None
){
462 /* turn off checkbutton */
463 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self
->compose
->savemsg_checkbtn
), FALSE
);
466 PyErr_SetString(PyExc_TypeError
, "function takes exactly one argument which may be a folder object, a string, or None");
476 static PyObject
* ComposeWindow_set_modified(clawsmail_ComposeWindowObject
*self
, PyObject
*args
)
479 gboolean old_modified
;
481 if(!PyArg_ParseTuple(args
, "b", &modified
))
484 old_modified
= self
->compose
->modified
;
486 self
->compose
->modified
= (modified
!= 0);
488 /* If the modified state changed, rewrite window title.
489 * This partly duplicates functionality in compose.c::compose_set_title().
490 * While it's nice to not have to modify Claws Mail for this to work,
491 * it would be cleaner to export that function in Claws Mail. */
492 if((strcmp(gtk_window_get_title(GTK_WINDOW(self
->compose
->window
)), _("Compose message")) != 0) &&
493 (old_modified
!= self
->compose
->modified
)) {
498 edited
= self
->compose
->modified
? _(" [Edited]") : "";
499 subject
= gtk_editable_get_chars(GTK_EDITABLE(self
->compose
->subject_entry
), 0, -1);
500 if(subject
&& strlen(subject
))
501 str
= g_strdup_printf(_("%s - Compose message%s"),
504 str
= g_strdup_printf(_("[no subject] - Compose message%s"), edited
);
505 gtk_window_set_title(GTK_WINDOW(self
->compose
->window
), str
);
516 static PyObject
* get_account(clawsmail_ComposeWindowObject
*self
, void *closure
)
518 if(self
->compose
->account
) {
519 return clawsmail_account_new(self
->compose
->account
);
524 static int set_account(clawsmail_ComposeWindowObject
*self
, PyObject
*value
, void *closure
)
526 PrefsAccount
*target_account
;
529 PyErr_SetString(PyExc_TypeError
, "Cannot delete 'account' attribute");
533 if(!clawsmail_account_check(value
)) {
534 PyErr_SetString(PyExc_TypeError
, "ComposeWindow.account: Can only assign an account");
539 target_account
= clawsmail_account_get_account(value
);
540 if(!target_account
) {
541 PyErr_SetString(PyExc_TypeError
, "Account value broken");
545 if(!self
->compose
|| !self
->compose
->account_combo
) {
546 PyErr_SetString(PyExc_RuntimeError
, "ComposeWindow: Cannot access account");
550 combobox_select_by_data(GTK_COMBO_BOX(self
->compose
->account_combo
), target_account
->account_id
);
557 static PyMethodDef ComposeWindow_methods
[] = {
558 {"set_subject", (PyCFunction
)ComposeWindow_set_subject
, METH_VARARGS
,
559 "set_subject(text) - set subject to text\n"
561 "Set the subject to text. text must be a string."},
563 {"get_subject", (PyCFunction
)ComposeWindow_get_subject
, METH_NOARGS
,
564 "get_subject() - get subject\n"
566 "Get a string of the current subject entry."},
568 {"set_from", (PyCFunction
)ComposeWindow_set_from
, METH_VARARGS
,
569 "set_from(text) - set From header entry to text\n"
571 "Set the From header entry to text. text must be a string.\n"
572 "Beware: No sanity checking is performed."},
574 {"get_from", (PyCFunction
)ComposeWindow_get_from
, METH_NOARGS
,
575 "get_from - get From header entry\n"
577 "Get a string of the current From header entry."},
579 {"add_To", (PyCFunction
)ComposeWindow_add_To
, METH_VARARGS
,
580 "add_To(text) - append another To header with text\n"
582 "Add another header line with the combo box set to To:, and the\n"
583 "content set to text."},
585 {"add_Cc", (PyCFunction
)ComposeWindow_add_Cc
, METH_VARARGS
,
586 "add_Cc(text) - append another Cc header with text\n"
588 "Add another header line with the combo box set to Cc:, and the\n"
589 "content set to text."},
591 {"add_Bcc", (PyCFunction
)ComposeWindow_add_Bcc
, METH_VARARGS
,
592 "add_Bcc(text) - append another Bcc header with text\n"
594 "Add another header line with the combo box set to Bcc:, and the\n"
595 "content set to text."},
597 {"add_header", (PyCFunction
)ComposeWindow_add_header
, METH_VARARGS
,
598 "add_header(headername, text) - add a custom header\n"
600 "Adds a custom header with the header set to headername, and the\n"
601 "contents set to text."},
603 {"get_header_list", (PyCFunction
)ComposeWindow_get_header_list
, METH_NOARGS
,
604 "get_header_list() - get list of headers\n"
606 "Gets a list of headers that are currently defined in the compose window.\n"
607 "The return value is a list of tuples, where the first tuple element is\n"
608 "the header name (entry in the combo box) and the second element is the contents."},
610 {"set_header_list", (PyCFunction
)ComposeWindow_set_header_list
, METH_VARARGS
,
611 "set_header_list(list_of_header_value_pairs) - set list of headers\n"
613 "Sets the list of headers that are currently defined in the compose window.\n"
614 "This function overwrites the current setting.\n\n"
615 "The parameter is expected to be a list of header name and value tuples,\n"
616 "analogous to the return value of the get_header_list() function."},
618 {"attach", (PyCFunction
)ComposeWindow_attach
, METH_VARARGS
,
619 "attach(filenames) - attach a list of files\n"
621 "Attach files to the mail. The filenames argument is a list of\n"
622 "string of the filenames that are being attached."},
624 {"get_account_selection", (PyCFunction
)ComposeWindow_get_account_selection
, METH_NOARGS
,
625 "get_account_selection() - get account selection widget\n"
627 "Returns the account selection combo box as a gtk.ComboBox"},
629 {"save_message_to", (PyCFunction
)ComposeWindow_save_message_to
, METH_VARARGS
,
630 "save_message_to(folder) - save message to folder id\n"
632 "Set the folder where the sent message will be saved to. folder may be\n"
633 "a Folder, a string of the folder identifier (e.g. #mh/foo/bar), or\n"
634 "None is which case the message will not be saved at all."},
636 {"set_modified", (PyCFunction
)ComposeWindow_set_modified
, METH_VARARGS
,
637 "set_modified(bool) - set or unset modification marker of compose window\n"
639 "Set or unset the modification marker of the compose window. This marker determines\n"
640 "for example whether you get a confirmation dialog when closing the compose window\n"
642 "In the usual case, Claws Mail keeps track of the modification status itself.\n"
643 "However, there are cases when it might be desirable to overwrite the marker,\n"
644 "for example because a compose_any script modifies the body or subject which\n"
645 "can be regarded compose window preprocessing and should not trigger a confirmation\n"
646 "dialog on close like a manual edit."},
651 static PyMemberDef ComposeWindow_members
[] = {
652 {"ui_manager", T_OBJECT_EX
, offsetof(clawsmail_ComposeWindowObject
, ui_manager
), 0,
653 "ui_manager - the gtk.UIManager of the compose window"},
655 {"text", T_OBJECT_EX
, offsetof(clawsmail_ComposeWindowObject
, text
), 0,
656 "text - the gtk.TextView widget of the message body"},
658 {"replyinfo", T_OBJECT_EX
, offsetof(clawsmail_ComposeWindowObject
, replyinfo
), 0,
659 "replyinfo - The MessageInfo object of the message that is being replied to, or None"},
661 {"forwardinfo", T_OBJECT_EX
, offsetof(clawsmail_ComposeWindowObject
, fwdinfo
), 0,
662 "forwardinfo - The MessageInfo object of the message that is being forwarded, or None"},
667 static PyGetSetDef ComposeWindow_getset
[] = {
668 {"account", (getter
)get_account
, (setter
)set_account
,
669 "account - the account corresponding to this compose window", NULL
},
674 static PyTypeObject clawsmail_ComposeWindowType
= {
675 PyVarObject_HEAD_INIT(NULL
, 0)
676 "clawsmail.ComposeWindow", /*tp_name*/
677 sizeof(clawsmail_ComposeWindowObject
), /*tp_basicsize*/
679 (destructor
)ComposeWindow_dealloc
, /*tp_dealloc*/
686 0, /*tp_as_sequence*/
694 Py_TPFLAGS_DEFAULT
, /*tp_flags*/
696 "ComposeWindow objects. Optional argument to constructor: sender account address. ",
699 0, /* tp_richcompare */
700 0, /* tp_weaklistoffset */
703 ComposeWindow_methods
, /* tp_methods */
704 ComposeWindow_members
, /* tp_members */
705 ComposeWindow_getset
, /* tp_getset */
708 0, /* tp_descr_get */
709 0, /* tp_descr_set */
710 0, /* tp_dictoffset */
711 (initproc
)ComposeWindow_init
, /* tp_init */
719 0, /* tp_subclasses */
722 #if ((PY_MAJOR_VERSION == 2 && PY_MINOR_VERSION >= 6) || \
723 (PY_MAJOR_VERSION == 3))
724 0, /* tp_version_tag */
726 #if (PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION >= 4)
731 gboolean
cmpy_add_composewindow(PyObject
*module
)
733 clawsmail_ComposeWindowType
.tp_new
= PyType_GenericNew
;
734 if(PyType_Ready(&clawsmail_ComposeWindowType
) < 0)
737 Py_INCREF(&clawsmail_ComposeWindowType
);
738 return (PyModule_AddObject(module
, "ComposeWindow", (PyObject
*)&clawsmail_ComposeWindowType
) == 0);
741 PyObject
* clawsmail_compose_new(PyObject
*module
, Compose
*compose
)
743 PyObject
*class, *dict
;
744 PyObject
*self
, *args
, *kw
;
751 dict
= PyModule_GetDict(module
);
752 class = PyDict_GetItemString(dict
, "ComposeWindow");
753 args
= Py_BuildValue("()");
754 kw
= Py_BuildValue("{s:b}", "__open_window", 0);
755 self
= PyObject_Call(class, args
, kw
);
758 composewindow_set_compose((clawsmail_ComposeWindowObject
*)self
, compose
);