1 /*
2  * Claws Mail -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999-2020 the Claws Mail Team and Hiroyuki Yamamoto
4  *
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.
9  *
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.
14  *
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/>.
17  */
18 
19 #include "defs.h"
20 #include <glib.h>
21 #include <glib/gi18n.h>
22 #include <ctype.h>
23 #include <string.h>
24 #include <stdlib.h>
25 #include <errno.h>
26 #include <gtk/gtk.h>
27 #include <stdio.h>
28 
29 #include "utils.h"
30 #include "procheader.h"
31 #include "matcher.h"
32 #include "filtering.h"
33 #include "prefs_gtk.h"
34 #include "compose.h"
35 #include "prefs_common.h"
36 #include "addritem.h"
37 #ifndef USE_ALT_ADDRBOOK
38 	#include "addrbook.h"
39 	#include "addressbook.h"
40 #else
41 	#include "addressbook-dbus.h"
42 	#include "addressadd.h"
43 #endif
44 #include "addr_compl.h"
45 #include "tags.h"
46 #include "log.h"
47 #include "account.h"
48 #include "addrindex.h"
49 #include "folder_item_prefs.h"
50 
51 GSList * pre_global_processing = NULL;
52 GSList * post_global_processing = NULL;
53 GSList * filtering_rules = NULL;
54 
55 gboolean debug_filtering_session = FALSE;
56 
57 static gboolean filtering_is_final_action(FilteringAction *filtering_action);
58 
filteringaction_new(int type,int account_id,gchar * destination,gint labelcolor,gint score,gchar * header)59 FilteringAction * filteringaction_new(int type, int account_id,
60 				      gchar * destination,
61 				      gint labelcolor, gint score, gchar * header)
62 {
63 	FilteringAction * action;
64 
65 	action = g_new0(FilteringAction, 1);
66 
67 	action->type = type;
68 	action->account_id = account_id;
69 	if (destination) {
70 		action->destination	  = g_strdup(destination);
71 	} else {
72 		action->destination       = NULL;
73 	}
74 	if (header) {
75 		action->header	  = g_strdup(header);
76 	} else {
77 		action->header       = NULL;
78 	}
79 	action->labelcolor = labelcolor;
80         action->score = score;
81 	return action;
82 }
83 
filteringaction_free(FilteringAction * action)84 void filteringaction_free(FilteringAction * action)
85 {
86 	cm_return_if_fail(action);
87 	g_free(action->header);
88 	g_free(action->destination);
89 	g_free(action);
90 }
91 
action_list_sort(gconstpointer a,gconstpointer b)92 static gint action_list_sort(gconstpointer a, gconstpointer b)
93 {
94 	int first  = filtering_is_final_action((FilteringAction *) a) ? 1 : 0;
95 	int second = filtering_is_final_action((FilteringAction *) b) ? 1 : 0;
96 
97 	return (first - second);
98 }
99 
filtering_action_list_sort(GSList * action_list)100 GSList *filtering_action_list_sort(GSList *action_list)
101 {
102 	return g_slist_sort(action_list, action_list_sort);
103 }
104 
filteringprop_new(gboolean enabled,const gchar * name,gint account_id,MatcherList * matchers,GSList * action_list)105 FilteringProp * filteringprop_new(gboolean enabled,
106 				  const gchar *name,
107 				  gint account_id,
108 				  MatcherList * matchers,
109 				  GSList * action_list)
110 {
111 	FilteringProp * filtering;
112 
113 	filtering = g_new0(FilteringProp, 1);
114 	filtering->enabled = enabled;
115 	filtering->name = name ? g_strdup(name): NULL;
116 	filtering->account_id = account_id;
117 	filtering->matchers = matchers;
118 	filtering->action_list = filtering_action_list_sort(action_list);
119 
120 	return filtering;
121 }
122 
filteringaction_copy(FilteringAction * src)123 static FilteringAction * filteringaction_copy(FilteringAction * src)
124 {
125         FilteringAction * new;
126 
127         new = g_new0(FilteringAction, 1);
128 
129 	new->type = src->type;
130 	new->account_id = src->account_id;
131 	if (src->destination)
132 		new->destination = g_strdup(src->destination);
133 	else
134 		new->destination = NULL;
135 	new->labelcolor = src->labelcolor;
136 	new->score = src->score;
137 
138         return new;
139 }
140 
filteringprop_copy(FilteringProp * src)141 FilteringProp * filteringprop_copy(FilteringProp *src)
142 {
143 	FilteringProp * new;
144 	GSList *tmp;
145 
146 	new = g_new0(FilteringProp, 1);
147 	new->matchers = g_new0(MatcherList, 1);
148 
149 	for (tmp = src->matchers->matchers; tmp != NULL && tmp->data != NULL;) {
150 		MatcherProp *matcher = (MatcherProp *)tmp->data;
151 
152 		new->matchers->matchers = g_slist_append(new->matchers->matchers,
153 						   matcherprop_copy(matcher));
154 		tmp = tmp->next;
155 	}
156 
157 	new->matchers->bool_and = src->matchers->bool_and;
158 
159         new->action_list = NULL;
160 
161         for (tmp = src->action_list ; tmp != NULL ; tmp = tmp->next) {
162                 FilteringAction *filtering_action;
163 
164                 filtering_action = tmp->data;
165 
166                 new->action_list = g_slist_append(new->action_list,
167                     filteringaction_copy(filtering_action));
168         }
169 
170 	new->enabled = src->enabled;
171 	new->name = g_strdup(src->name);
172 
173 	return new;
174 }
175 
filteringprop_free(FilteringProp * prop)176 void filteringprop_free(FilteringProp * prop)
177 {
178         GSList * tmp;
179 
180 	cm_return_if_fail(prop);
181 	matcherlist_free(prop->matchers);
182 
183         for (tmp = prop->action_list ; tmp != NULL ; tmp = tmp->next) {
184                 filteringaction_free(tmp->data);
185         }
186 	g_slist_free(prop->action_list);
187 	g_free(prop->name);
188 	g_free(prop);
189 }
190 
191 /* move and copy messages by batches to be faster on IMAP */
filtering_move_and_copy_msgs(GSList * msgs)192 void filtering_move_and_copy_msgs(GSList *msgs)
193 {
194 	GSList *messages = g_slist_copy(msgs);
195 	FolderItem *last_item = NULL;
196 	FiltOp cur_op = IS_NOTHING;
197 
198 	debug_print("checking %d messages\n", g_slist_length(msgs));
199 	while (messages) {
200 		GSList *batch = NULL, *cur;
201 		gint found = 0;
202 		for (cur = messages; cur; cur = cur->next) {
203 			MsgInfo *info = (MsgInfo *)cur->data;
204 			if (last_item == NULL) {
205 				if (info->filter_op == IS_COPY || info->filter_op == IS_MOVE)
206 					last_item = info->to_filter_folder;
207 				else if (info->filter_op == IS_DELE)
208 					last_item = info->folder;
209 			}
210 			if (last_item == NULL)
211 				continue;
212 			if (cur_op == IS_NOTHING) {
213 				if (info->filter_op == IS_COPY)
214 					cur_op = IS_COPY;
215 				else if (info->filter_op == IS_MOVE)
216 					cur_op = IS_MOVE;
217 				else if (info->filter_op == IS_DELE)
218 					cur_op = IS_DELE;
219 			}
220 			if (info->filter_op == IS_COPY || info->filter_op == IS_MOVE) {
221 				if (info->to_filter_folder == last_item
222 				&&  cur_op == info->filter_op) {
223 					found++;
224 					batch = g_slist_prepend(batch, info);
225 				}
226 			} else if (info->filter_op == IS_DELE) {
227 				if (info->folder == last_item
228 				&&  cur_op == info->filter_op) {
229 					found++;
230 					batch = g_slist_prepend(batch, info);
231 				}
232 			}
233 		}
234 		if (found == 0) {
235 			debug_print("no more messages to move/copy/del\n");
236 			break;
237 		} else {
238 			debug_print("%d messages to %s in %s\n", found,
239 				cur_op==IS_COPY ? "copy":(cur_op==IS_DELE ?"delete":"move"),
240 				last_item->name ? last_item->name:"(noname)");
241 		}
242 		for (cur = batch; cur; cur = cur->next) {
243 			MsgInfo *info = (MsgInfo *)cur->data;
244 			messages = g_slist_remove(messages, info);
245 			info->to_filter_folder = NULL;
246 			info->filter_op = IS_NOTHING;
247 		}
248 		batch = g_slist_reverse(batch);
249 		if (g_slist_length(batch)) {
250 			MsgInfo *info = (MsgInfo *)batch->data;
251 			if (cur_op == IS_COPY && last_item != info->folder) {
252 				folder_item_copy_msgs(last_item, batch);
253 			} else if (cur_op == IS_MOVE && last_item != info->folder) {
254 				if (folder_item_move_msgs(last_item, batch) < 0)
255 					folder_item_move_msgs(
256 						folder_get_default_inbox(),
257 						batch);
258 			} else if (cur_op == IS_DELE && last_item == info->folder) {
259 				folder_item_remove_msgs(last_item, batch);
260 			}
261 			/* we don't reference the msginfos, because caller will do */
262 			if (prefs_common.real_time_sync)
263 				folder_item_synchronise(last_item);
264 			g_slist_free(batch);
265 			batch = NULL;
266 			GTK_EVENTS_FLUSH();
267 		}
268 		last_item = NULL;
269 		cur_op = IS_NOTHING;
270 	}
271 	/* we don't reference the msginfos, because caller will do */
272 	g_slist_free(messages);
273 }
274 
275 /*
276   fitleringaction_apply
277   runs the action on one MsgInfo
278   return value : return TRUE if the action could be applied
279 */
280 
281 #define FLUSH_COPY_IF_NEEDED(info) {					\
282 	if (info->filter_op == IS_COPY && info->to_filter_folder) {	\
283 		debug_print("must debatch pending copy\n");		\
284 		folder_item_copy_msg(info->to_filter_folder, info);	\
285 		info->filter_op = IS_NOTHING;				\
286 	}								\
287 }
288 
filteringaction_apply(FilteringAction * action,MsgInfo * info)289 static gboolean filteringaction_apply(FilteringAction * action, MsgInfo * info)
290 {
291 	FolderItem * dest_folder;
292 	gint val;
293 	Compose * compose;
294 	PrefsAccount * account;
295 	gchar * cmd;
296 
297 	switch(action->type) {
298 	case MATCHACTION_MOVE:
299 		if (MSG_IS_LOCKED(info->flags))
300 			return FALSE;
301 
302 		dest_folder =
303 			folder_find_item_from_identifier(action->destination);
304 		if (!dest_folder) {
305 			debug_print("*** folder not found '%s'\n",
306 				action->destination ?action->destination :"(null)");
307 			return FALSE;
308 		}
309 
310 		FLUSH_COPY_IF_NEEDED(info);
311 		/* mark message to be moved */
312 		info->filter_op = IS_MOVE;
313 		info->to_filter_folder = dest_folder;
314 		return TRUE;
315 
316 	case MATCHACTION_COPY:
317 		dest_folder =
318 			folder_find_item_from_identifier(action->destination);
319 
320 		if (!dest_folder) {
321 			debug_print("*** folder not found '%s'\n",
322 				action->destination ?action->destination :"(null)");
323 			return FALSE;
324 		}
325 
326 		FLUSH_COPY_IF_NEEDED(info);
327 		/* mark message to be copied */
328 		info->filter_op = IS_COPY;
329 		info->to_filter_folder = dest_folder;
330 		return TRUE;
331 
332 	case MATCHACTION_SET_TAG:
333 	case MATCHACTION_UNSET_TAG:
334 		val = tags_get_id_for_str(action->destination);
335 		if (val == -1) {
336 			debug_print("*** tag '%s' not found\n",
337 				action->destination ?action->destination :"(null)");
338 			return FALSE;
339 		}
340 		FLUSH_COPY_IF_NEEDED(info);
341 		procmsg_msginfo_update_tags(info, (action->type == MATCHACTION_SET_TAG), val);
342 		return TRUE;
343 
344 	case MATCHACTION_CLEAR_TAGS:
345 		FLUSH_COPY_IF_NEEDED(info);
346 		procmsg_msginfo_clear_tags(info);
347 		return TRUE;
348 
349 	case MATCHACTION_DELETE:
350 		FLUSH_COPY_IF_NEEDED(info);
351 		info->filter_op = IS_DELE;
352 		return TRUE;
353 
354 	case MATCHACTION_MARK:
355 		FLUSH_COPY_IF_NEEDED(info);
356 		procmsg_msginfo_set_flags(info, MSG_MARKED, 0);
357 		return TRUE;
358 
359 	case MATCHACTION_UNMARK:
360 		FLUSH_COPY_IF_NEEDED(info);
361 		procmsg_msginfo_unset_flags(info, MSG_MARKED, 0);
362 		return TRUE;
363 
364 	case MATCHACTION_LOCK:
365 		FLUSH_COPY_IF_NEEDED(info);
366 		procmsg_msginfo_set_flags(info, MSG_LOCKED, 0);
367 		return TRUE;
368 
369 	case MATCHACTION_UNLOCK:
370 		FLUSH_COPY_IF_NEEDED(info);
371 		procmsg_msginfo_unset_flags(info, MSG_LOCKED, 0);
372 		return TRUE;
373 
374 	case MATCHACTION_MARK_AS_READ:
375 		FLUSH_COPY_IF_NEEDED(info);
376 		procmsg_msginfo_unset_flags(info, MSG_UNREAD | MSG_NEW, 0);
377 		return TRUE;
378 
379 	case MATCHACTION_MARK_AS_UNREAD:
380 		FLUSH_COPY_IF_NEEDED(info);
381 		procmsg_msginfo_change_flags(info, MSG_UNREAD, 0, MSG_NEW, 0);
382 		return TRUE;
383 
384 	case MATCHACTION_MARK_AS_SPAM:
385 		FLUSH_COPY_IF_NEEDED(info);
386 		procmsg_spam_learner_learn(info, NULL, TRUE);
387 		procmsg_msginfo_change_flags(info, MSG_SPAM, 0, MSG_NEW|MSG_UNREAD, 0);
388 		return TRUE;
389 
390 	case MATCHACTION_MARK_AS_HAM:
391 		FLUSH_COPY_IF_NEEDED(info);
392 		procmsg_spam_learner_learn(info, NULL, FALSE);
393 		procmsg_msginfo_unset_flags(info, MSG_SPAM, 0);
394 		return TRUE;
395 
396 	case MATCHACTION_COLOR:
397 		FLUSH_COPY_IF_NEEDED(info);
398 		procmsg_msginfo_unset_flags(info, MSG_CLABEL_FLAG_MASK, 0);
399 		procmsg_msginfo_set_flags(info, MSG_COLORLABEL_TO_FLAGS(action->labelcolor), 0);
400 		return TRUE;
401 
402 	case MATCHACTION_FORWARD:
403 	case MATCHACTION_FORWARD_AS_ATTACHMENT:
404 		account = account_find_from_id(action->account_id);
405 		compose = compose_forward(account, info,
406 			action->type == MATCHACTION_FORWARD ? FALSE : TRUE,
407 			NULL, TRUE, TRUE);
408 		compose_entry_append(compose, action->destination,
409 				     compose->account->protocol == A_NNTP
410 					    ? COMPOSE_NEWSGROUPS
411 					    : COMPOSE_TO, PREF_NONE);
412 
413 		val = compose_send(compose);
414 
415 		return val == 0 ? TRUE : FALSE;
416 
417 	case MATCHACTION_REDIRECT:
418 		account = account_find_from_id(action->account_id);
419 		compose = compose_redirect(account, info, TRUE);
420 		if (compose->account->protocol == A_NNTP)
421 			break;
422 		else
423 			compose_entry_append(compose, action->destination,
424 					     COMPOSE_TO, PREF_NONE);
425 
426 		val = compose_send(compose);
427 
428 		return val == 0 ? TRUE : FALSE;
429 
430 	case MATCHACTION_EXECUTE:
431 		cmd = matching_build_command(action->destination, info);
432 		if (cmd == NULL)
433 			return FALSE;
434 		else {
435 			if (system(cmd) == -1)
436 				g_warning("couldn't run %s", cmd);
437 			g_free(cmd);
438 		}
439 		return TRUE;
440 
441 	case MATCHACTION_SET_SCORE:
442 		FLUSH_COPY_IF_NEEDED(info);
443 		info->score = action->score;
444 		return TRUE;
445 
446 	case MATCHACTION_CHANGE_SCORE:
447 		FLUSH_COPY_IF_NEEDED(info);
448 		info->score += action->score;
449 		return TRUE;
450 
451 	case MATCHACTION_STOP:
452                 return FALSE;
453 
454 	case MATCHACTION_HIDE:
455 		FLUSH_COPY_IF_NEEDED(info);
456                 info->hidden = TRUE;
457                 return TRUE;
458 
459 	case MATCHACTION_IGNORE:
460 		FLUSH_COPY_IF_NEEDED(info);
461                 procmsg_msginfo_set_flags(info, MSG_IGNORE_THREAD, 0);
462                 return TRUE;
463 
464 	case MATCHACTION_WATCH:
465 		FLUSH_COPY_IF_NEEDED(info);
466                 procmsg_msginfo_set_flags(info, MSG_WATCH_THREAD, 0);
467                 return TRUE;
468 
469 	case MATCHACTION_ADD_TO_ADDRESSBOOK:
470 		{
471 #ifndef USE_ALT_ADDRBOOK
472 			AddressDataSource *book = NULL;
473 			AddressBookFile *abf = NULL;
474 			ItemFolder *folder = NULL;
475 #endif
476 			gchar *buf = NULL;
477 			Header *header = NULL;
478 			gint errors = 0;
479 
480 #ifndef USE_ALT_ADDRBOOK
481 			if (!addressbook_peek_folder_exists(action->destination, &book, &folder)) {
482 				g_warning("addressbook folder not found '%s'", action->destination?action->destination:"(null)");
483 				return FALSE;
484 			}
485 			if (!book) {
486 				g_warning("addressbook_peek_folder_exists returned NULL book");
487 				return FALSE;
488 			}
489 
490 			abf = book->rawDataSource;
491 #endif
492 			/* get the header */
493 			if (procheader_get_header_from_msginfo(info, &buf, action->header) < 0)
494 				return FALSE;
495 
496 			header = procheader_parse_header(buf);
497 			g_free(buf);
498 
499 			/* add all addresses that are not already in */
500 			if (header && *header->body && (*header->body != '\0')) {
501 				GSList *address_list = NULL;
502 				GSList *walk = NULL;
503 				gchar *path = NULL;
504 
505 				if (action->destination == NULL ||
506 						strcasecmp(action->destination, "Any") == 0 ||
507 						*(action->destination) == '\0')
508 					path = NULL;
509 				else
510 					path = action->destination;
511 				start_address_completion(path);
512 
513 				address_list = g_slist_append(address_list, header->body);
514 				for (walk = address_list; walk != NULL; walk = walk->next) {
515 					gchar *stripped_addr = g_strdup(walk->data);
516 					extract_address(stripped_addr);
517 
518 					if (complete_matches_found(walk->data) == 0) {
519 						gchar *name = procheader_get_fromname(walk->data);
520 						debug_print("adding '%s <%s>' to addressbook '%s'\n",
521 								name, stripped_addr, action->destination);
522 #ifndef USE_ALT_ADDRBOOK
523 						if (!addrbook_add_contact(abf, folder, name, stripped_addr, NULL)) {
524 #else
525 						if (!addressadd_selection(name, stripped_addr, NULL, NULL)) {
526 #endif
527 							g_warning("contact could not be added");
528 							errors++;
529 						}
530 						g_free(name);
531 					} else {
532 						debug_print("address '%s' already found in addressbook '%s', skipping\n",
533 								stripped_addr, action->destination);
534 					}
535 					g_free(stripped_addr);
536 				}
537 
538 				g_slist_free(address_list);
539 				end_address_completion();
540 			} else {
541 				g_warning("header '%s' not set or empty", action->header?action->header:"(null)");
542 			}
543 			return (errors == 0);
544 		}
545 	default:
546 		break;
547 	}
548 	return FALSE;
549 }
550 
551 gboolean filteringaction_apply_action_list(GSList *action_list, MsgInfo *info)
552 {
553 	GSList *p;
554 	cm_return_val_if_fail(action_list, FALSE);
555 	cm_return_val_if_fail(info, FALSE);
556 	for (p = action_list; p && p->data; p = g_slist_next(p)) {
557 		FilteringAction *a = (FilteringAction *) p->data;
558 		if (filteringaction_apply(a, info)) {
559 			if (filtering_is_final_action(a))
560 				break;
561 		} else
562 			return FALSE;
563 
564 	}
565 	return TRUE;
566 }
567 
568 static gboolean filtering_match_condition(FilteringProp *filtering, MsgInfo *info,
569 							PrefsAccount *ac_prefs)
570 
571 /* this function returns true if a filtering rule applies regarding to its account
572    data and if it does, if the conditions list match.
573 
574    per-account data of a filtering rule is either matched against current account
575    when filtering is done manually, or against the account currently used for
576    retrieving messages when it's an manual or automatic fetching of messages.
577    per-account data match doesn't apply to pre-/post-/folder-processing rules.
578 
579    when filtering messages manually:
580     - either the filtering rule is not account-based and it will be processed
581 	- or it's per-account and we check if we HAVE TO match it against the current
582 	  account (according to user-land settings, per-account rules might have to
583 	  be skipped, or only the rules that match the current account have to be
584 	  applied, or all rules will have to be applied regardless to if they are
585 	  account-based or not)
586 
587    notes about debugging output in that function:
588    when not matching, log_status_skip() is used, otherwise log_status_ok() is used
589    no debug output is done when filtering_debug_level is low
590 */
591 {
592 	gboolean matches = FALSE;
593 
594 	if (ac_prefs != NULL) {
595 		matches = ((filtering->account_id == 0)
596 					|| (filtering->account_id == ac_prefs->account_id));
597 
598 		/* debug output */
599 		if (debug_filtering_session) {
600 			if (matches && prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
601 				if (filtering->account_id == 0) {
602 					log_status_ok(LOG_DEBUG_FILTERING,
603 							_("rule is not account-based\n"));
604 				} else {
605 					if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
606 						log_status_ok(LOG_DEBUG_FILTERING,
607 								_("rule is account-based [id=%d, name='%s'], "
608 								"matching the account currently used to retrieve messages\n"),
609 								ac_prefs->account_id, ac_prefs?ac_prefs->account_name:_("NON_EXISTENT"));
610 					}
611 				}
612 			}
613 			else
614 			if (!matches) {
615 				if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
616 					log_status_skip(LOG_DEBUG_FILTERING,
617 							_("rule is account-based, "
618 							"not matching the account currently used to retrieve messages\n"));
619 				} else {
620 					if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
621 						PrefsAccount *account = account_find_from_id(filtering->account_id);
622 
623 						log_status_skip(LOG_DEBUG_FILTERING,
624 								_("rule is account-based [id=%d, name='%s'], "
625 								"not matching the account currently used to retrieve messages [id=%d, name='%s']\n"),
626 								filtering->account_id, account?account->account_name:_("NON_EXISTENT"),
627 								ac_prefs->account_id, ac_prefs?ac_prefs->account_name:_("NON_EXISTENT"));
628 					}
629 				}
630 			}
631 		}
632 	} else {
633 		switch (prefs_common.apply_per_account_filtering_rules) {
634 		case FILTERING_ACCOUNT_RULES_FORCE:
635 			/* apply filtering rules regardless to the account info */
636 			matches = TRUE;
637 
638 			/* debug output */
639 			if (debug_filtering_session) {
640 				if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
641 					if (filtering->account_id == 0) {
642 						log_status_ok(LOG_DEBUG_FILTERING,
643 								_("rule is not account-based, "
644 								"all rules are applied on user request anyway\n"));
645 					} else {
646 						PrefsAccount *account = account_find_from_id(filtering->account_id);
647 
648 						log_status_ok(LOG_DEBUG_FILTERING,
649 								_("rule is account-based [id=%d, name='%s'], "
650 								"but all rules are applied on user request\n"),
651 								filtering->account_id, account?account->account_name:_("NON_EXISTENT"));
652 					}
653 				}
654 			}
655 			break;
656 		case FILTERING_ACCOUNT_RULES_SKIP:
657 			/* don't apply filtering rules that belong to an account */
658 			matches = (filtering->account_id == 0);
659 
660 			/* debug output */
661 			if (debug_filtering_session) {
662 				if (!matches) {
663 					if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
664 						PrefsAccount *account = account_find_from_id(filtering->account_id);
665 
666 						log_status_skip(LOG_DEBUG_FILTERING,
667 								_("rule is account-based [id=%d, name='%s'], "
668 								"skipped on user request\n"),
669 								filtering->account_id, account?account->account_name:_("NON_EXISTENT"));
670 					} else {
671 						log_status_skip(LOG_DEBUG_FILTERING,
672 								_("rule is account-based, "
673 								"skipped on user request\n"));
674 					}
675 				} else {
676 					if (matches && prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
677 						log_status_ok(LOG_DEBUG_FILTERING,
678 								_("rule is not account-based\n"));
679 					}
680 				}
681 			}
682 			break;
683 		case FILTERING_ACCOUNT_RULES_USE_CURRENT:
684 			matches = ((filtering->account_id == 0)
685 					|| (filtering->account_id == cur_account->account_id));
686 
687 			/* debug output */
688 			if (debug_filtering_session) {
689 				if (!matches) {
690 					if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
691 						PrefsAccount *account = account_find_from_id(filtering->account_id);
692 
693 						log_status_skip(LOG_DEBUG_FILTERING,
694 								_("rule is account-based [id=%d, name='%s'], "
695 								"not matching current account [id=%d, name='%s']\n"),
696 								filtering->account_id, account?account->account_name:_("NON_EXISTENT"),
697 								cur_account->account_id, cur_account?cur_account->account_name:_("NON_EXISTENT"));
698 					} else {
699 						log_status_skip(LOG_DEBUG_FILTERING,
700 								_("rule is account-based, "
701 								"not matching current account\n"));
702 					}
703 				} else {
704 					if (matches && prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_HIGH) {
705 						if (filtering->account_id == 0) {
706 							log_status_ok(LOG_DEBUG_FILTERING,
707 									_("rule is not account-based\n"));
708 						} else {
709 							PrefsAccount *account = account_find_from_id(filtering->account_id);
710 
711 							log_status_ok(LOG_DEBUG_FILTERING,
712 									_("rule is account-based [id=%d, name='%s'], "
713 									"current account [id=%d, name='%s']\n"),
714 									account->account_id, account?account->account_name:_("NON_EXISTENT"),
715 									cur_account->account_id, cur_account?cur_account->account_name:_("NON_EXISTENT"));
716 						}
717 					}
718 				}
719 			}
720 			break;
721 		}
722 	}
723 
724 	return matches && matcherlist_match(filtering->matchers, info);
725 }
726 
727 /*!
728  *\brief	Apply a rule on message.
729  *
730  *\param	filtering List of filtering rules.
731  *\param	info Message to apply rules on.
732  *\param	final Variable returning TRUE or FALSE if one of the
733  *		encountered actions was final.
734  *		See also \ref filtering_is_final_action.
735  *
736  *\return	gboolean TRUE to continue applying rules.
737  */
738 static gboolean filtering_apply_rule(FilteringProp *filtering, MsgInfo *info,
739     gboolean * final)
740 {
741 	gboolean result = TRUE;
742 	gchar    *buf;
743         GSList * tmp;
744 
745         * final = FALSE;
746         for (tmp = filtering->action_list ; tmp != NULL ; tmp = tmp->next) {
747                 FilteringAction * action;
748 
749                 action = tmp->data;
750 		buf = filteringaction_to_string(action);
751 		if (debug_filtering_session)
752 			log_print(LOG_DEBUG_FILTERING, _("applying action [ %s ]\n"), buf);
753 
754                 if (FALSE == (result = filteringaction_apply(action, info))) {
755 					if (debug_filtering_session) {
756 						if (action->type != MATCHACTION_STOP)
757 							log_warning(LOG_DEBUG_FILTERING, _("action could not apply\n"));
758 						log_print(LOG_DEBUG_FILTERING,
759 								_("no further processing after action [ %s ]\n"), buf);
760 					}
761  					debug_print("No further processing after rule %s\n", buf);
762                 }
763                 g_free(buf);
764                 if (filtering_is_final_action(action)) {
765                         * final = TRUE;
766                         break;
767                 }
768 
769         }
770 	return result;
771 }
772 
773 /*!
774  *\brief	Check if an action is "final", i.e. should break further
775  *		processing.
776  *
777  *\param	filtering_action Action to check.
778  *
779  *\return	gboolean TRUE if \a filtering_action is final.
780  */
781 static gboolean filtering_is_final_action(FilteringAction *filtering_action)
782 {
783 	switch(filtering_action->type) {
784 	case MATCHACTION_MOVE:
785 	case MATCHACTION_DELETE:
786 	case MATCHACTION_STOP:
787 		return TRUE; /* MsgInfo invalid for message */
788 	default:
789 		return FALSE;
790 	}
791 }
792 
793 gboolean processing_enabled(GSList *filtering_list)
794 {
795 	GSList	*l;
796 	for (l = filtering_list; l != NULL; l = g_slist_next(l)) {
797 		FilteringProp * filtering = (FilteringProp *) l->data;
798 		if (filtering->enabled)
799 			return TRUE;
800 	}
801 	return FALSE;
802 }
803 
804 static gboolean filter_msginfo(GSList * filtering_list, MsgInfo * info, PrefsAccount* ac_prefs)
805 {
806 	GSList	*l;
807 	gboolean final;
808 	gboolean apply_next;
809 
810 	cm_return_val_if_fail(info != NULL, TRUE);
811 
812 	for (l = filtering_list, final = FALSE, apply_next = FALSE; l != NULL; l = g_slist_next(l)) {
813 		FilteringProp * filtering = (FilteringProp *) l->data;
814 
815 		if (filtering->enabled) {
816 			if (debug_filtering_session) {
817 				gchar *buf = filteringprop_to_string(filtering);
818 				if (filtering->name && *filtering->name != '\0') {
819 					log_print(LOG_DEBUG_FILTERING,
820 						_("processing rule '%s' [ %s ]\n"),
821 						filtering->name, buf);
822 				} else {
823 					log_print(LOG_DEBUG_FILTERING,
824 						_("processing rule <unnamed> [ %s ]\n"),
825 						buf);
826 				}
827 				g_free(buf);
828 			}
829 
830 			if (filtering_match_condition(filtering, info, ac_prefs)) {
831 				apply_next = filtering_apply_rule(filtering, info, &final);
832 				if (final)
833 					break;
834 			}
835 
836 		} else {
837 			if (debug_filtering_session) {
838 				gchar *buf = filteringprop_to_string(filtering);
839 				if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
840 					if (filtering->name && *filtering->name != '\0') {
841 						log_status_skip(LOG_DEBUG_FILTERING,
842 								_("disabled rule '%s' [ %s ]\n"),
843 								filtering->name, buf);
844 					} else {
845 						log_status_skip(LOG_DEBUG_FILTERING,
846 								_("disabled rule <unnamed> [ %s ]\n"),
847 								buf);
848 					}
849 				}
850 				g_free(buf);
851 			}
852 		}
853 	}
854 
855     /* put in inbox if the last rule was not a final one, or
856      * a final rule could not be applied.
857      * Either of these cases is likely. */
858     if (!final || !apply_next) {
859 		return FALSE;
860 	}
861 
862 	return TRUE;
863 }
864 
865 /*!
866  *\brief	Filter a message against a list of rules.
867  *
868  *\param	flist List of filter rules.
869  *\param	info Message.
870  *
871  *\return	gboolean TRUE if filter rules handled the message.
872  *
873  *\note		Returning FALSE means the message was not handled,
874  *		and that the calling code should do the default
875  *		processing. E.g. \ref inc.c::inc_start moves the
876  *		message to the inbox.
877  */
878 gboolean filter_message_by_msginfo(GSList *flist, MsgInfo *info, PrefsAccount* ac_prefs,
879 								   FilteringInvocationType context, gchar *extra_info)
880 {
881 	gboolean ret;
882 
883 	if (prefs_common.enable_filtering_debug) {
884 		gchar *tmp = _("undetermined");
885 
886 		switch (context) {
887 		case FILTERING_INCORPORATION:
888 			tmp = _("incorporation");
889 			debug_filtering_session = prefs_common.enable_filtering_debug_inc;
890 			break;
891 		case FILTERING_MANUALLY:
892 			tmp = _("manually");
893 			debug_filtering_session = prefs_common.enable_filtering_debug_manual;
894 			break;
895 		case FILTERING_FOLDER_PROCESSING:
896 			tmp = _("folder processing");
897 			debug_filtering_session = prefs_common.enable_filtering_debug_folder_proc;
898 			break;
899 		case FILTERING_PRE_PROCESSING:
900 			tmp = _("pre-processing");
901 			debug_filtering_session = prefs_common.enable_filtering_debug_pre_proc;
902 			break;
903 		case FILTERING_POST_PROCESSING:
904 			tmp = _("post-processing");
905 			debug_filtering_session = prefs_common.enable_filtering_debug_post_proc;
906 			break;
907 		default:
908 			debug_filtering_session = FALSE;
909 			break;
910 		}
911 
912 		if (debug_filtering_session) {
913 			gchar *file = procmsg_get_message_file_path(info);
914 			gchar *spc = g_strnfill(LOG_TIME_LEN + 1, ' ');
915 
916 			/* show context info and essential info about the message */
917 			if (prefs_common.filtering_debug_level >= FILTERING_DEBUG_LEVEL_MED) {
918 				log_print(LOG_DEBUG_FILTERING,
919 						_("filtering message (%s%s%s)\n"
920 						"%smessage file: %s\n%s%s %s\n%s%s %s\n%s%s %s\n%s%s %s\n"),
921 						tmp, extra_info ? _(": ") : "", extra_info ? extra_info : "",
922 						spc, file, spc, prefs_common_translated_header_name("Date:"), info->date,
923 						spc, prefs_common_translated_header_name("From:"), info->from,
924 						spc, prefs_common_translated_header_name("To:"), info->to,
925 						spc, prefs_common_translated_header_name("Subject:"), info->subject);
926 			} else {
927 				log_print(LOG_DEBUG_FILTERING,
928 						_("filtering message (%s%s%s)\n"
929 						"%smessage file: %s\n"),
930 						tmp, extra_info ? _(": ") : "", extra_info ? extra_info : "",
931 						spc, file);
932 			}
933 			g_free(file);
934 			g_free(spc);
935 		}
936 	} else
937 		debug_filtering_session = FALSE;
938 
939 	ret = filter_msginfo(flist, info, ac_prefs);
940 	debug_filtering_session = FALSE;
941 	return ret;
942 }
943 
944 gchar *filteringaction_to_string(FilteringAction *action)
945 {
946 	const gchar *command_str;
947 	gchar * quoted_dest;
948 	gchar * quoted_header;
949 	GString *dest = g_string_new("");
950 	gchar *deststr = NULL;
951 
952 	command_str = get_matchparser_tab_str(action->type);
953 
954 	if (command_str == NULL)
955 		return NULL;
956 
957 	switch(action->type) {
958 	case MATCHACTION_MOVE:
959 	case MATCHACTION_COPY:
960 	case MATCHACTION_EXECUTE:
961 	case MATCHACTION_SET_TAG:
962 	case MATCHACTION_UNSET_TAG:
963 		quoted_dest = matcher_quote_str(action->destination);
964 		g_string_append_printf(dest, "%s \"%s\"", command_str, quoted_dest);
965 		g_free(quoted_dest);
966 		break;
967 
968 	case MATCHACTION_DELETE:
969 	case MATCHACTION_MARK:
970 	case MATCHACTION_UNMARK:
971 	case MATCHACTION_LOCK:
972 	case MATCHACTION_UNLOCK:
973 	case MATCHACTION_MARK_AS_READ:
974 	case MATCHACTION_MARK_AS_UNREAD:
975 	case MATCHACTION_MARK_AS_SPAM:
976 	case MATCHACTION_MARK_AS_HAM:
977 	case MATCHACTION_STOP:
978 	case MATCHACTION_HIDE:
979 	case MATCHACTION_IGNORE:
980 	case MATCHACTION_WATCH:
981 	case MATCHACTION_CLEAR_TAGS:
982 		g_string_append_printf(dest, "%s", command_str);
983 		break;
984 
985 	case MATCHACTION_REDIRECT:
986 	case MATCHACTION_FORWARD:
987 	case MATCHACTION_FORWARD_AS_ATTACHMENT:
988 		quoted_dest = matcher_quote_str(action->destination);
989 		g_string_append_printf(dest, "%s %d \"%s\"", command_str, action->account_id, quoted_dest);
990 		g_free(quoted_dest);
991 		break;
992 
993 	case MATCHACTION_COLOR:
994 		g_string_append_printf(dest, "%s %d", command_str, action->labelcolor);
995 		break;
996 
997 	case MATCHACTION_CHANGE_SCORE:
998 	case MATCHACTION_SET_SCORE:
999 		g_string_append_printf(dest, "%s %d", command_str, action->score);
1000 		break;
1001 
1002 	case MATCHACTION_ADD_TO_ADDRESSBOOK:
1003 		quoted_header = matcher_quote_str(action->header);
1004 		quoted_dest = matcher_quote_str(action->destination);
1005 		g_string_append_printf(dest, "%s \"%s\" \"%s\"", command_str, quoted_header, quoted_dest);
1006 		g_free(quoted_dest);
1007 		g_free(quoted_header);
1008 		break;
1009 
1010 	default:
1011 		return NULL;
1012 	}
1013 	deststr = dest->str;
1014 	g_string_free(dest, FALSE);
1015 	return deststr;
1016 }
1017 
1018 gchar * filteringaction_list_to_string(GSList * action_list)
1019 {
1020 	gchar *action_list_str;
1021         GSList * tmp;
1022 	gchar *list_str;
1023 
1024         action_list_str = NULL;
1025         for (tmp = action_list ; tmp != NULL ; tmp = tmp->next) {
1026                 gchar *action_str;
1027                 FilteringAction * action;
1028 
1029                 action = tmp->data;
1030 
1031                 action_str = filteringaction_to_string(action);
1032 
1033                 if (action_list_str != NULL) {
1034                         list_str = g_strconcat(action_list_str, " ", action_str, NULL);
1035                         g_free(action_list_str);
1036                 }
1037                 else {
1038                         list_str = g_strdup(action_str);
1039                 }
1040 		g_free(action_str);
1041                 action_list_str = list_str;
1042         }
1043 
1044         return action_list_str;
1045 }
1046 
1047 gchar * filteringprop_to_string(FilteringProp * prop)
1048 {
1049 	gchar *list_str;
1050 	gchar *action_list_str;
1051 	gchar *filtering_str;
1052 
1053 	if (prop == NULL)
1054 		return NULL;
1055 
1056         action_list_str = filteringaction_list_to_string(prop->action_list);
1057 
1058 	if (action_list_str == NULL)
1059 		return NULL;
1060 
1061 	list_str = matcherlist_to_string(prop->matchers);
1062 
1063 	if (list_str == NULL) {
1064                 g_free(action_list_str);
1065 		return NULL;
1066         }
1067 
1068 	filtering_str = g_strconcat(list_str, " ", action_list_str, NULL);
1069 	g_free(action_list_str);
1070 	g_free(list_str);
1071 
1072 	return filtering_str;
1073 }
1074 
1075 static void prefs_filtering_free(GSList * prefs_filtering)
1076 {
1077  	while (prefs_filtering != NULL) {
1078  		FilteringProp * filtering = (FilteringProp *)
1079 			prefs_filtering->data;
1080  		filteringprop_free(filtering);
1081  		prefs_filtering = g_slist_remove(prefs_filtering, filtering);
1082  	}
1083 }
1084 
1085 static gboolean prefs_filtering_free_func(GNode *node, gpointer data)
1086 {
1087 	FolderItem *item = node->data;
1088 
1089 	cm_return_val_if_fail(item, FALSE);
1090 	cm_return_val_if_fail(item->prefs, FALSE);
1091 
1092 	prefs_filtering_free(item->prefs->processing);
1093 	item->prefs->processing = NULL;
1094 
1095 	return FALSE;
1096 }
1097 
1098 void prefs_filtering_clear(void)
1099 {
1100 	GList * cur;
1101 
1102 	for (cur = folder_get_list() ; cur != NULL ; cur = g_list_next(cur)) {
1103 		Folder *folder;
1104 
1105 		folder = (Folder *) cur->data;
1106 		g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
1107 				prefs_filtering_free_func, NULL);
1108 	}
1109 
1110 	prefs_filtering_free(filtering_rules);
1111 	filtering_rules = NULL;
1112 	prefs_filtering_free(pre_global_processing);
1113 	pre_global_processing = NULL;
1114 	prefs_filtering_free(post_global_processing);
1115 	post_global_processing = NULL;
1116 }
1117 
1118 void prefs_filtering_clear_folder(Folder *folder)
1119 {
1120 	cm_return_if_fail(folder);
1121 	cm_return_if_fail(folder->node);
1122 
1123 	g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
1124 			prefs_filtering_free_func, NULL);
1125 	/* FIXME: Note folder settings were changed, where the updates? */
1126 }
1127 
1128 gboolean filtering_peek_per_account_rules(GSList *filtering_list)
1129 /* return TRUE if there's at least one per-account filtering rule */
1130 {
1131 	GSList *l;
1132 
1133 	for (l = filtering_list; l != NULL; l = g_slist_next(l)) {
1134 		FilteringProp * filtering = (FilteringProp *) l->data;
1135 
1136 		if (filtering->enabled && (filtering->account_id != 0)) {
1137 			return TRUE;
1138 		}
1139 	}
1140 
1141 	return FALSE;
1142 }
1143 
1144 gboolean filtering_action_list_rename_path(GSList *action_list, const gchar *old_path,
1145 					   const gchar *new_path)
1146 {
1147 	gchar *base;
1148 	gchar *prefix;
1149 	gchar *suffix;
1150 	gchar *dest_path;
1151 	gchar *old_path_with_sep;
1152 	gint destlen;
1153 	gint prefixlen;
1154 	gint oldpathlen;
1155         GSList * action_cur;
1156 	const gchar *separator=G_DIR_SEPARATOR_S;
1157 	gboolean matched = FALSE;
1158 #ifdef G_OS_WIN32
1159 again:
1160 #endif
1161 	oldpathlen = strlen(old_path);
1162 	old_path_with_sep = g_strconcat(old_path,separator,NULL);
1163 
1164 	for(action_cur = action_list ; action_cur != NULL ;
1165 		action_cur = action_cur->next) {
1166 
1167 		FilteringAction *action = action_cur->data;
1168 
1169 		if (action->type == MATCHACTION_SET_TAG ||
1170 		    action->type == MATCHACTION_UNSET_TAG)
1171 			continue;
1172 		if (!action->destination)
1173 			continue;
1174 
1175 		destlen = strlen(action->destination);
1176 
1177 		if (destlen > oldpathlen) {
1178 			prefixlen = destlen - oldpathlen;
1179 			suffix = action->destination + prefixlen;
1180 
1181 			if (!strncmp(old_path, suffix, oldpathlen)) {
1182 				prefix = g_malloc0(prefixlen + 1);
1183 				strncpy2(prefix, action->destination, prefixlen);
1184 
1185 				base = suffix + oldpathlen;
1186 				while (*base == G_DIR_SEPARATOR) base++;
1187                                 if (*base == '\0')
1188                                 	dest_path = g_strconcat(prefix, separator,
1189                                 				new_path, NULL);
1190 				else
1191 					dest_path = g_strconcat(prefix,
1192 								separator,
1193 								new_path,
1194 								separator,
1195 								base, NULL);
1196 
1197 					g_free(prefix);
1198 					g_free(action->destination);
1199 					action->destination = dest_path;
1200 					matched = TRUE;
1201 			} else { /* for non-leaf folders */
1202 				/* compare with trailing slash */
1203 				if (!strncmp(old_path_with_sep, action->destination, oldpathlen+1)) {
1204 
1205 					suffix = action->destination + oldpathlen + 1;
1206 					dest_path = g_strconcat(new_path, separator,
1207 								suffix, NULL);
1208 					g_free(action->destination);
1209 					action->destination = dest_path;
1210 					matched = TRUE;
1211 				}
1212 			}
1213 		} else {
1214 			/* folder-moving a leaf */
1215 			if (!strcmp(old_path, action->destination)) {
1216                         	dest_path = g_strdup(new_path);
1217                         	g_free(action->destination);
1218                         	action->destination = dest_path;
1219                         	matched = TRUE;
1220 			}
1221 		}
1222 	}
1223 
1224 	g_free(old_path_with_sep);
1225 #ifdef G_OS_WIN32
1226 	if (!strcmp(separator, G_DIR_SEPARATOR_S) && !matched) {
1227 		separator = "/";
1228 		goto again;
1229 	}
1230 #endif
1231 
1232 	return matched;
1233 }
1234