merge new cache branch
[claws.git] / src / filtering.c
1 /*
2  * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999-2002 Hiroyuki Yamamoto & The Sylpheed Claws Team
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 2 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, write to the Free Software
17  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18  */
19
20 #include "defs.h"
21 #include <ctype.h>
22 #include <string.h>
23 #include <stdlib.h>
24 #include <errno.h>
25 #include <gtk/gtk.h>
26 #include <stdio.h>
27 #include "intl.h"
28 #include "utils.h"
29 #include "procheader.h"
30 #include "matcher.h"
31 #include "filtering.h"
32 #include "prefs.h"
33 #include "compose.h"
34
35 #define PREFSBUFSIZE            1024
36
37 GSList * global_processing = NULL;
38
39 #define STRLEN_WITH_CHECK(expr) \
40         strlen_with_check(#expr, __LINE__, expr)
41                 
42 static inline gint strlen_with_check(const gchar *expr, gint fline, const gchar *str)
43 {
44         if (str) 
45                 return strlen(str);
46         else {
47                 debug_print("%s(%d) - invalid string %s\n", __FILE__, fline, expr);
48                 return 0;
49         }
50 }
51
52 FilteringAction * filteringaction_new(int type, int account_id,
53                                       gchar * destination,
54                                       gint labelcolor)
55 {
56         FilteringAction * action;
57
58         action = g_new0(FilteringAction, 1);
59
60         action->type = type;
61         action->account_id = account_id;
62         if (destination) {
63                 action->destination       = g_strdup(destination);
64                 action->unesc_destination = matcher_unescape_str(g_strdup(destination));
65         } else {
66                 action->destination       = NULL;
67                 action->unesc_destination = NULL;
68         }
69         action->labelcolor = labelcolor;        
70         return action;
71 }
72
73 void filteringaction_free(FilteringAction * action)
74 {
75         g_return_if_fail(action);
76         if (action->destination)
77                 g_free(action->destination);
78         if (action->unesc_destination)
79                 g_free(action->unesc_destination);
80         g_free(action);
81 }
82
83 FilteringProp * filteringprop_new(MatcherList * matchers,
84                                   FilteringAction * action)
85 {
86         FilteringProp * filtering;
87
88         filtering = g_new0(FilteringProp, 1);
89         filtering->matchers = matchers;
90         filtering->action = action;
91
92         return filtering;
93 }
94
95 void filteringprop_free(FilteringProp * prop)
96 {
97         matcherlist_free(prop->matchers);
98         filteringaction_free(prop->action);
99         g_free(prop);
100 }
101
102 /*
103   fitleringaction_apply
104   runs the action on one MsgInfo
105   return value : return TRUE if the action could be applied
106 */
107
108 #define CHANGE_FLAGS(msginfo) \
109 { \
110 if (msginfo->folder->folder->change_flags != NULL) \
111 msginfo->folder->folder->change_flags(msginfo->folder->folder, \
112                                       msginfo->folder, \
113                                       msginfo); \
114 }
115
116 static gboolean filteringaction_apply(FilteringAction * action, MsgInfo * info,
117                                       GHashTable *folder_table)
118 {
119         FolderItem * dest_folder;
120         gint val;
121         Compose * compose;
122         PrefsAccount * account;
123         gchar * cmd;
124
125         switch(action->type) {
126         case MATCHACTION_MOVE:
127                 dest_folder =
128                         folder_find_item_from_identifier(action->destination);
129                 if (!dest_folder)
130                         return FALSE;
131                 
132                 if (folder_item_move_msg(dest_folder, info) == -1) {
133                         debug_print("*** could not move message\n");
134                         return FALSE;
135                 }       
136
137                 if (folder_table) {
138                         val = GPOINTER_TO_INT(g_hash_table_lookup
139                                               (folder_table, dest_folder));
140                         if (val == 0) {
141                                 g_hash_table_insert(folder_table, dest_folder,
142                                                     GINT_TO_POINTER(1));
143                         }
144                 }
145                 return TRUE;
146
147         case MATCHACTION_COPY:
148                 dest_folder =
149                         folder_find_item_from_identifier(action->destination);
150
151                 if (!dest_folder)
152                         return FALSE;
153
154                 if (folder_item_copy_msg(dest_folder, info) == -1)
155                         return FALSE;
156
157                 if (folder_table) {
158                         val = GPOINTER_TO_INT(g_hash_table_lookup
159                                               (folder_table, dest_folder));
160                         if (val == 0) {
161                                 g_hash_table_insert(folder_table, dest_folder,
162                                                     GINT_TO_POINTER(1));
163                         }
164                 }
165                 return TRUE;
166
167         case MATCHACTION_DELETE:
168                 if (folder_item_remove_msg(info->folder, info->msgnum) == -1)
169                         return FALSE;
170                 return TRUE;
171
172         case MATCHACTION_MARK:
173                 MSG_SET_PERM_FLAGS(info->flags, MSG_MARKED);
174                 return TRUE;
175
176         case MATCHACTION_UNMARK:
177                 MSG_UNSET_PERM_FLAGS(info->flags, MSG_MARKED);
178                 return TRUE;
179                 
180         case MATCHACTION_MARK_AS_READ:
181                 MSG_UNSET_PERM_FLAGS(info->flags, MSG_UNREAD | MSG_NEW);
182                 return TRUE;
183
184         case MATCHACTION_MARK_AS_UNREAD:
185                 debug_print("*** setting unread flags\n");
186                 MSG_SET_PERM_FLAGS(info->flags, MSG_UNREAD | MSG_NEW);
187                 return TRUE;
188         
189         case MATCHACTION_COLOR:
190                 MSG_UNSET_PERM_FLAGS(info->flags, MSG_CLABEL_FLAG_MASK); 
191                 MSG_SET_COLORLABEL_VALUE(info->flags, action->labelcolor);
192                 return TRUE;
193
194         case MATCHACTION_FORWARD:
195                 account = account_find_from_id(action->account_id);
196                 compose = compose_forward(account, info, FALSE, NULL);
197                 if (compose->account->protocol == A_NNTP)
198                         compose_entry_append(compose, action->destination,
199                                              COMPOSE_NEWSGROUPS);
200                 else
201                         compose_entry_append(compose, action->destination,
202                                              COMPOSE_TO);
203
204                 val = compose_send(compose);
205                 if (val == 0) {
206                         gtk_widget_destroy(compose->window);
207                         return TRUE;
208                 }
209
210                 gtk_widget_destroy(compose->window);
211                 return FALSE;
212
213         case MATCHACTION_FORWARD_AS_ATTACHMENT:
214
215                 account = account_find_from_id(action->account_id);
216                 compose = compose_forward(account, info, TRUE, NULL);
217                 if (compose->account->protocol == A_NNTP)
218                         compose_entry_append(compose, action->destination,
219                                              COMPOSE_NEWSGROUPS);
220                 else
221                         compose_entry_append(compose, action->destination,
222                                              COMPOSE_TO);
223
224                 val = compose_send(compose);
225                 if (val == 0) {
226                         gtk_widget_destroy(compose->window);
227                         return TRUE;
228                 }
229                 gtk_widget_destroy(compose->window);
230                 return FALSE;
231
232         case MATCHACTION_REDIRECT:
233                 account = account_find_from_id(action->account_id);
234                 compose = compose_redirect(account, info);
235                 if (compose->account->protocol == A_NNTP)
236                         break;
237                 else
238                         compose_entry_append(compose, action->destination,
239                                              COMPOSE_TO);
240
241                 val = compose_send(compose);
242                 if (val == 0) {
243                         gtk_widget_destroy(compose->window);
244                         return TRUE;
245                 }
246
247                 gtk_widget_destroy(compose->window);
248                 return FALSE;
249
250         case MATCHACTION_EXECUTE:
251                 cmd = matching_build_command(action->unesc_destination, info);
252                 if (cmd == NULL)
253                         return FALSE;
254                 else {
255                         system(cmd);
256                         g_free(cmd);
257                 }
258                 return TRUE;
259
260         default:
261                 return FALSE;
262         }
263 }
264
265 static gboolean filtering_match_condition(FilteringProp *filtering, MsgInfo *info)
266 {
267         return matcherlist_match(filtering->matchers, info);
268 }
269
270 static gboolean filtering_apply_rule(FilteringProp *filtering, MsgInfo *info, 
271                                      GHashTable *foldertable)
272 {
273         gboolean result;
274         gchar    actionstr;
275         gchar    buf[50];
276
277         if (FALSE == (result = filteringaction_apply(filtering->action, info, foldertable))) {
278                 g_warning(_("action %s could not be applied"), 
279                 filteringaction_to_string(buf, sizeof buf, filtering->action));
280         }
281         return result;
282 }
283
284 static gboolean filtering_is_final_action(FilteringProp *filtering)
285 {
286         switch(filtering->action->type) {
287         case MATCHACTION_MOVE:
288         case MATCHACTION_DELETE:
289                 return TRUE; /* MsgInfo invalid for message */
290         case MATCHACTION_EXECUTE:
291         case MATCHACTION_COPY:
292         case MATCHACTION_MARK:
293         case MATCHACTION_MARK_AS_READ:
294         case MATCHACTION_UNMARK:
295         case MATCHACTION_MARK_AS_UNREAD:
296         case MATCHACTION_FORWARD:
297         case MATCHACTION_FORWARD_AS_ATTACHMENT:
298         case MATCHACTION_REDIRECT:
299                 return FALSE; /* MsgInfo still valid for message */
300         default:
301                 return FALSE;
302         }
303 }
304
305 static void filter_msginfo(GSList * filtering_list, FolderItem *inbox,
306                            MsgInfo * info, GHashTable *folder_table)
307 {
308         GSList  *l;
309         gboolean final;
310         gboolean applied;
311         gint val;
312         
313         if (info == NULL) {
314                 g_warning(_("msginfo is not set"));
315                 return;
316         }
317         
318         for (l = filtering_list, final = FALSE, applied = FALSE; l != NULL; l = g_slist_next(l)) {
319                 FilteringProp * filtering = (FilteringProp *) l->data;
320
321                 if (filtering_match_condition(filtering, info)) {
322                         applied = filtering_apply_rule(filtering, info, folder_table);
323                         if (TRUE == (final = filtering_is_final_action(filtering)))
324                                 break;
325                 }               
326         }
327
328         /* put in inbox if a final rule could not be applied, or
329          * the last rule was not a final one. */
330         if ((final && !applied) || !final) {
331                 if (inbox != info->folder) {
332                         if (folder_item_move_msg(inbox, info) == -1) {
333                                 debug_print(_("*** Could not drop message in inbox; check .processing\n"));
334                                 return;
335                         }       
336                         if (folder_table) {
337                                 val = GPOINTER_TO_INT(g_hash_table_lookup
338                                                       (folder_table, inbox));
339                                 if (val == 0) {
340                                         folder_item_scan(inbox);
341                                         g_hash_table_insert(folder_table, inbox,
342                                                             GINT_TO_POINTER(1));
343                                 }
344                         }
345                 }       
346         }
347 }
348
349 /*!
350  *\brief        filters a message based on its message info data
351  *
352  *\param        flist filter and actions list
353  *\param        info message
354  *\param        ftable table with changed folders after call
355  */
356 void filter_message_by_msginfo_with_inbox(GSList *flist, MsgInfo *info, GHashTable *ftable, FolderItem *def_inbox)
357 {
358         FolderItem *inbox;
359
360         if ((def_inbox == NULL)) {
361                 debug_print("using default inbox as final destination!\n");
362                 inbox = folder_get_default_inbox();
363         } else
364                 inbox = def_inbox;
365
366         /*
367          * message is already in a folder. the filtering code will
368          * handle duplicate moves and copies.
369          */
370         filter_msginfo(flist, inbox, info, ftable);
371 }
372
373 void filter_message_by_msginfo(GSList *flist, MsgInfo *info, GHashTable *ftable)
374 {
375         filter_message_by_msginfo_with_inbox(flist, info, ftable, info->folder);
376 }
377
378 /*!
379  *\brief        filters a message waiting to be processed in the
380  *              .processing folder. 
381  *
382   *\param       filtering_list list of filters and actions
383   *\param       inbox default inbox when no filter could be applied
384   *\param       msgnum message number in processing folder
385   *\param       folder_table table with folders that have been
386   *             changed after the call to this function
387   */
388 void filter_message(GSList *filtering_list, FolderItem *inbox,
389                     gint msgnum, GHashTable *folder_table)
390 {
391         MsgInfo *msginfo;
392         gchar *filename;
393         MsgFlags  msgflags = { 0, 0 };
394         FolderItem *item = folder_get_default_processing();
395
396         if (item == NULL) {
397                 g_warning(_("folderitem not set"));
398                 return;
399         }
400
401         filename = folder_item_fetch_msg(item, msgnum);
402
403         if (filename == NULL) {
404                 g_warning(_("filename is not set"));
405                 return;
406         }
407
408         msginfo = procheader_parse_file(filename, msgflags, TRUE, FALSE);
409         
410         g_free(filename);
411
412         if (msginfo == NULL) {
413                 g_warning(_("could not get info for %s"), filename);
414                 return;
415         }
416
417         msginfo->folder = item;
418         msginfo->msgnum = msgnum;
419
420         filter_msginfo(filtering_list, inbox, msginfo, folder_table);
421
422         procmsg_msginfo_free(msginfo);
423 }
424
425 gchar *filteringaction_to_string(gchar *dest, gint destlen, FilteringAction *action)
426 {
427         gchar *command_str;
428
429         command_str = get_matchparser_tab_str(action->type);
430
431         if (command_str == NULL)
432                 return NULL;
433
434         switch(action->type) {
435         case MATCHACTION_MOVE:
436         case MATCHACTION_COPY:
437         case MATCHACTION_EXECUTE:
438                 g_snprintf(dest, destlen, "%s \"%s\"", command_str, action->destination);
439                 return dest;
440
441         case MATCHACTION_DELETE:
442         case MATCHACTION_MARK:
443         case MATCHACTION_UNMARK:
444         case MATCHACTION_MARK_AS_READ:
445         case MATCHACTION_MARK_AS_UNREAD:
446                 g_snprintf(dest, destlen, "%s", command_str);
447                 return dest;
448
449         case MATCHACTION_REDIRECT:
450         case MATCHACTION_FORWARD:
451         case MATCHACTION_FORWARD_AS_ATTACHMENT:
452                 g_snprintf(dest, destlen, "%s %d \"%s\"", command_str, action->account_id, action->destination); 
453                 return dest; 
454
455         case MATCHACTION_COLOR:
456                 g_snprintf(dest, destlen, "%s %d", command_str, action->labelcolor);
457                 return dest;  
458         case MATCHACTION_DELETE_ON_SERVER:
459                 g_snprintf(dest, destlen, "%s", command_str);
460                 return dest;
461         default:
462                 return NULL;
463         }
464 }
465
466 gchar * filteringprop_to_string(FilteringProp * prop)
467 {
468         gchar *list_str;
469         gchar *action_str;
470         gchar *filtering_str;
471         gchar  buf[256];
472
473         action_str = filteringaction_to_string(buf, sizeof buf, prop->action);
474
475         if (action_str == NULL)
476                 return NULL;
477
478         list_str = matcherlist_to_string(prop->matchers);
479
480         if (list_str == NULL)
481                 return NULL;
482
483         filtering_str = g_strconcat(list_str, " ", action_str, NULL);
484         g_free(list_str);
485
486         return filtering_str;
487 }
488
489 void prefs_filtering_free(GSList * prefs_filtering)
490 {
491         while (prefs_filtering != NULL) {
492                 FilteringProp * filtering = (FilteringProp *)
493                         prefs_filtering->data;
494                 filteringprop_free(filtering);
495                 prefs_filtering = g_slist_remove(prefs_filtering, filtering);
496         }
497 }
498
499 static gboolean prefs_filtering_free_func(GNode *node, gpointer data)
500 {
501         FolderItem *item = node->data;
502
503         if(!item->prefs)
504                 return FALSE;
505
506         prefs_filtering_free(item->prefs->processing);
507         item->prefs->processing = NULL;
508
509         return FALSE;
510 }
511
512 void prefs_filtering_clear()
513 {
514         GList * cur;
515
516         for (cur = folder_get_list() ; cur != NULL ; cur = g_list_next(cur)) {
517                 Folder *folder;
518
519                 folder = (Folder *) cur->data;
520                 g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
521                                 prefs_filtering_free_func, NULL);
522         }
523
524         prefs_filtering_free(global_processing);
525         global_processing = NULL;
526 }