ftp.nice.ch/pub/next/unix/network/news/slrn0.9.0.0.s.tar.gz#/slrn/src/score.c

This is score.c in view mode; [Download] [Up]

#include "config.h"
#include "features.h"

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <time.h>

#ifdef HAVE_STDLIB_H
# include <stdlib.h>
#endif

#include <slang.h>
#include "jdmacros.h"



#include "slrn.h"
#include "group.h"
#include "art.h"
#include "score.h"
#include "misc.h"
#include "server.h"
#include "hash.h"

/* This file will implement article scoring for slrn.  Basically the idea is
 * that the user will provide a set of regular expressions that will act on
 * the header of an article and return an integer or 'score'.  If this score
 * is less than zero, the article will be killed.  If it is a positive number
 * greater than some user defined value, the article will be flagged as
 * interesting.  If the score is zero, the article is not killed and it is not
 * flagged as interesting either.
 */

char *Slrn_Score_File;
int Slrn_Perform_Scoring = SLRN_XOVER_SCORING | SLRN_EXPENSIVE_SCORING;

/* These two structures are pseudo-score types containing no compiled
 * regular expressions.
 */
typedef struct PScore_Regexp_Type
{
#define MAX_KEYWORD_LEN 24
   char keyword[MAX_KEYWORD_LEN];      /* used only by generic type */
   unsigned int header_type;
#define SCORE_SUBJECT 1
#define SCORE_FROM 2
#define SCORE_XREF 3
#define SCORE_NEWSGROUP 4
#define SCORE_REFERENCES 5
#define SCORE_LINES 6
#define SCORE_MESSAGE_ID 7
#define SCORE_DATE 8
   /* generic requires extra server interaction */
#define SCORE_GENERIC 16
   unsigned int flags;
#define NOT_FLAG 1
#define USE_INTEGER 2
   union
     {
	unsigned char *regexp_str;
	int ival;
     }
   ireg;
   struct PScore_Regexp_Type *next;
}
PScore_Regexp_Type;

typedef struct PScore_Type
{
   int score;
   unsigned int flags;
#define RETURN_THIS_SCORE 1
#define SCORE_IS_OR_TYPE 2
   struct PScore_Regexp_Type *pregexp_list;
   struct PScore_Type *next;
}
PScore_Type;

typedef struct
{
   SLRegexp_Type regexp;
   unsigned char buf[256];	       /* for compiled pattern */
}
Our_SLRegexp_Type;

/* These two structures are compiled versions of the above. */
typedef struct Score_Regexp_Type
{
   unsigned int header_type;
   char *generic_keyword;	       /* pointer to space in pscore */
   int not_flag;
   int do_osearch;
   union
     {
	int ival;		       /* used by certain headers */
	Our_SLRegexp_Type re;
	SLsearch_Type se;
     }
   search;
   struct Score_Regexp_Type *next;
}
Score_Regexp_Type;

typedef struct Score_Type
{
   PScore_Type *pscore;		       /* points at structure this is derived from */
   struct Score_Regexp_Type regexp_list;
   struct Score_Type *next;
}
Score_Type;

#define MAX_GROUP_REGEXP_SIZE	256
#define MAX_GROUP_NAME_LEN	80

typedef struct Group_Score_Name_Type
{
   SLRegexp_Type group_regexp;
   char name[MAX_GROUP_NAME_LEN];      /* group name or pattern */
   unsigned char buf[MAX_GROUP_REGEXP_SIZE];/* for compiled pattern */
   struct Group_Score_Name_Type *next;
}
Group_Score_Name_Type;

typedef struct Group_Score_Type
{
   unsigned int gst_not_flag;
   Group_Score_Name_Type gsnt;
   PScore_Type *pst;
   struct Group_Score_Type *next;
}
Group_Score_Type;

static Group_Score_Type *Group_Score_Root;
static Group_Score_Type *Group_Score_Next;
static Score_Type *Score_Root, *Score_Tail;

int Slrn_Apply_Score = 1;

int slrn_score_header (Slrn_Header_Type *h, char *newsgroup)
{
   Score_Type *st;
   int score = 0;
   char *s;
   int ival = 0;
   
#if SLRN_HAS_MSGID_CACHE
   s = slrn_is_msgid_cached (h->msgid, newsgroup, 1);
   if (s != NULL)
     {
	/* Kill it if this wasn't the newsgroup were we saw it */
	if (strcmp (s, newsgroup))
	  return -9999;
     }
#endif
   
   s = NULL;

   st = Score_Root;
   while (st != NULL)
     {
	unsigned int len;
	Score_Regexp_Type *srt = &st->regexp_list;
	char buf[1024];
	int or_type = st->pscore->flags & SCORE_IS_OR_TYPE;
	
	while (srt != NULL)
	  {
	     switch (srt->header_type)
	       {
		case SCORE_LINES:
		  ival = h->lines;
		  goto integer_compare;/* ugly, ugly, ugly!! */
		  
		case SCORE_SUBJECT:
		  s = h->subject;
		  break;
		  
		case SCORE_FROM:
		  s = h->from;
		  break;
		  
		case SCORE_DATE:
		  s = h->date;
		  break;

		case SCORE_MESSAGE_ID:
		  s = h->msgid;
		  break;
		  
		case SCORE_XREF:
		  s = h->xref;
		  break;
		  
		case SCORE_REFERENCES:
		  s = h->refs;
		  break;
		  
		case SCORE_NEWSGROUP:
		  s = newsgroup;
		  break;
		  
		case SCORE_GENERIC:
		  /* Yuk.  This will be slow! */
		  if (Slrn_Score_After_XOver)
		    {
		       if (((Slrn_Perform_Scoring & SLRN_EXPENSIVE_SCORING) == 0)
			   || (-1 == Slrn_Server_Obj->sv_xhdr_command (srt->generic_keyword,
							h->number, buf, sizeof(buf))))
			 s = NULL;
		       else s = buf;
		    }
		  else
		    {
		       if (Slrn_Perform_Scoring & SLRN_EXPENSIVE_SCORING)
			 s = Slrn_Server_Obj->sv_get_extra_xover_header (srt->generic_keyword);
		       else s = NULL;
		    }
		  break;
		  
		default:
		  s = NULL;	       /* not supposed to happen */
	       }
	     
	     if (s == NULL)
	       {
		  if (or_type) goto next_srt;
		  break;
	       }
	     
	     len = strlen (s);
	     
	     if (srt->do_osearch)
	       {
		  SLsearch_Type *se = &srt->search.se;
		  
		  if ((len < se->key_len)
		      || (NULL == SLsearch ((unsigned char *) s,
					    (unsigned char *) s + len,
					    se)))
		    {
		       if (srt->not_flag == 0)
			 {
			    if (or_type) goto next_srt;
			    break;
			 }
		    }
		  else if (srt->not_flag)
		    {
		       if (or_type) goto next_srt;
		       break;
		    }
	       }
	     else
	       {
		  SLRegexp_Type *re;
		  
		  re = &srt->search.re.regexp;
		  if ((len < re->min_length)
		      || (NULL == SLang_regexp_match ((unsigned char *)s, len, re)))
		    {
		       if (srt->not_flag == 0)
			 {
			    if (or_type) goto next_srt;
			    break;
			 }
		    }
		  else if (srt->not_flag)
		    {
		       if (or_type) goto next_srt;
		       break;
		    }
	       }
	     /* Get here if above matched */
	     if (or_type) break;
	     srt = srt->next;
	     continue;
	     
	     /* This is ugly but I am worried about speed. --- we only get
	      * here for those headers that have integer values.
	      */
	     integer_compare:
	     if (ival < srt->search.ival)
	       {
		  if (srt->not_flag == 0)
		    {
		       if (or_type) goto next_srt;
		       break;
		    }
	       }
	     else if (srt->not_flag)
	       {
		  if (or_type) goto next_srt;
		  break;
	       }
	     
	     /* If we get here, the regular expression matched. */
	     if (or_type) break;
	     
	     next_srt:
	     srt = srt->next;
	  }
	
	if (((srt == NULL) && (or_type == 0))
	    || (or_type && (srt != NULL)))
	  {
	     int st_score = st->pscore->score;
	     
	     if (st->pscore->flags & RETURN_THIS_SCORE)
	       return st_score;
	     
	     if ((st_score == 9999)
		 || (st_score == -9999))
	       return st_score;
	     
	     if (st_score == 0)
	       return score;
	     
	     score += st_score;
	  }
	
	st = st->next;
     }
   return score;
}


static int chain_group_regexp (PScore_Type *pst, int *generic)
{
   PScore_Regexp_Type *psrt;
   Score_Regexp_Type *srt;
   Score_Type *st;
   SLRegexp_Type *re;
   
   while (pst != NULL)
     {
	if (NULL == (st = (Score_Type *) SLMALLOC (sizeof (Score_Type))))
	  {
	     return -1;
	  }
	memset ((char *) st, 0, sizeof (Score_Type));
	
	if (Score_Root == NULL)
	  {
	     Score_Root = st;
	  }
	else Score_Tail->next = st;
	
	Score_Tail = st;
	
	st->pscore = pst;
	
	psrt = pst->pregexp_list;
	srt = &st->regexp_list;
	
	while (psrt != NULL)
	  {
	     unsigned int flags = psrt->flags;
	     
	     if (SCORE_GENERIC == (srt->header_type = psrt->header_type))
	       *generic += 1;
	     
	     srt->generic_keyword = psrt->keyword;
	     srt->not_flag = (0 != (flags & NOT_FLAG));
	     
	     if (flags & USE_INTEGER)
	       {
		  srt->search.ival = psrt->ireg.ival;
	       }
	     else
	       {
		  re = &srt->search.re.regexp;
		  re->pat = psrt->ireg.regexp_str;
		  re->buf = srt->search.re.buf;
		  re->buf_len = sizeof (srt->search.re.buf);
		  re->case_sensitive = 0;
		  
		  if (0 != SLang_regexp_compile (re))
		    {
		       return -1;
		    }
		  
		  /* If an ordinary search is ok, use it. */
		  if (re->osearch)
		    {
		       srt->do_osearch = 1;
		       SLsearch_init ((char *) psrt->ireg.regexp_str, 1, 0, &srt->search.se);
		    }
	       }
	     
	     psrt = psrt->next;
	     if (psrt == NULL) break;
	     
	     if (NULL == (srt->next = (Score_Regexp_Type *) SLMALLOC (sizeof (Score_Regexp_Type))))
	       {
		  return -1;
	       }
	     srt = srt->next;
	     memset ((char *) srt, 0, sizeof (Score_Regexp_Type));
	  }
	pst = pst->next;
     }
   return 0;
}

static void free_group_chain (void)
{
   Score_Regexp_Type *srt;
   
   while (Score_Root != NULL)
     {
	Score_Type *next = Score_Root->next;
	srt = &Score_Root->regexp_list;
	srt = srt->next;	       /* first not malloced */
	while (srt != NULL)
	  {
	     Score_Regexp_Type *srt_next = srt->next;
	     SLFREE (srt);
	     srt = srt_next;
	  }
	SLFREE (Score_Root);
	Score_Root = next;
     }
   Score_Tail = NULL;
}

int slrn_open_score (char *group_name)
{
   Group_Score_Type *gsc = Group_Score_Root;
   unsigned int n;
   int generic;
   
   if ((Slrn_Perform_Scoring == 0) || (Group_Score_Root == NULL))
     {
	Slrn_Apply_Score = 0;
	return 0;
     }
   
   n = strlen (group_name);
   generic = 0;
   while (gsc != NULL)
     {
	Group_Score_Name_Type *gsnt;
	SLRegexp_Type *re;
	unsigned int gst_not_flag;
	
	gsnt = &gsc->gsnt;
	gst_not_flag = gsc->gst_not_flag;
	
	while (gsnt != NULL)
	  {
	     int match;
	     
	     re = &gsnt->group_regexp;
	
	     match = ((re->min_length <= n)
		      && (NULL != SLang_regexp_match ((unsigned char *) group_name, n, re)));
	     
	     /* Note: the following could be replaced by simply
	      *  match = (match ^ gst_not_flag);
	      * however, I may have more uses for this flag in the future.
	      */
	     match = ((match && (gst_not_flag == 0))
		      || ((match == 0) && gst_not_flag));
	       
	     if (match)
	       {
		  if (-1 == chain_group_regexp (gsc->pst, &generic))
		    {
		       free_group_chain ();
		       return -1;
		    }
		  break;
	       }
	     gsnt = gsnt->next;
	  }
	gsc = gsc->next;
     }
   
   if (Score_Root == NULL) return 0;
   
   Slrn_Apply_Score = 1;
   
   if ((generic == 0)
       || (0 == (Slrn_Perform_Scoring & SLRN_EXPENSIVE_SCORING)))
     return 1;
   
   if ((generic >= 1) || !Slrn_Server_Obj->sv_has_xover || !Slrn_Server_Obj->sv_has_cmd ("XHDR"))
     {
	Slrn_Score_After_XOver = 0;
	Slrn_Server_Obj->sv_open_suspend_xover ();
     }
   return 1;
}

void slrn_close_score (void)
{
   free_group_chain ();
   Slrn_Server_Obj->sv_close_suspend_xover ();
}



static int add_group_regexp (PScore_Type *pst,
			     unsigned char *str,
			     unsigned char *keyword, unsigned int type, int not_flag)
{
   unsigned int len;
   PScore_Regexp_Type *psrt;
   
   *str++ = 0;			       /* null terminate keyword by zeroing
					* out the colon.  This is by agreement
					* with the calling routine.
					*/
   
   if (*str == ' ') str++;	       /* space following colon not meaningful */

   len = (unsigned int) (slrn_trim_string ((char *) str) - (char *) str);
   if (0 == len) return -1;
   
   if (NULL == (psrt = (PScore_Regexp_Type *) SLMALLOC (sizeof (PScore_Regexp_Type))))
     slrn_exit_error ("Memory Allocation Failure.");
   
   memset ((char *) psrt, 0, sizeof (PScore_Regexp_Type));
   
   if (type != SCORE_LINES)
     {
	if (NULL == (psrt->ireg.regexp_str = (unsigned char *) SLMALLOC (len + 1)))
	  slrn_exit_error ("Memory Allocation Failure.");
	strcpy ((char *)psrt->ireg.regexp_str, (char *) str);
     }
   else
     {
	psrt->ireg.ival = atoi((char *)str);
	psrt->flags |= USE_INTEGER;
     }
   
   psrt->header_type = type;
   strncpy (psrt->keyword, (char *) keyword, MAX_KEYWORD_LEN);
   psrt->keyword[MAX_KEYWORD_LEN - 1] = 0;
   
   if (not_flag) psrt->flags |= NOT_FLAG;
   psrt->next = NULL;
   if (pst->pregexp_list == NULL)
     {
	pst->pregexp_list = psrt;
     }
   else
     {
	PScore_Regexp_Type *last = pst->pregexp_list;
	while (last->next != NULL) last = last->next;
	last->next = psrt;
     }
   return 0;
}


static int compile_group_names (char *group, Group_Score_Type *gst)
{
   char *comma;
   Group_Score_Name_Type *gsnt;
   unsigned int num_processed = 0;
   
   gsnt = &gst->gsnt;
   
   comma = group;
   while (comma != NULL)
     {
	SLRegexp_Type *re;
	
	group = slrn_skip_whitespace (group);
	
	comma = slrn_strchr ((char *) group, ',');
	if (comma != NULL) 
	  {
	     *comma++ = 0;
	  }
	
	(void) slrn_trim_string (group);
	if (*group == 0) continue;
	
	strncpy (gsnt->name, group, MAX_GROUP_NAME_LEN - 1);
	/* Note: because of the memset, this string is null terminated. */
   
	re = &gsnt->group_regexp;
	re->pat = (unsigned char *) slrn_fix_regexp ((char *)group);
	re->buf = gsnt->buf;
	re->buf_len = MAX_GROUP_REGEXP_SIZE;
	re->case_sensitive = 0;
	if (0 != SLang_regexp_compile (re))
	  {
	     return -1;
	  }
	
	if (comma != NULL)
	  {
	     Group_Score_Name_Type *gsnt1;
	     
	     gsnt1 = (Group_Score_Name_Type *) SLMALLOC (sizeof (Group_Score_Name_Type));
	     if (gsnt1 == NULL) 
	       slrn_exit_error ("Memory allocation failure.");
	     memset ((char *) gsnt1, 0, sizeof (Group_Score_Name_Type));
	     
	     gsnt->next = gsnt1;
	     gsnt = gsnt1;
	     group = comma;
	  }
	num_processed++;
     }
   if (num_processed) return 0;
   return -1;
}

static PScore_Type *create_new_score (unsigned char *group, int new_group_flag, unsigned int gst_not_flag,
				      int score, unsigned int pscore_flags)
{
   PScore_Type *pst;
   
   if (new_group_flag)
     {
	Group_Score_Type *gst, *tail;
	
	tail = Group_Score_Next = Group_Score_Root;
	while (Group_Score_Next != NULL)
	  {
	     if ((Group_Score_Next->gsnt.next == NULL)
		 && (Group_Score_Next->gst_not_flag == gst_not_flag)
		 && !strcmp ((char *) group, Group_Score_Next->gsnt.name))
	       break;
	     tail = Group_Score_Next;
	     Group_Score_Next = Group_Score_Next->next;
	  }
	
	if (Group_Score_Next == NULL)
	  {
	     gst = (Group_Score_Type *) SLMALLOC (sizeof (Group_Score_Type));
	     if (gst == NULL)
	       slrn_exit_error ("Memory allocation failure.");
	     
	     memset ((char *)gst, 0, sizeof (Group_Score_Type));
	     
	     if (-1 == compile_group_names ((char *)group, gst))
	       return NULL;
	     
	     gst->gst_not_flag = gst_not_flag;

	     if (Group_Score_Root == NULL)
	       {
		  Group_Score_Root = gst;
	       }
	     else tail->next = gst;
	     
	     Group_Score_Next = gst;
	  }
     }
   
   /* Now create the PseudoScore type and add it. */
   if (NULL == (pst = (PScore_Type *) SLMALLOC (sizeof (PScore_Type))))
     slrn_exit_error ("Memory Allocation Failure.");
   
   memset ((char *) pst, 0, sizeof (PScore_Type));
   pst->score = score;
   pst->flags = pscore_flags;
   
   if (Group_Score_Next->pst == NULL)
     {
	Group_Score_Next->pst = pst;
     }
   else
     {
	PScore_Type *last = Group_Score_Next->pst;
	while (last->next != NULL) last = last->next;
	last->next = pst;
     }
   return pst;
}


static void score_error (char *msg, char *line, unsigned int linenum, char *file)
{
   slrn_error ("Error processing %s\nLine %u:\n%s\n%s\n",
	       file, linenum, line, msg);
}



static void free_group_scores (void)
{
   while (Group_Score_Root != NULL)
     {
	Group_Score_Type *gnext = Group_Score_Root->next;
	PScore_Type *pst = Group_Score_Root->pst;
	Group_Score_Name_Type *gsnt;
	
	gsnt = Group_Score_Root->gsnt.next;
	while (gsnt != NULL)
	  {
	     Group_Score_Name_Type *next = gsnt->next;
	     SLFREE (gsnt);
	     gsnt = next;
	  }
	
	while (pst != NULL)
	  {
	     PScore_Type *pnext = pst->next;
	     PScore_Regexp_Type *r = pst->pregexp_list;
	     
	     while (r != NULL)
	       {
		  PScore_Regexp_Type *rnext = r->next;
		  
		  if (((r->flags & USE_INTEGER) == 0)
		      && (r->ireg.regexp_str != NULL))
		    SLFREE (r->ireg.regexp_str);
		  SLFREE (r);
		  r = rnext;
	       }
	     SLFREE (pst);
	     pst = pnext;
	  }
	
	SLFREE (Group_Score_Root);
	Group_Score_Root = gnext;
     }
}

static int has_score_expired (unsigned char *s, unsigned long today)
{
   unsigned long mm, dd, yyyy;
   unsigned long score_time;
   
   s = (unsigned char *) slrn_skip_whitespace ((char *) s);
   if (*s == 0) return 0;
   
   if (((3 != sscanf ((char *) s, "%lu/%lu/%lu", &mm, &dd, &yyyy))
	&& (3 != sscanf ((char *) s, "%lu-%lu-%lu", &dd, &mm, &yyyy)))
       || (dd > 31)
       || (mm > 12)
       || (yyyy < 1900))
     return -1;
   
   score_time = (yyyy - 1900) * 10000 + (mm - 1) * 100 + dd;
   if (score_time > today) return 0;
   return 1;
}

static unsigned long get_today (void)
{
   unsigned long mm, yy, dd;
   time_t tloc;
   struct tm *tm_struct;
   
   time (&tloc);
   tm_struct = localtime (&tloc);
   yy = tm_struct->tm_year;
   mm = tm_struct->tm_mon;
   dd = tm_struct->tm_mday;
   
   return yy * 10000 + mm * 100 + dd;
}

int slrn_read_score_file (char *name)
{
   char file[256];
   unsigned char group[256];
   char line[1024];
   FILE *fp;
   int start_new_group = 1, not_flag, gnt_not_flag;
   unsigned int linenum = 0;
   int score = 0;
   PScore_Type *pst = NULL;
   unsigned long today;
   int start_new_score = 0, score_has_expired = 0;
   unsigned int pscore_flags = 0;
   
   today = get_today ();
   
   if (Group_Score_Root != NULL)
     {
	free_group_scores ();
     }
   
   fp = slrn_open_home_file (name, "r", file, 0);
   if (fp == NULL)
     {
	Slrn_Apply_Score = 0;
	return 0;
     }
   
   group[0] = '*';
   group[1] = 0;
   
   while (fgets (line, sizeof (line) - 1, fp))
     {
	unsigned char *lp;
	
	linenum++;
	lp = (unsigned char *) slrn_skip_whitespace (line);
	if ((*lp == '#') || (*lp == '%') || (*lp <= ' ')) continue;
	
	if (*lp == '[')
	  {
	     unsigned char *g, *gmax, ch;
	     g = group;
	     gmax = g + sizeof (group);
	     
	     lp++;
	     lp = (unsigned char *) slrn_skip_whitespace ((char *)lp);
	     
	     gnt_not_flag = 0;
	     if (*lp == '~')
	       {
		  gnt_not_flag = 1;
		  lp = (unsigned char *) slrn_skip_whitespace ((char *)lp + 1);
	       }
	     
	     while (((ch = *lp++) != 0)
		    && (ch != ']') && (g < gmax))
	       *g++ = ch;
	     
	     if ((ch != ']') || (g == gmax))
	       {
		  score_error ("Syntax Error.", line, linenum, file);
		  goto error_return;
	       }
	     *g = 0;
	     start_new_group = 1;
	     score_has_expired = 0;
	     start_new_score = 0;
	     continue;
	  }
	
	if (!strncmp ((char *)lp, "Score:", 6))
	  {
	     unsigned char *lpp = lp + 6;
	     pscore_flags = 0;
	     if (*lpp == ':')
	       {
		  lpp++;
		  pscore_flags |= SCORE_IS_OR_TYPE;
	       }
	     lpp = (unsigned char *) slrn_skip_whitespace ((char *) lpp);
	     if (*lpp == '=')
	       {
		  pscore_flags |= RETURN_THIS_SCORE;
		  lpp++;
	       }
	     score = atoi ((char *)lpp);
	     start_new_score = 1;
	     continue;
	  }
	
	if (start_new_score)
	  {
	     if (!strncmp ((char *)lp, "Expires:", 8))
	       {
		  int ret;
		  ret = has_score_expired (lp + 8, today);
		  if (ret == -1)
		    {
		       score_error ("Expecting 'Expires: MM/DD/YYYY' or 'Expires: DD-MM-YYYY'",
				    line, linenum, file);
		       goto error_return;
		    }
		  if (ret)
		    {
		       slrn_message ("%s has expired score on line %d",
				     file, linenum);
		       start_new_score = 0;
		       score_has_expired = 1;
		    }
		  else score_has_expired = 0;
		  continue;
	       }
	     
	     
	     if (NULL == (pst = create_new_score (group, start_new_group, gnt_not_flag,
						  score, pscore_flags)))
	       {
		  score_error ("Bad group regular expression.", line, linenum, file);
		  goto error_return;
	       }
	     start_new_group = 0;
	     start_new_score = 0;
	     score_has_expired = 0;
	  }
	
	if (score_has_expired) continue;
	
	if (pst == NULL)
	  {
	     score_error ("Expecting Score keyword.", line, linenum, file);
	     goto error_return;
	  }
	
	if (*lp == '~')
	  {
	     not_flag = 1;
	     lp++;
	  }
	else not_flag = 0;
	
	
	/* Otherwise the line is a kill one */
	if (!strncmp ((char *)lp, "Subject:", 8))
	  add_group_regexp (pst, lp + 7, lp, SCORE_SUBJECT, not_flag);
	else if (!strncmp ((char *)lp, "From:", 5))
	  add_group_regexp (pst, lp + 4, lp, SCORE_FROM, not_flag);
	else if (!strncmp ((char *)lp, "Xref:", 5))
	  add_group_regexp (pst, lp + 4, lp, SCORE_XREF, not_flag);
	else if (!strncmp ((char *)lp, "Newsgroup:", 10))
	  add_group_regexp (pst, lp + 9, lp, SCORE_NEWSGROUP, not_flag);
	else if (!strncmp ((char *)lp, "References:", 11))
	  add_group_regexp (pst, lp + 10, lp, SCORE_REFERENCES, not_flag);
	else if (!strncmp ((char *)lp, "Lines:", 6))
	  add_group_regexp (pst, lp + 5, lp, SCORE_LINES, not_flag);
	else if (!strncmp ((char *)lp, "Date:", 5))
	  add_group_regexp (pst, lp + 5, lp, SCORE_DATE, not_flag);
	else if (!strncmp ((char *)lp, "Message-Id:", 11))
	  add_group_regexp (pst, lp + 10, lp, SCORE_MESSAGE_ID, not_flag);
	else
	  {
	     unsigned char *lpp = lp;
	     while (*lpp && (*lpp != ':')) lpp++;
	     if (*lpp != ':')
	       {
		  score_error ("Missing COLON.", line, linenum, file);
		  goto error_return;
	       }
	     
	     add_group_regexp (pst, lpp, lp, SCORE_GENERIC, not_flag);
	  }
     }
   
   slrn_fclose (fp);
   Slrn_Apply_Score = 1;
   return 1;
   
   error_return:
   slrn_fclose (fp);
   return -1;
}


int slrn_edit_score (Slrn_Header_Type *h, char *newsgroup)
{
   char ch = 'e';
   int ich;
   char file[256];
   char qregexp[512];
   unsigned int mm = 0, dd = 0, yy = 0;
   int use_expired = 0;
   unsigned int linenum = 0;
   char *q, *ng = newsgroup;
   int score;
   FILE *fp;
   time_t myclock;
   
   
   if (Slrn_Score_File == NULL)
     {
	slrn_error ("A Score file has not been specified.");
	return -1;
     }
   
   if (Slrn_Prefix_Arg_Ptr == NULL)
     {
	ch = slrn_get_response ("SsFfRrEeCc", "Pick Score type: S-ubject, F-rom, R-eferences, E-dit, C-ancel");
	ch |= 0x20;
	if (ch == 'c') return -1;
   
	score = -9999;
	if (-1 == slrn_read_integer ("Score", &score, &score))
	  return -1;
	if (score == 0) return -1;
	
	if ('a' == (0x20 | slrn_get_response ("TtaA", "Which newsgroups: T-his group, A-ll groups")))
	  ng = "*";

	while (1)
	  {
	     *qregexp = 0;
	     if (-1 == slrn_read_input ("Expires (MM/DD/YYYY or DD-MM-YYYY or leave blank):", qregexp, 1))
	       return -1;
	     if (*qregexp)
	       {
		  if (((3 != sscanf (qregexp, "%u/%u/%u", &mm, &dd, &yy))
		       && (3 != sscanf (qregexp, "%u-%u-%u", &dd, &mm, &yy)))
		      || (dd > 31)
		      || (mm > 12)
		      || (yy < 1900))
		    continue;
		  use_expired = 1;
		  break;
	       }
	     else
	       {
		  use_expired = 0;
		  break;
	       }
	  }
     }
   
   if ((NULL == (fp = slrn_open_home_file (Slrn_Score_File, "r+", file, 1)))
       && (NULL == (fp = slrn_open_home_file (Slrn_Score_File, "w+", file, 1))))
     {
	slrn_error ("Unable to open %s", file);
	return -1;
     }
   
   if (Slrn_Prefix_Arg_Ptr == NULL)
     {
	linenum = 1;
	while (EOF != (ich = getc (fp)))
	  {
	     if (ich == '\n')	linenum++;
	  }
	
	myclock = time((time_t *) 0);
	fprintf (fp, "\n%%Score created by slrn on %s\n[%s]\nScore: %d\n",
		 (char *) ctime(&myclock), ng, score);
	
	if (use_expired)
	  fprintf (fp, "Expires: %u/%u/%u\n", mm, dd, yy);
	else fprintf (fp, "%%Expires: \n");
	
	q = SLregexp_quote_string (h->subject, qregexp, sizeof (qregexp));
	if (q == NULL) q = h->subject;
	fprintf (fp, "%c\tSubject: %s\n",
		 ((ch == 's') ? ' ' : '%'), q);
	
	q = SLregexp_quote_string (h->from, qregexp, sizeof (qregexp));
	if (q == NULL) q = h->from;
	fprintf (fp, "%c\tFrom: %s\n",
		 ((ch == 'f') ? ' ' : '%'), q);
	
	q = SLregexp_quote_string (h->msgid, qregexp, sizeof (qregexp));
	if (q == NULL) q = h->msgid;
	fprintf (fp, "%c\tReferences: %s\n",
		 ((ch == 'r') ? ' ' : '%'), q);
	
	
	q = SLregexp_quote_string (h->xref, qregexp, sizeof(qregexp));
	if (q != NULL)
	  {
	     /* The way slrn handles Xref, we only need this: */
	     fprintf (fp, "%%\t%s\n", q);
	  }
	
	q = SLregexp_quote_string (newsgroup, qregexp, sizeof(qregexp));
	if (q == NULL) q = newsgroup;
	fprintf (fp, "%%\tNewsgroup: %s\n", q);
     }
   
   Slrn_Prefix_Arg_Ptr = NULL;
   slrn_fclose (fp);

   if (ch == 'e')
     {
	if (-1 == slrn_edit_file (file, linenum + 1))
	  {
	     slrn_error ("Error calling editor.");
	     return -1;
	  }
     }
   slrn_message ("Reloading score file...");
   slrn_smg_refresh ();
   (void) slrn_read_score_file (Slrn_Score_File);
   slrn_message ("Exit group for changes to take effect.");
   return 0;
}

These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.