chiark / gitweb /
debugging for thing that crashed
[inn-innduct.git] / storage / expire.c
1 /*  $Id: expire.c 6775 2004-05-17 06:23:42Z rra $
2 **
3 **  Code for overview-driven expiration.
4 **
5 **  In order to expire on a per-newsgroup (instead of per-storage-class)
6 **  basis, one has to use overview-driven expiration.  This contains all of
7 **  the code to do that.  It provides OVgroupbasedexpire, OVhisthasmsgid, and
8 **  OVgroupmatch for the use of various overview methods.
9 */
10
11 #include "config.h"
12 #include "clibrary.h"
13 #include <ctype.h>
14 #include <errno.h>
15
16 #include "inn/innconf.h"
17 #include "libinn.h"
18 #include "ov.h"
19 #include "ovinterface.h"
20 #include "paths.h"
21 #include "storage.h"
22
23 enum KRP {Keep, Remove, Poison};
24
25 /* Statistics */
26 static long             EXPprocessed;
27 static long             EXPunlinked;
28 static long             EXPoverindexdrop;
29
30 #define NGH_HASH(Name, p, j)    \
31         for (p = Name, j = 0; *p; ) j = (j << 5) + j + *p++
32 #define NGH_SIZE        2048
33 #define NGH_BUCKET(j)   &NGHtable[j & (NGH_SIZE - 1)]
34
35 #define OVFMT_UNINIT    -2
36 #define OVFMT_NODATE    -1
37 #define OVFMT_NOXREF    -1
38
39 static int              Dateindex = OVFMT_UNINIT; 
40 static int              Xrefindex = OVFMT_UNINIT;
41 static int              Messageidindex = OVFMT_UNINIT;
42
43 typedef struct _NEWSGROUP {
44     char                *Name;
45     char                *Rest;
46     unsigned long       Last;
47     unsigned long       Lastpurged;
48         /* These fields are new. */
49     time_t              Keep;
50     time_t              Default;
51     time_t              Purge;
52     /* X flag => remove entire article when it expires in this group */
53     bool                Poison;
54 } NEWSGROUP;
55
56 typedef struct _NGHASH {
57     int         Size;
58     int         Used;
59     NEWSGROUP   **Groups;
60 } NGHASH;
61
62 #define MAGIC_TIME      49710.
63
64 typedef struct _BADGROUP {
65     struct _BADGROUP    *Next;
66     char                *Name;
67 } BADGROUP;
68
69 /*
70 **  Information about the schema of the news overview files.
71 */
72 typedef struct _ARTOVERFIELD {  
73     char        *Header;
74     int         Length;
75     bool        HasHeader;
76     bool        NeedsHeader;
77 } ARTOVERFIELD;
78
79 static BADGROUP         *EXPbadgroups;
80 static int              nGroups;
81 static NEWSGROUP        *Groups;
82 static NEWSGROUP        EXPdefault;
83 static NGHASH           NGHtable[NGH_SIZE];
84
85 static char             **arts;
86 static enum KRP         *krps;
87
88 static ARTOVERFIELD *   ARTfields;
89 static int              ARTfieldsize;
90 static bool             ReadOverviewfmt = false;
91
92
93 /* FIXME: The following variables are shared between this file and ov.c.
94    This should be cleaned up with a better internal interface. */
95 extern time_t   OVnow;
96 extern char *   ACTIVE;
97 extern FILE *   EXPunlinkfile;
98 extern bool     OVignoreselfexpire;
99 extern bool     OVusepost;
100 extern bool     OVkeep;
101 extern bool     OVearliest;
102 extern bool     OVquiet;
103 extern int      OVnumpatterns;
104 extern char **  OVpatterns;
105
106
107 /*
108 **  Hash a newsgroup and see if we get it.
109 */
110 static NEWSGROUP *
111 NGfind(char *Name)
112 {
113     char                *p;
114     int                 i;
115     unsigned int        j;
116     NEWSGROUP           **ngp;
117     char                c;
118     NGHASH              *htp;
119
120     NGH_HASH(Name, p, j);
121     htp = NGH_BUCKET(j);
122     for (c = *Name, ngp = htp->Groups, i = htp->Used; --i >= 0; ngp++)
123         if (c == ngp[0]->Name[0] && strcmp(Name, ngp[0]->Name) == 0)
124             return ngp[0];
125     return NULL;
126 }
127
128 /*
129 **  Sorting predicate to put newsgroups in rough order of their activity.
130 */
131 static int
132 NGcompare(const void *p1, const void *p2)
133 {
134     const NEWSGROUP * const * ng1 = p1;
135     const NEWSGROUP * const * ng2 = p2;
136
137     return ng1[0]->Last - ng2[0]->Last;
138 }
139
140 /*
141 **  Split a line at a specified field separator into a vector and return
142 **  the number of fields found, or -1 on error.
143 */
144 static int
145 EXPsplit(char *p, char sep, char **argv, int count)
146 {
147     int i;
148
149     if (!p)
150       return 0;
151
152     while (*p == sep)
153       ++p;
154
155     if (!*p)
156       return 0;
157
158     if (!p)
159       return 0;
160
161     while (*p == sep)
162       ++p;
163
164     if (!*p)
165       return 0;
166
167     for (i = 1, *argv++ = p; *p; )
168         if (*p++ == sep) {
169             p[-1] = '\0';
170             for (; *p == sep; p++);
171             if (!*p)
172                 return i;
173             if (++i == count)
174                 /* Overflow. */
175                 return -1;
176             *argv++ = p;
177         }
178     return i;
179 }
180
181 /*
182 **  Build the newsgroup structures from the active file.
183 */
184 static void
185 BuildGroups(char *active)
186 {
187     NGHASH              *htp;
188     NEWSGROUP           *ngp;
189     char                *p;
190     char                *q;
191     int                 i;
192     unsigned            j;
193     int                 lines;
194     int                 NGHbuckets;
195     char                *fields[5];
196
197     /* Count the number of groups. */
198     for (p = active, i = 0; (p = strchr(p, '\n')) != NULL; p++, i++)
199         continue;
200     nGroups = i;
201     Groups = xmalloc(i * sizeof(NEWSGROUP));
202
203     /* Set up the default hash buckets. */
204     NGHbuckets = i / NGH_SIZE;
205     if (NGHbuckets == 0)
206         NGHbuckets = 1;
207     for (i = NGH_SIZE, htp = NGHtable; --i >= 0; htp++) {
208         htp->Size = NGHbuckets;
209         htp->Groups = xmalloc(htp->Size * sizeof(NEWSGROUP *));
210         htp->Used = 0;
211     }
212
213     /* Fill in the array. */
214     lines = 0;
215     for (p = active, ngp = Groups, i = nGroups; --i >= 0; ngp++, p = q + 1) {
216         lines++;
217         if ((q = strchr(p, '\n')) == NULL) {
218             fprintf(stderr, "%s: line %d missing newline\n", ACTIVE, lines);
219             exit(1);
220         }
221         if (*p == '.')
222              continue;
223         *q = '\0';
224         if (EXPsplit(p, ' ', fields, ARRAY_SIZE(fields)) != 4) {
225             fprintf(stderr, "%s: line %d wrong number of fields\n", ACTIVE, lines);
226             exit(1);
227         }
228         ngp->Name = fields[0];
229         ngp->Last = atol(fields[1]);
230         ngp->Rest = fields[3];
231
232         /* Find the right bucket for the group, make sure there is room. */
233         NGH_HASH(ngp->Name, p, j);
234         htp = NGH_BUCKET(j);
235         if (htp->Used >= htp->Size) {
236             htp->Size += NGHbuckets;
237             htp->Groups = xrealloc(htp->Groups, htp->Size * sizeof(NEWSGROUP *));
238         }
239         htp->Groups[htp->Used++] = ngp;
240     }
241
242     /* Sort each hash bucket. */
243     for (i = NGH_SIZE, htp = NGHtable; --i >= 0; htp++)
244     if (htp->Used > 1)
245         qsort(htp->Groups, htp->Used, sizeof htp->Groups[0], NGcompare);
246
247     /* Ok, now change our use of the Last field.  Set them all to maxint. */
248     for (i = NGH_SIZE, htp = NGHtable; --i >= 0; htp++) {
249         NEWSGROUP       **ngpa;
250         int             k;
251
252         for (ngpa = htp->Groups, k = htp->Used; --k >= 0; ngpa++) {
253             ngpa[0]->Last = ~(unsigned long) 0;
254             ngpa[0]->Lastpurged = 0;
255         }
256     }
257 }
258
259 /*
260 **  Parse a number field converting it into a "when did this start?".
261 **  This makes the "keep it" tests fast, but inverts the logic of
262 **  just about everything you expect.  Print a message and return false
263 **  on error.
264 */
265 static bool
266 EXPgetnum(int line, char *word, time_t *v, const char *name)
267 {
268     char                *p;
269     bool                SawDot;
270     double              d;
271
272     if (strcasecmp(word, "never") == 0) {
273         *v = (time_t)0;
274         return true;
275     }
276
277     /* Check the number.  We don't have strtod yet. */
278     for (p = word; ISWHITE(*p); p++)
279         continue;
280     if (*p == '+' || *p == '-')
281         p++;
282     for (SawDot = false; *p; p++)
283         if (*p == '.') {
284             if (SawDot)
285                 break;
286             SawDot = true;
287         }
288         else if (!CTYPE(isdigit, (int)*p))
289             break;
290     if (*p) {
291         fprintf(stderr, "Line %d, bad `%c' character in %s field\n",
292                 line, *p, name);
293         return false;
294     }
295     d = atof(word);
296     if (d > MAGIC_TIME)
297         *v = (time_t)0;
298     else
299         *v = OVnow - (time_t)(d * 86400.);
300     return true;
301 }
302
303 /*
304 **  Set the expiration fields for all groups that match this pattern.
305 */
306 static void
307 EXPmatch(char *p, NEWSGROUP *v, char mod)
308 {
309     NEWSGROUP           *ngp;
310     int                 i;
311     bool                negate;
312
313     negate = *p == '!';
314     if (negate)
315         p++;
316     for (ngp = Groups, i = nGroups; --i >= 0; ngp++)
317         if (negate ? !uwildmat(ngp->Name, p) : uwildmat(ngp->Name, p))
318             if (mod == 'a'
319              || (mod == 'm' && ngp->Rest[0] == NF_FLAG_MODERATED)
320              || (mod == 'u' && ngp->Rest[0] != NF_FLAG_MODERATED)) {
321                 ngp->Keep      = v->Keep;
322                 ngp->Default   = v->Default;
323                 ngp->Purge     = v->Purge;
324                 ngp->Poison    = v->Poison;
325             }
326 }
327
328 /*
329 **  Parse the expiration control file.  Return true if okay.
330 */
331 static bool
332 EXPreadfile(FILE *F)
333 {
334     char                *p;
335     int                 i;
336     int                 j;
337     int                 k;
338     char                mod;
339     NEWSGROUP           v;
340     bool                SawDefault;
341     char                buff[BUFSIZ];
342     char                *fields[7];
343     char                **patterns;
344
345     /* Scan all lines. */
346     SawDefault = false;
347     patterns = xmalloc(nGroups * sizeof(char *));
348     
349     for (i = 1; fgets(buff, sizeof buff, F) != NULL; i++) {
350         if ((p = strchr(buff, '\n')) == NULL) {
351             fprintf(stderr, "Line %d too long\n", i);
352             free(patterns);
353             return false;
354         }
355         *p = '\0';
356         p = strchr(buff, '#');
357         if (p)
358             *p = '\0';
359         else
360             p = buff + strlen(buff);
361         while (--p >= buff) {
362             if (isspace((int)*p))
363                 *p = '\0';
364             else
365                 break;
366         }
367         if (buff[0] == '\0')
368             continue;
369         if ((j = EXPsplit(buff, ':', fields, ARRAY_SIZE(fields))) == -1) {
370             fprintf(stderr, "Line %d too many fields\n", i);
371             free(patterns);
372             return false;
373         }
374
375         /* Expired-article remember line? */
376         if (strcmp(fields[0], "/remember/") == 0) {
377             continue;
378         }
379
380         /* Regular expiration line -- right number of fields? */
381         if (j != 5) {
382             fprintf(stderr, "Line %d bad format\n", i);
383             free(patterns);
384             return false;
385         }
386
387         /* Parse the fields. */
388         if (strchr(fields[1], 'M') != NULL)
389             mod = 'm';
390         else if (strchr(fields[1], 'U') != NULL)
391             mod = 'u';
392         else if (strchr(fields[1], 'A') != NULL)
393             mod = 'a';
394         else {
395             fprintf(stderr, "Line %d bad modflag\n", i);
396             free(patterns);
397             return false;
398         }
399         v.Poison = (strchr(fields[1], 'X') != NULL);
400         if (!EXPgetnum(i, fields[2], &v.Keep,    "keep")
401          || !EXPgetnum(i, fields[3], &v.Default, "default")
402          || !EXPgetnum(i, fields[4], &v.Purge,   "purge")) {
403             free(patterns);
404             return false;
405         }
406         /* These were turned into offsets, so the test is the opposite
407          * of what you think it should be.  If Purge isn't forever,
408          * make sure it's greater then the other two fields. */
409         if (v.Purge) {
410             /* Some value not forever; make sure other values are in range. */
411             if (v.Keep && v.Keep < v.Purge) {
412                 fprintf(stderr, "Line %d keep>purge\n", i);
413                 free(patterns);
414                 return false;
415             }
416             if (v.Default && v.Default < v.Purge) {
417                 fprintf(stderr, "Line %d default>purge\n", i);
418                 free(patterns);
419                 return false;
420             }
421         }
422
423         /* Is this the default line? */
424         if (fields[0][0] == '*' && fields[0][1] == '\0' && mod == 'a') {
425             if (SawDefault) {
426                 fprintf(stderr, "Line %d duplicate default\n", i);
427                 free(patterns);
428                 return false;
429             }
430             EXPdefault.Keep    = v.Keep;
431             EXPdefault.Default = v.Default;
432             EXPdefault.Purge   = v.Purge;
433             EXPdefault.Poison  = v.Poison;
434             SawDefault = true;
435         }
436
437         /* Assign to all groups that match the pattern and flags. */
438         if ((j = EXPsplit(fields[0], ',', patterns, nGroups)) == -1) {
439             fprintf(stderr, "Line %d too many patterns\n", i);
440             free(patterns);
441             return false;
442         }
443         for (k = 0; k < j; k++)
444             EXPmatch(patterns[k], &v, mod);
445     }
446     free(patterns);
447
448     return true;
449 }
450
451 /*
452 **  Handle a newsgroup that isn't in the active file.
453 */
454 static NEWSGROUP *
455 EXPnotfound(char *Entry)
456 {
457     static NEWSGROUP    Removeit;
458     BADGROUP            *bg;
459
460     /* See if we already know about this group. */
461     for (bg = EXPbadgroups; bg; bg = bg->Next)
462         if (strcmp(Entry, bg->Name) == 0)
463             break;
464     if (bg == NULL) {
465         bg = xmalloc(sizeof(BADGROUP));
466         bg->Name = xstrdup(Entry);
467         bg->Next = EXPbadgroups;
468         EXPbadgroups = bg;
469     }
470     /* remove it all now. */
471     if (Removeit.Keep == 0) {
472         Removeit.Keep = OVnow;
473         Removeit.Default = OVnow;
474         Removeit.Purge = OVnow;
475     }
476     return &Removeit;
477 }
478
479 /*
480 **  Should we keep the specified article?
481 */
482 static enum KRP
483 EXPkeepit(char *Entry, time_t when, time_t expires)
484 {
485     NEWSGROUP           *ngp;
486     enum KRP            retval = Remove;
487
488     if ((ngp = NGfind(Entry)) == NULL)
489         ngp = EXPnotfound(Entry);
490
491     /* Bad posting date? */
492     if (when > OVrealnow + 86400) {
493         /* Yes -- force the article to go right now. */
494         when = expires ? ngp->Purge : ngp->Default;
495     }
496
497     /* If no expiration, make sure it wasn't posted before the default. */
498     if (expires == 0) {
499         if (when >= ngp->Default)
500             retval = Keep;
501
502     /* Make sure it's not posted before the purge cut-off and
503      * that it's not due to expire. */
504     } else {
505         if (when >= ngp->Purge && (expires >= OVnow || when >= ngp->Keep))
506             retval = Keep;
507     }
508     if (retval == Keep) {
509         return Keep;
510     } else {
511         return ngp->Poison ? Poison : Remove;
512     }
513 }
514
515 /*
516 **  An article can be removed.  Either print a note, or actually remove it.
517 **  Takes in the Xref information so that it can pass this to the storage
518 **  API callback used to generate the list of files to remove.
519 */
520 void
521 OVEXPremove(TOKEN token, bool deletedgroups, char **xref, int ngroups)
522 {
523     EXPunlinked++;
524     if (deletedgroups) {
525         EXPprocessed++;
526         EXPoverindexdrop++;
527     }
528     if (EXPunlinkfile && xref != NULL) {
529         SMprintfiles(EXPunlinkfile, token, xref, ngroups);
530         if (!ferror(EXPunlinkfile))
531             return;
532         fprintf(stderr, "Can't write to -z file, %s\n", strerror(errno));
533         fprintf(stderr, "(Will ignore it for rest of run.)\n");
534         fclose(EXPunlinkfile);
535         EXPunlinkfile = NULL;
536     }
537     if (!SMcancel(token) && SMerrno != SMERR_NOENT && SMerrno != SMERR_UNINIT)
538         fprintf(stderr, "Can't unlink %s: %s\n", TokenToText(token),
539                 SMerrorstr);
540 }
541
542 /*
543 **  Read the overview schema.
544 */
545 static void
546 ARTreadschema(void)
547 {
548     FILE                        *F;
549     char                        *p;
550     char                        *path;
551     ARTOVERFIELD                *fp;
552     int                         i;
553     char                        buff[SMBUF];
554     bool                        foundxref = false;
555     bool                        foundxreffull = false;
556
557     /* Open file, count lines. */
558     path = concatpath(innconf->pathetc, _PATH_SCHEMA);
559     F = fopen(path, "r");
560     if (F == NULL)
561         return;
562     for (i = 0; fgets(buff, sizeof buff, F) != NULL; i++)
563         continue;
564     fseeko(F, 0, SEEK_SET);
565     ARTfields = xmalloc((i + 1) * sizeof(ARTOVERFIELD));
566
567     /* Parse each field. */
568     for (fp = ARTfields; fgets(buff, sizeof buff, F) != NULL; ) {
569         /* Ignore blank and comment lines. */
570         if ((p = strchr(buff, '\n')) != NULL)
571             *p = '\0';
572         if ((p = strchr(buff, '#')) != NULL)
573             *p = '\0';
574         if (buff[0] == '\0')
575             continue;
576         if ((p = strchr(buff, ':')) != NULL) {
577             *p++ = '\0';
578             fp->NeedsHeader = (strcmp(p, "full") == 0);
579         }
580         else
581             fp->NeedsHeader = false;
582         fp->HasHeader = false;
583         fp->Header = xstrdup(buff);
584         fp->Length = strlen(buff);
585         if (strcasecmp(buff, "Xref") == 0) {
586             foundxref = true;
587             foundxreffull = fp->NeedsHeader;
588         }
589         fp++;
590     }
591     ARTfieldsize = fp - ARTfields;
592     fclose(F);
593     if (!foundxref || !foundxreffull) {
594         fprintf(stderr, "'Xref:full' must be included in %s", path);
595         exit(1);
596     }
597     free(path);
598 }
599
600 /*
601 **  Return a field from the overview line or NULL on error.  Return a copy
602 **  since we might be re-using the line later.
603 */
604 static char *
605 OVERGetHeader(const char *p, int field)
606 {
607     static char         *buff;
608     static int          buffsize;
609     int                 i;
610     ARTOVERFIELD        *fp;
611     char                *next;
612
613     fp = &ARTfields[field];
614
615     /* Skip leading headers. */
616     for (; field-- >= 0 && *p; p++)
617         if ((p = strchr(p, '\t')) == NULL)
618             return NULL;
619     if (*p == '\0')
620         return NULL;
621
622     if (fp->HasHeader)
623         p += fp->Length + 2;
624
625     if (fp->NeedsHeader) {              /* find an exact match */
626          while (strncmp(fp->Header, p, fp->Length) != 0) {
627               if ((p = strchr(p, '\t')) == NULL) 
628                 return NULL;
629               p++;
630          }
631          p += fp->Length + 2;
632     }
633
634     /* Figure out length; get space. */
635     if ((next = strpbrk(p, "\n\r\t")) != NULL) {
636         i = next - p;
637     } else {
638         i = strlen(p);
639     }
640     if (buffsize == 0) {
641         buffsize = i;
642         buff = xmalloc(buffsize + 1);
643     }
644     else if (buffsize < i) {
645         buffsize = i;
646         buff = xrealloc(buff, buffsize + 1);
647     }
648
649     strncpy(buff, p, i);
650     buff[i] = '\0';
651     return buff;
652 }
653
654 /*
655 **  Read overview.fmt and find index for headers
656 */
657 static void
658 OVfindheaderindex(void)
659 {
660     FILE        *F;
661     char        *active;
662     char        *path;
663     int         i;
664
665     if (ReadOverviewfmt)
666         return;
667     if (innconf->groupbaseexpiry) {
668         ACTIVE = concatpath(innconf->pathdb, _PATH_ACTIVE);
669         if ((active = ReadInFile(ACTIVE, (struct stat *)NULL)) == NULL) {
670             fprintf(stderr, "Can't read %s, %s\n",
671             ACTIVE, strerror(errno));
672             exit(1);
673         }
674         BuildGroups(active);
675         arts = xmalloc(nGroups * sizeof(char *));
676         krps = xmalloc(nGroups * sizeof(enum KRP));
677         path = concatpath(innconf->pathetc, _PATH_EXPIRECTL);
678         F = fopen(path, "r");
679         free(path);
680         if (!EXPreadfile(F)) {
681             fclose(F);
682             fprintf(stderr, "Format error in expire.ctl\n");
683             exit(1);
684         }
685         fclose(F);
686     }
687     ARTreadschema();
688     if (Dateindex == OVFMT_UNINIT) {
689         for (Dateindex = OVFMT_NODATE, i = 0; i < ARTfieldsize; i++) {
690             if (strcasecmp(ARTfields[i].Header, "Date") == 0) {
691                 Dateindex = i;
692             } else if (strcasecmp(ARTfields[i].Header, "Xref") == 0) {
693                 Xrefindex = i;
694             } else if (strcasecmp(ARTfields[i].Header, "Message-ID") == 0) {
695                 Messageidindex = i;
696             }
697         }
698     }
699     ReadOverviewfmt = true;
700     return;
701 }
702
703 /*
704 **  Do the work of expiring one line.  Assumes article still exists in the
705 **  spool.  Returns true if article should be purged, or return false.
706 */
707 bool
708 OVgroupbasedexpire(TOKEN token, const char *group, const char *data,
709                    int len UNUSED, time_t arrived, time_t expires)
710 {
711     static char         *Group = NULL;
712     char                *p;
713     int                 i;
714     int                 count;
715     time_t              when;
716     bool                poisoned;
717     bool                keeper;
718     bool                delete;
719     bool                purge;
720     char                *Xref;
721
722     if (SMprobe(SELFEXPIRE, &token, NULL)) {
723         if (!OVignoreselfexpire)
724             /* this article should be kept */
725             return false;
726     }
727     if (!ReadOverviewfmt) {
728         OVfindheaderindex();
729     }
730
731     if (OVusepost) {
732         if ((p = OVERGetHeader(data, Dateindex)) == NULL) {
733             EXPoverindexdrop++;
734             return true;
735         }
736         if ((when = parsedate(p, NULL)) == -1) {
737             EXPoverindexdrop++;
738             return true;
739         }
740     } else {
741         when = arrived;
742     }
743     if ((Xref = OVERGetHeader(data, Xrefindex)) == NULL) {
744         if (Group != NULL) {
745             free(Group);
746         }
747         Group = concat(group, ":", (char *) 0);
748         Xref = Group;
749     } else {
750         if ((Xref = strchr(Xref, ' ')) == NULL) {
751             EXPoverindexdrop++;
752             return true;
753         }
754         for (Xref++; *Xref == ' '; Xref++)
755             ;
756     }
757     if ((count = EXPsplit(Xref, ' ', arts, nGroups)) == -1) {
758         EXPoverindexdrop++;
759         return true;
760     }
761
762     /* arts is now an array of strings, each of which is a group name, a
763        colon, and an article number.  EXPkeepit wants just pure group names,
764        so replace the colons with nuls (deleting the overview entry if it
765        isn't in the expected form). */
766     for (i = 0; i < count; i++) {
767         p = strchr(arts[i], ':');
768         if (p == NULL) {
769             fflush(stdout);
770             fprintf(stderr, "Bad entry, \"%s\"\n", arts[i]);
771             EXPoverindexdrop++;
772             return true;
773         }
774         *p = '\0';
775     }
776
777     /* First check all postings */
778     poisoned = false;
779     keeper = false;
780     delete = false;
781     purge = true;
782     for (i = 0; i < count; ++i) {
783         if ((krps[i] = EXPkeepit(arts[i], when, expires)) == Poison)
784             poisoned = true;
785         if (OVkeep && (krps[i] == Keep))
786             keeper = true;
787         if ((krps[i] == Remove) && strcmp(group, arts[i]) == 0)
788             delete = true;
789         if ((krps[i] == Keep))
790             purge = false;
791     }
792     EXPprocessed++;
793
794     if (OVearliest) {
795         if (delete || poisoned || token.type == TOKEN_EMPTY) {
796             /* delete article if this is first entry */
797             if (strcmp(group, arts[0]) == 0) {
798                 for (i = 0; i < count; i++)
799                     arts[i][strlen(arts[i])] = ':';
800                 OVEXPremove(token, false, arts, count);
801             }
802             EXPoverindexdrop++;
803             return true;
804         }
805     } else { /* not earliest mode */
806         if ((!keeper && delete) || token.type == TOKEN_EMPTY) {
807             /* delete article if purge is set, indicating that it has
808                expired out of every group to which it was posted */
809             if (purge) {
810                 for (i = 0; i < count; i++)
811                     arts[i][strlen(arts[i])] = ':';
812                 OVEXPremove(token, false, arts, count);
813             }
814             EXPoverindexdrop++;
815             return true;
816         }
817     }
818
819     /* this article should be kept */
820     return false;
821 }
822
823 bool
824 OVhisthasmsgid(struct history *h, const char *data)
825 {
826     char *p;
827
828     if (!ReadOverviewfmt) {
829         OVfindheaderindex();
830     }
831     if ((p = OVERGetHeader(data, Messageidindex)) == NULL)
832         return false;
833     return HISlookup(h, p, NULL, NULL, NULL, NULL);
834 }
835
836 bool
837 OVgroupmatch(const char *group)
838 {
839     int i;
840     bool wanted = false;
841
842     if (OVnumpatterns == 0 || group == NULL)
843         return true;
844     for (i = 0; i < OVnumpatterns; i++) {
845         switch (OVpatterns[i][0]) {
846         case '!':
847             if (!wanted && uwildmat(group, &OVpatterns[i][1]))
848                 break;
849         case '@':
850             if (uwildmat(group, &OVpatterns[i][1])) {
851                 return false;
852             }
853             break;
854         default:
855             if (uwildmat(group, OVpatterns[i]))
856                 wanted = true;
857         }
858     }
859     return wanted;
860 }
861
862 void
863 OVEXPcleanup(void)
864 {
865     int i;
866     BADGROUP *bg, *bgnext;
867     ARTOVERFIELD *fp;
868     NGHASH *htp;
869
870     if (EXPprocessed != 0) {
871         if (!OVquiet) {
872             printf("    Article lines processed %8ld\n", EXPprocessed);
873             printf("    Articles dropped        %8ld\n", EXPunlinked);
874             printf("    Overview index dropped  %8ld\n", EXPoverindexdrop);
875         }
876         EXPprocessed = EXPunlinked = EXPoverindexdrop = 0;
877     }
878     if (innconf->ovgrouppat != NULL) {
879         for (i = 0 ; i < OVnumpatterns ; i++)
880             free(OVpatterns[i]);
881         free(OVpatterns);
882     }
883     for (bg = EXPbadgroups; bg; bg = bgnext) {
884         bgnext = bg->Next;
885         free(bg->Name);
886         free(bg);
887     }
888     for (fp = ARTfields, i = 0; i < ARTfieldsize ; i++, fp++) {
889         free(fp->Header);
890     }
891     free(ARTfields);
892     if (ACTIVE != NULL) {
893         free(ACTIVE);
894         ACTIVE = NULL;
895     }
896     if (Groups != NULL) {
897         free(Groups);
898         Groups = NULL;
899     }
900     for (i = 0, htp = NGHtable ; i < NGH_SIZE ; i++, htp++) {
901         if (htp->Groups != NULL) {
902             free(htp->Groups);
903             htp->Groups = NULL;
904         }
905     }
906 }