2 * Claws Mail -- a GTK based, lightweight, and fast e-mail client
3 * Copyright (C) 1999-2021 the Claws Mail Team
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
22 #include "claws-features.h"
27 #include <sys/types.h>
31 #include <glib/gi18n.h>
37 #include "common/claws.h"
38 #include "common/version.h"
40 #include "common/utils.h"
45 #include "prefs_gtk.h"
48 #include "spamassassin.h"
51 #include "prefs_common.h"
52 #include "alertpanel.h"
53 #include "addr_compl.h"
54 #include "file-utils.h"
56 #ifdef HAVE_SYSEXITS_H
62 #ifdef HAVE_SYS_ERRNO_H
63 #include <sys/errno.h>
68 #ifdef HAVE_SYS_TIME_H
78 #define PLUGIN_NAME (_("SpamAssassin"))
81 CHILD_RUNNING
= 1 << 0,
82 TIMEOUT_RUNNING
= 1 << 1,
85 static gulong hook_id
= HOOK_NONE
;
86 static int flags
= SPAMC_RAW_MODE
| SPAMC_SAFE_FALLBACK
| SPAMC_CHECK_ONLY
;
87 static MessageCallback message_callback
;
89 static SpamAssassinConfig config
;
91 static PrefParam param
[] = {
92 {"enable", "FALSE", &config
.enable
, P_BOOL
,
94 {"transport", "0", &config
.transport
, P_INT
,
96 {"hostname", "localhost", &config
.hostname
, P_STRING
,
98 {"port", "783", &config
.port
, P_INT
,
100 {"socket", "", &config
.socket
, P_STRING
,
102 {"process_emails", "TRUE", &config
.process_emails
, P_BOOL
,
104 {"receive_spam", "TRUE", &config
.receive_spam
, P_BOOL
,
106 {"save_folder", NULL
, &config
.save_folder
, P_STRING
,
108 {"max_size", "250", &config
.max_size
, P_INT
,
110 {"timeout", "30", &config
.timeout
, P_INT
,
112 {"username", "", &config
.username
, P_STRING
,
114 {"mark_as_read", "TRUE", &config
.mark_as_read
, P_BOOL
,
116 {"whitelist_ab", "FALSE", &config
.whitelist_ab
, P_BOOL
,
118 {"whitelist_ab_folder", N_("Any"), &config
.whitelist_ab_folder
, P_STRING
,
120 {"compress", "FALSE", &config
.compress
, P_BOOL
,
123 {NULL
, NULL
, NULL
, P_OTHER
, NULL
, NULL
, NULL
}
126 gboolean
timeout_func(gpointer data
)
128 gint
*running
= (gint
*) data
;
130 if (*running
& CHILD_RUNNING
)
133 *running
&= ~TIMEOUT_RUNNING
;
140 MSG_FILTERING_ERROR
= 2
143 static void update_flags(void)
145 /* set the SPAMC_USE_ZLIB flag according to config */
147 flags
|= SPAMC_USE_ZLIB
;
149 flags
&= ~SPAMC_USE_ZLIB
;
152 static MsgStatus
msg_is_spam(FILE *fp
)
154 struct transport trans
;
156 gboolean is_spam
= FALSE
;
162 transport_init(&trans
);
163 switch (config
.transport
) {
164 case SPAMASSASSIN_TRANSPORT_LOCALHOST
:
165 trans
.type
= TRANSPORT_LOCALHOST
;
166 trans
.port
= config
.port
;
168 case SPAMASSASSIN_TRANSPORT_TCP
:
169 trans
.type
= TRANSPORT_TCP
;
170 trans
.hostname
= config
.hostname
;
171 trans
.port
= config
.port
;
173 case SPAMASSASSIN_TRANSPORT_UNIX
:
174 trans
.type
= TRANSPORT_UNIX
;
175 trans
.socketpath
= config
.socket
;
181 if (transport_setup(&trans
, flags
) != EX_OK
) {
182 log_error(LOG_PROTOCOL
, _("SpamAssassin plugin couldn't connect to spamd.\n"));
183 debug_print("failed to setup transport\n");
184 return MSG_FILTERING_ERROR
;
187 m
.type
= MESSAGE_NONE
;
188 m
.max_len
= config
.max_size
* 1024;
189 m
.timeout
= config
.timeout
;
191 if (message_read(fileno(fp
), flags
, &m
) != EX_OK
) {
192 debug_print("failed to read message\n");
194 return MSG_FILTERING_ERROR
;
197 if (message_filter(&trans
, config
.username
, flags
, &m
) != EX_OK
) {
198 log_error(LOG_PROTOCOL
, _("SpamAssassin plugin filtering failed.\n"));
199 debug_print("filtering the message failed\n");
201 return MSG_FILTERING_ERROR
;
204 if (m
.is_spam
== EX_ISSPAM
)
209 return is_spam
? MSG_IS_SPAM
:MSG_IS_HAM
;
212 static gboolean
mail_filtering_hook(gpointer source
, gpointer data
)
214 MailFilteringData
*mail_filtering_data
= (MailFilteringData
*) source
;
215 MsgInfo
*msginfo
= mail_filtering_data
->msginfo
;
216 gboolean is_spam
= FALSE
, error
= FALSE
;
217 static gboolean warned_error
= FALSE
;
222 /* SPAMASSASSIN_DISABLED : keep test for compatibility purpose */
223 if (!config
.enable
|| config
.transport
== SPAMASSASSIN_DISABLED
) {
224 log_warning(LOG_PROTOCOL
, _("SpamAssassin plugin is disabled by its preferences.\n"));
227 debug_print("Filtering message %d\n", msginfo
->msgnum
);
228 if (message_callback
!= NULL
)
229 message_callback(_("SpamAssassin: filtering message..."));
231 if ((fp
= procmsg_open_message(msginfo
, FALSE
)) == NULL
) {
232 debug_print("failed to open message file\n");
236 if (config
.whitelist_ab
) {
237 gchar
*ab_folderpath
;
238 gboolean whitelisted
= FALSE
;
240 if (*config
.whitelist_ab_folder
== '\0' ||
241 strcasecmp(config
.whitelist_ab_folder
, "Any") == 0) {
242 /* match the whole addressbook */
243 ab_folderpath
= NULL
;
245 /* match the specific book/folder of the addressbook */
246 ab_folderpath
= config
.whitelist_ab_folder
;
249 start_address_completion(ab_folderpath
);
251 found_in_addressbook(msginfo
->from
))
253 end_address_completion();
256 debug_print("message is ham (whitelisted)\n");
263 _exit(msg_is_spam(fp
));
267 running
|= CHILD_RUNNING
;
269 g_timeout_add(50, timeout_func
, &running
);
270 running
|= TIMEOUT_RUNNING
;
272 while(running
& CHILD_RUNNING
) {
275 ret
= waitpid(pid
, &status
, WNOHANG
);
277 if (WIFEXITED(status
)) {
278 MsgStatus result
= MSG_IS_HAM
;
279 running
&= ~CHILD_RUNNING
;
280 result
= WEXITSTATUS(status
);
281 is_spam
= (result
== MSG_IS_SPAM
) ? TRUE
: FALSE
;
282 error
= (result
== MSG_FILTERING_ERROR
);
285 running
&= ~CHILD_RUNNING
;
286 } /* ret == 0 continue */
288 g_main_context_iteration(NULL
, TRUE
);
291 while (running
& TIMEOUT_RUNNING
)
292 g_main_context_iteration(NULL
, TRUE
);
298 debug_print("message is spam\n");
299 procmsg_msginfo_set_flags(msginfo
, MSG_SPAM
, 0);
300 if (config
.receive_spam
) {
301 FolderItem
*save_folder
= NULL
;
303 if ((!config
.save_folder
) ||
304 (config
.save_folder
[0] == '\0') ||
305 ((save_folder
= folder_find_item_from_identifier(config
.save_folder
)) == NULL
)) {
306 if (mail_filtering_data
->account
&& mail_filtering_data
->account
->set_trash_folder
) {
307 save_folder
= folder_find_item_from_identifier(
308 mail_filtering_data
->account
->trash_folder
);
310 debug_print("found trash folder from account's advanced settings\n");
312 if (save_folder
== NULL
&& mail_filtering_data
->account
&&
313 mail_filtering_data
->account
->folder
) {
314 save_folder
= mail_filtering_data
->account
->folder
->trash
;
316 debug_print("found trash folder from account's trash\n");
318 if (save_folder
== NULL
&& mail_filtering_data
->account
&&
319 !mail_filtering_data
->account
->folder
) {
320 if (mail_filtering_data
->account
->inbox
) {
321 FolderItem
*item
= folder_find_item_from_identifier(
322 mail_filtering_data
->account
->inbox
);
323 if (item
&& item
->folder
->trash
) {
324 save_folder
= item
->folder
->trash
;
325 debug_print("found trash folder from account's inbox\n");
328 if (!save_folder
&& mail_filtering_data
->account
->local_inbox
) {
329 FolderItem
*item
= folder_find_item_from_identifier(
330 mail_filtering_data
->account
->local_inbox
);
331 if (item
&& item
->folder
->trash
) {
332 save_folder
= item
->folder
->trash
;
333 debug_print("found trash folder from account's local_inbox\n");
337 if (save_folder
== NULL
) {
338 debug_print("using default trash folder\n");
339 save_folder
= folder_get_default_trash();
342 if (config
.mark_as_read
)
343 procmsg_msginfo_unset_flags(msginfo
, ~0, 0);
344 procmsg_msginfo_set_flags(msginfo
, MSG_SPAM
, 0);
345 msginfo
->filter_op
= IS_MOVE
;
346 msginfo
->to_filter_folder
= save_folder
;
348 folder_item_remove_msg(msginfo
->folder
, msginfo
->msgnum
);
353 debug_print("message is ham\n");
354 procmsg_msginfo_unset_flags(msginfo
, MSG_SPAM
, 0);
358 gchar
*msg
= _("The SpamAssassin plugin couldn't filter "
359 "a message. The probable cause of the error "
360 "is an unreachable spamd daemon. Please make "
361 "sure spamd is running and accessible.");
362 if (!prefs_common_get_prefs()->no_recv_err_panel
) {
364 alertpanel_error("%s", msg
);
368 log_error(LOG_PROTOCOL
, "%s\n", msg
);
375 SpamAssassinConfig
*spamassassin_get_config(void)
380 gchar
* spamassassin_create_tmp_spamc_wrapper(gboolean spam
)
383 gchar
*fname
= get_tmp_file();
386 contents
= g_strdup_printf(
387 "spamc -d %s -p %u -u %s -t %u -s %u %s -L %s<\"$*\";exit $?",
388 config
.hostname
, config
.port
,
389 config
.username
, config
.timeout
,
390 config
.max_size
* 1024, config
.compress
?"-z":"",
392 if (str_write_to_file(contents
, fname
, TRUE
) < 0) {
398 /* returned pointer must be free'ed by caller */
402 int spamassassin_learn(MsgInfo
*msginfo
, GSList
*msglist
, gboolean spam
)
406 const gchar
*shell
= g_getenv("SHELL");
407 gchar
*spamc_wrapper
= NULL
;
409 if (msginfo
== NULL
&& msglist
== NULL
) {
413 if (config
.transport
== SPAMASSASSIN_TRANSPORT_TCP
414 && prefs_common_get_prefs()->work_offline
415 && !inc_offline_should_override(TRUE
,
416 _("Claws Mail needs network access in order "
417 "to feed the mail to the remote learner."))) {
421 /* process *either* a msginfo or a msglist */
423 file
= procmsg_get_message_file(msginfo
);
427 if (config
.transport
== SPAMASSASSIN_TRANSPORT_TCP
) {
428 spamc_wrapper
= spamassassin_create_tmp_spamc_wrapper(spam
);
429 if (spamc_wrapper
!= NULL
) {
430 cmd
= g_strconcat(shell
?shell
:"sh", " ",
431 spamc_wrapper
, " ", file
, NULL
);
434 cmd
= g_strdup_printf("sa-learn -u %s%s %s %s",
436 prefs_common_get_prefs()->work_offline
?" -L":"",
437 spam
?"--spam":"--ham", file
);
441 GSList
*cur
= msglist
;
444 if (config
.transport
== SPAMASSASSIN_TRANSPORT_TCP
) {
445 /* execute n-times the spamc command */
446 for (; cur
; cur
= cur
->next
) {
447 info
= (MsgInfo
*)cur
->data
;
448 gchar
*tmpcmd
= NULL
;
449 gchar
*tmpfile
= get_tmp_file();
451 if (spamc_wrapper
== NULL
) {
452 spamc_wrapper
= spamassassin_create_tmp_spamc_wrapper(spam
);
455 if (spamc_wrapper
&& tmpfile
&&
456 copy_file(procmsg_get_message_file(info
), tmpfile
, TRUE
) == 0) {
457 tmpcmd
= g_strconcat(shell
?shell
:"sh", " ", spamc_wrapper
, " ",
459 debug_print("%s\n", tmpcmd
);
460 execute_command_line(tmpcmd
, FALSE
, NULL
);
466 g_free(spamc_wrapper
);
469 cmd
= g_strdup_printf("sa-learn -u %s%s %s",
471 prefs_common_get_prefs()->work_offline
?" -L":"",
472 spam
?"--spam":"--ham");
474 /* concatenate all message tmpfiles to the sa-learn command-line */
475 for (; cur
; cur
= cur
->next
) {
476 info
= (MsgInfo
*)cur
->data
;
477 gchar
*tmpcmd
= NULL
;
478 gchar
*tmpfile
= get_tmp_file();
481 copy_file(procmsg_get_message_file(info
), tmpfile
, TRUE
) == 0) {
482 tmpcmd
= g_strconcat(cmd
, " ", tmpfile
, NULL
);
494 debug_print("%s\n", cmd
);
495 /* only run sync calls to sa-learn/spamc to prevent system lockdown */
496 execute_command_line(cmd
, FALSE
, NULL
);
499 g_free(spamc_wrapper
);
504 void spamassassin_save_config(void)
509 debug_print("Saving SpamAssassin Page\n");
511 rcpath
= g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S
, COMMON_RC
, NULL
);
512 pfile
= prefs_write_open(rcpath
);
514 if (!pfile
|| (prefs_set_block_label(pfile
, "SpamAssassin") < 0))
517 if (prefs_write_param(param
, pfile
->fp
) < 0) {
518 g_warning("failed to write SpamAssassin configuration to file");
519 prefs_file_close_revert(pfile
);
522 if (fprintf(pfile
->fp
, "\n") < 0) {
523 FILE_OP_ERROR(rcpath
, "fprintf");
524 prefs_file_close_revert(pfile
);
526 prefs_file_close(pfile
);
529 gboolean
spamassassin_check_username(void)
531 if (config
.username
== NULL
|| config
.username
[0] == '\0') {
532 config
.username
= (gchar
*)g_get_user_name();
533 if (config
.username
== NULL
) {
534 if (hook_id
!= HOOK_NONE
) {
535 spamassassin_unregister_hook();
537 procmsg_unregister_spam_learner(spamassassin_learn
);
538 procmsg_spam_set_folder(NULL
, NULL
);
545 void spamassassin_set_message_callback(MessageCallback callback
)
547 message_callback
= callback
;
550 gint
plugin_init(gchar
**error
)
556 if (!check_plugin_version(MAKE_NUMERIC_VERSION(2,9,2,72),
557 VERSION_NUMERIC
, PLUGIN_NAME
, error
))
560 prefs_set_default(param
);
561 rcpath
= g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S
, COMMON_RC
, NULL
);
562 prefs_read_config(param
, "SpamAssassin", rcpath
, NULL
);
564 if (!spamassassin_check_username()) {
565 *error
= g_strdup(_("Failed to get username"));
569 /* no Unix socket in Windows, and in case our config comes from Unix, switch to TCP */
570 if (config
.transport
== SPAMASSASSIN_TRANSPORT_UNIX
)
571 config
.transport
= SPAMASSASSIN_TRANSPORT_TCP
;
573 spamassassin_gtk_init();
575 debug_print("SpamAssassin plugin loaded\n");
577 if (config
.process_emails
) {
578 spamassassin_register_hook();
581 if (!config
.enable
|| config
.transport
== SPAMASSASSIN_DISABLED
) {
582 log_warning(LOG_PROTOCOL
, _("SpamAssassin plugin is loaded but disabled by its preferences.\n"));
585 if (config
.transport
== SPAMASSASSIN_TRANSPORT_TCP
)
586 debug_print("Enabling learner with a remote spamassassin server requires spamc/spamd 3.1.x\n");
587 procmsg_register_spam_learner(spamassassin_learn
);
588 procmsg_spam_set_folder(config
.save_folder
, spamassassin_get_spam_folder
);
595 gboolean
plugin_done(void)
597 if (hook_id
!= HOOK_NONE
) {
598 spamassassin_unregister_hook();
600 g_free(config
.hostname
);
601 g_free(config
.save_folder
);
602 spamassassin_gtk_done();
603 procmsg_unregister_spam_learner(spamassassin_learn
);
604 procmsg_spam_set_folder(NULL
, NULL
);
605 debug_print("SpamAssassin plugin unloaded\n");
609 const gchar
*plugin_name(void)
614 const gchar
*plugin_desc(void)
616 return _("This plugin can check all messages that are received from an "
617 "IMAP, LOCAL or POP account for spam using a SpamAssassin "
618 "server. You will need a SpamAssassin Server (spamd) running "
621 "It can also be used for marking messages as Ham or Spam.\n"
623 "When a message is identified as spam it can be deleted or "
624 "saved in a specially designated folder.\n"
626 "Options can be found in /Configuration/Preferences/Plugins/SpamAssassin");
629 const gchar
*plugin_type(void)
634 const gchar
*plugin_licence(void)
639 const gchar
*plugin_version(void)
644 struct PluginFeature
*plugin_provides(void)
646 static struct PluginFeature features
[] =
647 { {PLUGIN_FILTERING
, N_("Spam detection")},
648 {PLUGIN_FILTERING
, N_("Spam learning")},
649 {PLUGIN_NOTHING
, NULL
}};
653 void spamassassin_register_hook(void)
655 if (hook_id
== HOOK_NONE
)
656 hook_id
= hooks_register_hook(MAIL_FILTERING_HOOKLIST
, mail_filtering_hook
, NULL
);
657 if (hook_id
== HOOK_NONE
) {
658 g_warning("failed to register mail filtering hook");
659 config
.process_emails
= FALSE
;
663 void spamassassin_unregister_hook(void)
665 if (hook_id
!= HOOK_NONE
) {
666 hooks_unregister_hook(MAIL_FILTERING_HOOKLIST
, hook_id
);
671 FolderItem
*spamassassin_get_spam_folder(MsgInfo
*msginfo
)
673 FolderItem
*item
= folder_find_item_from_identifier(config
.save_folder
);
675 if (item
|| msginfo
== NULL
|| msginfo
->folder
== NULL
)
678 if (msginfo
->folder
->folder
&&
679 msginfo
->folder
->folder
->account
&&
680 msginfo
->folder
->folder
->account
->set_trash_folder
) {
681 item
= folder_find_item_from_identifier(
682 msginfo
->folder
->folder
->account
->trash_folder
);
686 msginfo
->folder
->folder
&&
687 msginfo
->folder
->folder
->trash
)
688 item
= msginfo
->folder
->folder
->trash
;
691 item
= folder_get_default_trash();
693 debug_print("SA spam dir: %s\n", folder_item_get_path(item
));