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