1 /* -*-mode:c; c-style:k&r; c-basic-offset:4; -*- */
2 /* Balsa E-Mail Client
3  *
4  * Copyright (C) 1997-2013 Stuart Parmenter and others,
5  *                         See the file AUTHORS for a list.
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2, or (at your option)
10  * any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
20  * 02111-1307, USA.
21  */
22 /*
23  * filter.c
24  *
25  * The mail filtering porting of balsa
26  *
27  * Mostly skeletonic
28  *
29  * Primary author: Emmanuel ALLAUD
30  */
31 
32 #if defined(HAVE_CONFIG_H) && HAVE_CONFIG_H
33 # include "config.h"
34 #endif                          /* HAVE_CONFIG_H */
35 #include "filter.h"
36 
37 #include <ctype.h>
38 #include <string.h>
39 
40 #if HAVE_CANBERRA
41 #include <canberra-gtk.h>
42 #endif                          /* HAVE_CANBERRA */
43 
44 #include "libbalsa.h"
45 #include "libbalsa_private.h"
46 
47 #include "filter-funcs.h"
48 #include "filter-private.h"
49 #include "misc.h"
50 #include <glib/gi18n.h>
51 
52 /* filters_trash_mbox points to a mailbox that is used as Trash by filters
53  *  code. */
54 static LibBalsaMailbox* filters_trash_mbox = NULL;
55 
56 static UrlToMailboxMapper url_to_mailbox_mapper = NULL;
57 static GSList** filter_list = NULL;
58 void
libbalsa_filters_set_trash(LibBalsaMailbox * new_trash)59 libbalsa_filters_set_trash(LibBalsaMailbox* new_trash)
60 {
61     filters_trash_mbox = new_trash;
62 }
63 
64 void
libbalsa_filters_set_url_mapper(UrlToMailboxMapper u2mm)65 libbalsa_filters_set_url_mapper(UrlToMailboxMapper u2mm)
66 {
67     url_to_mailbox_mapper = u2mm;
68 }
69 void
libbalsa_filters_set_filter_list(GSList ** list)70 libbalsa_filters_set_filter_list(GSList** list)
71 {
72     filter_list = list;
73 }
74 
75 /*------------------------------------------------------------------------
76   ---- Helper functions (also exported to have a fine-grained API) -------
77 */
78 /* libbalsa_condition_regex_set:
79    steals the string.
80 */
81 void
libbalsa_condition_regex_set(LibBalsaConditionRegex * reg,gchar * str)82 libbalsa_condition_regex_set(LibBalsaConditionRegex * reg, gchar *str)
83 {
84     g_free(reg->string);
85     reg->string = str;
86 }
87 
88 const gchar*
libbalsa_condition_regex_get(LibBalsaConditionRegex * reg)89 libbalsa_condition_regex_get(LibBalsaConditionRegex * reg)
90 {
91     return reg->string;
92 }
93 
94 void
libbalsa_condition_prepend_regex(LibBalsaCondition * cond,LibBalsaConditionRegex * new_reg)95 libbalsa_condition_prepend_regex(LibBalsaCondition* cond,
96                                  LibBalsaConditionRegex * new_reg)
97 {
98     g_warning("%s: fixme!\n", __func__);
99 #if 0
100   cond->match.regexs =
101       g_slist_prepend(cond->match.regexs, new_reg);
102 #endif
103 }
104 
105 gboolean
libbalsa_condition_matches(LibBalsaCondition * cond,LibBalsaMessage * message)106 libbalsa_condition_matches(LibBalsaCondition* cond,
107 			   LibBalsaMessage * message)
108 {
109     gboolean match = FALSE;
110     gchar * str;
111     GString * body;
112     gboolean will_ref;
113 
114     g_return_val_if_fail(cond, FALSE);
115     g_return_val_if_fail(message->headers != NULL, FALSE);
116 
117     switch (cond->type) {
118     case CONDITION_STRING:
119         will_ref =
120             (CONDITION_CHKMATCH(cond,CONDITION_MATCH_CC) ||
121              CONDITION_CHKMATCH(cond,CONDITION_MATCH_BODY));
122         if(will_ref) {
123             gboolean is_refed =
124                 libbalsa_message_body_ref(message, FALSE, FALSE);
125             if (!is_refed) {
126                 libbalsa_information(LIBBALSA_INFORMATION_ERROR,
127                                      _("Unable to load message body to "
128                                        "match filter"));
129                 return FALSE;  /* We don't want to match if an error occurred */
130             }
131         }
132         /* do the work */
133 	if (CONDITION_CHKMATCH(cond,CONDITION_MATCH_TO)
134             && message->headers->to_list) {
135             str =
136                 internet_address_list_to_string(message->headers->to_list,
137                                                 FALSE);
138 	    match=libbalsa_utf8_strstr(str,cond->match.string.string);
139 	    g_free(str);
140             if(match) break;
141 	}
142 	if (CONDITION_CHKMATCH(cond,CONDITION_MATCH_FROM)
143             && message->headers->from) {
144             str =
145                 internet_address_list_to_string(message->headers->from,
146                                                 FALSE);
147 	    match=libbalsa_utf8_strstr(str,cond->match.string.string);
148 	    g_free(str);
149 	    if (match) break;
150 	}
151 	if (CONDITION_CHKMATCH(cond,CONDITION_MATCH_SUBJECT)) {
152 	    if (libbalsa_utf8_strstr(LIBBALSA_MESSAGE_GET_SUBJECT(message),
153                                      cond->match.string.string)) {
154                 match = TRUE;
155                 break;
156             }
157 	}
158 	if (CONDITION_CHKMATCH(cond,CONDITION_MATCH_CC)
159             && message->headers->cc_list) {
160             str =
161                 internet_address_list_to_string(message->headers->cc_list,
162                                                 FALSE);
163 	    match=libbalsa_utf8_strstr(str,cond->match.string.string);
164 	    g_free(str);
165 	    if (match) break;
166 	}
167 	if (CONDITION_CHKMATCH(cond,CONDITION_MATCH_US_HEAD)) {
168             if (cond->match.string.user_header) {
169                 const gchar *header =
170                     libbalsa_message_get_user_header(message,
171                                                      cond->match.string.
172                                                      user_header);
173 
174                 if (libbalsa_utf8_strstr(header,
175                                          cond->match.string.string)) {
176                     match = TRUE;
177                     break;
178                 }
179             }
180 	}
181 	if (CONDITION_CHKMATCH(cond,CONDITION_MATCH_BODY)) {
182 	    if (!message->mailbox)
183 		return FALSE; /* We don't want to match if an error occurred */
184             body =
185                 content2reply(message->body_list, NULL, 0, FALSE, FALSE);
186 	    if (body) {
187 		if (body->str)
188                     match = libbalsa_utf8_strstr(body->str,
189                                                  cond->match.string.string);
190 		g_string_free(body,TRUE);
191 	    }
192 	}
193         if(will_ref) libbalsa_message_body_unref(message);
194 	break;
195     case CONDITION_REGEX:
196         break;
197     case CONDITION_DATE:
198         match = message->headers->date>=cond->match.date.date_low
199 	       && (cond->match.date.date_high==0 ||
200                    message->headers->date<=cond->match.date.date_high);
201         break;
202     case CONDITION_FLAG:
203         match = LIBBALSA_MESSAGE_HAS_FLAG(message, cond->match.flags);
204         break;
205     case CONDITION_AND:
206         match =
207 	    libbalsa_condition_matches(cond->match.andor.left, message) &&
208 	    libbalsa_condition_matches(cond->match.andor.right, message);
209         break;
210     case CONDITION_OR:
211         match =
212 	    libbalsa_condition_matches(cond->match.andor.left, message) ||
213 	    libbalsa_condition_matches(cond->match.andor.right, message);
214         break;
215     case CONDITION_NONE:
216         break;
217     }
218     /* To avoid warnings */
219     return cond->negate ? !match : match;
220 }
221 
222 /*--------- Filtering functions -------------------------------*/
223 
224 gint
filters_prepare_to_run(GSList * filters)225 filters_prepare_to_run(GSList * filters)
226 {
227     LibBalsaFilter* fil;
228     gboolean ok=TRUE;
229 
230     for(;filters;filters=g_slist_next(filters)) {
231 	fil=(LibBalsaFilter*) filters->data;
232 	if (!FILTER_CHKFLAG(fil,FILTER_VALID)) {
233 		libbalsa_information(LIBBALSA_INFORMATION_ERROR,
234                                      _("Invalid filter: %s"),fil->name);
235 	    ok=FALSE;
236 	}
237 	else if (!FILTER_CHKFLAG(fil,FILTER_COMPILED))
238 	    ok=libbalsa_filter_compile_regexs(fil);
239     }
240 
241     return ok;
242 }
243 
244 /* Apply the filter's action to the messages in the list; returns TRUE
245  * if message(s) were moved to the trash. */
246 gboolean
libbalsa_filter_mailbox_messages(LibBalsaFilter * filt,LibBalsaMailbox * mailbox,GArray * msgnos)247 libbalsa_filter_mailbox_messages(LibBalsaFilter * filt,
248 				 LibBalsaMailbox * mailbox,
249 				 GArray * msgnos)
250 {
251     gboolean result=FALSE;
252     LibBalsaMailbox *mbox;
253     GError *err = NULL;
254     gchar **parts, **p;
255 
256     if (msgnos->len == 0)
257 	return FALSE;
258 
259 #if HAVE_CANBERRA
260     if (filt->sound) {
261         GdkScreen *screen;
262         gint rc;
263 
264         screen = gdk_screen_get_default();
265         rc = ca_context_play(ca_gtk_context_get_for_screen(screen), 0,
266                              CA_PROP_MEDIA_FILENAME, filt->sound, NULL);
267         g_message("(%s) play %s, %s", __func__, filt->sound, ca_strerror(rc));
268     }
269 #endif                          /* HAVE_CANBERRA */
270     if (filt->popup_text)
271 	libbalsa_information(LIBBALSA_INFORMATION_MESSAGE,
272 			     "%s",
273 			     filt->popup_text);
274 
275     libbalsa_lock_mailbox(mailbox);
276 
277     switch (filt->action) {
278     case FILTER_COPY:
279 	mbox = url_to_mailbox_mapper(filt->action_string);
280 	if (!mbox)
281 	    libbalsa_information(LIBBALSA_INFORMATION_ERROR,
282 				 _("Bad mailbox name for filter: %s"),
283 				 filt->name);
284 	else if (!libbalsa_mailbox_messages_copy(mailbox, msgnos, mbox, &err))
285 	    libbalsa_information(LIBBALSA_INFORMATION_ERROR,
286 				 _("Error when copying messages: %s"),
287                                  err ? err->message : "?");
288 	else if (mbox == filters_trash_mbox)
289 	    result = TRUE;
290 	break;
291     case FILTER_TRASH:
292 	if (!filters_trash_mbox ||
293 	    !libbalsa_mailbox_messages_move(mailbox, msgnos,
294 					    filters_trash_mbox, &err))
295 	    libbalsa_information(LIBBALSA_INFORMATION_ERROR,
296 				 _("Error when trashing messages: %s"),
297                                  err ? err->message : "?");
298 	else
299 	    result = TRUE;
300 	break;
301     case FILTER_MOVE:
302 	mbox = url_to_mailbox_mapper(filt->action_string);
303 	if (!mbox)
304 	    libbalsa_information(LIBBALSA_INFORMATION_ERROR,
305 				 _("Bad mailbox name for filter: %s"),
306 				 filt->name);
307 	else if (!libbalsa_mailbox_messages_move(mailbox, msgnos, mbox, &err))
308 	    libbalsa_information(LIBBALSA_INFORMATION_ERROR,
309 				 _("Error when moving messages: %s"),
310                                  err ? err->message : "?");
311 	else if (mbox == filters_trash_mbox)
312 	    result = TRUE;
313 	break;
314     case FILTER_COLOR:
315         parts = g_strsplit(filt->action_string, ";", 2);
316         for (p = parts; *p; p++) {
317             if (g_str_has_prefix(*p, "foreground:"))
318                 libbalsa_mailbox_set_foreground(mailbox, msgnos,
319                                                 (*p) + 11);
320             if (g_str_has_prefix(*p, "background:"))
321                 libbalsa_mailbox_set_background(mailbox, msgnos,
322                                                 (*p) + 11);
323         }
324         g_strfreev(parts);
325         break;
326     case FILTER_PRINT:
327 	/* FIXME : to be implemented */
328 	break;
329     case FILTER_RUN:
330 	/* FIXME : to be implemented */
331 	break;
332     case FILTER_NOTHING:
333     case FILTER_N_TYPES:
334 	/* Nothing to do */
335 	break;
336     }
337     g_clear_error(&err);
338     libbalsa_unlock_mailbox(mailbox);
339 
340     return result;
341 }
342 
343 /*--------- End of Filtering functions -------------------------------*/
344 
345 LibBalsaFilter*
libbalsa_filter_get_by_name(const gchar * fname)346 libbalsa_filter_get_by_name(const gchar * fname)
347 {
348     GSList * list;
349     gint fnamelen;
350 
351     g_return_val_if_fail(filter_list, NULL);
352     if (!fname || fname[0]=='\0') return NULL;
353 
354     fnamelen = strlen(fname);
355     for (list = *filter_list;list;list = g_slist_next(list)) {
356 	gint len = strlen(((LibBalsaFilter*)list->data)->name);
357 
358 	if (strncmp(fname,((LibBalsaFilter*)list->data)->name,
359 		    MAX(len,fnamelen))==0)
360 	    return (LibBalsaFilter*)list->data;
361     }
362     return NULL;
363 }
364 
365 /* Check whether the condition tests the message body, and if so,
366  * whether it's already loaded; used by the imap mailbox driver to
367  * decide whether to do a server-side match. */
368 gboolean
libbalsa_condition_can_match(LibBalsaCondition * cond,LibBalsaMessage * message)369 libbalsa_condition_can_match(LibBalsaCondition * cond,
370 			     LibBalsaMessage * message)
371 {
372     if (!message)
373 	return FALSE;
374 
375     switch (cond->type) {
376     case CONDITION_STRING:
377 	return !(CONDITION_CHKMATCH(cond, CONDITION_MATCH_BODY)
378 		 && message->body_list == NULL)
379 	    && !(CONDITION_CHKMATCH(cond, CONDITION_MATCH_US_HEAD)
380 		 && message->mime_msg == NULL);
381     case CONDITION_AND:
382     case CONDITION_OR:
383 	return libbalsa_condition_can_match(cond->match.andor.left,
384 					    message)
385 	    && libbalsa_condition_can_match(cond->match.andor.right,
386 					    message);
387     default:
388 	return TRUE;
389     }
390 }
391 
392 /* Check whether a condition looks only at flags; if it does, test
393  * whether the given message's flags match it, and return the result in
394  * *match; used by mailbox backends to decide when the full
395  * LibBalsaMessage is needed. */
396 gboolean
libbalsa_condition_is_flag_only(LibBalsaCondition * cond,LibBalsaMailbox * mailbox,guint msgno,gboolean * match)397 libbalsa_condition_is_flag_only(LibBalsaCondition * cond,
398                                 LibBalsaMailbox * mailbox,
399                                 guint msgno,
400 				gboolean * match)
401 {
402     gboolean retval;
403     gboolean left_match, right_match;
404 
405     switch (cond->type) {
406     case CONDITION_FLAG:
407         if (match)
408             *match =
409                 libbalsa_mailbox_msgno_has_flags(mailbox, msgno,
410                                                  cond->match.flags, 0);
411         retval = TRUE;
412         break;
413     case CONDITION_AND:
414         retval =
415             libbalsa_condition_is_flag_only(cond->match.andor.left,
416                                             mailbox, msgno,
417 					    match ? &left_match : NULL)
418             && libbalsa_condition_is_flag_only(cond->match.andor.right,
419                                                mailbox, msgno,
420                                                match ? &right_match : NULL);
421         if (retval && match)
422             *match = left_match && right_match;
423         break;
424     case CONDITION_OR:
425         retval =
426             libbalsa_condition_is_flag_only(cond->match.andor.left,
427                                             mailbox, msgno,
428 					    match ? &left_match : NULL)
429             && libbalsa_condition_is_flag_only(cond->match.andor.right,
430                                                mailbox, msgno,
431                                                match ? &right_match : NULL);
432         if (retval && match)
433             *match = left_match || right_match;
434         break;
435     default:
436         return FALSE;
437     }
438 
439     if (retval && match && cond->negate)
440         *match = !*match;
441 
442     return retval;
443 }
444