1 /*
2 * Project : tin - a Usenet reader
3 * Module : filter.c
4 * Author : I. Lea
5 * Created : 1992-12-28
6 * Updated : 2020-05-28
7 * Notes : Filter articles. Kill & auto selection are supported.
8 *
9 * Copyright (c) 1991-2021 Iain Lea <iain@bricbrac.de>
10 * All rights reserved.
11 *
12 * Redistribution and use in source and binary forms, with or without
13 * modification, are permitted provided that the following conditions
14 * are met:
15 *
16 * 1. Redistributions of source code must retain the above copyright notice,
17 * this list of conditions and the following disclaimer.
18 *
19 * 2. Redistributions in binary form must reproduce the above copyright
20 * notice, this list of conditions and the following disclaimer in the
21 * documentation and/or other materials provided with the distribution.
22 *
23 * 3. Neither the name of the copyright holder nor the names of its
24 * contributors may be used to endorse or promote products derived from
25 * this software without specific prior written permission.
26 *
27 * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
28 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
29 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
30 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
31 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
32 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
33 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
34 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
35 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
36 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
37 * POSSIBILITY OF SUCH DAMAGE.
38 */
39
40
41 #ifndef TIN_H
42 # include "tin.h"
43 #endif /* !TIN_H */
44 #ifndef VERSION_H
45 # include "version.h"
46 #endif /* !VERSION_H */
47 #ifndef TCURSES_H
48 # include "tcurses.h"
49 #endif /* !TCURSES_H */
50
51
52 #define IS_READ(i) (arts[i].status == ART_READ)
53 #define IS_KILLED(i) (arts[i].killed)
54 #define IS_KILLED_UNREAD(i) (arts[i].killed == ART_KILLED_UNREAD)
55 #define IS_SELECTED(i) (arts[i].selected)
56
57 /*
58 * SET_FILTER in group grp, current article arts[i], with rule ptr[j]
59 *
60 * filtering is now done this way:
61 * a. set score for all articles and rules
62 * b. check each article if the score is above or below the limit
63 *
64 * SET_FILTER is now somewhat shorter, as the real filtering is done
65 * at the end of filter_articles()
66 */
67
68 #define SET_FILTER(grp, i, j) \
69 if (ptr[j].score > 0) { \
70 arts[i].score = (SCORE_MAX - ptr[j].score >= arts[i].score) ? \
71 (arts[i].score + ptr[j].score) : SCORE_MAX ; \
72 } else { \
73 arts[i].score = (-SCORE_MAX - ptr[j].score <= arts[i].score) ? \
74 (arts[i].score + ptr[j].score) : -SCORE_MAX ; }
75
76 /*
77 * These will probably go away when filtering is rewritten
78 * Easier access to hashed msgids. Note that in REFS(), y must be free()d
79 * msgid is mandatory in an article and cannot be NULL
80 */
81 #define MSGID(x) (x->refptr ? x->refptr->txt : "")
82 #define REFS(x,y) ((y = get_references(x->refptr ? x->refptr->parent : NULL)) ? y : "")
83
84 /*
85 * global filter array
86 */
87 struct t_filters glob_filter = { 0, 0, (struct t_filter *) 0 };
88
89
90 /*
91 * Global filter file offset
92 */
93 int filter_file_offset;
94
95
96 /*
97 * Local prototypes
98 */
99 static int get_choice(int x, const char *help, const char *prompt, char *list[], int list_size);
100 static int set_filter_scope(struct t_group *group);
101 static struct t_filter_comment *add_filter_comment(struct t_filter_comment *ptr, char *text);
102 static struct t_filter_comment *free_filter_comment(struct t_filter_comment *ptr);
103 static struct t_filter_comment *copy_filter_comment(struct t_filter_comment *from, struct t_filter_comment *to);
104 static t_bool add_filter_rule(struct t_group *group, struct t_article *art, struct t_filter_rule *rule, t_bool quick_filter_rule);
105 static int test_regex(const char *string, char *regex, t_bool nocase, struct regex_cache *cache);
106 static void expand_filter_array(struct t_filters *ptr);
107 static void fmt_filter_menu_prompt(char *dest, size_t dest_len, const char *fmt_str, int len, const char *text);
108 static void free_filter_item(struct t_filter *ptr);
109 static void print_filter_menu(void);
110 static void set_filter(struct t_filter *ptr);
111 static void write_filter_array(FILE *fp, struct t_filters *ptr);
112 #if 0 /* currently unused */
113 static FILE *open_xhdr_fp(char *header, long min, long max);
114 #endif /* 0 */
115
116
117 /*
118 * Add one more entry to the filter-comment-list.
119 * If ptr == NULL the list will be created.
120 */
121 static struct t_filter_comment *
add_filter_comment(struct t_filter_comment * ptr,char * text)122 add_filter_comment(
123 struct t_filter_comment *ptr,
124 char *text)
125 {
126 if (ptr == NULL) {
127 ptr = my_malloc(sizeof(struct t_filter_comment));
128 ptr->text = my_strdup(text);
129 ptr->next = (struct t_filter_comment *) 0;
130 } else
131 ptr->next = add_filter_comment(ptr->next, text);
132
133 return ptr;
134 }
135
136
137 /*
138 * Free all entries in a filter-comment-list.
139 * Set ptr to NULL and return it.
140 */
141 static struct t_filter_comment *
free_filter_comment(struct t_filter_comment * ptr)142 free_filter_comment(
143 struct t_filter_comment *ptr)
144 {
145 struct t_filter_comment *tmp, *next;
146
147 tmp = ptr;
148 while (tmp != NULL) {
149 next = tmp->next;
150 free(tmp->text);
151 free(tmp);
152 tmp = next;
153 }
154
155 return tmp;
156 }
157
158
159 /*
160 * Copy the filter-comment-list 'from' into the list 'to'.
161 */
162 static struct t_filter_comment *
copy_filter_comment(struct t_filter_comment * from,struct t_filter_comment * to)163 copy_filter_comment(
164 struct t_filter_comment *from,
165 struct t_filter_comment *to)
166 {
167 if (from != NULL) {
168 to = my_malloc(sizeof(struct t_filter_comment));
169 to->text = my_strdup(from->text);
170 to->next = copy_filter_comment(from->next, NULL);
171 }
172
173 return to;
174 }
175
176
177 static void
expand_filter_array(struct t_filters * ptr)178 expand_filter_array(
179 struct t_filters *ptr)
180 {
181 int num;
182 size_t block;
183
184 num = ++ptr->max;
185
186 block = num * sizeof(struct t_filter);
187
188 if (num == 1) /* allocate */
189 ptr->filter = my_malloc(block);
190 else /* reallocate */
191 ptr->filter = my_realloc(ptr->filter, block);
192 }
193
194
195 /*
196 * Looks for a matching filter hit (wildmat or pcre regex) in the supplied string
197 * If the cache is not yet initialised, compile and optimise the regex
198 * Returns 1 if we hit the rule
199 * Returns 0 if we had no match
200 * In case of error prints an error message and returns -1
201 */
202 static int
test_regex(const char * string,char * regex,t_bool nocase,struct regex_cache * cache)203 test_regex(
204 const char *string,
205 char *regex,
206 t_bool nocase,
207 struct regex_cache *cache)
208 {
209 int regex_errpos;
210
211 if (!tinrc.wildcard) {
212 if (wildmat(string, regex, nocase))
213 return 1;
214 } else {
215 if (!cache->re)
216 compile_regex(regex, cache, (nocase ? PCRE_CASELESS : 0));
217 if (cache->re) {
218 regex_errpos = pcre_exec(cache->re, cache->extra, string, strlen(string), 0, 0, NULL, 0);
219 if (regex_errpos >= 0)
220 return 1;
221 else if (regex_errpos != PCRE_ERROR_NOMATCH) { /* also exclude PCRE_ERROR_BADUTF8 ? */
222 error_message(2, _(txt_pcre_error_num), regex_errpos);
223 #ifdef DEBUG
224 if (debug & DEBUG_FILTER) {
225 debug_print_file("FILTER", _(txt_pcre_error_num), regex_errpos);
226 debug_print_file("FILTER", "\t regex: %s", regex);
227 debug_print_file("FILTER", "\tstring: %s", string);
228 }
229 #endif /* DEBUG */
230 return -1;
231 }
232 }
233 }
234 return 0;
235 }
236
237
238 /*
239 * set_filter() initialises a struct t_filter with default values
240 */
241 static void
set_filter(struct t_filter * ptr)242 set_filter(
243 struct t_filter *ptr)
244 {
245 if (ptr != NULL) {
246 ptr->comment = (struct t_filter_comment *) 0;
247 ptr->scope = NULL;
248 ptr->inscope = TRUE;
249 ptr->icase = FALSE;
250 ptr->fullref = FILTER_MSGID;
251 ptr->subj = NULL;
252 ptr->from = NULL;
253 ptr->msgid = NULL;
254 ptr->lines_cmp = FILTER_LINES_NO;
255 ptr->lines_num = 0;
256 ptr->gnksa_cmp = FILTER_LINES_NO;
257 ptr->gnksa_num = 0;
258 ptr->score = 0;
259 ptr->xref = NULL;
260 ptr->path = NULL;
261 ptr->time = (time_t) 0;
262 ptr->next = (struct t_filter *) 0;
263 }
264 }
265
266
267 /*
268 * free_filter_item() frees all filter data (char *)
269 */
270 static void
free_filter_item(struct t_filter * ptr)271 free_filter_item(
272 struct t_filter *ptr)
273 {
274 ptr->comment = free_filter_comment(ptr->comment);
275 FreeAndNull(ptr->scope);
276 FreeAndNull(ptr->subj);
277 FreeAndNull(ptr->from);
278 FreeAndNull(ptr->msgid);
279 FreeAndNull(ptr->xref);
280 FreeAndNull(ptr->path);
281 }
282
283
284 /*
285 * free_filter_array() frees t_filter structs t_filters contains pointers to
286 */
287 void
free_filter_array(struct t_filters * ptr)288 free_filter_array(
289 struct t_filters *ptr)
290 {
291 int i;
292
293 if (ptr != NULL) {
294 for (i = 0; i < ptr->num; i++)
295 free_filter_item(ptr->filter + i);
296
297 FreeAndNull(ptr->filter);
298 ptr->num = 0;
299 ptr->max = 0;
300 }
301 }
302
303
304 /*
305 * read ~/.tin/filter file contents into filter array
306 */
307 t_bool
read_filter_file(const char * file)308 read_filter_file(
309 const char *file)
310 {
311 FILE *fp;
312 char buf[HEADER_LEN];
313 char scope[HEADER_LEN];
314 char comment_line[LEN]; /* one line of comment */
315 char subj[HEADER_LEN];
316 char from[HEADER_LEN];
317 char msgid[HEADER_LEN];
318 char buffer[HEADER_LEN];
319 char gnksa[HEADER_LEN];
320 char xref[HEADER_LEN];
321 char path[HEADER_LEN];
322 char scbuf[PATH_LEN];
323 int i = 0;
324 int icase = 0;
325 int score = 0;
326 long secs = 0L;
327 struct t_filter_comment *comment = NULL;
328 struct t_filter *ptr = NULL;
329 t_bool expired = FALSE;
330 t_bool expired_time = FALSE;
331 time_t current_secs = (time_t) 0;
332 static t_bool first_read = TRUE;
333 struct t_version *upgrade = NULL;
334
335 if ((fp = fopen(file, "r")) == NULL)
336 return FALSE;
337
338 if (!batch_mode || verbose)
339 wait_message(0, _(txt_reading_filter_file));
340
341 (void) time(¤t_secs);
342
343 /*
344 * Reset all filter arrays if doing a reread of the active file
345 */
346 if (!first_read)
347 free_filter_array(&glob_filter);
348
349 filter_file_offset = 1;
350 scope[0] = '\0';
351 while (fgets(buf, (int) sizeof(buf), fp) != NULL) {
352 if (*buf == '\n')
353 continue;
354 if (*buf == '#') {
355 if (scope[0] == '\0')
356 filter_file_offset++;
357 if (upgrade == NULL && first_read && match_string(buf, "# Filter file V", NULL, 0)) {
358 first_read = FALSE;
359 upgrade = check_upgrade(buf, "# Filter file V", FILTER_VERSION);
360 if (upgrade->state != RC_IGNORE)
361 upgrade_prompt_quit(upgrade, FILTER_FILE); /* TODO: do something (more) useful here */
362 }
363 continue;
364 }
365
366 switch (my_tolower((unsigned char) buf[0])) {
367 case 'c':
368 if (match_integer(buf + 1, "ase=", &icase, 1)) {
369 if (ptr && !expired_time)
370 ptr[i].icase = (unsigned) icase;
371
372 break;
373 }
374 if (match_string(buf + 1, "omment=", comment_line, sizeof(comment_line))) {
375 str_trim(comment_line);
376 comment = add_filter_comment(comment, comment_line);
377 }
378 break;
379
380 case 'f':
381 if (match_string(buf + 1, "rom=", from, sizeof(from))) {
382 if (ptr && !expired_time) {
383 if (tinrc.wildcard && ptr[i].from != NULL) {
384 /* merge with already read value */
385 ptr[i].from = my_realloc(ptr[i].from, strlen(ptr[i].from) + strlen(from) + 2);
386 strcat(ptr[i].from, "|");
387 strcat(ptr[i].from, from);
388 } else {
389 FreeIfNeeded(ptr[i].from);
390 ptr[i].from = my_strdup(from);
391 }
392 }
393 }
394 break;
395
396 case 'g':
397 if (match_string(buf + 1, "roup=", scope, sizeof(scope))) {
398 str_trim(scope);
399 #ifdef DEBUG
400 if (debug & DEBUG_FILTER)
401 debug_print_file("FILTER", "\nnum=[%d] group=[%s]", glob_filter.num, scope);
402 #endif /* DEBUG */
403 if (glob_filter.num >= glob_filter.max)
404 expand_filter_array(&glob_filter);
405
406 ptr = glob_filter.filter;
407 i = glob_filter.num++;
408 set_filter(&ptr[i]);
409 expired_time = FALSE;
410 ptr[i].scope = my_strdup(scope);
411 if (comment != NULL) {
412 ptr[i].comment = copy_filter_comment(comment, ptr[i].comment);
413 comment = free_filter_comment(comment);
414 }
415 subj[0] = '\0';
416 from[0] = '\0';
417 msgid[0] = '\0';
418 buffer[0] = '\0';
419 xref[0] = '\0';
420 path[0] = '\0';
421 icase = 0;
422 secs = 0L;
423 break;
424 }
425 if (match_string(buf + 1, "nksa=", gnksa, sizeof(gnksa))) {
426 if (ptr && !expired_time) {
427 if (gnksa[0] == '<') {
428 ptr[i].gnksa_cmp = FILTER_LINES_LT;
429 ptr[i].gnksa_num = atoi(&gnksa[1]);
430 } else if (gnksa[0] == '>') {
431 ptr[i].gnksa_cmp = FILTER_LINES_GT;
432 ptr[i].gnksa_num = atoi(&gnksa[1]);
433 } else {
434 ptr[i].gnksa_cmp = FILTER_LINES_EQ;
435 ptr[i].gnksa_num = atoi(gnksa);
436 }
437 }
438 }
439 break;
440
441 case 'l':
442 if (match_string(buf + 1, "ines=", buffer, sizeof(buffer))) {
443 if (ptr && !expired_time) {
444 if (buffer[0] == '<') {
445 ptr[i].lines_cmp = FILTER_LINES_LT;
446 ptr[i].lines_num = atoi(&buffer[1]);
447 } else if (buffer[0] == '>') {
448 ptr[i].lines_cmp = FILTER_LINES_GT;
449 ptr[i].lines_num = atoi(&buffer[1]);
450 } else {
451 ptr[i].lines_cmp = FILTER_LINES_EQ;
452 ptr[i].lines_num = atoi(buffer);
453 }
454 }
455 }
456 break;
457
458 case 'm':
459 if (match_string(buf + 1, "sgid=", msgid, sizeof(msgid))) {
460 if (ptr && !expired_time) {
461 if (tinrc.wildcard && ptr[i].msgid != NULL && ptr[i].fullref == FILTER_MSGID) {
462 /* merge with already read value */
463 ptr[i].msgid = my_realloc(ptr[i].msgid, strlen(ptr[i].msgid) + strlen(msgid) + 2);
464 strcat(ptr[i].msgid, "|");
465 strcat(ptr[i].msgid, msgid);
466 } else {
467 FreeIfNeeded(ptr[i].msgid);
468 ptr[i].msgid = my_strdup(msgid);
469 ptr[i].fullref = FILTER_MSGID;
470 }
471 }
472 break;
473 }
474 if (match_string(buf + 1, "sgid_last=", msgid, sizeof(msgid))) {
475 if (ptr && !expired_time) {
476 if (tinrc.wildcard && ptr[i].msgid != NULL && ptr[i].fullref == FILTER_MSGID_LAST) {
477 /* merge with already read value */
478 ptr[i].msgid = my_realloc(ptr[i].msgid, strlen(ptr[i].msgid) + strlen(msgid) + 2);
479 strcat(ptr[i].msgid, "|");
480 strcat(ptr[i].msgid, msgid);
481 } else {
482 FreeIfNeeded(ptr[i].msgid);
483 ptr[i].msgid = my_strdup(msgid);
484 ptr[i].fullref = FILTER_MSGID_LAST;
485 }
486 }
487 break;
488 }
489 if (match_string(buf + 1, "sgid_only=", msgid, sizeof(msgid))) {
490 if (ptr && !expired_time) {
491 if (tinrc.wildcard && ptr[i].msgid != NULL && ptr[i].fullref == FILTER_MSGID_ONLY) {
492 /* merge with already read value */
493 ptr[i].msgid = my_realloc(ptr[i].msgid, strlen(ptr[i].msgid) + strlen(msgid) + 2);
494 strcat(ptr[i].msgid, "|");
495 strcat(ptr[i].msgid, msgid);
496 } else {
497 FreeIfNeeded(ptr[i].msgid);
498 ptr[i].msgid = my_strdup(msgid);
499 ptr[i].fullref = FILTER_MSGID_ONLY;
500 }
501 }
502 }
503 break;
504
505 case 'p':
506 if (match_string(buf + 1, "ath=", path, sizeof(path))) {
507 str_trim(path);
508 if (ptr && !expired_time) {
509 if (tinrc.wildcard && ptr[i].path != NULL) {
510 /* merge with already read value */
511 ptr[i].path = my_realloc(ptr[i].path, strlen(ptr[i].path) + strlen(path) + 2);
512 strcat(ptr[i].path, "|");
513 strcat(ptr[i].path, path);
514 } else {
515 FreeIfNeeded(ptr[i].path);
516 ptr[i].path = my_strdup(path);
517 }
518 }
519 }
520 break;
521
522 case 'r':
523 if (match_string(buf + 1, "efs_only=", msgid, sizeof(msgid))) {
524 if (ptr && !expired_time) {
525 if (tinrc.wildcard && ptr[i].msgid != NULL && ptr[i].fullref == FILTER_REFS_ONLY) {
526 /* merge with already read value */
527 ptr[i].msgid = my_realloc(ptr[i].msgid, strlen(ptr[i].msgid) + strlen(msgid) + 2);
528 strcat(ptr[i].msgid, "|");
529 strcat(ptr[i].msgid, msgid);
530 } else {
531 FreeIfNeeded(ptr[i].msgid);
532 ptr[i].msgid = my_strdup(msgid);
533 ptr[i].fullref = FILTER_REFS_ONLY;
534 }
535 }
536 }
537 break;
538
539 case 's':
540 if (match_string(buf + 1, "ubj=", subj, sizeof(subj))) {
541 if (ptr && !expired_time) {
542 if (tinrc.wildcard && ptr[i].subj != NULL) {
543 /* merge with already read value */
544 ptr[i].subj = my_realloc(ptr[i].subj, strlen(ptr[i].subj) + strlen(subj) + 2);
545 strcat(ptr[i].subj, "|");
546 strcat(ptr[i].subj, subj);
547 } else {
548 FreeIfNeeded(ptr[i].subj);
549 ptr[i].subj = my_strdup(subj);
550 }
551 #ifdef DEBUG
552 if (debug & DEBUG_FILTER)
553 debug_print_file("FILTER", "buf=[%s] Gsubj=[%s]", ptr[i].subj, glob_filter.filter[i].subj);
554 #endif /* DEBUG */
555 }
556 break;
557 }
558
559 /*
560 * read score for rule
561 */
562 if (match_string(buf + 1, "core=", scbuf, PATH_LEN)) {
563 score = atoi(scbuf);
564 #ifdef DEBUG
565 if (debug & DEBUG_FILTER)
566 debug_print_file("FILTER", "score=[%d]", score);
567 #endif /* DEBUG */
568 if (ptr && !expired_time) {
569 if (score > SCORE_MAX)
570 score = SCORE_MAX;
571 else {
572 if (score < -SCORE_MAX)
573 score = -SCORE_MAX;
574 else {
575 if (!score) {
576 if (!strncasecmp(scbuf, "kill", 4))
577 score = tinrc.score_kill;
578 else {
579 if (!strncasecmp(scbuf, "hot", 3))
580 score = tinrc.score_select;
581 }
582 }
583 }
584 }
585 ptr[i].score = score;
586 }
587 }
588 break;
589
590 case 't':
591 if (match_long(buf + 1, "ime=", &secs)) {
592 if (ptr && !expired_time) {
593 ptr[i].time = (time_t) secs;
594 /* rule expired? */
595 if (secs && current_secs > (time_t) secs) {
596 #ifdef DEBUG
597 if (debug & DEBUG_FILTER)
598 debug_print_file("FILTER", "EXPIRED secs=[%lu] current_secs=[%lu]", (unsigned long int) secs, (unsigned long int) current_secs);
599 #endif /* DEBUG */
600 glob_filter.num--;
601 expired_time = TRUE;
602 expired = TRUE;
603 }
604 }
605 }
606 break;
607
608 case 'x':
609 /*
610 * TODO: format has changed in FILTER_VERSION 1.0.0,
611 * should we comment out older xref rules like below?
612 */
613 if (match_string(buf + 1, "ref=", xref, sizeof(xref))) {
614 str_trim(xref);
615 if (ptr && !expired_time) {
616 if (tinrc.wildcard && ptr[i].xref != NULL) {
617 /* merge with already read value */
618 ptr[i].xref = my_realloc(ptr[i].xref, strlen(ptr[i].xref) + strlen(xref) + 2);
619 strcat(ptr[i].xref, "|");
620 strcat(ptr[i].xref, xref);
621 } else {
622 FreeIfNeeded(ptr[i].xref);
623 ptr[i].xref = my_strdup(xref);
624 }
625 }
626 break;
627 }
628 if (upgrade && upgrade->state == RC_UPGRADE) {
629 char foo[HEADER_LEN];
630
631 if (match_string(buf + 1, "ref_max=", foo, LEN - 1)) {
632 /*
633 * TODO: add to the right rule, give better explanation.
634 */
635 snprintf(foo, HEADER_LEN, "%s%s", _(txt_removed_rule), str_trim(buf));
636 comment = add_filter_comment(comment, foo);
637 break;
638 }
639 if (match_string(buf + 1, "ref_score=", foo, LEN - 1)) {
640 /*
641 * TODO: add to the right rule, give better explanation.
642 */
643 snprintf(foo, HEADER_LEN, "%s%s", _(txt_removed_rule), str_trim(buf));
644 comment = add_filter_comment(comment, foo);
645 }
646 }
647 break;
648
649 default:
650 break;
651 }
652 }
653
654 if (comment) /* stray comment without scope */
655 (void) free_filter_comment(comment);
656
657 fclose(fp);
658
659 if (expired || (upgrade && upgrade->state == RC_UPGRADE))
660 write_filter_file(file);
661
662 if (!cmd_line && !batch_mode)
663 clear_message();
664
665 FreeAndNull(upgrade);
666 return TRUE;
667 }
668
669
670 /*
671 * write filter strings to ~/.tin/filter
672 */
673 void
write_filter_file(const char * filename)674 write_filter_file(
675 const char *filename)
676 {
677 FILE *fp;
678 char *file_tmp;
679 int i;
680 long fpos;
681
682 if (no_write)
683 return;
684
685 /* generate tmp-filename */
686 file_tmp = get_tmpfilename(filename);
687
688 if (!backup_file(filename, file_tmp)) {
689 error_message(2, _(txt_filesystem_full_backup), filename);
690 free(file_tmp);
691 return;
692 }
693
694 if ((fp = fopen(filename, "w+")) == NULL) {
695 free(file_tmp);
696 return;
697 }
698
699 /* TODO: -> lang.c */
700 fprintf(fp, "# Filter file V%s for the TIN newsreader\n#\n", FILTER_VERSION);
701 fprintf(fp, "%s", _(txt_filter_file));
702
703 fflush(fp);
704
705 /* determine the file offset */
706 if (!batch_mode) {
707 if ((fpos = ftell(fp)) <= 0) {
708 clearerr(fp);
709 fclose(fp);
710 rename_file(file_tmp, filename);
711 free(file_tmp);
712 error_message(2, _(txt_filesystem_full), filename);
713 return;
714 }
715 rewind(fp);
716 filter_file_offset = 1;
717 while ((i = fgetc(fp)) != EOF) {
718 if (i == '\n')
719 filter_file_offset++;
720 }
721 if (fseek(fp, fpos, SEEK_SET)) {
722 clearerr(fp);
723 fclose(fp);
724 rename_file(file_tmp, filename);
725 free(file_tmp);
726 error_message(2, _(txt_filesystem_full), filename);
727 return;
728 }
729 }
730
731 /*
732 * Save global filters
733 */
734 write_filter_array(fp, &glob_filter);
735
736 if ((i = ferror(fp)) || fclose(fp)) {
737 error_message(2, _(txt_filesystem_full), filename);
738 rename_file(file_tmp, filename);
739 if (i) {
740 clearerr(fp);
741 fclose(fp);
742 }
743 } else
744 unlink(file_tmp);
745
746 free(file_tmp);
747 }
748
749
750 static void
write_filter_array(FILE * fp,struct t_filters * ptr)751 write_filter_array(
752 FILE *fp,
753 struct t_filters *ptr)
754 {
755 int i;
756 struct t_filter_comment *comment;
757 time_t theTime = time(NULL);
758
759 if (ptr == NULL)
760 return;
761
762 for (i = 0; i < ptr->num; i++) {
763 #ifdef DEBUG
764 if (debug & DEBUG_FILTER)
765 debug_print_file("FILTER", "WRITE i=[%d] subj=[%s] from=[%s]\n", i, BlankIfNull(ptr->filter[i].subj), BlankIfNull(ptr->filter[i].from));
766 #endif /* DEBUG */
767
768 if (ptr->filter[i].time && theTime > ptr->filter[i].time)
769 continue;
770 #ifdef DEBUG
771 if (debug & DEBUG_FILTER)
772 debug_print_file("FILTER", "Scope=[%s]" cCRLF, (ptr->filter[i].scope != NULL ? ptr->filter[i].scope : "*"));
773 #endif /* DEBUG */
774
775 fprintf(fp, "\n"); /* makes filter file more readable */
776
777 /* comments appear always first, if there are any... */
778 if (ptr->filter[i].comment != NULL) {
779 /*
780 * Save the start of the list, in case write_filter_array is
781 * called multiple times. Otherwise the list would get lost.
782 */
783 comment = ptr->filter[i].comment;
784 while (ptr->filter[i].comment != NULL) {
785 fprintf(fp, "comment=%s\n", ptr->filter[i].comment->text);
786 ptr->filter[i].comment = ptr->filter[i].comment->next;
787 }
788 ptr->filter[i].comment = comment;
789 }
790
791 fprintf(fp, "group=%s\n", (ptr->filter[i].scope != NULL ? ptr->filter[i].scope : "*"));
792
793 fprintf(fp, "case=%u\n", ptr->filter[i].icase);
794
795 if (ptr->filter[i].score == tinrc.score_kill)
796 fprintf(fp, "score=kill\n");
797 else if (ptr->filter[i].score == tinrc.score_select)
798 fprintf(fp, "score=hot\n");
799 else
800 fprintf(fp, "score=%d\n", ptr->filter[i].score);
801
802 if (ptr->filter[i].subj != NULL)
803 fprintf(fp, "subj=%s\n", ptr->filter[i].subj);
804
805 if (ptr->filter[i].from != NULL)
806 fprintf(fp, "from=%s\n", ptr->filter[i].from);
807
808 if (ptr->filter[i].msgid != NULL) {
809 switch (ptr->filter[i].fullref) {
810 case FILTER_MSGID:
811 fprintf(fp, "msgid=%s\n", ptr->filter[i].msgid);
812 break;
813
814 case FILTER_MSGID_LAST:
815 fprintf(fp, "msgid_last=%s\n", ptr->filter[i].msgid);
816 break;
817
818 case FILTER_MSGID_ONLY:
819 fprintf(fp, "msgid_only=%s\n", ptr->filter[i].msgid);
820 break;
821
822 case FILTER_REFS_ONLY:
823 fprintf(fp, "refs_only=%s\n", ptr->filter[i].msgid);
824 break;
825
826 default:
827 break;
828 }
829 }
830
831 if (ptr->filter[i].lines_cmp != FILTER_LINES_NO) {
832 switch (ptr->filter[i].lines_cmp) {
833 case FILTER_LINES_EQ:
834 fprintf(fp, "lines=%d\n", ptr->filter[i].lines_num);
835 break;
836
837 case FILTER_LINES_LT:
838 fprintf(fp, "lines=<%d\n", ptr->filter[i].lines_num);
839 break;
840
841 case FILTER_LINES_GT:
842 fprintf(fp, "lines=>%d\n", ptr->filter[i].lines_num);
843 break;
844
845 default:
846 break;
847 }
848 }
849
850 if (ptr->filter[i].gnksa_cmp != FILTER_LINES_NO) {
851 switch (ptr->filter[i].gnksa_cmp) {
852 case FILTER_LINES_EQ:
853 fprintf(fp, "gnksa=%d\n", ptr->filter[i].gnksa_num);
854 break;
855
856 case FILTER_LINES_LT:
857 fprintf(fp, "gnksa=<%d\n", ptr->filter[i].gnksa_num);
858 break;
859
860 case FILTER_LINES_GT:
861 fprintf(fp, "gnksa=>%d\n", ptr->filter[i].gnksa_num);
862 break;
863
864 default:
865 break;
866 }
867 }
868
869 if (ptr->filter[i].xref != NULL)
870 fprintf(fp, "xref=%s\n", ptr->filter[i].xref);
871
872 if (ptr->filter[i].path != NULL)
873 fprintf(fp, "path=%s\n", ptr->filter[i].path);
874
875 if (ptr->filter[i].time) {
876 char timestring[25];
877 if (my_strftime(timestring, sizeof(timestring) - 1, "%Y-%m-%d %H:%M:%S UTC", gmtime(&(ptr->filter[i].time))))
878 fprintf(fp, "time=%lu (%s)\n", (unsigned long int) ptr->filter[i].time, timestring);
879 }
880 }
881 fflush(fp);
882 }
883
884
885 /*
886 * Interactive filter menu
887 */
888 static int
get_choice(int x,const char * help,const char * prompt,char * list[],int list_size)889 get_choice(
890 int x,
891 const char *help,
892 const char *prompt,
893 char *list[],
894 int list_size)
895 {
896 int ch, y, i = 0;
897
898 if (help)
899 show_menu_help(help);
900
901 if (list == NULL || list_size < 1)
902 return -1;
903
904 y = strwidth(prompt);
905
906 do {
907 MoveCursor(x, y);
908 my_fputs(list[i], stdout);
909 my_flush();
910 CleartoEOLN();
911 ch = ReadCh();
912 switch (ch) {
913 case ' ':
914 i++;
915 i %= list_size;
916 break;
917
918 case ESC: /* (ESC) common arrow keys */
919 # ifdef HAVE_KEY_PREFIX
920 case KEY_PREFIX:
921 # endif /* HAVE_KEY_PREFIX */
922 switch (get_arrow_key(ch)) {
923 case KEYMAP_UP:
924 i--;
925 if (i < 0)
926 i = list_size - 1;
927 ch = ' '; /* don't exit the while loop yet */
928 break;
929
930 case KEYMAP_DOWN:
931 i++;
932 i %= list_size;
933 ch = ' '; /* don't exit the while loop yet */
934 break;
935
936 default:
937 break;
938 }
939 break;
940
941 default:
942 break;
943 }
944 } while (ch != '\n' && ch != '\r' && ch != iKeyAbort); /* TODO: replace hard coded keynames */
945
946 if (ch == iKeyAbort)
947 return -1;
948
949 return i;
950 }
951
952
953 static const char *ptr_filter_comment;
954 static const char *ptr_filter_lines;
955 static const char *ptr_filter_menu;
956 static const char *ptr_filter_scope;
957 static const char *ptr_filter_text;
958 static const char *ptr_filter_time;
959 static const char *ptr_filter_groupname;
960 static char text_subj[PATH_LEN];
961 static char text_from[PATH_LEN];
962 static char text_msgid[PATH_LEN];
963 static char text_score[PATH_LEN];
964
965
966 static void
print_filter_menu(void)967 print_filter_menu(
968 void)
969 {
970 ClearScreen();
971
972 center_line(0, TRUE, ptr_filter_menu);
973
974 MoveCursor(INDEX_TOP, 0);
975 my_printf("%s%s%s", ptr_filter_comment, cCRLF, cCRLF);
976 my_printf("%s%s", ptr_filter_text, cCRLF);
977 my_printf("%s%s%s", _(txt_filter_text_type), cCRLF, cCRLF);
978 my_printf("%s%s", text_subj, cCRLF);
979 my_printf("%s%s", text_from, cCRLF);
980 my_printf("%s%s%s", text_msgid, cCRLF, cCRLF);
981 my_printf("%s%s", ptr_filter_lines, cCRLF);
982 my_printf("%s%s", text_score, cCRLF);
983 my_printf("%s%s%s", ptr_filter_time, cCRLF, cCRLF);
984 my_printf("%s%s", ptr_filter_scope, ptr_filter_groupname);
985 my_flush();
986 }
987
988
989 #if defined(SIGWINCH) || defined(SIGTSTP)
990 void
refresh_filter_menu(void)991 refresh_filter_menu(
992 void)
993 {
994 print_filter_menu();
995
996 /*
997 * TODO:
998 * - refresh already entered and accepted information (follow control
999 * flow in filter_menu below)
1000 * - refresh help line
1001 * - set cursor into current input field
1002 * - refresh already entered data or selected item in current input field
1003 * (not everywhere possible yet -- must change getline.c for refreshing
1004 * string input)
1005 */
1006 }
1007 #endif /* SIGWINCH || SIGTSTP */
1008
1009
1010 /*
1011 * a help function for filter_menu
1012 * formats a menu option in a multibyte-safe way
1013 *
1014 * this function in closely tight to the way how the filter menu is build
1015 */
1016 static void
fmt_filter_menu_prompt(char * dest,size_t dest_len,const char * fmt_str,int len,const char * text)1017 fmt_filter_menu_prompt(
1018 char *dest, /* where to store the resulting string */
1019 size_t dest_len, /* size of dest */
1020 const char *fmt_str, /* format string */
1021 int len, /* maximal len of the include string */
1022 const char *text) /* the include string */
1023 {
1024 char *buf;
1025 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
1026 wchar_t *wbuf, *wbuf2;
1027
1028 if ((wbuf = char2wchar_t(text)) != NULL) {
1029 wbuf2 = wcspart(wbuf, len, TRUE);
1030 if ((buf = wchar_t2char(wbuf2)) == NULL) {
1031 /* conversion failed, truncate original string */
1032 buf = my_malloc(len + 1);
1033 snprintf(buf, len + 1, "%-*.*s", len, len, text);
1034 }
1035
1036 free(wbuf);
1037 free(wbuf2);
1038 } else
1039 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
1040 {
1041 buf = my_malloc(len + 1);
1042 snprintf(buf, len + 1, "%-*.*s", len, len, text);
1043 }
1044 snprintf(dest, dest_len, fmt_str, buf);
1045 free(buf);
1046 }
1047
1048
1049 /*
1050 * Interactive filter menu so that the user can dynamically enter parameters.
1051 * Can be configured for kill or auto-selection screens.
1052 */
1053 t_bool
filter_menu(t_function type,struct t_group * group,struct t_article * art)1054 filter_menu(
1055 t_function type,
1056 struct t_group *group,
1057 struct t_article *art)
1058 {
1059 const char *ptr_filter_from;
1060 const char *ptr_filter_msgid;
1061 const char *ptr_filter_subj;
1062 const char *ptr_filter_help_scope;
1063 const char *ptr_filter_quit_edit_save;
1064 char *ptr;
1065 char **list;
1066 char comment_line[LEN];
1067 char buf[LEN];
1068 char keyedit[MAXKEYLEN], keyquit[MAXKEYLEN], keysave[MAXKEYLEN];
1069 char text_time[PATH_LEN];
1070 char double_time[PATH_LEN];
1071 char quat_time[PATH_LEN];
1072 int i, len, clen, flen;
1073 struct t_filter_rule rule;
1074 t_bool proceed;
1075 t_bool ret;
1076 t_function func, default_func = FILTER_SAVE;
1077
1078 signal_context = cFilter;
1079
1080 rule.comment = (struct t_filter_comment *) 0;
1081 rule.text[0] = '\0';
1082 rule.scope[0] = '\0';
1083 rule.counter = 0;
1084 rule.lines_cmp = FILTER_LINES_NO;
1085 rule.lines_num = 0;
1086 rule.from_ok = FALSE;
1087 rule.lines_ok = FALSE;
1088 rule.msgid_ok = FALSE;
1089 rule.fullref = FILTER_MSGID;
1090 rule.subj_ok = FALSE;
1091 rule.icase = FALSE;
1092 rule.score = 0;
1093 rule.expire_time = FALSE;
1094 rule.check_string = FALSE;
1095
1096 comment_line[0] = '\0';
1097
1098 /*
1099 * setup correct text for user selected menu
1100 */
1101 printascii(keyedit, func_to_key(FILTER_EDIT, filter_keys));
1102 printascii(keyquit, func_to_key(GLOBAL_QUIT, filter_keys));
1103 printascii(keysave, func_to_key(FILTER_SAVE, filter_keys));
1104
1105 if (type == GLOBAL_MENU_FILTER_KILL) {
1106 ptr_filter_from = _(txt_kill_from);
1107 ptr_filter_lines = _(txt_kill_lines);
1108 ptr_filter_menu = _(txt_kill_menu);
1109 ptr_filter_msgid = _(txt_kill_msgid);
1110 ptr_filter_scope = _(txt_kill_scope);
1111 ptr_filter_subj = _(txt_kill_subj);
1112 ptr_filter_text = _(txt_kill_text);
1113 ptr_filter_time = _(txt_kill_time);
1114 ptr_filter_help_scope = _(txt_help_kill_scope);
1115 ptr_filter_quit_edit_save = _(txt_quit_edit_save_kill);
1116 } else { /* type == GLOBAL_MENU_FILTER_SELECT */
1117 ptr_filter_from = _(txt_select_from);
1118 ptr_filter_lines = _(txt_select_lines);
1119 ptr_filter_menu = _(txt_select_menu);
1120 ptr_filter_msgid = _(txt_select_msgid);
1121 ptr_filter_scope = _(txt_select_scope);
1122 ptr_filter_subj = _(txt_select_subj);
1123 ptr_filter_text = _(txt_select_text);
1124 ptr_filter_time = _(txt_select_time);
1125 ptr_filter_help_scope = _(txt_help_select_scope);
1126 ptr_filter_quit_edit_save = _(txt_quit_edit_save_select);
1127 }
1128
1129 ptr_filter_comment = _(txt_filter_comment);
1130 ptr_filter_groupname = group->name;
1131
1132 clen = strwidth(_(txt_no));
1133 clen = MAX(clen, strwidth(_(txt_yes)));
1134 clen = MAX(clen, strwidth(_(txt_full)));
1135 clen = MAX(clen, strwidth(_(txt_last)));
1136 clen = MAX(clen, strwidth(_(txt_only)));
1137
1138 flen = strwidth(ptr_filter_subj) - 2;
1139 flen = MAX(flen, strwidth(ptr_filter_from) - 2);
1140 flen = MAX(flen, strwidth(ptr_filter_msgid) - 2);
1141
1142 len = cCOLS - flen - clen - 1 + 4;
1143
1144 snprintf(text_time, sizeof(text_time), _(txt_time_default_days), tinrc.filter_days);
1145 fmt_filter_menu_prompt(text_subj, sizeof(text_subj), ptr_filter_subj, len, art->subject);
1146 snprintf(text_score, sizeof(text_score), _(txt_filter_score), (type == GLOBAL_MENU_FILTER_KILL ? -tinrc.score_kill : tinrc.score_select));
1147 fmt_filter_menu_prompt(text_from, sizeof(text_from), ptr_filter_from, len, art->from);
1148 fmt_filter_menu_prompt(text_msgid, sizeof(text_msgid), ptr_filter_msgid, len - 4, MSGID(art));
1149
1150 print_filter_menu();
1151
1152 /*
1153 * None, one or multiple lines of comment.
1154 * Continue until an empty line is entered.
1155 * The empty line is ignored.
1156 */
1157 show_menu_help(_(txt_help_filter_comment));
1158 while ((proceed = prompt_menu_string(INDEX_TOP, ptr_filter_comment, comment_line)) && comment_line[0] != '\0') {
1159 rule.comment = add_filter_comment(rule.comment, comment_line);
1160 comment_line[0] = '\0';
1161 }
1162 if (!proceed) {
1163 rule.comment = free_filter_comment(rule.comment);
1164 return FALSE;
1165 }
1166
1167 /*
1168 * Text which might be used to filter on subj, from or msgid
1169 */
1170 show_menu_help(_(txt_help_filter_text));
1171 if (!prompt_menu_string(INDEX_TOP + 2, ptr_filter_text, rule.text)) {
1172 rule.comment = free_filter_comment(rule.comment);
1173 return FALSE;
1174 }
1175
1176 if (*rule.text) {
1177 list = my_malloc(sizeof(char *) * 8);
1178 list[0] = (char *) _(txt_subj_line_only_case);
1179 list[1] = (char *) _(txt_subj_line_only);
1180 list[2] = (char *) _(txt_from_line_only_case);
1181 list[3] = (char *) _(txt_from_line_only);
1182 list[4] = (char *) _(txt_msgid_refs_line);
1183 list[5] = (char *) _(txt_msgid_line_last);
1184 list[6] = (char *) _(txt_msgid_line_only);
1185 list[7] = (char *) _(txt_refs_line_only);
1186
1187 i = get_choice(INDEX_TOP + 3, _(txt_help_filter_text_type), _(txt_filter_text_type), list, 8);
1188 free(list);
1189
1190 if (i == -1) {
1191 rule.comment = free_filter_comment(rule.comment);
1192 return FALSE;
1193 }
1194
1195 rule.counter = i;
1196 switch (i) {
1197 case FILTER_SUBJ_CASE_IGNORE:
1198 case FILTER_FROM_CASE_IGNORE:
1199 rule.icase = TRUE;
1200 break;
1201
1202 case FILTER_SUBJ_CASE_SENSITIVE:
1203 case FILTER_FROM_CASE_SENSITIVE:
1204 case FILTER_MSGID:
1205 case FILTER_MSGID_LAST:
1206 case FILTER_MSGID_ONLY:
1207 case FILTER_REFS_ONLY:
1208 break;
1209
1210 default: /* should not happen */
1211 /* CONSTANTCONDITION */
1212 assert(0 != 0);
1213 break;
1214 }
1215 }
1216
1217 if (!*rule.text) {
1218 rule.check_string = TRUE;
1219 /*
1220 * Subject:
1221 */
1222 list = my_malloc(sizeof(char *) * 2);
1223 list[0] = (char *) _(txt_yes);
1224 list[1] = (char *) _(txt_no);
1225 i = get_choice(INDEX_TOP + 5, _(txt_help_filter_subj), text_subj, list, 2);
1226 free(list);
1227
1228 if (i == -1) {
1229 rule.comment = free_filter_comment(rule.comment);
1230 return FALSE;
1231 } else
1232 rule.subj_ok = (i == 0);
1233
1234 /*
1235 * From:
1236 */
1237 list = my_malloc(sizeof(char *) * 2);
1238 if (rule.subj_ok) {
1239 list[0] = (char *) _(txt_no);
1240 list[1] = (char *) _(txt_yes);
1241 } else {
1242 list[0] = (char *) _(txt_yes);
1243 list[1] = (char *) _(txt_no);
1244 }
1245 i = get_choice(INDEX_TOP + 6, _(txt_help_filter_from), text_from, list, 2);
1246 free(list);
1247
1248 if (i == -1) {
1249 rule.comment = free_filter_comment(rule.comment);
1250 return FALSE;
1251 } else
1252 rule.from_ok = rule.subj_ok ? (i != 0) : (i == 0);
1253
1254 /*
1255 * Message-ID:
1256 */
1257 list = my_malloc(sizeof(char *) * 4);
1258 if (rule.subj_ok || rule.from_ok) {
1259 list[0] = (char *) _(txt_no);
1260 list[1] = (char *) _(txt_full);
1261 list[2] = (char *) _(txt_last);
1262 list[3] = (char *) _(txt_only);
1263 } else {
1264 list[0] = (char *) _(txt_full);
1265 list[1] = (char *) _(txt_last);
1266 list[2] = (char *) _(txt_only);
1267 list[3] = (char *) _(txt_no);
1268 }
1269 i = get_choice(INDEX_TOP + 7, _(txt_help_filter_msgid), text_msgid, list, 4);
1270 free(list);
1271
1272 if (i == -1) {
1273 rule.comment = free_filter_comment(rule.comment);
1274 return FALSE;
1275 } else {
1276 switch ((rule.subj_ok || rule.from_ok) ? i : i + 1) {
1277 case 0:
1278 case 4:
1279 rule.msgid_ok = FALSE;
1280 rule.fullref = FILTER_MSGID;
1281 break;
1282
1283 case 1:
1284 rule.msgid_ok = TRUE;
1285 rule.fullref = FILTER_MSGID;
1286 break;
1287
1288 case 2:
1289 rule.msgid_ok = TRUE;
1290 rule.fullref = FILTER_MSGID_LAST;
1291 break;
1292
1293 case 3:
1294 rule.msgid_ok = TRUE;
1295 rule.fullref = FILTER_MSGID_ONLY;
1296 break;
1297
1298 default: /* should not happen */
1299 /* CONSTANTCONDITION */
1300 assert(0 != 0);
1301 break;
1302 }
1303 }
1304
1305 }
1306
1307 /*
1308 * Lines:
1309 */
1310 show_menu_help(_(txt_help_filter_lines));
1311
1312 buf[0] = '\0';
1313
1314 if (!prompt_menu_string(INDEX_TOP + 9, ptr_filter_lines, buf)) {
1315 rule.comment = free_filter_comment(rule.comment);
1316 return FALSE;
1317 }
1318
1319 /*
1320 * Get the < > sign if any for the lines rule
1321 */
1322 ptr = buf;
1323 while (*ptr == ' ')
1324 ptr++;
1325
1326 if (*ptr == '>') {
1327 rule.lines_cmp = FILTER_LINES_GT;
1328 ptr++;
1329 } else if (*ptr == '<') {
1330 rule.lines_cmp = FILTER_LINES_LT;
1331 ptr++;
1332 } else if (*ptr == '=') {
1333 rule.lines_cmp = FILTER_LINES_EQ;
1334 ptr++;
1335 }
1336
1337 if (*ptr)
1338 rule.lines_num = abs(atoi(ptr));
1339
1340 if (rule.lines_num && rule.lines_cmp == FILTER_LINES_NO)
1341 rule.lines_cmp = FILTER_LINES_EQ;
1342
1343 if (rule.lines_cmp != FILTER_LINES_NO && rule.lines_num)
1344 rule.lines_ok = TRUE;
1345
1346 /*
1347 * Scoring value
1348 */
1349 snprintf(buf, sizeof(buf), _(txt_filter_score_help), SCORE_MAX);
1350 show_menu_help(buf);
1351
1352 buf[0] = '\0';
1353 if (!prompt_menu_string(INDEX_TOP + 10, text_score, buf)) {
1354 rule.comment = free_filter_comment(rule.comment);
1355 return FALSE;
1356 }
1357
1358 /* check if a score has been entered */
1359 if (buf[0] != '\0')
1360 /* use entered score */
1361 rule.score = atoi(buf);
1362 else {
1363 /* use default score */
1364 if (type == GLOBAL_MENU_FILTER_KILL)
1365 rule.score = tinrc.score_kill;
1366 else /* type == GLOBAL_MENU_FILTER_SELECT */
1367 rule.score = tinrc.score_select;
1368 }
1369
1370 if (!rule.score) { /* ignore 0 scores */
1371 rule.comment = free_filter_comment(rule.comment);
1372 return FALSE;
1373 }
1374
1375 /*
1376 * assure we are in range
1377 */
1378 if (rule.score < 0)
1379 rule.score = abs(rule.score);
1380 if (rule.score > SCORE_MAX)
1381 rule.score = SCORE_MAX;
1382
1383 /* get the right sign for the score */
1384 if (type == GLOBAL_MENU_FILTER_KILL)
1385 rule.score = -rule.score;
1386
1387 /*
1388 * Expire time
1389 */
1390 snprintf(double_time, sizeof(double_time), "2x %s", text_time);
1391 snprintf(quat_time, sizeof(quat_time), "4x %s", text_time);
1392 list = my_malloc(sizeof(char *) * 4);
1393 list[0] = (char *) _(txt_unlimited_time);
1394 list[1] = text_time;
1395 list[2] = double_time;
1396 list[3] = quat_time;
1397 i = get_choice(INDEX_TOP + 11, _(txt_help_filter_time), ptr_filter_time, list, 4);
1398 free(list);
1399
1400 if (i == -1) {
1401 rule.comment = free_filter_comment(rule.comment);
1402 return FALSE;
1403 }
1404
1405 rule.expire_time = i;
1406
1407 /*
1408 * Scope
1409 */
1410 if (*rule.text || rule.subj_ok || rule.from_ok || rule.msgid_ok || rule.lines_ok) {
1411 int j = 0;
1412
1413 list = my_malloc(sizeof(char *) * 2); /* at least 2 scopes */
1414 list[j++] = my_strdup(group->name);
1415 list[j] = my_strdup(list[j - 1]);
1416 while ((ptr = strrchr(list[j], '.')) != NULL) {
1417 *(++ptr) = '*';
1418 *(++ptr) = '\0';
1419 j++;
1420 list = my_realloc(list, sizeof(char *) * (j + 1)); /* one element more */
1421 list[j] = my_strdup(list[j - 1]);
1422 list[j][strlen(list[j]) - 2] = '\0';
1423 }
1424 free(list[j]); /* this copy isn't needed anymore */
1425 list[j] = (char *) _(txt_all_groups);
1426
1427 if ((i = get_choice(INDEX_TOP + 13, ptr_filter_help_scope, ptr_filter_scope, list, j + 1)) > 0)
1428 my_strncpy(rule.scope, i == j ? "*" : list[i], sizeof(rule.scope) - 1);
1429
1430 for (j--; j >= 0; j--)
1431 free(list[j]);
1432 free(list);
1433
1434 if (i == -1) {
1435 rule.comment = free_filter_comment(rule.comment);
1436 return FALSE;
1437 }
1438 } else {
1439 rule.comment = free_filter_comment(rule.comment);
1440 return FALSE;
1441 }
1442
1443 forever {
1444 func = prompt_slk_response(default_func, filter_keys,
1445 ptr_filter_quit_edit_save, keyquit, keyedit, keysave);
1446 switch (func) {
1447
1448 case FILTER_EDIT:
1449 add_filter_rule(group, art, &rule, FALSE); /* save the rule */
1450 rule.comment = free_filter_comment(rule.comment);
1451 if (!invoke_editor(filter_file, filter_file_offset, NULL))
1452 return FALSE;
1453 unfilter_articles(group);
1454 (void) read_filter_file(filter_file);
1455 return TRUE;
1456 /* keep lint quiet: */
1457 /* FALLTHROUGH */
1458
1459 case GLOBAL_QUIT:
1460 case GLOBAL_ABORT:
1461 rule.comment = free_filter_comment(rule.comment);
1462 return FALSE;
1463 /* keep lint quiet: */
1464 /* FALLTHROUGH */
1465
1466 case FILTER_SAVE:
1467 /*
1468 * Add the filter rule and save it to the filter file
1469 */
1470 ret = add_filter_rule(group, art, &rule, FALSE);
1471 rule.comment = free_filter_comment(rule.comment);
1472 return ret;
1473 /* keep lint quiet: */
1474 /* FALLTHROUGH */
1475
1476 default:
1477 break;
1478 }
1479 }
1480 /* NOTREACHED */
1481 return FALSE;
1482 }
1483
1484
1485 /*
1486 * Quick command to add an auto-select / kill filter to specified groups filter
1487 */
1488 t_bool
quick_filter(t_function type,struct t_group * group,struct t_article * art)1489 quick_filter(
1490 t_function type,
1491 struct t_group *group,
1492 struct t_article *art)
1493 {
1494 char *scope;
1495 char txt[LEN];
1496 int header, expire, icase;
1497 struct t_filter_rule rule;
1498 t_bool ret;
1499
1500 if (type == GLOBAL_QUICK_FILTER_KILL) {
1501 header = group->attribute->quick_kill_header;
1502 expire = group->attribute->quick_kill_expire;
1503 /* ON=case sensitive, OFF=ignore case -> invert */
1504 icase = bool_not(group->attribute->quick_kill_case);
1505 scope = group->attribute->quick_kill_scope;
1506 } else { /* type == GLOBAL_QUICK_FILTER_SELECT */
1507 header = group->attribute->quick_select_header;
1508 expire = group->attribute->quick_select_expire;
1509 /* ON=case sensitive, OFF=ignore case -> invert */
1510 icase = bool_not(group->attribute->quick_select_case);
1511 scope = group->attribute->quick_select_scope;
1512 }
1513
1514 #ifdef DEBUG
1515 if (debug & DEBUG_FILTER)
1516 error_message(2, "%s header=[%d] scope=[%s] expire=[%s] case=[%d]", (type == GLOBAL_QUICK_FILTER_KILL) ? "KILL" : "SELECT", header, BlankIfNull(scope), txt_onoff[expire != FALSE ? 1 : 0], icase);
1517 #endif /* DEBUG */
1518
1519 /*
1520 * Setup rules
1521 */
1522 if (strlen(BlankIfNull(scope)) > (sizeof(rule.scope) - 1))
1523 return FALSE;
1524 my_strncpy(rule.scope, BlankIfNull(scope), sizeof(rule.scope) - 1);
1525 rule.counter = 0;
1526 rule.lines_cmp = FILTER_LINES_NO;
1527 rule.lines_num = 0;
1528 rule.lines_ok = (header == FILTER_LINES);
1529 rule.msgid_ok = (header == FILTER_MSGID) || (header == FILTER_MSGID_LAST);
1530 rule.fullref = header; /* value is directly used to select correct filter type */
1531 rule.from_ok = (header == FILTER_FROM_CASE_SENSITIVE || header == FILTER_FROM_CASE_IGNORE);
1532 rule.subj_ok = (header == FILTER_SUBJ_CASE_SENSITIVE || header == FILTER_SUBJ_CASE_IGNORE);
1533
1534 /* create an auto-comment. */
1535 if (type == GLOBAL_QUICK_FILTER_KILL)
1536 snprintf(txt, sizeof(txt), "%s%s%c%s%s%s", _(txt_filter_rule_created), "'", ']', "' (", _(txt_help_article_quick_kill), ").");
1537 else
1538 snprintf(txt, sizeof(txt), "%s%s%c%s%s%s", _(txt_filter_rule_created), "'", '[', "' (", _(txt_help_article_quick_select), ").");
1539 rule.comment = add_filter_comment(NULL, txt);
1540
1541 rule.text[0] = '\0';
1542 rule.icase = icase;
1543 rule.expire_time = expire;
1544 rule.check_string = TRUE;
1545 rule.score = (type == GLOBAL_QUICK_FILTER_KILL) ? tinrc.score_kill : tinrc.score_select;
1546
1547 ret = add_filter_rule(group, art, &rule, TRUE);
1548 rule.comment = free_filter_comment(rule.comment);
1549 return ret;
1550 }
1551
1552
1553 /*
1554 * Quick command to add an auto-select filter to the article that user
1555 * has just posted. Selects on Subject: line with limited expire time.
1556 * Don't process if GROUP_TYPE_MAIL || GROUP_TYPE_SAVE
1557 */
1558 t_bool
quick_filter_select_posted_art(struct t_group * group,const char * subj,const char * a_message_id)1559 quick_filter_select_posted_art(
1560 struct t_group *group,
1561 const char *subj,
1562 const char *a_message_id) /* return value is always ignored */
1563 {
1564 t_bool filtered = FALSE;
1565 char txt[LEN];
1566
1567 if (group->type == GROUP_TYPE_NEWS) {
1568 struct t_article art;
1569 struct t_filter_rule rule;
1570
1571 #ifdef __cplusplus /* keep C++ quiet */
1572 rule.scope[0] = '\0';
1573 #endif /* __cplusplus */
1574
1575 if (strlen(group->name) > (sizeof(rule.scope) - 1)) /* groupname to long? */
1576 return FALSE;
1577
1578 /*
1579 * Setup rules
1580 */
1581 rule.counter = 0;
1582 rule.lines_cmp = FILTER_LINES_NO;
1583 rule.lines_num = 0;
1584 rule.from_ok = FALSE;
1585 rule.lines_ok = FALSE;
1586 rule.msgid_ok = FALSE;
1587 rule.fullref = FILTER_MSGID;
1588 rule.subj_ok = TRUE;
1589 rule.text[0] = '\0';
1590 rule.icase = FALSE;
1591 rule.expire_time = TRUE;
1592 rule.check_string = TRUE;
1593 rule.score = tinrc.score_select;
1594
1595 strcpy(rule.scope, group->name);
1596
1597 /* create an auto-comment. */
1598 snprintf(txt, sizeof(txt), "%s%s", _(txt_filter_rule_created), "add_posted_to_filter=ON.");
1599 rule.comment = add_filter_comment(NULL, txt);
1600
1601 /*
1602 * Setup dummy article with posted articles subject
1603 * xor Message-ID
1604 */
1605 set_article(&art);
1606 if (*a_message_id) {
1607 /* initialize art->refptr */
1608 struct {
1609 struct t_msgid *next;
1610 struct t_msgid *parent;
1611 struct t_msgid *sibling;
1612 struct t_msgid *child;
1613 int article;
1614 char txt[HEADER_LEN];
1615 } refptr_dummyart;
1616
1617 rule.subj_ok = FALSE;
1618 rule.msgid_ok = TRUE;
1619 refptr_dummyart.next = (struct t_msgid *) 0;
1620 refptr_dummyart.parent = (struct t_msgid *) 0;
1621 refptr_dummyart.sibling = (struct t_msgid *) 0;
1622 refptr_dummyart.child = (struct t_msgid *) 0;
1623 refptr_dummyart.article = ART_NORMAL;
1624 my_strncpy(refptr_dummyart.txt, a_message_id, HEADER_LEN);
1625 /* Hack */
1626 art.refptr = (struct t_msgid *) &refptr_dummyart;
1627
1628 filtered = add_filter_rule(group, &art, &rule, FALSE);
1629 } else {
1630 art.subject = my_strdup(subj);
1631 filtered = add_filter_rule(group, &art, &rule, FALSE);
1632 FreeIfNeeded(art.subject);
1633 }
1634 rule.comment = free_filter_comment(rule.comment);
1635 }
1636 return filtered;
1637 }
1638
1639
1640 /*
1641 * API to add filter rule to the local or global filter array
1642 */
1643 static t_bool
add_filter_rule(struct t_group * group,struct t_article * art,struct t_filter_rule * rule,t_bool quick_filter_rule)1644 add_filter_rule(
1645 struct t_group *group,
1646 struct t_article *art,
1647 struct t_filter_rule *rule,
1648 t_bool quick_filter_rule)
1649 {
1650 char acbuf[PATH_LEN];
1651 char sbuf[(sizeof(acbuf) / 2)]; /* half as big as acbuf so quote_wild(sbuf) fits into acbuf */
1652 int i = glob_filter.num;
1653 t_bool filtered = FALSE;
1654 time_t current_time;
1655 struct t_filter *ptr;
1656
1657 if (glob_filter.num >= glob_filter.max)
1658 expand_filter_array(&glob_filter);
1659
1660 ptr = glob_filter.filter;
1661
1662 ptr[i].inscope = TRUE;
1663 ptr[i].icase = FALSE;
1664 ptr[i].fullref = FILTER_MSGID;
1665 ptr[i].comment = (struct t_filter_comment *) 0;
1666 ptr[i].scope = NULL;
1667 ptr[i].subj = NULL;
1668 ptr[i].from = NULL;
1669 ptr[i].msgid = NULL;
1670 ptr[i].lines_cmp = rule->lines_cmp;
1671 ptr[i].lines_num = rule->lines_num;
1672 ptr[i].gnksa_cmp = FILTER_LINES_NO;
1673 ptr[i].gnksa_num = 0;
1674 ptr[i].score = rule->score;
1675 ptr[i].xref = NULL;
1676 ptr[i].path = NULL;
1677
1678 if (rule->comment != NULL)
1679 ptr[i].comment = copy_filter_comment(rule->comment, ptr[i].comment);
1680
1681 if (rule->scope[0] == '\0') /* replace empty scope with current group name */
1682 ptr[i].scope = my_strdup(group->name);
1683 else {
1684 if ((rule->scope[0] != '*') && (rule->scope[1] != '\0')) /* copy non-global scope */
1685 ptr[i].scope = my_strdup(rule->scope);
1686 }
1687
1688 (void) time(¤t_time);
1689 switch (rule->expire_time) {
1690 case 1:
1691 ptr[i].time = current_time + (time_t) (tinrc.filter_days * DAY);
1692 break;
1693
1694 case 2:
1695 ptr[i].time = current_time + (time_t) (tinrc.filter_days * DAY * 2);
1696 break;
1697
1698 case 3:
1699 ptr[i].time = current_time + (time_t) (tinrc.filter_days * DAY * 4);
1700 break;
1701
1702 default:
1703 ptr[i].time = (time_t) 0;
1704 break;
1705 }
1706
1707 ptr[i].icase = rule->icase;
1708 if (*rule->text) {
1709 snprintf(acbuf, sizeof(acbuf), REGEX_FMT, quote_wild_whitespace(rule->text));
1710
1711 switch (rule->counter) {
1712 case FILTER_SUBJ_CASE_IGNORE:
1713 case FILTER_SUBJ_CASE_SENSITIVE:
1714 ptr[i].subj = my_strdup(acbuf);
1715 break;
1716
1717 case FILTER_FROM_CASE_IGNORE:
1718 case FILTER_FROM_CASE_SENSITIVE:
1719 ptr[i].from = my_strdup(acbuf);
1720 break;
1721
1722 case FILTER_MSGID:
1723 case FILTER_MSGID_LAST:
1724 case FILTER_MSGID_ONLY:
1725 case FILTER_REFS_ONLY:
1726 ptr[i].msgid = my_strdup(acbuf);
1727 ptr[i].fullref = rule->counter;
1728 break;
1729
1730 default: /* should not happen */
1731 /* CONSTANTCONDITION */
1732 assert(0 != 0);
1733 break;
1734 }
1735 filtered = TRUE;
1736 glob_filter.num++;
1737 } else {
1738 /*
1739 * STRCPY() truncates subject/from/message-id so it fits
1740 * into acbuf even after quote_wild()
1741 */
1742 if (rule->subj_ok) {
1743 STRCPY(sbuf, art->subject);
1744 snprintf(acbuf, sizeof(acbuf), REGEX_FMT, (rule->check_string ? quote_wild(sbuf) : sbuf));
1745 ptr[i].subj = my_strdup(acbuf);
1746 }
1747 if (rule->from_ok) {
1748 STRCPY(sbuf, art->from);
1749 snprintf(acbuf, sizeof(acbuf), REGEX_FMT, quote_wild(sbuf));
1750 ptr[i].from = my_strdup(acbuf);
1751 }
1752 /*
1753 * message-ids should be quoted
1754 */
1755 if (rule->msgid_ok) {
1756 /*
1757 * If threading by references is set, and a quick kill is applied
1758 * (in group level), it is applied with the data of the root
1759 * article of the thread built by tin.
1760 * In case of threading by references, if tin's root is not the
1761 * *real* root of thread (which is the first entry in references
1762 * field) any applying of filtering for MSGID (or MSGID_LAST)
1763 * doesn't work, because the filter is applied with the data of
1764 * tin's root article which doesn't cover the other articles in
1765 * the thread.
1766 * So the thread remains open (in group level). To overcome this,
1767 * the first msgid from references field is taken in this case.
1768 */
1769 if (quick_filter_rule && group->attribute->thread_articles == THREAD_REFS &&
1770 (group->attribute->quick_kill_header == FILTER_MSGID ||
1771 group->attribute->quick_kill_header == FILTER_REFS_ONLY) &&
1772 art->refptr->parent != NULL)
1773 {
1774 /* not real parent, take first references entry as MID */
1775 struct t_msgid *xptr;
1776
1777 for (xptr = art->refptr->parent; xptr->parent != NULL; xptr = xptr->parent)
1778 ;
1779 STRCPY(sbuf, xptr->txt);
1780 } else {
1781 STRCPY(sbuf, MSGID(art));
1782 }
1783 snprintf(acbuf, sizeof(acbuf), REGEX_FMT, quote_wild(sbuf));
1784 ptr[i].msgid = my_strdup(acbuf);
1785 ptr[i].fullref = rule->fullref;
1786 }
1787 if (rule->subj_ok || rule->from_ok || rule->msgid_ok || rule->lines_ok) {
1788 filtered = TRUE;
1789 glob_filter.num++;
1790 }
1791 }
1792
1793 if (filtered) {
1794 #ifdef DEBUG
1795 if (debug & DEBUG_FILTER)
1796 wait_message(2, "inscope=[%s] scope=[%s] case=[%d] subj=[%s] from=[%s] msgid=[%s] fullref=[%u] line=[%d %d] time=[%lu]", bool_unparse(ptr[i].inscope), BlankIfNull(rule->scope), ptr[i].icase, BlankIfNull(ptr[i].subj), BlankIfNull(ptr[i].from), BlankIfNull(ptr[i].msgid), ptr[i].fullref, ptr[i].lines_cmp, ptr[i].lines_num, (unsigned long int) ptr[i].time);
1797 #endif /* DEBUG */
1798 write_filter_file(filter_file);
1799 }
1800 return filtered;
1801 }
1802
1803
1804 /*
1805 * We assume that any articles which are tagged as killed are also
1806 * tagged as being read BECAUSE they were killed. We retag them as
1807 * being unread if they were unread before killing (ART_KILLED_UNREAD).
1808 * Selected articles will be un"select"ed.
1809 */
1810 void
unfilter_articles(struct t_group * group)1811 unfilter_articles(
1812 struct t_group *group)
1813 {
1814 int i;
1815
1816 for_each_art(i) {
1817 arts[i].score = 0;
1818 if (IS_KILLED(i)) {
1819 if (IS_KILLED_UNREAD(i))
1820 art_mark(group, &arts[i], ART_UNREAD);
1821 arts[i].killed = ART_NOTKILLED;
1822 }
1823 if (IS_SELECTED(i))
1824 arts[i].selected = FALSE;
1825 }
1826 }
1827
1828
1829 /*
1830 * Filter any articles in specified group.
1831 * Apply global filter rules followed by group filter rules.
1832 * In global rules check if scope field set to determine if
1833 * filter applies to current group.
1834 */
1835 t_bool
filter_articles(struct t_group * group)1836 filter_articles(
1837 struct t_group *group)
1838 {
1839 char buf[LEN];
1840 int num, inscope;
1841 int i, j;
1842 struct t_filter *ptr;
1843 struct regex_cache *regex_cache_subj = NULL;
1844 struct regex_cache *regex_cache_from = NULL;
1845 struct regex_cache *regex_cache_msgid = NULL;
1846 struct regex_cache *regex_cache_xref = NULL;
1847 struct regex_cache *regex_cache_path = NULL;
1848 t_bool filtered = FALSE;
1849 t_bool error = FALSE;
1850
1851 /*
1852 * check if there are any global filter rules
1853 */
1854 if (group->glob_filter->num == 0)
1855 return filtered;
1856
1857 /*
1858 * Apply global filter rules first if there are any entries
1859 */
1860 /*
1861 * Check if any scope rules are active for this group
1862 * ie. group=comp.os.linux.help scope=comp.os.linux.*
1863 */
1864 inscope = set_filter_scope(group);
1865 if (!cmd_line && !batch_mode)
1866 wait_message(0, _(txt_filter_global_rules), inscope, group->glob_filter->num);
1867 num = group->glob_filter->num;
1868 ptr = group->glob_filter->filter;
1869
1870 /*
1871 * set up cache tables for all types of filter rules
1872 * (only for regexp matching)
1873 */
1874 if (tinrc.wildcard) {
1875 size_t msiz;
1876
1877 msiz = sizeof(struct regex_cache) * num;
1878 regex_cache_subj = my_malloc(msiz);
1879 regex_cache_from = my_malloc(msiz);
1880 regex_cache_msgid = my_malloc(msiz);
1881 regex_cache_xref = my_malloc(msiz);
1882 regex_cache_path = my_malloc(msiz);
1883 for (j = 0; j < num; j++) {
1884 regex_cache_subj[j].re = NULL;
1885 regex_cache_subj[j].extra = NULL;
1886 regex_cache_from[j].re = NULL;
1887 regex_cache_from[j].extra = NULL;
1888 regex_cache_msgid[j].re = NULL;
1889 regex_cache_msgid[j].extra = NULL;
1890 regex_cache_xref[j].re = NULL;
1891 regex_cache_xref[j].extra = NULL;
1892 regex_cache_path[j].re = NULL;
1893 regex_cache_path[j].extra = NULL;
1894 }
1895 }
1896
1897 /*
1898 * loop through all arts applying global & local filtering rules
1899 */
1900 for (i = 0; (i < top_art) && !error; i++) {
1901 #ifdef HAVE_SELECT
1902 if (wait_for_input()) /* allow abort */
1903 break; /* to free the mem */
1904 #endif /* HAVE_SELECT */
1905
1906 arts[i].score = 0;
1907
1908 if (tinrc.kill_level == KILL_UNREAD && IS_READ(i)) /* skip only when the article is read */
1909 continue;
1910
1911 for (j = 0; j < num && !error; j++) {
1912 if (ptr[j].inscope) {
1913 /*
1914 * Filter on Subject: line
1915 */
1916 if (ptr[j].subj != NULL) {
1917 switch (test_regex(arts[i].subject, ptr[j].subj, ptr[j].icase, ®ex_cache_subj[j])) {
1918 case 1:
1919 SET_FILTER(group, i, j);
1920 break;
1921
1922 case -1:
1923 error = TRUE;
1924 break;
1925
1926 default:
1927 break;
1928 }
1929 }
1930
1931 /*
1932 * Filter on From: line
1933 */
1934 if (ptr[j].from != NULL) {
1935 if (arts[i].name != NULL)
1936 snprintf(buf, sizeof(buf), "%s (%s)", arts[i].from, arts[i].name);
1937 else
1938 STRCPY(buf, arts[i].from);
1939
1940 switch (test_regex(buf, ptr[j].from, ptr[j].icase, ®ex_cache_from[j])) {
1941 case 1:
1942 SET_FILTER(group, i, j);
1943 break;
1944
1945 case -1:
1946 error = TRUE;
1947 break;
1948
1949 default:
1950 break;
1951 }
1952 }
1953
1954 /*
1955 * Filter on Message-ID: line
1956 * Apply to Message-ID: & References: lines or
1957 * Message-ID: & last entry from References: line
1958 * Case is important here
1959 */
1960 if (ptr[j].msgid != NULL) {
1961 char *refs = NULL;
1962 const char *myrefs = NULL;
1963 const char *mymsgid = NULL;
1964 int x;
1965 struct t_article *art = &arts[i];
1966 /*
1967 * TODO: nice idea del'd; better apply one rule on all
1968 * fitting articles, so we can switch to an appropriate
1969 * algorithm for each kind of rule, including the
1970 * deleted one.
1971 */
1972
1973 /* myrefs does not need to be freed */
1974
1975 /* use full references header or just the last entry? */
1976 switch (ptr[j].fullref) {
1977 case FILTER_MSGID:
1978 myrefs = REFS(art, refs);
1979 mymsgid = MSGID(art);
1980 break;
1981
1982 case FILTER_MSGID_LAST:
1983 myrefs = art->refptr ? (art->refptr->parent ? art->refptr->parent->txt : "") : "";
1984 mymsgid = MSGID(art);
1985 break;
1986
1987 case FILTER_MSGID_ONLY:
1988 myrefs = "";
1989 mymsgid = MSGID(art);
1990 break;
1991
1992 case FILTER_REFS_ONLY:
1993 myrefs = REFS(art, refs);
1994 mymsgid = "";
1995 break;
1996
1997 default: /* should not happen */
1998 /* CONSTANTCONDITION */
1999 assert(0 != 0);
2000 break;
2001 }
2002
2003 if ((x = test_regex(myrefs, ptr[j].msgid, FALSE, ®ex_cache_msgid[j])) == 0) /* no match */
2004 x = test_regex(mymsgid, ptr[j].msgid, FALSE, ®ex_cache_msgid[j]);
2005
2006 switch (x) {
2007 case 1:
2008 SET_FILTER(group, i, j);
2009 #ifdef DEBUG
2010 if (debug & DEBUG_FILTER)
2011 debug_print_file("FILTER", "Group[%s] Rule[%d][%s] ArtMSGID[%s] Artnum[%d]", group->name, j, ptr[j].msgid, mymsgid, i);
2012 #endif /* DEBUG */
2013 break;
2014
2015 case -1:
2016 error = TRUE;
2017 break;
2018
2019 default:
2020 break;
2021 }
2022 FreeIfNeeded(refs);
2023 }
2024 /*
2025 * Filter on Lines: line
2026 */
2027 if ((ptr[j].lines_cmp != FILTER_LINES_NO) && (arts[i].line_count >= 0)) {
2028 switch (ptr[j].lines_cmp) {
2029 case FILTER_LINES_EQ:
2030 if (arts[i].line_count == ptr[j].lines_num) {
2031 SET_FILTER(group, i, j);
2032 }
2033 break;
2034
2035 case FILTER_LINES_LT:
2036 if (arts[i].line_count < ptr[j].lines_num) {
2037 SET_FILTER(group, i, j);
2038 }
2039 break;
2040
2041 case FILTER_LINES_GT:
2042 if (arts[i].line_count > ptr[j].lines_num) {
2043 SET_FILTER(group, i, j);
2044 }
2045 break;
2046
2047 default:
2048 break;
2049 }
2050 }
2051
2052 /*
2053 * Filter on GNKSA code
2054 */
2055 if ((ptr[j].gnksa_cmp != FILTER_LINES_NO) && (arts[i].gnksa_code >= 0)) {
2056 switch (ptr[j].gnksa_cmp) {
2057 case FILTER_LINES_EQ:
2058 if (arts[i].gnksa_code == ptr[j].gnksa_num) {
2059 SET_FILTER(group, i, j);
2060 }
2061 break;
2062
2063 case FILTER_LINES_LT:
2064 if (arts[i].gnksa_code < ptr[j].gnksa_num) {
2065 SET_FILTER(group, i, j);
2066 }
2067 break;
2068
2069 case FILTER_LINES_GT:
2070 if (arts[i].gnksa_code > ptr[j].gnksa_num) {
2071 SET_FILTER(group, i, j);
2072 }
2073 break;
2074
2075 default:
2076 break;
2077 }
2078 }
2079
2080 /*
2081 * Filter on Xref: lines
2082 *
2083 * "news.foo.bar foo.bar:666 bar.bar:999"
2084 * is turned into the Newsgroups like string
2085 * foo.bar,bar.bar
2086 */
2087 if (arts[i].xref && *arts[i].xref) {
2088 if (ptr[j].score && ptr[j].xref != NULL) {
2089 char *s, *e, *k;
2090 t_bool skip = FALSE;
2091
2092 s = arts[i].xref;
2093 if (strchr(s, ' ') || strchr(s, '\t')) {
2094 while (*s && !isspace((int) *s)) /* skip server name */
2095 s++;
2096 while (*s && isspace((int) *s))
2097 s++;
2098 }
2099 #ifdef DEBUG
2100 else { /* server name missing in overview, i.e. colobus 2.1 */
2101 if (debug & DEBUG_FILTER) { /* TODO: lang.c, _()? */
2102 debug_print_file("FILTER", "Malformed overview entry: servername missing.");
2103 debug_print_file("FILTER", "\t Xref: %s", arts[i].xref);
2104 }
2105 }
2106 #endif /* DEBUG */
2107 if (strlen(s)) {
2108 /* reformat */
2109 k = e = my_malloc(strlen(s) + 1);
2110 while (*s) {
2111 if (*s == ':') {
2112 *e++ = ',';
2113 skip = TRUE;
2114 }
2115 if (*s != ':' && !isspace((int) *s) && !skip)
2116 *e++ = *s;
2117 if (isspace((int) *s))
2118 skip = FALSE;
2119 s++;
2120 }
2121 *--e = '\0';
2122 } else {
2123 #ifdef DEBUG
2124 if (debug & DEBUG_FILTER) /* TODO: lang.c, _()? */
2125 debug_print_file("FILTER", "Skipping Xref filter");
2126 #endif /* DEBUG */
2127 error = TRUE;
2128 break;
2129 }
2130
2131 switch (test_regex(k, ptr[j].xref, ptr[j].icase, ®ex_cache_xref[j])) {
2132 case 1:
2133 SET_FILTER(group, i, j);
2134 break;
2135
2136 case -1:
2137 error = TRUE;
2138 break;
2139
2140 default:
2141 break;
2142 }
2143 free(k);
2144 }
2145 }
2146
2147 /*
2148 * Filter on Path: lines
2149 */
2150 if (arts[i].path && *arts[i].path) {
2151 if (ptr[j].path != NULL) {
2152 switch (test_regex(arts[i].path, ptr[j].path, ptr[j].icase, ®ex_cache_path[j])) {
2153 case 1:
2154 SET_FILTER(group, i, j);
2155 break;
2156
2157 case -1:
2158 error = TRUE;
2159 break;
2160
2161 default:
2162 break;
2163 }
2164 }
2165 }
2166 }
2167 }
2168 /*
2169 * if (i % (MODULO_COUNT_NUM * 20) == 0)
2170 * show_progress("Filter", i, top_art);
2171 */
2172 }
2173
2174 /*
2175 * throw away the contents of all regex_caches
2176 */
2177 if (tinrc.wildcard) {
2178 for (j = 0; j < num; j++) {
2179 FreeIfNeeded(regex_cache_subj[j].re);
2180 FreeIfNeeded(regex_cache_subj[j].extra);
2181 FreeIfNeeded(regex_cache_from[j].re);
2182 FreeIfNeeded(regex_cache_from[j].extra);
2183 FreeIfNeeded(regex_cache_msgid[j].re);
2184 FreeIfNeeded(regex_cache_msgid[j].extra);
2185 FreeIfNeeded(regex_cache_xref[j].re);
2186 FreeIfNeeded(regex_cache_xref[j].extra);
2187 FreeIfNeeded(regex_cache_path[j].re);
2188 FreeIfNeeded(regex_cache_path[j].extra);
2189 }
2190 free(regex_cache_subj);
2191 free(regex_cache_from);
2192 free(regex_cache_msgid);
2193 free(regex_cache_xref);
2194 free(regex_cache_path);
2195 }
2196
2197 /*
2198 * now entering the main filter loop:
2199 * all articles have scored, so do kill & select
2200 */
2201 if (!error) {
2202 for_each_art(i) {
2203 if (arts[i].score <= tinrc.score_limit_kill) {
2204 if (arts[i].status == ART_UNREAD)
2205 arts[i].killed = ART_KILLED_UNREAD;
2206 else
2207 arts[i].killed = ART_KILLED;
2208 filtered = TRUE;
2209 art_mark(group, &arts[i], ART_READ);
2210 if (group->attribute->show_only_unread_arts)
2211 arts[i].keep_in_base = FALSE;
2212 } else if (arts[i].score >= tinrc.score_limit_select) {
2213 arts[i].selected = TRUE;
2214 }
2215 }
2216 }
2217
2218 if (!cmd_line && !batch_mode)
2219 clear_message();
2220
2221 return filtered;
2222 }
2223
2224
2225 /*
2226 * Check if we have to filter on Path: for the given group
2227 */
2228 t_bool
filter_on_path(struct t_group * group)2229 filter_on_path(
2230 struct t_group *group)
2231 {
2232 int i;
2233 struct t_filter *flt;
2234 t_bool ret = FALSE;
2235
2236 if (group->glob_filter->num == 0)
2237 return ret;
2238
2239 if (set_filter_scope(group)) {
2240 flt = group->glob_filter->filter;
2241 for (i = 0; i < group->glob_filter->num; i++) {
2242 if (flt[i].inscope && flt[i].path) {
2243 ret = TRUE;
2244 break;
2245 }
2246 }
2247 }
2248 return ret;
2249 }
2250
2251
2252 static int
set_filter_scope(struct t_group * group)2253 set_filter_scope(
2254 struct t_group *group)
2255 {
2256 int i, num, inscope;
2257 struct t_filter *ptr, *prev;
2258
2259 inscope = num = group->glob_filter->num;
2260 prev = ptr = group->glob_filter->filter;
2261
2262 for (i = 0; i < num; i++) {
2263 ptr[i].inscope = TRUE;
2264 ptr[i].next = (struct t_filter *) 0;
2265 if (ptr[i].scope != NULL) {
2266 if (!match_group_list(group->name, ptr[i].scope)) {
2267 ptr[i].inscope = FALSE;
2268 inscope--;
2269 }
2270 }
2271 if (i != 0 && ptr[i].inscope)
2272 prev = prev->next = &ptr[i];
2273 }
2274 return inscope;
2275 }
2276
2277
2278 /*
2279 * This will come in useful for filtering on non-overview hdr fields
2280 */
2281 #if 0
2282 static FILE *
2283 open_xhdr_fp(
2284 char *header,
2285 long min,
2286 long max)
2287 {
2288 # ifdef NNTP_ABLE
2289 if (read_news_via_nntp && !read_saved_news && nntp_caps.hdr_cmd) {
2290 char buf[NNTP_STRLEN];
2291
2292 snprintf(buf, sizeof(buf), "%s %s %ld-%ld", nntp_caps.hdr_cmd, header, min, max);
2293 return (nntp_command(buf, OK_HEAD, NULL, 0));
2294 } else
2295 # endif /* NNTP_ABLE */
2296 return (FILE *) 0; /* Some trick implementation for local spool... */
2297 }
2298 #endif /* 0 */
2299