rever to filter from inbox approach
[claws.git] / src / filtering.c
1 /*
2  * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999-2001 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 /* (alfons) - Just a quick note of how this filtering module works on 
21  * new (arriving) messages.
22  * 
23  * 1) as an initialization step, code in inc.c and mbox.c set up the 
24  *    drop folder to the inbox (see inc.c and mbox.c).
25  *
26  * 2) the message is actually being copied to the drop folder using
27  *    folder_item_add_msg(dropfolder, file, TRUE). this function
28  *    eventually calls mh->add_msg(). however, the important thing
29  *    about this function is, is that the folder is not yet updated
30  *    to reflect the copy. i don't know about the validity of this
31  *    assumption, however, the filtering code assumes this and
32  *    updates the marks itself.
33  *
34  * 3) technically there's nothing wrong with the matcher (the 
35  *    piece of code which matches search strings). there's
36  *    one gotcha in procmsg.c:procmsg_get_message_file(): it
37  *    only reads a message file based on a MsgInfo. for design
38  *    reasons the filtering system should read directly from
39  *    a file (based on the file's name).
40  *
41  * 4) after the matcher sorts out any matches, it looks at the
42  *    action. this part again pushes the folder system design
43  *    to its limits. based on the assumption in 2), the matcher
44  *    knows the message has not been added to the folder system yet.
45  *    it can happily update mark files, and in fact it does.
46  * 
47  */ 
48
49 #include "defs.h"
50 #include <ctype.h>
51 #include <string.h>
52 #include <stdlib.h>
53 #include <errno.h>
54 #include <gtk/gtk.h>
55 #include <stdio.h>
56 #include "intl.h"
57 #include "utils.h"
58 #include "procheader.h"
59 #include "matcher.h"
60 #include "filtering.h"
61 #include "prefs.h"
62 #include "compose.h"
63
64 #define PREFSBUFSIZE            1024
65
66 GSList * global_processing = NULL;
67
68 #define STRLEN_WITH_CHECK(expr) \
69         strlen_with_check(#expr, __LINE__, expr)
70                 
71 static inline gint strlen_with_check(const gchar *expr, gint fline, const gchar *str)
72 {
73         if (str) 
74                 return strlen(str);
75         else {
76                 debug_print("%s(%d) - invalid string %s\n", __FILE__, fline, expr);
77                 return 0;
78         }
79 }
80
81 FilteringAction * filteringaction_new(int type, int account_id,
82                                       gchar * destination,
83                                       gint labelcolor)
84 {
85         FilteringAction * action;
86
87         action = g_new0(FilteringAction, 1);
88
89         action->type = type;
90         action->account_id = account_id;
91         if (destination)
92                 action->destination = g_strdup(destination);
93         action->labelcolor = labelcolor;        
94         return action;
95 }
96
97 void filteringaction_free(FilteringAction * action)
98 {
99         if (action->destination)
100                 g_free(action->destination);
101         g_free(action);
102 }
103
104 FilteringProp * filteringprop_new(MatcherList * matchers,
105                                   FilteringAction * action)
106 {
107         FilteringProp * filtering;
108
109         filtering = g_new0(FilteringProp, 1);
110         filtering->matchers = matchers;
111         filtering->action = action;
112
113         return filtering;
114 }
115
116 void filteringprop_free(FilteringProp * prop)
117 {
118         matcherlist_free(prop->matchers);
119         filteringaction_free(prop->action);
120         g_free(prop);
121 }
122
123 /* filteringaction_update_mark() - updates a mark for a message. note that
124  * the message should not have been moved or copied. remember that the
125  * procmsg_open_mark_file(PATH, TRUE) actually _appends_ a new record.
126  */
127 static gboolean filteringaction_update_mark(MsgInfo * info)
128 {
129         gchar * dest_path;
130         FILE * fp;
131
132         if (info->folder->folder->type == F_MH) {
133                 dest_path = folder_item_get_path(info->folder);
134                 if (!is_dir_exist(dest_path))
135                         make_dir_hier(dest_path);
136                 
137                 if (dest_path == NULL) {
138                         g_warning(_("Can't open mark file.\n"));
139                         return FALSE;
140                 }
141                 
142                 if ((fp = procmsg_open_mark_file(dest_path, TRUE))
143                     == NULL) {
144                         g_warning(_("Can't open mark file.\n"));
145                         return FALSE;
146                 }
147                 
148                 procmsg_write_flags(info, fp);
149                 fclose(fp);
150                 return TRUE;
151         }
152         return FALSE;
153 }
154
155 static gchar * filteringaction_execute_command(gchar * cmd, MsgInfo * info)
156 {
157         gchar * s = cmd;
158         gchar * filename = NULL;
159         gchar * processed_cmd;
160         gchar * p;
161         gint size;
162
163         matcher_unescape_str(cmd);
164
165         size = strlen(cmd) + 1;
166         while (*s != '\0') {
167                 if (*s == '%') {
168                         s++;
169                         switch (*s) {
170                         case '%':
171                                 size -= 1;
172                                 break;
173                         case 's': /* subject */
174                                 size += STRLEN_WITH_CHECK(info->subject) - 2;
175                                 break;
176                         case 'f': /* from */
177                                 size += STRLEN_WITH_CHECK(info->from) - 2;
178                                 break;
179                         case 't': /* to */
180                                 size += STRLEN_WITH_CHECK(info->to) - 2;
181                                 break;
182                         case 'c': /* cc */
183                                 size += STRLEN_WITH_CHECK(info->cc) - 2;
184                                 break;
185                         case 'd': /* date */
186                                 size += STRLEN_WITH_CHECK(info->date) - 2;
187                                 break;
188                         case 'i': /* message-id */
189                                 size += STRLEN_WITH_CHECK(info->msgid) - 2;
190                                 break;
191                         case 'n': /* newsgroups */
192                                 size += STRLEN_WITH_CHECK(info->newsgroups) - 2;
193                                 break;
194                         case 'r': /* references */
195                                 size += STRLEN_WITH_CHECK(info->references) - 2;
196                                 break;
197                         case 'F': /* file */
198                                 filename = folder_item_fetch_msg(info->folder, info->msgnum);
199                                 if (filename == NULL) {
200                                         g_warning(_("filename is not set"));
201                                         return NULL;
202                                 }
203                                 else
204                                         size += strlen(filename) - 2;
205                                 break;
206                         }
207                         s++;
208                 }
209                 else s++;
210         }
211
212
213         processed_cmd = g_new0(gchar, size);
214         s = cmd;
215         p = processed_cmd;
216
217         while (*s != '\0') {
218                 if (*s == '%') {
219                         s++;
220                         switch (*s) {
221                         case '%':
222                                 *p = '%';
223                                 p++;
224                                 break;
225                         case 's': /* subject */
226                                 if (info->subject != NULL)
227                                         strcpy(p, info->subject);
228                                 else
229                                         strcpy(p, "(none)");
230                                 p += strlen(p);
231                                 break;
232                         case 'f': /* from */
233                                 if (info->from != NULL)
234                                         strcpy(p, info->from);
235                                 else
236                                         strcpy(p, "(none)");
237                                 p += strlen(p);
238                                 break;
239                         case 't': /* to */
240                                 if (info->to != NULL)
241                                         strcpy(p, info->to);
242                                 else
243                                         strcpy(p, "(none)");
244                                 p += strlen(p);
245                                 break;
246                         case 'c': /* cc */
247                                 if (info->cc != NULL)
248                                         strcpy(p, info->cc);
249                                 else
250                                         strcpy(p, "(none)");
251                                 p += strlen(p);
252                                 break;
253                         case 'd': /* date */
254                                 if (info->date != NULL)
255                                         strcpy(p, info->date);
256                                 else
257                                         strcpy(p, "(none)");
258                                 p += strlen(p);
259                                 break;
260                         case 'i': /* message-id */
261                                 if (info->msgid != NULL)
262                                         strcpy(p, info->msgid);
263                                 else
264                                         strcpy(p, "(none)");
265                                 p += strlen(p);
266                                 break;
267                         case 'n': /* newsgroups */
268                                 if (info->newsgroups != NULL)
269                                         strcpy(p, info->newsgroups);
270                                 else
271                                         strcpy(p, "(none)");
272                                 p += strlen(p);
273                                 break;
274                         case 'r': /* references */
275                                 if (info->references != NULL)
276                                         strcpy(p, info->references);
277                                 else
278                                         strcpy(p, "(none)");
279                                 p += strlen(p);
280                                 break;
281                         case 'F': /* file */
282                                 strcpy(p, filename);
283                                 p += strlen(p);
284                                 break;
285                         default:
286                                 *p = '%';
287                                 p++;
288                                 *p = *s;
289                                 p++;
290                                 break;
291                         }
292                         s++;
293                 }
294                 else {
295                         *p = *s;
296                         p++;
297                         s++;
298                 }
299         }
300         return processed_cmd;
301 }
302
303 /*
304   fitleringaction_apply
305   runs the action on one MsgInfo
306   return value : return TRUE if the action could be applied
307 */
308
309 #define CHANGE_FLAGS(msginfo) \
310 { \
311 if (msginfo->folder->folder->change_flags != NULL) \
312 msginfo->folder->folder->change_flags(msginfo->folder->folder, \
313                                       msginfo->folder, \
314                                       msginfo); \
315 }
316
317 static gboolean filteringaction_apply(FilteringAction * action, MsgInfo * info,
318                                       GHashTable *folder_table)
319 {
320         FolderItem * dest_folder;
321         gint val;
322         Compose * compose;
323         PrefsAccount * account;
324         gchar * cmd;
325
326         switch(action->type) {
327         case MATCHACTION_MOVE:
328                 dest_folder =
329                         folder_find_item_from_identifier(action->destination);
330                 if (!dest_folder)
331                         return FALSE;
332                 
333                 if (folder_item_move_msg(dest_folder, info) == -1) {
334                         return FALSE;
335                 }       
336
337                 /* WRONG: can not update the mark, because the message has 
338                  * been moved. info pertains to original location. 
339                  * folder_item_move_msg() already updated the mark for the
340                  * destination folder.
341                 info->flags = 0;
342                 filteringaction_update_mark(info);
343                  */
344                 if (folder_table) {
345                         val = GPOINTER_TO_INT(g_hash_table_lookup
346                                               (folder_table, dest_folder));
347                         if (val == 0) {
348                                 folder_item_scan(dest_folder);
349                                 g_hash_table_insert(folder_table, dest_folder,
350                                                     GINT_TO_POINTER(1));
351                         }
352                         val = GPOINTER_TO_INT(g_hash_table_lookup
353                                               (folder_table, info->folder));
354                         if (val == 0) {
355                                 folder_item_scan(info->folder);
356                                 g_hash_table_insert(folder_table, info->folder,
357                                                     GINT_TO_POINTER(1));
358                         }
359                 }
360                 return TRUE;
361
362         case MATCHACTION_COPY:
363                 dest_folder =
364                         folder_find_item_from_identifier(action->destination);
365
366                 if (!dest_folder)
367                         return FALSE;
368
369                 /* NOTE: the following call *will* update the mark file for
370                  * the destination folder. but the original message will
371                  * still be there in the inbox. */
372
373                 if (folder_item_copy_msg(dest_folder, info) == -1)
374                         return FALSE;
375
376                 if (folder_table) {
377                         val = GPOINTER_TO_INT(g_hash_table_lookup
378                                               (folder_table, dest_folder));
379                         if (val == 0) {
380                                 folder_item_scan(dest_folder);
381                                 g_hash_table_insert(folder_table, dest_folder,
382                                                     GINT_TO_POINTER(1));
383                         }
384                 }
385                 return TRUE;
386
387         case MATCHACTION_DELETE:
388                 if (folder_item_remove_msg(info->folder, info->msgnum) == -1)
389                         return FALSE;
390
391                 /* WRONG: can not update the mark. this would actually add
392                  * a bogus record to the mark file for the message's original 
393                  * folder. 
394                 info->flags = 0;
395                 filteringaction_update_mark(info);
396                  */
397
398                 return TRUE;
399
400         case MATCHACTION_MARK:
401                 MSG_SET_PERM_FLAGS(info->flags, MSG_MARKED);
402                 filteringaction_update_mark(info);
403
404                 CHANGE_FLAGS(info);
405
406                 return TRUE;
407
408         case MATCHACTION_UNMARK:
409                 MSG_UNSET_PERM_FLAGS(info->flags, MSG_MARKED);
410                 filteringaction_update_mark(info);
411
412                 CHANGE_FLAGS(info);
413
414                 return TRUE;
415                 
416         case MATCHACTION_MARK_AS_READ:
417                 MSG_UNSET_PERM_FLAGS(info->flags, MSG_UNREAD | MSG_NEW);
418                 filteringaction_update_mark(info);
419
420                 CHANGE_FLAGS(info);
421
422                 return TRUE;
423
424         case MATCHACTION_MARK_AS_UNREAD:
425                 MSG_SET_PERM_FLAGS(info->flags, MSG_UNREAD | MSG_NEW);
426                 filteringaction_update_mark(info);
427
428                 CHANGE_FLAGS(info);
429                 
430                 return TRUE;
431
432         case MATCHACTION_FORWARD:
433
434                 account = account_find_from_id(action->account_id);
435                 compose = compose_forward(account, info, FALSE);
436                 if (compose->account->protocol == A_NNTP)
437                         compose_entry_append(compose, action->destination,
438                                              COMPOSE_NEWSGROUPS);
439                 else
440                         compose_entry_append(compose, action->destination,
441                                              COMPOSE_TO);
442
443                 val = compose_send(compose);
444                 if (val == 0) {
445                         gtk_widget_destroy(compose->window);
446                         return TRUE;
447                 }
448
449                 gtk_widget_destroy(compose->window);
450                 return FALSE;
451
452         case MATCHACTION_FORWARD_AS_ATTACHMENT:
453
454                 account = account_find_from_id(action->account_id);
455                 compose = compose_forward(account, info, TRUE);
456                 if (compose->account->protocol == A_NNTP)
457                         compose_entry_append(compose, action->destination,
458                                              COMPOSE_NEWSGROUPS);
459                 else
460                         compose_entry_append(compose, action->destination,
461                                              COMPOSE_TO);
462
463                 val = compose_send(compose);
464                 if (val == 0) {
465                         gtk_widget_destroy(compose->window);
466                         return TRUE;
467                 }
468
469                 gtk_widget_destroy(compose->window);
470                 return FALSE;
471
472         case MATCHACTION_BOUNCE:
473
474                 account = account_find_from_id(action->account_id);
475                 compose = compose_bounce(account, info);
476                 if (compose->account->protocol == A_NNTP)
477                         break;
478                 else
479                         compose_entry_append(compose, action->destination,
480                                              COMPOSE_TO);
481
482                 val = compose_send(compose);
483                 if (val == 0) {
484                         gtk_widget_destroy(compose->window);
485                         return TRUE;
486                 }
487
488                 gtk_widget_destroy(compose->window);
489                 return FALSE;
490
491         case MATCHACTION_EXECUTE:
492
493                 cmd = matching_build_command(action->destination, info);
494                 if (cmd == NULL)
495                         return TRUE;
496                 else {
497                         system(cmd);
498                         g_free(cmd);
499                 }
500
501                 return TRUE;
502
503         default:
504                 return FALSE;
505         }
506 }
507
508 /* filteringprop_apply() - runs the action on one MsgInfo if it matches the 
509  * criterium. certain actions can be followed by other actions. in this
510  * case the function returns FALSE. if an action can not be followed
511  * by others, the function returns TRUE.
512  *
513  * remember that this is because of the fact that msg flags are always
514  * _appended_ to mark files. currently sylpheed does not insert messages 
515  * at a certain index. 
516  * now, after having performed a certain action, the MsgInfo is still
517  * valid for the message. in *this* case the function returns FALSE.
518  */
519 static gboolean filteringprop_apply(FilteringProp * filtering, MsgInfo * info,
520                                     GHashTable *folder_table)
521 {
522         if (matcherlist_match(filtering->matchers, info)) {
523                 gboolean result;
524                 gchar   *action_str;
525                 gchar    buf[256]; 
526
527                 if (FALSE == (result = filteringaction_apply(filtering->action, info,
528                                                folder_table))) {
529                         action_str = filteringaction_to_string(buf, sizeof buf, filtering->action);
530                         g_warning(_("action %s could not be applied"), action_str);
531                 }
532
533                 switch(filtering->action->type) {
534                 case MATCHACTION_MOVE:
535                 case MATCHACTION_DELETE:
536                         return TRUE; /* MsgInfo invalid for message */
537                 case MATCHACTION_EXECUTE:
538                 case MATCHACTION_COPY:
539                 case MATCHACTION_MARK:
540                 case MATCHACTION_MARK_AS_READ:
541                 case MATCHACTION_UNMARK:
542                 case MATCHACTION_MARK_AS_UNREAD:
543                 case MATCHACTION_FORWARD:
544                 case MATCHACTION_FORWARD_AS_ATTACHMENT:
545                 case MATCHACTION_BOUNCE:
546                         return FALSE; /* MsgInfo still valid for message */
547                 default:
548                         return FALSE;
549                 }
550         }
551         else
552                 return FALSE;
553 }
554
555 void filter_msginfo(GSList * filtering_list, MsgInfo * info,
556                     GHashTable *folder_table)
557 {
558         GSList * l;
559
560         if (info == NULL) {
561                 g_warning(_("msginfo is not set"));
562                 return;
563         }
564         
565         for(l = filtering_list ; l != NULL ; l = g_slist_next(l)) {
566                 FilteringProp * filtering = (FilteringProp *) l->data;
567                 
568                 if (filteringprop_apply(filtering, info, folder_table))
569                         break;
570         }
571 }
572
573 void filter_msginfo_move_or_delete(GSList * filtering_list, MsgInfo * info,
574                                    GHashTable *folder_table)
575 {
576         GSList * l;
577
578         if (info == NULL) {
579                 g_warning(_("msginfo is not set"));
580                 return;
581         }
582         
583         for(l = filtering_list ; l != NULL ; l = g_slist_next(l)) {
584                 FilteringProp * filtering = (FilteringProp *) l->data;
585
586                 switch (filtering->action->type) {
587                 case MATCHACTION_MOVE:
588                 case MATCHACTION_DELETE:
589                         if (filteringprop_apply(filtering, info, folder_table))
590                                 return;
591                 }
592         }
593 }
594
595 void filter_message(GSList * filtering_list, FolderItem * item,
596                     gint msgnum, GHashTable *folder_table)
597 {
598         MsgInfo * msginfo;
599         gchar * filename;
600         MsgFlags  msgflags = { 0, 0 };
601
602         if (item == NULL) {
603                 g_warning(_("folderitem not set"));
604                 return;
605         }
606
607         filename = folder_item_fetch_msg(item, msgnum);
608
609         if (filename == NULL) {
610                 g_warning(_("filename is not set"));
611                 return;
612         }
613
614         msginfo = procheader_parse(filename, msgflags, TRUE);
615         
616         g_free(filename);
617
618         if (msginfo == NULL) {
619                 g_warning(_("could not get info for %s"), filename);
620                 return;
621         }
622
623         msginfo->folder = item;
624         msginfo->msgnum = msgnum;
625
626         filter_msginfo(filtering_list, msginfo, folder_table);
627 }
628
629 gchar *filteringaction_to_string(gchar *dest, gint destlen, FilteringAction *action)
630 {
631         gchar *command_str;
632
633         command_str = get_matchparser_tab_str(action->type);
634
635         if (command_str == NULL)
636                 return NULL;
637
638         switch(action->type) {
639         case MATCHACTION_MOVE:
640         case MATCHACTION_COPY:
641         case MATCHACTION_EXECUTE:
642                 g_snprintf(dest, destlen, "%s \"%s\"", command_str, action->destination);
643                 return dest;
644
645         case MATCHACTION_DELETE:
646         case MATCHACTION_MARK:
647         case MATCHACTION_UNMARK:
648         case MATCHACTION_MARK_AS_READ:
649         case MATCHACTION_MARK_AS_UNREAD:
650                 g_snprintf(dest, destlen, "%s", command_str);
651                 return dest;
652
653         case MATCHACTION_BOUNCE:
654         case MATCHACTION_FORWARD:
655         case MATCHACTION_FORWARD_AS_ATTACHMENT:
656                 g_snprintf(dest, destlen, "%s %d \"%s\"", command_str, action->account_id, action->destination); 
657                 return dest; 
658
659         case MATCHACTION_COLOR:
660                 g_snprintf(dest, destlen, "%s %d", command_str, action->labelcolor);
661                 return dest;  
662
663         default:
664                 return NULL;
665         }
666 }
667
668 gchar * filteringprop_to_string(FilteringProp * prop)
669 {
670         gchar *list_str;
671         gchar *action_str;
672         gchar *filtering_str;
673         gchar  buf[256];
674
675         action_str = filteringaction_to_string(buf, sizeof buf, prop->action);
676
677         if (action_str == NULL)
678                 return NULL;
679
680         list_str = matcherlist_to_string(prop->matchers);
681
682         if (list_str == NULL)
683                 return NULL;
684
685         filtering_str = g_strconcat(list_str, " ", action_str, NULL);
686         g_free(list_str);
687
688         return filtering_str;
689 }
690
691 void prefs_filtering_free(GSList * prefs_filtering)
692 {
693         while (prefs_filtering != NULL) {
694                 FilteringProp * filtering = (FilteringProp *)
695                         prefs_filtering->data;
696                 filteringprop_free(filtering);
697                 prefs_filtering = g_slist_remove(prefs_filtering, filtering);
698         }
699 }
700
701 static gboolean prefs_filtering_free_func(GNode *node, gpointer data)
702 {
703         FolderItem *item = node->data;
704
705         if(!item->prefs)
706                 return FALSE;
707
708         prefs_filtering_free(item->prefs->processing);
709         item->prefs->processing = NULL;
710
711         return FALSE;
712 }
713
714 void prefs_filtering_clear()
715 {
716         GList * cur;
717
718         for (cur = folder_get_list() ; cur != NULL ; cur = g_list_next(cur)) {
719                 Folder *folder;
720
721                 folder = (Folder *) cur->data;
722                 g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
723                                 prefs_filtering_free_func, NULL);
724         }
725
726         prefs_filtering_free(global_processing);
727         global_processing = NULL;
728 }