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