No issues found
1 /*
2 * evolution-bogofilter.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 #ifdef HAVE_CONFIG_H
20 #include <config.h>
21 #endif
22
23 #include <config.h>
24 #include <sys/types.h>
25 #include <sys/wait.h>
26 #include <glib/gi18n-lib.h>
27
28 #include <camel/camel.h>
29
30 #include <libemail-engine/e-mail-junk-filter.h>
31
32 /* Standard GObject macros */
33 #define E_TYPE_BOGOFILTER \
34 (e_bogofilter_get_type ())
35 #define E_BOGOFILTER(obj) \
36 (G_TYPE_CHECK_INSTANCE_CAST \
37 ((obj), E_TYPE_BOGOFILTER, EBogofilter))
38
39 #ifndef BOGOFILTER_BINARY
40 #define BOGOFILTER_BINARY "/usr/bin/bogofilter"
41 #endif
42
43 #define BOGOFILTER_EXIT_STATUS_SPAM 0
44 #define BOGOFILTER_EXIT_STATUS_HAM 1
45 #define BOGOFILTER_EXIT_STATUS_UNSURE 2
46 #define BOGOFILTER_EXIT_STATUS_ERROR 3
47
48 typedef struct _EBogofilter EBogofilter;
49 typedef struct _EBogofilterClass EBogofilterClass;
50
51 struct _EBogofilter {
52 EMailJunkFilter parent;
53 gboolean convert_to_unicode;
54 };
55
56 struct _EBogofilterClass {
57 EMailJunkFilterClass parent_class;
58 };
59
60 enum {
61 PROP_0,
62 PROP_CONVERT_TO_UNICODE
63 };
64
65 /* Module Entry Points */
66 void e_module_load (GTypeModule *type_module);
67 void e_module_unload (GTypeModule *type_module);
68
69 /* Forward Declarations */
70 GType e_bogofilter_get_type (void);
71 static void e_bogofilter_interface_init (CamelJunkFilterInterface *interface);
72
73 G_DEFINE_DYNAMIC_TYPE_EXTENDED (
74 EBogofilter,
75 e_bogofilter,
76 E_TYPE_MAIL_JUNK_FILTER, 0,
77 G_IMPLEMENT_INTERFACE_DYNAMIC (
78 CAMEL_TYPE_JUNK_FILTER,
79 e_bogofilter_interface_init))
80
81 #ifdef G_OS_UNIX
82 static void
83 bogofilter_cancelled_cb (GCancellable *cancellable,
84 GPid *pid)
85 {
86 /* XXX On UNIX-like systems we can safely assume a GPid is the
87 * process ID and use it to terminate the process via signal. */
88 kill (*pid, SIGTERM);
89 }
90 #endif
91
92 static void
93 bogofilter_exited_cb (GPid *pid,
94 gint status,
95 gpointer user_data)
96 {
97 struct {
98 GMainLoop *loop;
99 gint exit_code;
100 } *source_data = user_data;
101
102 if (WIFEXITED (status))
103 source_data->exit_code = WEXITSTATUS (status);
104 else
105 source_data->exit_code = BOGOFILTER_EXIT_STATUS_ERROR;
106
107 g_main_loop_quit (source_data->loop);
108 }
109
110 static gint
111 bogofilter_command (const gchar **argv,
112 CamelMimeMessage *message,
113 GCancellable *cancellable,
114 GError **error)
115 {
116 CamelStream *stream;
117 GMainContext *context;
118 GSource *source;
119 GPid child_pid;
120 gssize bytes_written;
121 gint standard_input;
122 gulong handler_id = 0;
123 gboolean success;
124
125 struct {
126 GMainLoop *loop;
127 gint exit_code;
128 } source_data;
129
130 /* Spawn Bogofilter with an open stdin pipe. */
131 success = g_spawn_async_with_pipes (
132 NULL,
133 (gchar **) argv,
134 NULL,
135 G_SPAWN_DO_NOT_REAP_CHILD |
136 G_SPAWN_STDOUT_TO_DEV_NULL,
137 NULL, NULL,
138 &child_pid,
139 &standard_input,
140 NULL,
141 NULL,
142 error);
143
144 if (!success) {
145 gchar *command_line;
146
147 command_line = g_strjoinv (" ", (gchar **) argv);
148 g_prefix_error (
149 error, _("Failed to spawn Bogofilter (%s): "),
150 command_line);
151 g_free (command_line);
152
153 return BOGOFILTER_EXIT_STATUS_ERROR;
154 }
155
156 /* Stream the CamelMimeMessage to Bogofilter. */
157 stream = camel_stream_fs_new_with_fd (standard_input);
158 bytes_written = camel_data_wrapper_write_to_stream_sync (
159 CAMEL_DATA_WRAPPER (message), stream, cancellable, error);
160 success = (bytes_written >= 0) &&
161 (camel_stream_close (stream, cancellable, error) == 0);
162 g_object_unref (stream);
163
164 if (!success) {
165 g_spawn_close_pid (child_pid);
166 g_prefix_error (
167 error, _("Failed to stream mail "
168 "message content to Bogofilter: "));
169 return BOGOFILTER_EXIT_STATUS_ERROR;
170 }
171
172 /* Wait for the Bogofilter process to terminate
173 * using GLib's main loop for better portability. */
174
175 context = g_main_context_new ();
176
177 source = g_child_watch_source_new (child_pid);
178 g_source_set_callback (
179 source, (GSourceFunc)
180 bogofilter_exited_cb,
181 &source_data, NULL);
182 g_source_attach (source, context);
183 g_source_unref (source);
184
185 source_data.loop = g_main_loop_new (context, TRUE);
186 source_data.exit_code = 0;
187
188 #ifdef G_OS_UNIX
189 if (G_IS_CANCELLABLE (cancellable))
190 handler_id = g_cancellable_connect (
191 cancellable,
192 G_CALLBACK (bogofilter_cancelled_cb),
193 &child_pid, (GDestroyNotify) NULL);
194 #endif
195
196 g_main_loop_run (source_data.loop);
197
198 if (handler_id > 0)
199 g_cancellable_disconnect (cancellable, handler_id);
200
201 g_main_loop_unref (source_data.loop);
202 source_data.loop = NULL;
203
204 g_main_context_unref (context);
205
206 /* Clean up. */
207
208 g_spawn_close_pid (child_pid);
209
210 if (g_cancellable_set_error_if_cancelled (cancellable, error))
211 source_data.exit_code = BOGOFILTER_EXIT_STATUS_ERROR;
212
213 else if (source_data.exit_code == BOGOFILTER_EXIT_STATUS_ERROR)
214 g_set_error_literal (
215 error, CAMEL_ERROR, CAMEL_ERROR_GENERIC,
216 _("Bogofilter either crashed or "
217 "failed to process a mail message"));
218
219 return source_data.exit_code;
220 }
221
222 static void
223 bogofilter_init_wordlist (EBogofilter *extension)
224 {
225 CamelStream *stream;
226 CamelMimeParser *parser;
227 CamelMimeMessage *message;
228
229 /* Initialize the Bogofilter database with a welcome message. */
230
231 parser = camel_mime_parser_new ();
232 message = camel_mime_message_new ();
233
234 stream = camel_stream_fs_new_with_name (
235 WELCOME_MESSAGE, O_RDONLY, 0, NULL);
236 camel_mime_parser_init_with_stream (parser, stream, NULL);
237 camel_mime_parser_scan_from (parser, FALSE);
238 g_object_unref (stream);
239
240 camel_mime_part_construct_from_parser_sync (
241 CAMEL_MIME_PART (message), parser, NULL, NULL);
242
243 camel_junk_filter_learn_not_junk (
244 CAMEL_JUNK_FILTER (extension), message, NULL, NULL);
245
246 g_object_unref (message);
247 g_object_unref (parser);
248 }
249
250 static gboolean
251 bogofilter_get_convert_to_unicode (EBogofilter *extension)
252 {
253 return extension->convert_to_unicode;
254 }
255
256 static void
257 bogofilter_set_convert_to_unicode (EBogofilter *extension,
258 gboolean convert_to_unicode)
259 {
260 if (extension->convert_to_unicode == convert_to_unicode)
261 return;
262
263 extension->convert_to_unicode = convert_to_unicode;
264
265 g_object_notify (G_OBJECT (extension), "convert-to-unicode");
266 }
267
268 static void
269 bogofilter_set_property (GObject *object,
270 guint property_id,
271 const GValue *value,
272 GParamSpec *pspec)
273 {
274 switch (property_id) {
275 case PROP_CONVERT_TO_UNICODE:
276 bogofilter_set_convert_to_unicode (
277 E_BOGOFILTER (object),
278 g_value_get_boolean (value));
279 return;
280 }
281
282 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
283 }
284
285 static void
286 bogofilter_get_property (GObject *object,
287 guint property_id,
288 GValue *value,
289 GParamSpec *pspec)
290 {
291 switch (property_id) {
292 case PROP_CONVERT_TO_UNICODE:
293 g_value_set_boolean (
294 value, bogofilter_get_convert_to_unicode (
295 E_BOGOFILTER (object)));
296 return;
297 }
298
299 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
300 }
301
302 static gboolean
303 bogofilter_available (EMailJunkFilter *junk_filter)
304 {
305 return g_file_test (BOGOFILTER_BINARY, G_FILE_TEST_IS_EXECUTABLE);
306 }
307
308 static GtkWidget *
309 bogofilter_new_config_widget (EMailJunkFilter *junk_filter)
310 {
311 GtkWidget *box;
312 GtkWidget *widget;
313 gchar *markup;
314
315 box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12);
316
317 markup = g_markup_printf_escaped (
318 "<b>%s</b>", _("Bogofilter Options"));
319 widget = gtk_label_new (markup);
320 gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5);
321 gtk_label_set_use_markup (GTK_LABEL (widget), TRUE);
322 gtk_box_pack_start (GTK_BOX (box), widget, FALSE, FALSE, 0);
323 gtk_widget_show (widget);
324 g_free (markup);
325
326 widget = gtk_check_button_new_with_mnemonic (
327 _("Convert message text to _Unicode"));
328 gtk_widget_set_margin_left (widget, 12);
329 gtk_box_pack_start (GTK_BOX (box), widget, FALSE, FALSE, 0);
330 gtk_widget_show (widget);
331
332 g_object_bind_property (
333 junk_filter, "convert-to-unicode",
334 widget, "active",
335 G_BINDING_BIDIRECTIONAL |
336 G_BINDING_SYNC_CREATE);
337
338 return box;
339 }
340
341 static gboolean
342 bogofilter_classify (CamelJunkFilter *junk_filter,
343 CamelMimeMessage *message,
344 CamelJunkStatus *status,
345 GCancellable *cancellable,
346 GError **error)
347 {
348 EBogofilter *extension = E_BOGOFILTER (junk_filter);
349 static gboolean wordlist_initialized = FALSE;
350 gint exit_code;
351
352 const gchar *argv[] = {
353 BOGOFILTER_BINARY,
354 NULL, /* leave room for unicode option */
355 NULL
356 };
357
358 if (bogofilter_get_convert_to_unicode (extension))
359 argv[1] = "--unicode=yes";
360
361 retry:
362 exit_code = bogofilter_command (argv, message, cancellable, error);
363
364 switch (exit_code) {
365 case BOGOFILTER_EXIT_STATUS_SPAM:
366 *status = CAMEL_JUNK_STATUS_MESSAGE_IS_JUNK;
367 break;
368
369 case BOGOFILTER_EXIT_STATUS_HAM:
370 *status = CAMEL_JUNK_STATUS_MESSAGE_IS_NOT_JUNK;
371 break;
372
373 case BOGOFILTER_EXIT_STATUS_UNSURE:
374 *status = CAMEL_JUNK_STATUS_INCONCLUSIVE;
375 break;
376
377 case BOGOFILTER_EXIT_STATUS_ERROR:
378 if (!wordlist_initialized) {
379 wordlist_initialized = TRUE;
380 bogofilter_init_wordlist (extension);
381 goto retry;
382 }
383 break;
384
385 default:
386 g_warning (
387 "Bogofilter: Unexpected exit code (%d) "
388 "while classifying message", exit_code);
389 break;
390 }
391
392 /* Check that the return value and GError agree. */
393 if (exit_code != BOGOFILTER_EXIT_STATUS_ERROR)
394 g_warn_if_fail (error == NULL || *error == NULL);
395 else
396 g_warn_if_fail (error == NULL || *error != NULL);
397
398 return (exit_code != BOGOFILTER_EXIT_STATUS_ERROR);
399 }
400
401 static gboolean
402 bogofilter_learn_junk (CamelJunkFilter *junk_filter,
403 CamelMimeMessage *message,
404 GCancellable *cancellable,
405 GError **error)
406 {
407 EBogofilter *extension = E_BOGOFILTER (junk_filter);
408 gint exit_code;
409
410 const gchar *argv[] = {
411 BOGOFILTER_BINARY,
412 "--register-spam",
413 NULL, /* leave room for unicode option */
414 NULL
415 };
416
417 if (bogofilter_get_convert_to_unicode (extension))
418 argv[2] = "--unicode=yes";
419
420 exit_code = bogofilter_command (argv, message, cancellable, error);
421
422 if (exit_code != 0)
423 g_warning (
424 "Bogofilter: Unexpected exit code (%d) "
425 "while registering spam", exit_code);
426
427 /* Check that the return value and GError agree. */
428 if (exit_code != BOGOFILTER_EXIT_STATUS_ERROR)
429 g_warn_if_fail (error == NULL || *error == NULL);
430 else
431 g_warn_if_fail (error == NULL || *error != NULL);
432
433 return (exit_code != BOGOFILTER_EXIT_STATUS_ERROR);
434 }
435
436 static gboolean
437 bogofilter_learn_not_junk (CamelJunkFilter *junk_filter,
438 CamelMimeMessage *message,
439 GCancellable *cancellable,
440 GError **error)
441 {
442 EBogofilter *extension = E_BOGOFILTER (junk_filter);
443 gint exit_code;
444
445 const gchar *argv[] = {
446 BOGOFILTER_BINARY,
447 "--register-ham",
448 NULL, /* leave room for unicode option */
449 NULL
450 };
451
452 if (bogofilter_get_convert_to_unicode (extension))
453 argv[2] = "--unicode=yes";
454
455 exit_code = bogofilter_command (argv, message, cancellable, error);
456
457 if (exit_code != 0)
458 g_warning (
459 "Bogofilter: Unexpected exit code (%d) "
460 "while registering ham", exit_code);
461
462 /* Check that the return value and GError agree. */
463 if (exit_code != BOGOFILTER_EXIT_STATUS_ERROR)
464 g_warn_if_fail (error == NULL || *error == NULL);
465 else
466 g_warn_if_fail (error == NULL || *error != NULL);
467
468 return (exit_code != BOGOFILTER_EXIT_STATUS_ERROR);
469 }
470
471 static void
472 e_bogofilter_class_init (EBogofilterClass *class)
473 {
474 GObjectClass *object_class;
475 EMailJunkFilterClass *junk_filter_class;
476
477 object_class = G_OBJECT_CLASS (class);
478 object_class->set_property = bogofilter_set_property;
479 object_class->get_property = bogofilter_get_property;
480
481 junk_filter_class = E_MAIL_JUNK_FILTER_CLASS (class);
482 junk_filter_class->filter_name = "Bogofilter";
483 junk_filter_class->display_name = _("Bogofilter");
484 junk_filter_class->available = bogofilter_available;
485 junk_filter_class->new_config_widget = bogofilter_new_config_widget;
486
487 g_object_class_install_property (
488 object_class,
489 PROP_CONVERT_TO_UNICODE,
490 g_param_spec_boolean (
491 "convert-to-unicode",
492 "Convert to Unicode",
493 "Convert message text to Unicode",
494 TRUE,
495 G_PARAM_READWRITE));
496 }
497
498 static void
499 e_bogofilter_class_finalize (EBogofilterClass *class)
500 {
501 }
502
503 static void
504 e_bogofilter_interface_init (CamelJunkFilterInterface *interface)
505 {
506 interface->classify = bogofilter_classify;
507 interface->learn_junk = bogofilter_learn_junk;
508 interface->learn_not_junk = bogofilter_learn_not_junk;
509 }
510
511 static void
512 e_bogofilter_init (EBogofilter *extension)
513 {
514 GSettings *settings;
515
516 settings = g_settings_new ("org.gnome.evolution.bogofilter");
517 g_settings_bind (
518 settings, "utf8-for-spam-filter",
519 G_OBJECT (extension), "convert-to-unicode",
520 G_SETTINGS_BIND_DEFAULT);
521 g_object_unref (settings);
522 }
523
524 G_MODULE_EXPORT void
525 e_module_load (GTypeModule *type_module)
526 {
527 e_bogofilter_register_type (type_module);
528 }
529
530 G_MODULE_EXPORT void
531 e_module_unload (GTypeModule *type_module)
532 {
533 }