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