No issues found
1 /*
2 * evolution-spamassassin.c
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) version 3.
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 GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with the program; if not, see <http://www.gnu.org/licenses/>
16 *
17 */
18
19 #include <config.h>
20 #include <errno.h>
21 #include <sys/types.h>
22 #include <sys/wait.h>
23 #include <glib/gstdio.h>
24 #include <glib/gi18n-lib.h>
25
26 #include <camel/camel.h>
27
28 #include <shell/e-shell.h>
29 #include <e-util/e-mktemp.h>
30 #include <libemail-engine/e-mail-junk-filter.h>
31
32 /* Standard GObject macros */
33 #define E_TYPE_SPAM_ASSASSIN \
34 (e_spam_assassin_get_type ())
35 #define E_SPAM_ASSASSIN(obj) \
36 (G_TYPE_CHECK_INSTANCE_CAST \
37 ((obj), E_TYPE_SPAM_ASSASSIN, ESpamAssassin))
38
39 #ifndef SPAMASSASSIN_BINARY
40 #define SPAMASSASSIN_BINARY "/usr/bin/spamassassin"
41 #endif
42
43 #ifndef SA_LEARN_BINARY
44 #define SA_LEARN_BINARY "/usr/bin/sa-learn"
45 #endif
46
47 #ifndef SPAMC_BINARY
48 #define SPAMC_BINARY "/usr/bin/spamc"
49 #endif
50
51 #ifndef SPAMD_BINARY
52 #define SPAMD_BINARY "/usr/bin/spamd"
53 #endif
54
55 /* For starting our own daemon. */
56 #define DAEMON_MAX_RETRIES 100
57 #define DAEMON_RETRY_DELAY 0.05 /* seconds */
58
59 #define SPAM_ASSASSIN_EXIT_STATUS_SUCCESS 0
60 #define SPAM_ASSASSIN_EXIT_STATUS_ERROR -1
61
62 typedef struct _ESpamAssassin ESpamAssassin;
63 typedef struct _ESpamAssassinClass ESpamAssassinClass;
64
65 struct _ESpamAssassin {
66 EMailJunkFilter parent;
67
68 GMutex *socket_path_mutex;
69
70 gchar *pid_file;
71 gchar *socket_path;
72 gchar *spamc_binary;
73 gchar *spamd_binary;
74 gint version;
75
76 gboolean local_only;
77 gboolean use_daemon;
78 gboolean version_set;
79
80 /* spamc/spamd state */
81 gboolean spamd_tested;
82 gboolean spamd_using_allow_tell;
83 gboolean system_spamd_available;
84 gboolean use_spamc;
85 };
86
87 struct _ESpamAssassinClass {
88 EMailJunkFilterClass parent_class;
89 };
90
91 enum {
92 PROP_0,
93 PROP_LOCAL_ONLY,
94 PROP_SPAMC_BINARY,
95 PROP_SPAMD_BINARY,
96 PROP_SOCKET_PATH,
97 PROP_USE_DAEMON
98 };
99
100 /* Module Entry Points */
101 void e_module_load (GTypeModule *type_module);
102 void e_module_unload (GTypeModule *type_module);
103
104 /* Forward Declarations */
105 GType e_spam_assassin_get_type (void);
106 static void e_spam_assassin_interface_init (CamelJunkFilterInterface *interface);
107
108 G_DEFINE_DYNAMIC_TYPE_EXTENDED (
109 ESpamAssassin,
110 e_spam_assassin,
111 E_TYPE_MAIL_JUNK_FILTER, 0,
112 G_IMPLEMENT_INTERFACE_DYNAMIC (
113 CAMEL_TYPE_JUNK_FILTER,
114 e_spam_assassin_interface_init))
115
116 #ifdef G_OS_UNIX
117 static void
118 spam_assassin_cancelled_cb (GCancellable *cancellable,
119 GPid *pid)
120 {
121 /* XXX On UNIX-like systems we can safely assume a GPid is the
122 * process ID and use it to terminate the process via signal. */
123 kill (*pid, SIGTERM);
124 }
125 #endif
126
127 static void
128 spam_assassin_exited_cb (GPid *pid,
129 gint status,
130 gpointer user_data)
131 {
132 struct {
133 GMainLoop *loop;
134 gint exit_code;
135 } *source_data = user_data;
136
137 if (WIFEXITED (status))
138 source_data->exit_code = WEXITSTATUS (status);
139 else
140 source_data->exit_code = SPAM_ASSASSIN_EXIT_STATUS_ERROR;
141
142 g_main_loop_quit (source_data->loop);
143 }
144
145 static gint
146 spam_assassin_command_full (const gchar **argv,
147 CamelMimeMessage *message,
148 const gchar *input_data,
149 GByteArray *output_buffer,
150 gboolean wait_for_termination,
151 GCancellable *cancellable,
152 GError **error)
153 {
154 GMainContext *context;
155 GSpawnFlags flags = 0;
156 GSource *source;
157 GPid child_pid;
158 gint standard_input;
159 gint standard_output;
160 gulong handler_id = 0;
161 gboolean success;
162
163 struct {
164 GMainLoop *loop;
165 gint exit_code;
166 } source_data;
167
168 if (wait_for_termination)
169 flags |= G_SPAWN_DO_NOT_REAP_CHILD;
170 if (output_buffer == NULL)
171 flags |= G_SPAWN_STDOUT_TO_DEV_NULL;
172 flags |= G_SPAWN_STDERR_TO_DEV_NULL;
173
174 /* Spawn SpamAssassin with an open stdin pipe. */
175 success = g_spawn_async_with_pipes (
176 NULL,
177 (gchar **) argv,
178 NULL,
179 flags,
180 NULL, NULL,
181 &child_pid,
182 &standard_input,
183 (output_buffer != NULL) ? &standard_output : NULL,
184 NULL,
185 error);
186
187 if (!success) {
188 gchar *command_line;
189
190 command_line = g_strjoinv (" ", (gchar **) argv);
191 g_prefix_error (
192 error, _("Failed to spawn SpamAssassin (%s): "),
193 command_line);
194 g_free (command_line);
195
196 return SPAM_ASSASSIN_EXIT_STATUS_ERROR;
197 }
198
199 if (message != NULL) {
200 CamelStream *stream;
201 gssize bytes_written;
202
203 /* Stream the CamelMimeMessage to SpamAssassin. */
204 stream = camel_stream_fs_new_with_fd (standard_input);
205 bytes_written = camel_data_wrapper_write_to_stream_sync (
206 CAMEL_DATA_WRAPPER (message),
207 stream, cancellable, error);
208 success = (bytes_written >= 0) &&
209 (camel_stream_close (stream, cancellable, error) == 0);
210 g_object_unref (stream);
211
212 if (!success) {
213 g_spawn_close_pid (child_pid);
214 g_prefix_error (
215 error, _("Failed to stream mail "
216 "message content to SpamAssassin: "));
217 return SPAM_ASSASSIN_EXIT_STATUS_ERROR;
218 }
219
220 } else if (input_data != NULL) {
221 gssize bytes_written;
222
223 /* Write raw data directly to SpamAssassin. */
224 bytes_written = camel_write (
225 standard_input, input_data,
226 strlen (input_data), cancellable, error);
227 success = (bytes_written >= 0);
228
229 close (standard_input);
230
231 if (!success) {
232 g_spawn_close_pid (child_pid);
233 g_prefix_error (
234 error, _("Failed to write '%s' "
235 "to SpamAssassin: "), input_data);
236 return SPAM_ASSASSIN_EXIT_STATUS_ERROR;
237 }
238 }
239
240 if (output_buffer != NULL) {
241 CamelStream *input_stream;
242 CamelStream *output_stream;
243 gssize bytes_written;
244
245 input_stream = camel_stream_fs_new_with_fd (standard_output);
246
247 output_stream = camel_stream_mem_new ();
248 camel_stream_mem_set_byte_array (
249 CAMEL_STREAM_MEM (output_stream), output_buffer);
250
251 bytes_written = camel_stream_write_to_stream (
252 input_stream, output_stream, cancellable, error);
253 g_byte_array_append (output_buffer, (guint8 *) "", 1);
254 success = (bytes_written >= 0);
255
256 g_object_unref (input_stream);
257 g_object_unref (output_stream);
258
259 if (!success) {
260 g_spawn_close_pid (child_pid);
261 g_prefix_error (
262 error, _("Failed to read "
263 "output from SpamAssassin: "));
264 return SPAM_ASSASSIN_EXIT_STATUS_ERROR;
265 }
266 }
267
268 /* XXX I'm not sure if we should call g_spawn_close_pid()
269 * here or not. Only really matters on Windows anyway. */
270 if (!wait_for_termination)
271 return 0;
272
273 /* Wait for the SpamAssassin process to terminate
274 * using GLib's main loop for better portability. */
275
276 context = g_main_context_new ();
277
278 source = g_child_watch_source_new (child_pid);
279 g_source_set_callback (
280 source, (GSourceFunc)
281 spam_assassin_exited_cb,
282 &source_data, NULL);
283 g_source_attach (source, context);
284 g_source_unref (source);
285
286 source_data.loop = g_main_loop_new (context, TRUE);
287 source_data.exit_code = 0;
288
289 #ifdef G_OS_UNIX
290 if (G_IS_CANCELLABLE (cancellable))
291 handler_id = g_cancellable_connect (
292 cancellable,
293 G_CALLBACK (spam_assassin_cancelled_cb),
294 &child_pid, (GDestroyNotify) NULL);
295 #endif
296
297 g_main_loop_run (source_data.loop);
298
299 if (handler_id > 0)
300 g_cancellable_disconnect (cancellable, handler_id);
301
302 g_main_loop_unref (source_data.loop);
303 source_data.loop = NULL;
304
305 g_main_context_unref (context);
306
307 /* Clean up. */
308
309 g_spawn_close_pid (child_pid);
310
311 if (g_cancellable_set_error_if_cancelled (cancellable, error))
312 source_data.exit_code = SPAM_ASSASSIN_EXIT_STATUS_ERROR;
313
314 else if (source_data.exit_code == SPAM_ASSASSIN_EXIT_STATUS_ERROR)
315 g_set_error_literal (
316 error, CAMEL_ERROR, CAMEL_ERROR_GENERIC,
317 _("SpamAssassin either crashed or "
318 "failed to process a mail message"));
319
320 return source_data.exit_code;
321 }
322
323 static gint
324 spam_assassin_command (const gchar **argv,
325 CamelMimeMessage *message,
326 const gchar *input_data,
327 GCancellable *cancellable,
328 GError **error)
329 {
330 return spam_assassin_command_full (
331 argv, message, input_data, NULL, TRUE, cancellable, error);
332 }
333
334 static gboolean
335 spam_assassin_get_local_only (ESpamAssassin *extension)
336 {
337 return extension->local_only;
338 }
339
340 static void
341 spam_assassin_set_local_only (ESpamAssassin *extension,
342 gboolean local_only)
343 {
344 if (extension->local_only == local_only)
345 return;
346
347 extension->local_only = local_only;
348
349 g_object_notify (G_OBJECT (extension), "local-only");
350 }
351
352 static const gchar *
353 spam_assassin_get_spamc_binary (ESpamAssassin *extension)
354 {
355 return extension->spamc_binary;
356 }
357
358 static void
359 spam_assassin_set_spamc_binary (ESpamAssassin *extension,
360 const gchar *spamc_binary)
361 {
362 if (g_strcmp0 (extension->spamc_binary, spamc_binary) == 0)
363 return;
364
365 g_free (extension->spamc_binary);
366 extension->spamc_binary = g_strdup (spamc_binary);
367
368 g_object_notify (G_OBJECT (extension), "spamc-binary");
369 }
370
371 static const gchar *
372 spam_assassin_get_spamd_binary (ESpamAssassin *extension)
373 {
374 return extension->spamd_binary;
375 }
376
377 static void
378 spam_assassin_set_spamd_binary (ESpamAssassin *extension,
379 const gchar *spamd_binary)
380 {
381 if (g_strcmp0 (extension->spamd_binary, spamd_binary) == 0)
382 return;
383
384 g_free (extension->spamd_binary);
385 extension->spamd_binary = g_strdup (spamd_binary);
386
387 g_object_notify (G_OBJECT (extension), "spamd-binary");
388 }
389
390 static const gchar *
391 spam_assassin_get_socket_path (ESpamAssassin *extension)
392 {
393 return extension->socket_path;
394 }
395
396 static void
397 spam_assassin_set_socket_path (ESpamAssassin *extension,
398 const gchar *socket_path)
399 {
400 if (g_strcmp0 (extension->socket_path, socket_path) == 0)
401 return;
402
403 g_free (extension->socket_path);
404 extension->socket_path = g_strdup (socket_path);
405
406 g_object_notify (G_OBJECT (extension), "socket-path");
407 }
408
409 static gboolean
410 spam_assassin_get_use_daemon (ESpamAssassin *extension)
411 {
412 return extension->use_daemon;
413 }
414
415 static void
416 spam_assassin_set_use_daemon (ESpamAssassin *extension,
417 gboolean use_daemon)
418 {
419 if ((extension->use_daemon ? 1 : 0) == (use_daemon ? 1 : 0))
420 return;
421
422 extension->use_daemon = use_daemon;
423
424 g_object_notify (G_OBJECT (extension), "use-daemon");
425 }
426
427 static gboolean
428 spam_assassin_get_version (ESpamAssassin *extension,
429 gint *spam_assassin_version,
430 GCancellable *cancellable,
431 GError **error)
432 {
433 GByteArray *output_buffer;
434 gint exit_code;
435 guint ii;
436
437 const gchar *argv[] = {
438 SA_LEARN_BINARY,
439 "--version",
440 NULL
441 };
442
443 if (extension->version_set) {
444 if (spam_assassin_version != NULL)
445 *spam_assassin_version = extension->version;
446 return TRUE;
447 }
448
449 output_buffer = g_byte_array_new ();
450
451 exit_code = spam_assassin_command_full (
452 argv, NULL, NULL, output_buffer, TRUE, cancellable, error);
453
454 if (exit_code != 0) {
455 g_byte_array_free (output_buffer, TRUE);
456 return FALSE;
457 }
458
459 for (ii = 0; ii < output_buffer->len; ii++) {
460 if (g_ascii_isdigit (output_buffer->data[ii])) {
461 guint8 ch = output_buffer->data[ii];
462 extension->version = (ch - '0');
463 extension->version_set = TRUE;
464 break;
465 }
466 }
467
468 if (spam_assassin_version != NULL)
469 *spam_assassin_version = extension->version;
470
471 g_byte_array_free (output_buffer, TRUE);
472
473 return TRUE;
474 }
475
476 static void
477 spam_assassin_test_spamd_allow_tell (ESpamAssassin *extension)
478 {
479 gint exit_code;
480 GError *error = NULL;
481
482 const gchar *argv[] = {
483 SPAMC_BINARY,
484 "--learntype=forget",
485 NULL
486 };
487
488 /* Check if spamd is running with --allow-tell. */
489
490 exit_code = spam_assassin_command (argv, NULL, "\n", NULL, &error);
491 extension->spamd_using_allow_tell = (exit_code == 0);
492
493 if (error != NULL) {
494 g_warning ("%s", error->message);
495 g_error_free (error);
496 }
497 }
498
499 static gboolean
500 spam_assassin_test_spamd_running (ESpamAssassin *extension,
501 gboolean system_spamd)
502 {
503 const gchar *argv[5];
504 gint exit_code;
505 gint ii = 0;
506 GError *error = NULL;
507
508 g_mutex_lock (extension->socket_path_mutex);
509
510 argv[ii++] = extension->spamc_binary;
511 argv[ii++] = "--no-safe-fallback";
512 if (!system_spamd) {
513 argv[ii++] = "--socket";
514 argv[ii++] = extension->socket_path;
515 }
516 argv[ii] = NULL;
517
518 g_assert (ii < G_N_ELEMENTS (argv));
519
520 exit_code = spam_assassin_command (
521 argv, NULL, "From test@127.0.0.1", NULL, &error);
522
523 if (error != NULL) {
524 g_warning ("%s", error->message);
525 g_error_free (error);
526 }
527
528 g_mutex_unlock (extension->socket_path_mutex);
529
530 return (exit_code == 0);
531 }
532
533 static void
534 spam_assassin_kill_our_own_daemon (ESpamAssassin *extension)
535 {
536 gint pid;
537 gchar *contents = NULL;
538 GError *error = NULL;
539
540 g_mutex_lock (extension->socket_path_mutex);
541
542 g_free (extension->socket_path);
543 extension->socket_path = NULL;
544
545 g_mutex_unlock (extension->socket_path_mutex);
546
547 if (extension->pid_file == NULL)
548 return;
549
550 g_file_get_contents (extension->pid_file, &contents, NULL, &error);
551
552 if (error != NULL) {
553 g_warn_if_fail (contents == NULL);
554 g_warning ("%s", error->message);
555 g_error_free (error);
556 return;
557 }
558
559 g_return_if_fail (contents != NULL);
560
561 pid = atoi (contents);
562 g_free (contents);
563
564 if (pid > 0 && kill (pid, SIGTERM) == 0)
565 waitpid (pid, NULL, 0);
566 }
567
568 static void
569 spam_assassin_prepare_for_quit (EShell *shell,
570 EActivity *activity,
571 ESpamAssassin *extension)
572 {
573 spam_assassin_kill_our_own_daemon (extension);
574 }
575
576 static gboolean
577 spam_assassin_start_our_own_daemon (ESpamAssassin *extension)
578 {
579 const gchar *argv[8];
580 const gchar *user_runtime_dir;
581 gchar *pid_file;
582 gchar *socket_path;
583 gboolean started = FALSE;
584 gint exit_code;
585 gint ii = 0;
586 gint fd;
587 GError *error = NULL;
588
589 g_mutex_lock (extension->socket_path_mutex);
590
591 /* Don't put the PID files in Evolution's tmp directory
592 * (as defined in e-mktemp.c) because that gets cleaned
593 * every few hours, and these files need to persist. */
594 user_runtime_dir = g_get_user_runtime_dir ();
595
596 pid_file = g_build_filename (
597 user_runtime_dir, "spamd-pid-file-XXXXXX", NULL);
598
599 socket_path = g_build_filename (
600 user_runtime_dir, "spamd-socket-path-XXXXXX", NULL);
601
602 /* The template filename is modified in place. */
603 fd = g_mkstemp (pid_file);
604 if (fd >= 0) {
605 close (fd);
606 g_unlink (pid_file);
607 } else {
608 g_warning (
609 "Failed to create spamd-pid-file: %s",
610 g_strerror (errno));
611 goto exit;
612 }
613
614 /* The template filename is modified in place. */
615 fd = g_mkstemp (socket_path);
616 if (fd >= 0) {
617 close (fd);
618 g_unlink (socket_path);
619 } else {
620 g_warning (
621 "Failed to create spamd-socket-path: %s",
622 g_strerror (errno));
623 goto exit;
624 }
625
626 argv[ii++] = extension->spamd_binary;
627 argv[ii++] = "--socketpath";
628 argv[ii++] = socket_path;
629
630 if (spam_assassin_get_local_only (extension))
631 argv[ii++] = "--local";
632
633 argv[ii++] = "--max-children=1";
634 argv[ii++] = "--pidfile";
635 argv[ii++] = pid_file;
636 argv[ii] = NULL;
637
638 g_assert (ii < G_N_ELEMENTS (argv));
639
640 exit_code = spam_assassin_command_full (
641 argv, NULL, NULL, NULL, FALSE, NULL, &error);
642
643 if (error != NULL) {
644 g_warning ("%s", error->message);
645 g_error_free (error);
646 goto exit;
647 }
648
649 if (exit_code == SPAM_ASSASSIN_EXIT_STATUS_SUCCESS) {
650 /* Wait for the socket path to appear. */
651 for (ii = 0; ii < DAEMON_MAX_RETRIES; ii++) {
652 if (g_file_test (socket_path, G_FILE_TEST_EXISTS)) {
653 started = TRUE;
654 break;
655 }
656 g_usleep (DAEMON_RETRY_DELAY * G_USEC_PER_SEC);
657 }
658 }
659
660 /* Set these directly to avoid emitting "notify" signals. */
661 if (started) {
662 g_free (extension->pid_file);
663 extension->pid_file = pid_file;
664 pid_file = NULL;
665
666 g_free (extension->socket_path);
667 extension->socket_path = socket_path;
668 socket_path = NULL;
669
670 /* XXX EMailSession is too prone to reference leaks to leave
671 * this for our finalize() method. We want to be sure to
672 * kill the spamd process we started when Evolution shuts
673 * down, so connect to an EShell signal instead. */
674 g_signal_connect (
675 e_shell_get_default (), "prepare-for-quit",
676 G_CALLBACK (spam_assassin_prepare_for_quit),
677 extension);
678 }
679
680 exit:
681 g_free (pid_file);
682 g_free (socket_path);
683
684 g_mutex_unlock (extension->socket_path_mutex);
685
686 return started;
687 }
688
689 static void
690 spam_assassin_test_spamd (ESpamAssassin *extension)
691 {
692 const gchar *spamd_binary;
693 gboolean try_system_spamd;
694
695 /* XXX SpamAssassin could really benefit from a D-Bus interface
696 * these days. These tests are just needlessly painful for
697 * clients trying to talk to an already-running spamd. */
698
699 extension->use_spamc = FALSE;
700 spamd_binary = extension->spamd_binary;
701 try_system_spamd = (g_strcmp0 (spamd_binary, SPAMD_BINARY) == 0);
702
703 if (extension->local_only && try_system_spamd) {
704 gint exit_code;
705
706 /* Run a shell command to check for a running
707 * spamd process with a -L/--local option or a
708 * -p/--port option. */
709
710 const gchar *argv[] = {
711 "/bin/sh",
712 "-c",
713 "ps ax | grep -v grep | "
714 "grep -E 'spamd.*(\\-L|\\-\\-local)' | "
715 "grep -E -v '\\ \\-p\\ |\\ \\-\\-port\\ '",
716 NULL
717 };
718
719 exit_code = spam_assassin_command (
720 argv, NULL, NULL, NULL, NULL);
721 try_system_spamd = (exit_code == 0);
722 }
723
724 /* Try to use the system spamd first. */
725 if (try_system_spamd) {
726 if (spam_assassin_test_spamd_running (extension, TRUE)) {
727 extension->use_spamc = TRUE;
728 extension->system_spamd_available = TRUE;
729 }
730 }
731
732 /* If there's no system spamd running, try
733 * to use one with a user specified socket. */
734 if (!extension->use_spamc && extension->socket_path != NULL) {
735 if (spam_assassin_test_spamd_running (extension, FALSE)) {
736 extension->use_spamc = TRUE;
737 extension->system_spamd_available = FALSE;
738 }
739 }
740
741 /* Still unsuccessful? Try to start our own spamd. */
742 if (!extension->use_spamc) {
743 extension->use_spamc =
744 spam_assassin_start_our_own_daemon (extension) &&
745 spam_assassin_test_spamd_running (extension, FALSE);
746 }
747 }
748
749 static void
750 spam_assassin_set_property (GObject *object,
751 guint property_id,
752 const GValue *value,
753 GParamSpec *pspec)
754 {
755 switch (property_id) {
756 case PROP_LOCAL_ONLY:
757 spam_assassin_set_local_only (
758 E_SPAM_ASSASSIN (object),
759 g_value_get_boolean (value));
760 return;
761
762 case PROP_SPAMC_BINARY:
763 spam_assassin_set_spamc_binary (
764 E_SPAM_ASSASSIN (object),
765 g_value_get_string (value));
766 return;
767
768 case PROP_SPAMD_BINARY:
769 spam_assassin_set_spamd_binary (
770 E_SPAM_ASSASSIN (object),
771 g_value_get_string (value));
772 return;
773
774 case PROP_SOCKET_PATH:
775 spam_assassin_set_socket_path (
776 E_SPAM_ASSASSIN (object),
777 g_value_get_string (value));
778 return;
779
780 case PROP_USE_DAEMON:
781 spam_assassin_set_use_daemon (
782 E_SPAM_ASSASSIN (object),
783 g_value_get_boolean (value));
784 return;
785 }
786
787 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
788 }
789
790 static void
791 spam_assassin_get_property (GObject *object,
792 guint property_id,
793 GValue *value,
794 GParamSpec *pspec)
795 {
796 switch (property_id) {
797 case PROP_LOCAL_ONLY:
798 g_value_set_boolean (
799 value, spam_assassin_get_local_only (
800 E_SPAM_ASSASSIN (object)));
801 return;
802
803 case PROP_SPAMC_BINARY:
804 g_value_set_string (
805 value, spam_assassin_get_spamc_binary (
806 E_SPAM_ASSASSIN (object)));
807 return;
808
809 case PROP_SPAMD_BINARY:
810 g_value_set_string (
811 value, spam_assassin_get_spamd_binary (
812 E_SPAM_ASSASSIN (object)));
813 return;
814
815 case PROP_SOCKET_PATH:
816 g_value_set_string (
817 value, spam_assassin_get_socket_path (
818 E_SPAM_ASSASSIN (object)));
819 return;
820
821 case PROP_USE_DAEMON:
822 g_value_set_boolean (
823 value, spam_assassin_get_use_daemon (
824 E_SPAM_ASSASSIN (object)));
825 return;
826 }
827
828 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
829 }
830
831 static void
832 spam_assassin_finalize (GObject *object)
833 {
834 ESpamAssassin *extension = E_SPAM_ASSASSIN (object);
835
836 g_mutex_free (extension->socket_path_mutex);
837
838 g_free (extension->pid_file);
839 g_free (extension->socket_path);
840 g_free (extension->spamc_binary);
841 g_free (extension->spamd_binary);
842
843 /* Chain up to parent's finalize() method. */
844 G_OBJECT_CLASS (e_spam_assassin_parent_class)->finalize (object);
845 }
846
847 static gboolean
848 spam_assassin_available (EMailJunkFilter *junk_filter)
849 {
850 ESpamAssassin *extension = E_SPAM_ASSASSIN (junk_filter);
851 gboolean available;
852 GError *error = NULL;
853
854 available = spam_assassin_get_version (extension, NULL, NULL, &error);
855
856 /* XXX These tests block like crazy so maybe this isn't the best
857 * place to be doing this, but the first available() call is
858 * done at startup before the UI is shown. So hopefully the
859 * delay will not be noticeable. */
860 if (available && extension->use_daemon && !extension->spamd_tested) {
861 extension->spamd_tested = TRUE;
862 spam_assassin_test_spamd (extension);
863 spam_assassin_test_spamd_allow_tell (extension);
864 }
865
866 if (error != NULL) {
867 g_warning ("%s", error->message);
868 g_error_free (error);
869 }
870
871 return available;
872 }
873
874 static GtkWidget *
875 spam_assassin_new_config_widget (EMailJunkFilter *junk_filter)
876 {
877 GtkWidget *box;
878 GtkWidget *widget;
879 GtkWidget *container;
880 gchar *markup;
881
882 box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12);
883
884 markup = g_markup_printf_escaped (
885 "<b>%s</b>", _("SpamAssassin Options"));
886 widget = gtk_label_new (markup);
887 gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5);
888 gtk_label_set_use_markup (GTK_LABEL (widget), TRUE);
889 gtk_box_pack_start (GTK_BOX (box), widget, FALSE, FALSE, 0);
890 gtk_widget_show (widget);
891 g_free (markup);
892
893 widget = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
894 gtk_box_pack_start (GTK_BOX (box), widget, FALSE, FALSE, 0);
895 gtk_widget_show (widget);
896
897 container = widget;
898
899 widget = gtk_check_button_new_with_mnemonic (
900 _("I_nclude remote tests"));
901 gtk_widget_set_margin_left (widget, 12);
902 gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0);
903 gtk_widget_show (widget);
904
905 g_object_bind_property (
906 junk_filter, "local-only",
907 widget, "active",
908 G_BINDING_BIDIRECTIONAL |
909 G_BINDING_SYNC_CREATE |
910 G_BINDING_INVERT_BOOLEAN);
911
912 markup = g_markup_printf_escaped (
913 "<small>%s</small>",
914 _("This will make SpamAssassin more reliable, but slower."));
915 widget = gtk_label_new (markup);
916 gtk_widget_set_margin_left (widget, 36);
917 gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5);
918 gtk_label_set_use_markup (GTK_LABEL (widget), TRUE);
919 gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0);
920 gtk_widget_show (widget);
921 g_free (markup);
922
923 return box;
924 }
925
926 static gboolean
927 spam_assassin_classify (CamelJunkFilter *junk_filter,
928 CamelMimeMessage *message,
929 CamelJunkStatus *status,
930 GCancellable *cancellable,
931 GError **error)
932 {
933 ESpamAssassin *extension = E_SPAM_ASSASSIN (junk_filter);
934 const gchar *argv[7];
935 gboolean using_spamc;
936 gint exit_code;
937 gint ii = 0;
938
939 g_mutex_lock (extension->socket_path_mutex);
940
941 using_spamc = (extension->use_spamc && extension->use_daemon);
942
943 if (using_spamc) {
944 argv[ii++] = extension->spamc_binary;
945 argv[ii++] = "--check";
946 argv[ii++] = "--timeout=60";
947 if (!extension->system_spamd_available) {
948 argv[ii++] = "--socket";
949 argv[ii++] = extension->socket_path;
950 }
951 } else {
952 argv[ii++] = SPAMASSASSIN_BINARY;
953 argv[ii++] = "--exit-code";
954 if (extension->local_only)
955 argv[ii++] = "--local";
956 }
957 argv[ii] = NULL;
958
959 g_assert (ii < G_N_ELEMENTS (argv));
960
961 exit_code = spam_assassin_command (
962 argv, message, NULL, cancellable, error);
963
964 /* For either program, exit code 0 means the message is ham. */
965 if (exit_code == 0)
966 *status = CAMEL_JUNK_STATUS_MESSAGE_IS_NOT_JUNK;
967
968 /* spamassassin(1) only specifies zero and non-zero exit codes. */
969 else if (!using_spamc)
970 *status = CAMEL_JUNK_STATUS_MESSAGE_IS_JUNK;
971
972 /* Whereas spamc(1) explicitly states exit code 1 means spam. */
973 else if (exit_code == 1)
974 *status = CAMEL_JUNK_STATUS_MESSAGE_IS_JUNK;
975
976 /* Consider any other spamc(1) exit code to be inconclusive
977 * since it most likely failed to process the message. */
978 else
979 *status = CAMEL_JUNK_STATUS_INCONCLUSIVE;
980
981 /* Check that the return value and GError agree. */
982 if (exit_code != SPAM_ASSASSIN_EXIT_STATUS_ERROR)
983 g_warn_if_fail (error == NULL || *error == NULL);
984 else
985 g_warn_if_fail (error == NULL || *error != NULL);
986
987 g_mutex_unlock (extension->socket_path_mutex);
988
989 return (exit_code != SPAM_ASSASSIN_EXIT_STATUS_ERROR);
990 }
991
992 static gboolean
993 spam_assassin_learn_junk (CamelJunkFilter *junk_filter,
994 CamelMimeMessage *message,
995 GCancellable *cancellable,
996 GError **error)
997 {
998 ESpamAssassin *extension = E_SPAM_ASSASSIN (junk_filter);
999 const gchar *argv[5];
1000 gint exit_code;
1001 gint ii = 0;
1002
1003 if (extension->spamd_using_allow_tell) {
1004 argv[ii++] = extension->spamc_binary;
1005 argv[ii++] = "--learntype=spam";
1006 } else {
1007 argv[ii++] = SA_LEARN_BINARY;
1008 argv[ii++] = "--spam";
1009 if (extension->version >= 3)
1010 argv[ii++] = "--no-sync";
1011 else
1012 argv[ii++] = "--no-rebuild";
1013 if (extension->local_only)
1014 argv[ii++] = "--local";
1015 }
1016 argv[ii] = NULL;
1017
1018 g_assert (ii < G_N_ELEMENTS (argv));
1019
1020 exit_code = spam_assassin_command (
1021 argv, message, NULL, cancellable, error);
1022
1023 /* Check that the return value and GError agree. */
1024 if (exit_code == SPAM_ASSASSIN_EXIT_STATUS_SUCCESS)
1025 g_warn_if_fail (error == NULL || *error == NULL);
1026 else
1027 g_warn_if_fail (error == NULL || *error != NULL);
1028
1029 return (exit_code == SPAM_ASSASSIN_EXIT_STATUS_SUCCESS);
1030 }
1031
1032 static gboolean
1033 spam_assassin_learn_not_junk (CamelJunkFilter *junk_filter,
1034 CamelMimeMessage *message,
1035 GCancellable *cancellable,
1036 GError **error)
1037 {
1038 ESpamAssassin *extension = E_SPAM_ASSASSIN (junk_filter);
1039 const gchar *argv[5];
1040 gint exit_code;
1041 gint ii = 0;
1042
1043 if (extension->spamd_using_allow_tell) {
1044 argv[ii++] = extension->spamc_binary;
1045 argv[ii++] = "--learntype=ham";
1046 } else {
1047 argv[ii++] = SA_LEARN_BINARY;
1048 argv[ii++] = "--ham";
1049 if (extension->version >= 3)
1050 argv[ii++] = "--no-sync";
1051 else
1052 argv[ii++] = "--no-rebuild";
1053 if (extension->local_only)
1054 argv[ii++] = "--local";
1055 }
1056 argv[ii] = NULL;
1057
1058 g_assert (ii < G_N_ELEMENTS (argv));
1059
1060 exit_code = spam_assassin_command (
1061 argv, message, NULL, cancellable, error);
1062
1063 /* Check that the return value and GError agree. */
1064 if (exit_code == SPAM_ASSASSIN_EXIT_STATUS_SUCCESS)
1065 g_warn_if_fail (error == NULL || *error == NULL);
1066 else
1067 g_warn_if_fail (error == NULL || *error != NULL);
1068
1069 return (exit_code == SPAM_ASSASSIN_EXIT_STATUS_SUCCESS);
1070 }
1071
1072 static gboolean
1073 spam_assassin_synchronize (CamelJunkFilter *junk_filter,
1074 GCancellable *cancellable,
1075 GError **error)
1076 {
1077 ESpamAssassin *extension = E_SPAM_ASSASSIN (junk_filter);
1078 const gchar *argv[4];
1079 gint exit_code;
1080 gint ii = 0;
1081
1082 /* If we're using a spamd that allows learning,
1083 * there's no need to synchronize anything. */
1084 if (extension->spamd_using_allow_tell)
1085 return TRUE;
1086
1087 argv[ii++] = SA_LEARN_BINARY;
1088 if (extension->version >= 3)
1089 argv[ii++] = "--sync";
1090 else
1091 argv[ii++] = "--rebuild";
1092 if (extension->local_only)
1093 argv[ii++] = "--local";
1094 argv[ii] = NULL;
1095
1096 g_assert (ii < G_N_ELEMENTS (argv));
1097
1098 exit_code = spam_assassin_command (
1099 argv, NULL, NULL, cancellable, error);
1100
1101 /* Check that the return value and GError agree. */
1102 if (exit_code == SPAM_ASSASSIN_EXIT_STATUS_SUCCESS)
1103 g_warn_if_fail (error == NULL || *error == NULL);
1104 else
1105 g_warn_if_fail (error == NULL || *error != NULL);
1106
1107 return (exit_code == SPAM_ASSASSIN_EXIT_STATUS_SUCCESS);
1108 }
1109
1110 static void
1111 e_spam_assassin_class_init (ESpamAssassinClass *class)
1112 {
1113 GObjectClass *object_class;
1114 EMailJunkFilterClass *junk_filter_class;
1115
1116 object_class = G_OBJECT_CLASS (class);
1117 object_class->set_property = spam_assassin_set_property;
1118 object_class->get_property = spam_assassin_get_property;
1119 object_class->finalize = spam_assassin_finalize;
1120
1121 junk_filter_class = E_MAIL_JUNK_FILTER_CLASS (class);
1122 junk_filter_class->filter_name = "SpamAssassin";
1123 junk_filter_class->display_name = _("SpamAssassin");
1124 junk_filter_class->available = spam_assassin_available;
1125 junk_filter_class->new_config_widget = spam_assassin_new_config_widget;
1126
1127 g_object_class_install_property (
1128 object_class,
1129 PROP_LOCAL_ONLY,
1130 g_param_spec_boolean (
1131 "local-only",
1132 "Local Only",
1133 "Do not use tests requiring DNS lookups",
1134 TRUE,
1135 G_PARAM_READWRITE));
1136
1137 g_object_class_install_property (
1138 object_class,
1139 PROP_SPAMC_BINARY,
1140 g_param_spec_string (
1141 "spamc-binary",
1142 "spamc Binary",
1143 "File path for the spamc binary",
1144 NULL,
1145 G_PARAM_READWRITE));
1146
1147 g_object_class_install_property (
1148 object_class,
1149 PROP_SPAMD_BINARY,
1150 g_param_spec_string (
1151 "spamd-binary",
1152 "spamd Binary",
1153 "File path for the spamd binary",
1154 NULL,
1155 G_PARAM_READWRITE));
1156
1157 g_object_class_install_property (
1158 object_class,
1159 PROP_SOCKET_PATH,
1160 g_param_spec_string (
1161 "socket-path",
1162 "Socket Path",
1163 "Socket path for a SpamAssassin daemon",
1164 NULL,
1165 G_PARAM_READWRITE));
1166
1167 g_object_class_install_property (
1168 object_class,
1169 PROP_USE_DAEMON,
1170 g_param_spec_boolean (
1171 "use-daemon",
1172 "Use Daemon",
1173 "Whether to use a SpamAssassin daemon",
1174 FALSE,
1175 G_PARAM_READWRITE));
1176 }
1177
1178 static void
1179 e_spam_assassin_class_finalize (ESpamAssassinClass *class)
1180 {
1181 }
1182
1183 static void
1184 e_spam_assassin_interface_init (CamelJunkFilterInterface *interface)
1185 {
1186 interface->classify = spam_assassin_classify;
1187 interface->learn_junk = spam_assassin_learn_junk;
1188 interface->learn_not_junk = spam_assassin_learn_not_junk;
1189 interface->synchronize = spam_assassin_synchronize;
1190 }
1191
1192 static void
1193 e_spam_assassin_init (ESpamAssassin *extension)
1194 {
1195 GSettings *settings;
1196
1197 extension->socket_path_mutex = g_mutex_new ();
1198
1199 settings = g_settings_new ("org.gnome.evolution.spamassassin");
1200
1201 g_settings_bind (
1202 settings, "local-only",
1203 extension, "local-only",
1204 G_SETTINGS_BIND_DEFAULT);
1205 g_settings_bind (
1206 settings, "spamc-binary",
1207 G_OBJECT (extension), "spamc-binary",
1208 G_SETTINGS_BIND_DEFAULT);
1209 g_settings_bind (
1210 settings, "spamd-binary",
1211 G_OBJECT (extension), "spamd-binary",
1212 G_SETTINGS_BIND_DEFAULT);
1213 g_settings_bind (
1214 settings, "socket-path",
1215 extension, "socket-path",
1216 G_SETTINGS_BIND_DEFAULT);
1217 g_settings_bind (
1218 settings, "use-daemon",
1219 extension, "use-daemon",
1220 G_SETTINGS_BIND_DEFAULT);
1221
1222 g_object_unref (settings);
1223
1224 if (extension->spamc_binary == NULL)
1225 extension->spamc_binary = g_strdup (SPAMC_BINARY);
1226
1227 if (extension->spamd_binary == NULL)
1228 extension->spamd_binary = g_strdup (SPAMD_BINARY);
1229 }
1230
1231 G_MODULE_EXPORT void
1232 e_module_load (GTypeModule *type_module)
1233 {
1234 e_spam_assassin_register_type (type_module);
1235 }
1236
1237 G_MODULE_EXPORT void
1238 e_module_unload (GTypeModule *type_module)
1239 {
1240 }