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

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

/* Local spool support for slrn added by Olly Betts <olly@mantis.co.uk> */
#include "config.h"
#include "features.h"

#define DEBUG_SPOOL 0
#define DEBUG_SPOOL_FILENAME "SLRNDEBUG"

#include <stdio.h>

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

#include <string.h>
#include <errno.h>
#include <ctype.h>

#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif

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

#include <time.h>

#include "misc.h"
#include "slrn.h"

#ifndef SLRN_SPOOL_ROOT
# define SLRN_SPOOL_ROOT "/var/spool/news" /* a common place for the newsspool */
#endif

#ifndef SLRN_SPOOL_NOV_ROOT
# define SLRN_SPOOL_NOV_ROOT SLRN_SPOOL_ROOT
#endif

#ifndef SLRN_SPOOL_NOV_FILE
# define SLRN_SPOOL_NOV_FILE ".overview"
#endif

#include <sys/stat.h>
#include <assert.h>

#if HAVE_DIRENT_H
# include <dirent.h>
# define NAMLEN(dirent) strlen((dirent)->d_name)
#else
# define dirent direct
# define NAMLEN(dirent) (dirent)->d_namlen
# define NEED_D_NAMLEN
# if HAVE_SYS_NDIR_H
#  include <sys/ndir.h>
# endif
# if HAVE_SYS_DIR_H
#  include <sys/dir.h>
# endif
# if HAVE_NDIR_H
#  include <ndir.h>
# endif
#endif

#include <limits.h>

#ifndef NNTP_MAX_XOVER_SIZE
# define NNTP_MAX_XOVER_SIZE 1024
#endif

static int spool_put_server_cmd (char *, char *, unsigned int );
static int spool_select_group (char *, int *, int *);
static void spool_close_server (void);
static int spool_has_cmd (char *);
static int spool_initialize_server (void);
static char *spool_read_line (char *, unsigned int );
static int spool_xpat_cmd (char *, int, int, char *);
static int spool_xgtitle_cmd (char *);
static int spool_select_article (int, char *);
static int spool_open_xover (int, int);
static char *spool_read_xover (char *, unsigned int);
static void spool_close_xover (void);
static char *spool_head_from_msgid (char *, char *, unsigned int );
static int spool_xhdr_command (char *, int, char *, unsigned int);
static int spool_list_newsgroups (void);
static int spool_list_active (void);

static void spool_open_suspend_xover (void);
static void spool_close_suspend_xover (void);
static char *spool_get_extra_xover_header (char *);
static char *spool_read_create_xover_line (int, char *);

static Slrn_Server_Obj_Type Spool_Server_Obj;

/* some state that the NNTP server would take care of if we were using one */
static FILE *Spool_fh_local=NULL;
static char *Spool_Group=NULL;
static FILE *Spool_fh_nov=NULL; /* we use the overview file lots, so keep it open */
static int Spool_cur_artnum=0;
static int Spool_max_artnum=0;
static int Spool_fhead=0; /* if non-0 we're emulating "HEAD" so stop on blank line */
static int Spool_fFakingActive=0; /* if non-0 we're doing funky stuff with MH folders */

static int spool_fake_active( char *);
static char *spool_fakeactive_read_line(char *, int);
static int Spool_fakeactive_newsgroups=0;

#if DEBUG_SPOOL
# include <stdarg.h>
static void spool_debug (char *fmt, ...)
{
   FILE *fp = NULL;
   va_list ap;

   if (fp == NULL)
     {
	fp = fopen (DEBUG_SPOOL_FILENAME, "w");
	if (fp == NULL)
	  return;
     }

   fprintf (fp, "%lu: ", (unsigned long) clock ());

   if (fmt == NULL)
     {
	fputs ("(NULL)", fp);
     }
   else
     {
	va_start(ap, fmt);
	vfprintf(fp, fmt, ap);
	va_end (ap);
     }

   putc ('\n', fp);
   fflush (fp);
}
#endif

/* close any current file (unless it's the overview file) and NULL the FILE* */
static int spool_fclose_local (void)
{
   int res = 0;

   Spool_fhead=0;
   Spool_fFakingActive=0;

   if (Spool_fh_local != NULL)
     {
	if (Spool_fh_local != Spool_fh_nov)
	  res = fclose(Spool_fh_local);
	Spool_fh_local=NULL;
     }
   return res;
}

static char *spool_dircat (char *root, char *name, int map_dots)
{
   char *spool_group, *p, ch;
   unsigned int len;

   len = strlen (root);

   spool_group = SLMALLOC(strlen(name) + len + 2);
   if (spool_group == NULL)
     {
	slrn_exit_error ("Out of memory.");
     }

   strcpy (spool_group, root);

   p = spool_group + len;
   if (len && (*(p - 1) != '/'))
     *p++ = '/';

   strcpy (p, name);

   if (map_dots) while ((ch = *p) != 0)
     {
	if (ch == '.') *p = '/';
	p++;
     }

   return spool_group;
}

static int spool_find_artnum_from_msgid (char *msgid)
{
   char buf [8 * NNTP_MAX_XOVER_SIZE];
   char *p;

#if DEBUG_SPOOL
   spool_debug ("spool_find_artnum_from_msgid('%s')", msgid);
#endif

   spool_open_xover( 1, INT_MAX );

   while (NULL != spool_read_xover (buf, sizeof(buf)))
     {
	char *q;

	/* 5th field is message id. */

	if (NULL == (p = slrn_strchr(buf, '\t'))) continue;
	if (NULL == (p = slrn_strchr(p + 1, '\t'))) continue;
	if (NULL == (p = slrn_strchr(p + 1, '\t'))) continue;
	if (NULL == (p = slrn_strchr(p + 1, '\t'))) continue;

	p++; /* skip tab */
	q = slrn_strchr(p,'\t');
	if (q != NULL) *q='\0';

	if (0 == strcmp(msgid, p))
	  {
	     spool_close_xover();
#if DEBUG_SPOOL
	     spool_debug ("spool_find_artnum_from_msgid() returns %d",atoi(buf));
#endif
	     return atoi(buf);
	  }
     }

   spool_close_xover();
#if DEBUG_SPOOL
   spool_debug ("spool_find_artnum_from_msgid() found no match");
#endif
   return -1;
}

/* This is a separate function so localspool backend can just close the file */
static void spool_discard_output (char *line, unsigned int len)
{
   if (0 == Spool_fFakingActive)
     spool_fclose_local ();
   /* not critical to shut down part way through, as slrn doesn't try to */
   else while (NULL != spool_read_line (line, len))
     {
	;
     }
}

static int spool_nntp_head (char *line, char *buf, unsigned int len)
{
   char *p;

   spool_fclose_local();

   p = slrn_skip_whitespace (line + 5);

   /* !HACK! ought to check for "head <msg-id>" (with or without < >) */
   if (*p == '<')
     {
	Spool_cur_artnum = spool_find_artnum_from_msgid (p);
	/* !HACK! could check for -1 here */
     }
   else if (*p)
     Spool_cur_artnum = atoi(p);
   /* else no number given, so use current article number */

   sprintf (buf, "%d", Spool_cur_artnum);
   /* ugly temporary use of buffer !HACK! */
   p = buf;

   Spool_fh_local = fopen (p,"r");
   if (NULL == Spool_fh_local)
     {
	return ERR_NOARTIG; /* No such article in this group */
     }

   Spool_fhead = 1; /* set flag to stop after headers */
   sprintf (buf,"221 %d", Spool_cur_artnum);
   /* !HACK! INN also gives message-id in this message */
   return OK_HEAD; /* Head follows */
}

static int spool_nntp_next (char *line, char *buf, unsigned int len)
{
   int i;

   spool_fclose_local();

   i = Spool_cur_artnum;
   do
     {
	char num[80];

	i++;
	sprintf (num,"%d",i);
	if (1 == slrn_file_exists (num))
	  {
	     Spool_cur_artnum = i;
	     sprintf(buf,"223 %d",Spool_cur_artnum);

#if DEBUG_SPOOL
	     spool_debug ("NEXT found article %d",Spool_cur_artnum);
#endif

	     return OK_NOTEXT; /* No text sent -- stat, next, last */
	  }
     }
   while (i <= Spool_max_artnum); /* !HACK! better to find value from overview file or active file in case the group grows while we're in it? */

#if DEBUG_SPOOL
   spool_debug ("No NEXT -- %d > %d", Spool_cur_artnum, Spool_max_artnum);
#endif

   return ERR_NONEXT; /* No next article in this group */
}

static int spool_nntp_newgroups (char *line, char *buf, unsigned int len)
{
   /* expect something like "NEWGROUPS 960328 170939 GMT" GMT is optional */
   char *p;
   int Y,M,D,h,m,s;
   int c;
   int fGMT;
   int n;
   time_t threshold;
   struct tm t;
   long fpos;
   char buf1[512];
   int ch;
   int found;
   int first;

   c = sscanf(line+9,"%02d%02d%02d %02d%02d%02d%n",&Y,&M,&D,&h,&m,&s,&n);
   assert(c==6); /* group.c should have sanity checked the line */

   p = slrn_skip_whitespace(line + 9 + n);
   fGMT = (0 == strncmp (p, "GMT", 3));

   /* this !HACK! is good until 2051 -- round at 50 cos the RFC says to */
   Y += ( Y>50 ? 1900 : 2000 );
   /* for full version, use this instead:
    * if (Y<=50) Y+=100;
    * yr = this_year();
    * Y += ((yr-51)/100*100);
    */
#if DEBUG_SPOOL
   spool_debug ("%d read, %04d-%02d-%02d %d:%02d:%02d %s",
		c,Y,M,D,h,m,s,fGMT?"GMT":"(local)");
#endif
   t.tm_year = Y - 1900; /* tm_year is years since 1900 */
   t.tm_mon = M - 1; /* tm_mon has January as 0 */
   t.tm_mday = D;
   t.tm_hour = h;
   t.tm_min = m;
   t.tm_sec = s;
   t.tm_isdst = 0; /* say it's not DST (? !HACK!) */
   threshold = mktime( &t );

#if DEBUG_SPOOL
   spool_debug ("threshold (local) = %d",threshold);
#endif

   if (fGMT)
     {
	threshold=mktime(gmtime(&threshold)); /* !HACK! is this correct? */
#if DEBUG_SPOOL
	spool_debug ("threshold (GMT) = %d",threshold);
#endif
     }

   spool_fclose_local();
   Spool_fh_local = fopen (Slrn_ActiveTimes_File, "r");
   if (Spool_fh_local == NULL)
     {
	/* !HACK! at this point, it would be nice to be able to check for
	 * recently created directories with readdir() and to return those
	 * which would be useful for reading MH folders, etc.  This would
	 * be more than a little slow for a true newsspool though.
	 *
	 * Hmm, looked into this and it seems that you can't use the mtime
	 * or ctime to decide when a directory was created.  Hmmm.
	 */
#if DEBUG_SPOOL
	spool_debug ("Couldn't open active.times");
#endif
	return ERR_FAULT; /* Program fault, command not performed */
     }

   /* chunk size to step back through active.times by
    * when checking for new groups */
   /* 128 should be enough to find the last line in the probably most
    * common case of no new groups */
#define SLRN_SPOOL_ACTIVETIMES_STEP 128

   /* find start of a line */
   fseek (Spool_fh_local, 0, SEEK_END ); /* !HACK! check return value from this and all the rest... */
   fpos = ftell(Spool_fh_local);
#if DEBUG_SPOOL
   spool_debug ("ftell=%ld errno=%d (%s)",fpos,
		errno, strerror(errno));
   errno = 0;
#endif

   found=0;
   first=1;

   while (!found)
     {
	int i, len1;

	len1 = SLRN_SPOOL_ACTIVETIMES_STEP;

	if (fpos < (long)len1) len1=fpos; /* don't run of the start of the file */
	fpos -= len1;
	fseek (Spool_fh_local, fpos, SEEK_SET );
	if (fpos == 0) break;

#if DEBUG_SPOOL
	spool_debug ("ftell=%ld errno=%d (%s)", ftell (Spool_fh_local),
		     errno, strerror(errno));
#endif

	if (first)
	  {
	     /* on the first pass, we want to ignore the last byte \n at eof */
	     --len1;
	     first=0;
	  }

	for (i = 0; i < len1; i++)
	  {
	     ch = getc(Spool_fh_local);

	     assert(ch!=EOF); /* shouldn't happen */
	     if (ch != '\n') continue;

	     while ((i < len1)
		    && (NULL != fgets (buf1, sizeof(buf1), Spool_fh_local)))
	       {
		  i -= strlen(buf1);
		  p = buf1;
		  while (*p && (0 == isspace (*p)))
		    p++;

		  if (atol(p) < threshold)/* or <= ? !HACK! */
		    {
		       found = 1;
		       break;
		    }
	       }
	     break;
	  }
     }

   fpos=ftell(Spool_fh_local);
#if DEBUG_SPOOL
   spool_debug ("ftell=%ld errno=%d (%s)",fpos, errno, strerror(errno));
#endif

   while (NULL != fgets( buf1, sizeof(buf1), Spool_fh_local ))
     {
	p = buf1;
	while (*p && (0 == isspace (*p))) p++;

	if (atol(p) >= threshold) /* or just > ? !HACK! */
	  {
	     fseek( Spool_fh_local, fpos, SEEK_SET );
	     break;
	  }
	fpos = ftell(Spool_fh_local);
     }

   return OK_NEWGROUPS; /* New newsgroups follow */
}

typedef struct
{
   char *name;
   unsigned int len;
   int (*f) (char *, char *, unsigned int);
}
Spool_NNTP_Map_Type;

static Spool_NNTP_Map_Type Spool_NNTP_Maps [] =
{
     {"NEXT", 4, spool_nntp_next},
     {"HEAD", 4, spool_nntp_head},
     {"NEWGROUPS", 9, spool_nntp_newgroups},
     {NULL, 0, NULL}
};

static int spool_put_server_cmd (char *line, char *buf, unsigned int len)
{
   Spool_NNTP_Map_Type *nntpmap;

#if DEBUG_SPOOL
   spool_debug ("spool_put_server_cmd('%s')", line);
#endif

   nntpmap = Spool_NNTP_Maps;
   while (nntpmap->name != NULL)
     {
	if (!slrn_case_strncmp ((unsigned char *)nntpmap->name,
				(unsigned char *) line, nntpmap->len))
	  return (*nntpmap->f)(line, buf, len);

	nntpmap++;
     }

#if DEBUG_SPOOL
   spool_debug ("Hmmm, didn't know about that command");
#endif
   return ERR_COMMAND;
}

static int spool_is_name_all_digits (char *p)
{
   char *pmax;

   pmax = p + strlen (p);
   while (p < pmax)
     {
	if (!isdigit (*p))
	  return 0;
	p++;
     }
   return 1;
}

static int spool_read_minmax_from_dp (DIR *dp, int *min, int *max)
{
   struct dirent *ep;
   char *p;
   long l;
   long hi = 0;
   long lo = LONG_MAX;

   /* Scan through all the files, checking the ones with numbers for names */
   while ((ep = readdir(dp)) != NULL)
     {
	p = ep->d_name;
#ifdef NEED_D_NAMLEN
	p[ep->d_namlen] = 0;
#endif
	if (!isdigit(*p)) continue;

	if (0 == spool_is_name_all_digits (p))
	  continue;

	if (0 == (l = atol (p)))
	  continue;

	if (l < lo)
	  lo = l;
	if (l > hi)
	  hi = l;
     }

   if ((lo == LONG_MAX)
       && (hi == 0))
     return -1;

   *min=lo;
   *max=hi;

   return 0;
}

/* Get the lowest and highest article numbers by the simple method
 * or looking at the files in the directory.
 * Returns 1 on success, 0 on failure
 */
static int spool_read_minmax_from_dir( int *min, int *max )
{
   /* This is adapted from some code in INN's ng.c */
   DIR *dp;

   /* I suspect this is very unlikely to fail */
   if ((dp = opendir(".")) == NULL)
     return 0;

   if (-1 == spool_read_minmax_from_dp (dp, min, max))
     {
	*min = 1;
	*max = 0;
     }

   (void) closedir(dp);
   return 1;
}

#if SPOOL_ACTIVE_FOR_ART_RANGE
/* Get the lowest and highest article numbers from the active file
 * Returns 1 on success, 0 on failure
 * (failure => active file didn't open, or the group wasn't in it)
 */
static int spool_read_minmax_from_active( char *name, int *min, int *max )
{
   char buf[512];
   unsigned int len;

   spool_fclose_local();
   Spool_fh_local = fopen(Slrn_Active_File,"r");
   if (Spool_fh_local == NULL) return 0;

   len = strlen(name);
   buf[len] = 0;		       /* init this for test below */

   while (NULL != fgets (buf, sizeof(buf), Spool_fh_local))
     {
	/* quick, crude test first to see if it could possibly be a match */
	if ((buf[len] == ' ')
	    && (0 == memcmp (buf, name, len)))
	  {
	     spool_fclose_local ();
	     if (2 != sscanf (buf + len + 1, "%d%d", max, min))
	       return 0;

	     Spool_max_artnum = *max;
# if DEBUG_SPOOL
	     spool_debug ("from active:%s %d %d",
			  name,*min,*max);
# endif
	     return 1;
	  }
     }
   spool_fclose_local();

   return 0;
}
#endif

/* Get the lowest and highest article numbers from the overview file
 * Returns 1 on success, 0 on failure
 */
static int spool_read_minmax_from_overview( char *name, int *min, int *max )
{
   /* chunk size to step back through .overview files by
    * when trying to find start of last line */
#define SPOOL_NOV_STEP 1024
   /* If there's no .overview file, get min/max info from the active file */
   /* ditto if .overview file is empty */
   int ch;
   long fpos;
   int found;
   int first;

   /* !HACK! this assumes the overview file is rewound */
   Spool_Server_Obj.sv_has_xover = ((Spool_fh_nov != NULL)
				 && (1 == fscanf (Spool_fh_nov,"%d", min)));

   if (0 == Spool_Server_Obj.sv_has_xover)
     return 0;

   /* find start of last line */
   fseek (Spool_fh_nov, 0, SEEK_END); /* !HACK! check return value from this and all the rest... */

   fpos = ftell (Spool_fh_nov);

#if DEBUG_SPOOL
   spool_debug("ftell=%ld errno=%d (%s)",
	       fpos, errno, strerror(errno));
   errno = 0;
#endif

   found=0;
   first=1;

   while (!found && (fpos > 0))
     {
	int i, len;

	len = SPOOL_NOV_STEP;

	/* don't run of the start of the file */
	if (fpos < (long)len) len = fpos;

	fpos -= len;
	fseek(Spool_fh_nov, fpos, SEEK_SET);

#if DEBUG_SPOOL
	spool_debug("ftell=%ld errno=%d (%s)",
		    ftell (Spool_fh_nov), errno, strerror(errno));
	errno = 0;
#endif

	if (first)
	  {
	     /* on the first pass, we want to ignore the last byte \n at eof */
	     --len;
	     first = 0;
	  }

	for(i = 0; i < len; i++ )
	  {
	     ch = getc(Spool_fh_nov);

	     assert(ch!=EOF); /* shouldn't happen */
	     if (ch =='\n')
	       found = i + 1; /* and keep going in case there's another */
	  }
     }

   fseek (Spool_fh_nov, fpos + found, SEEK_SET);

#if DEBUG_SPOOL
   spool_debug("ftell=%ld errno=%d (%s)",
	       ftell (Spool_fh_nov), errno, strerror(errno));
   errno = 0;
#endif

   fscanf (Spool_fh_nov, "%d", max);

#if DEBUG_SPOOL
   spool_debug ("%s %d %d",name,*min,*max);
#endif
   return 1;
}

int spool_select_group (char *name, int *min, int *max)
{
   char *p, *q;

   /* close any open files */
   spool_fclose_local();
   if (Spool_fh_nov)
     fclose(Spool_fh_nov);

   if (Spool_Group != NULL) SLFREE(Spool_Group);

   Spool_Group = spool_dircat (Slrn_Spool_Root, name, 1);

#if DEBUG_SPOOL
   spool_debug ("spool_select_group(%s) spool_group dir = %s", name, Spool_Group);
#endif

   /* change directory to the spool directory to make opening articles easier */
   if (chdir (Spool_Group))
     return -1;

   p = spool_dircat (Slrn_Nov_Root, name, 1);
   q = spool_dircat (p, Slrn_Nov_File, 0);

   Spool_fh_nov = fopen(q,"rb");
   SLFREE(q);
   SLFREE(p);

   if (!spool_read_minmax_from_overview( name, min, max )
#if SPOOL_ACTIVE_FOR_ART_RANGE
       && !spool_read_minmax_from_active( name, min, max )
#endif
       && !spool_read_minmax_from_dir( min, max ))
     return -1;

   Spool_max_artnum = *max;

#if DEBUG_SPOOL
   spool_debug ("Group: %s %d - %d", name, *min, *max);
#endif
   return 0;
}

static int Spool_Server_Inited = 0;

static void spool_close_server (void)
{
#if DEBUG_SPOOL
   spool_debug ("spool_close_server()");
#endif
   if (Spool_Group != NULL)
     {
	SLFREE(Spool_Group);
	Spool_Group = NULL;
     }

   spool_fclose_local();

   if (NULL != Spool_fh_nov)
     {
	fclose (Spool_fh_nov);
	Spool_fh_nov = NULL;
     }
   Spool_Server_Inited = 0;
}

static int spool_has_cmd (char *cmd)
{
   return 0; /* deny everything */
}

int spool_initialize_server (void)
{
   int tt_inited = (Slrn_TT_Initialized & 1);

   if (Spool_Server_Inited) spool_close_server ();

#if DEBUG_SPOOL
   spool_debug ("spool_initialize_server(%s)", host);
#endif

   if (2 != slrn_file_exists (Slrn_Spool_Root))
     {
	if (tt_inited == 0)
	  slrn_tty_message (2, "Local spool directory '%s' doesn't exist", Slrn_Spool_Root);
	else
	  {
	     slrn_message ("Local spool directory '%s' doesn't exist", Slrn_Spool_Root);
	     slrn_smg_refresh ();
	  }
	return -1;
     }

   /* I think it's better to think that the *server* has XOVER, but
    * some (or all) groups may not.
    * So set this to 1 here, and then to 0 or 1 in spool_select_group if we
    * find an overview file
    */
   Spool_Server_Obj.sv_has_xover = 1;
   Spool_Server_Inited = 1;
   return 0;
}

static char *spool_read_line (char *line, unsigned int len)
{
   if (Spool_fFakingActive) return spool_fakeactive_read_line (line, len);

   if (!Spool_fh_local || fgets( line, len, Spool_fh_local)==NULL || (Spool_fhead && line[0]=='\n'))
     {
	spool_fclose_local();
	return NULL;
     }

   len=strlen(line);
   if (len && line[len-1]=='\n')
     line[len-1]='\0';

   /* client_get_server() just stops reading when it takes notice of len */
#if 0
   else
     {
	/* skip rest of line which wouldn't fit */

	while (((ch = getc(Spool_fh_local)) != '\n')
	       && (ch != EOF))
	  ; /* do nothing */
     }
#endif
   return line;
}

int spool_xpat_cmd (char *hdr, int rmin, int rmax, char *pat)
{
   return -1;
}

int spool_xgtitle_cmd (char *pattern)
{
   return -1;
}

int spool_select_article (int n, char *msgid)
{
   char num[20];

   /*    printf("spool_select_article(%d,%s)\n",n,msgid); */

   if (n == -1)
     {
	n = spool_find_artnum_from_msgid(msgid);
	if (n == -1)
	  return -1;
     }
   Spool_cur_artnum = n;
   sprintf (num,"%d",n);
   spool_fclose_local();
   Spool_fh_local = fopen (num,"rb"); /* fopen the article in question */
   if (Spool_fh_local == NULL)
     return -1;
   return 0;
}

static int Spool_XOver_Done;
static int Spool_XOver_Min;
static int Spool_XOver_Max;
static int Spool_XOver_Next;

static int Spool_Suspend_Xover_For_Kill = 0;

int spool_open_xover (int min, int max)
{
   int i, ch;
   long fp;

#if DEBUG_SPOOL
   spool_debug ("spool_open_xover(%d,%d)", min, max);
#endif

   spool_fclose_local();

   if (Spool_Server_Obj.sv_has_xover && !Spool_Suspend_Xover_For_Kill)
     {
	Spool_fh_local = Spool_fh_nov;
	if (Spool_fh_local == NULL)
	  return -1;

	/* find first record in range in overview file */
	/* first look at the current position and see where we are */
	/* this is worth trying as slrn will often read a series of ranges */
	fp = ftell( Spool_fh_local );

	if ((1 != fscanf(Spool_fh_local,"%d", &i))
	    || (i > min))
	  {
	     /* looks like we're after the start of the range */
	     /* therefore we'll have to rescan the file from the start */
	     rewind (Spool_fh_local);
	     i = -1;
	     /* this might be improved by doing some binary-chop style searching */
	  }
	else
	  {
	     while (((ch = getc(Spool_fh_local)) != '\n')
		    && (ch != EOF))
	       ; /* do nothing */
	  }

#if DEBUG_SPOOL
	spool_debug ("Starting with i=%d",i);
#endif

	while (i < min)
	  {
	     fp = ftell( Spool_fh_local );
	     if (1 != fscanf(Spool_fh_local,"%d", &i))
	       return -1;

	     while (((ch = getc (Spool_fh_local)) != '\n')
		    && (ch != EOF))
	       ; /* do nothing */
	  }

	fseek (Spool_fh_local, fp, SEEK_SET); /* reset to start of line */
     }

   Spool_XOver_Next = Spool_XOver_Min = min;
   Spool_XOver_Max = max;
   Spool_XOver_Done = 0;
   return 0;
}

char *spool_read_xover (char *the_buf, unsigned int len)
{
   char buf [NNTP_MAX_XOVER_SIZE];
   int id;
   int already_tried = 0;

#if DEBUG_SPOOL
   spool_debug ("spool_read_xover");
#endif

   if (Spool_Server_Obj.sv_has_xover && !Spool_Suspend_Xover_For_Kill)
     {
	char *p;

	p = spool_read_line (the_buf, len);
/*#ifdef LOCAL*/
	/* check if we've reached the end of the requested range */
	if ((p != NULL)
	    && (atoi(p) > Spool_XOver_Max))
	  p = NULL;

#if DEBUG_SPOOL
	spool_debug (p);
#endif

/*#endif*/
	return p;
     }

   if (Spool_XOver_Next > Spool_XOver_Max) return NULL;

   while (Spool_XOver_Done == 0)
     {
	sprintf (buf, "HEAD %d", Spool_XOver_Next);
	if (OK_HEAD != spool_put_server_cmd (buf, buf, sizeof(buf)))
	  {
	     already_tried = 0;
	     /* This is ugly */

	     /* If a head fails even though server says it has article, jump
	      * back and go on.
	      */
	     server_is_messed_up_head_failed:

	     do
	       {
		  switch (spool_put_server_cmd ("NEXT", buf, sizeof (buf)))
		    {
		     case OK_NOTEXT:
		       /* OK, got next article */
		       break;
		     case ERR_NONEXT:
		       Spool_XOver_Done = 1;
		       return NULL;
		     default:
		       slrn_exit_error ("Server failed NEXT request.");
		    }

		  id = atoi (buf + 4);
		  /* Try going back 5 articles */
		  if ((already_tried == 0) && (id < Spool_XOver_Next - 5))
		    {

		       sprintf (buf, "HEAD %d", Spool_XOver_Next - 5);

		       if (OK_HEAD == spool_put_server_cmd (buf, buf, sizeof(buf)))
			 {
			    /* read all header */
			    spool_discard_output(buf, sizeof(buf));
			 }
		       already_tried = 1;
		    }
	       }
	     while (id < Spool_XOver_Next);

	     if (OK_HEAD != spool_put_server_cmd ("HEAD", buf, sizeof(buf)))
	       {
		  sprintf (buf, "HEAD %d", id);

		  if (OK_HEAD != spool_put_server_cmd (buf, buf, sizeof(buf)))
		    {
		       already_tried++;
		       if (already_tried > 10)
			 {
			    slrn_exit_error ("Server Error: Head failed.");
			 }

		       goto server_is_messed_up_head_failed;
		    }
	       }
	  }
	/* extract article number */
	id = atoi(buf + 4);
	if (id >= Spool_XOver_Min) break;

	/* we just did a head.  Read it and do next until range is ok */
	spool_discard_output(buf, sizeof (buf));

	while (id < Spool_XOver_Min)
	  {
	     if (OK_NOTEXT == spool_put_server_cmd ("NEXT", buf, sizeof (buf)))
	       {
		  id = atoi(buf + 4);
		  continue;
	       }
	     Spool_XOver_Done = 1;
	     break;
	  }
     }

   if (Spool_XOver_Done) return NULL;
   if (id > Spool_XOver_Max)
     {
	/* clear out header text */
	Spool_XOver_Done = 1;
	spool_discard_output(buf, sizeof (buf));
	return NULL;
     }

   spool_read_create_xover_line (id, the_buf);

   if (id > Spool_XOver_Max)
     {
	Spool_XOver_Done = 1;
	return NULL;
     }

   if (OK_NOTEXT == spool_put_server_cmd ("NEXT", buf, sizeof (buf)))
     {
	Spool_XOver_Next = atoi(buf + 4);
	if (Spool_XOver_Next > Spool_XOver_Max)
	  {
	     Spool_XOver_Done = 1;
	  }
     }
   else Spool_XOver_Done = 1;
   return the_buf;
}

void spool_close_xover ()
{
#if DEBUG_SPOOL
   spool_debug ("spool_close_xover");
#endif
   Spool_XOver_Done = 1;
}

char *spool_head_from_msgid (char *msgid, char *buf, unsigned int len)
{
   int id;

   if ((msgid == NULL) || (*msgid == 0)) return NULL;
   id=spool_find_artnum_from_msgid(msgid);
   sprintf(buf,"%d",id); /* !HACK! brief buf abuse */
   Spool_fh_local=fopen(buf,"r");
   if (!Spool_fh_local)
     return NULL;
   Spool_fhead=1; /* set flag to stop after headers */
   return spool_read_create_xover_line (id, buf);
}

/* just hacked from the nntp_ version -- could be better coded */
int spool_xhdr_command (char *hdr, int num, char *buf, unsigned int buflen)
{
   char cmd[256], tmpbuf[1024];
   int found;
   unsigned int colon;

   sprintf (cmd, "HEAD %d", num);
   if (OK_HEAD != spool_put_server_cmd (cmd, buf, buflen))
     return -1;

   found = 0;
   colon = strlen (hdr);

   while (NULL != spool_read_line (tmpbuf, sizeof (tmpbuf)))
     {
	char *b;
	if (found
	    || slrn_case_strncmp ((unsigned char *) tmpbuf, (unsigned char *) hdr, colon)
	    || (tmpbuf[colon] != ':'))
	  continue;

	found = 1;

	b = tmpbuf + colon;
	if (*b == ' ') b++;
	strncpy (buf, b, buflen - 1);
	buf[buflen - 1] = 0;
     }
   return 0;
}

int spool_list_newsgroups (void)
{
   spool_fclose_local();
   Spool_fh_local=fopen(Slrn_Newsgroups_File,"r");
   if (!Spool_fh_local)
     {
	/* Use readdir() to return a list of newsgroups read from the
	 * "newsspool" so we can read MH folders, etc.  This would be more
	 * than a little slow for a true newsspool.
	 */
	spool_fake_active(Slrn_Spool_Root);
	Spool_fFakingActive=1;
	Spool_fakeactive_newsgroups=1;
	return 0;
/*	slrn_exit_error("Couldn't open newsgroups file '%s'", NEWSGROUPS); */
     }
   return 0;
}

int spool_list_active (void)
{
   spool_fclose_local();
   Spool_fh_local=fopen (Slrn_Active_File,"r");
   if (!Spool_fh_local)
     {
	spool_fake_active(Slrn_Spool_Root);
	Spool_fFakingActive=1;
	Spool_fakeactive_newsgroups=0;
	return 0;
	/* Use readdir() to return a list of newsgroups and article ranges read
	 * from the "newsspool" so we can read MH folders, etc.  This would
	 * be more than a little slow for a true newsspool.
	 */
/*	slrn_exit_error("Couldn't open active file '%s'", ACTIVE);*/
     }
   return 0;
}

typedef struct _Spool_DirTree_Type
{
   struct _Spool_DirTree_Type *parent;
   DIR *dp;
   int len;
   long lo, hi;
}
Spool_DirTree_Type;

static Spool_DirTree_Type *Spool_Head;

static char Spool_Buf[256];
static char Spool_nBuf[256];
static int Spool_Is_LeafDir;

static void spool_fake_active_in (char *dir)
{
   char *p;
   DIR *dp;
   Spool_DirTree_Type *tmp;

   p = Spool_Buf + strlen(Spool_Buf);

   if (dir != NULL)
     {
	*p = '/';
	strcpy (p + 1, dir);
     }

   if ((2 != slrn_file_exists (Spool_Buf))
       || (NULL == (dp = opendir (Spool_Buf))))
     {
	*p = 0;
	return;
     }

   Spool_Is_LeafDir = 1;
   tmp = (Spool_DirTree_Type *) SLMALLOC (sizeof(Spool_DirTree_Type));
   if (tmp == NULL)
     {
	slrn_exit_error ("Out of memory.");
     }

   tmp->dp = dp;
   tmp->parent = Spool_Head;
   tmp->hi = 0;
   tmp->lo = LONG_MAX;

   if (dir == NULL)
     tmp->len = 1;
   else
     {
	tmp->len = strlen (dir);

	p = Spool_nBuf + strlen(Spool_nBuf);
	if (p != Spool_nBuf) *p++ = '.';
	strcpy (p, dir);
     }

   Spool_Head = tmp;
}

static void spool_fake_active_out (void)
{
   Spool_DirTree_Type *tmp;
   int i;

   (void)closedir (Spool_Head->dp);
   Spool_Is_LeafDir = 0;

   Spool_Buf [strlen(Spool_Buf) - Spool_Head->len - 1] = '\0';

   i = strlen(Spool_nBuf) - Spool_Head->len - 1;

   if (i < 0) i = 0;
   Spool_nBuf[i]='\0';

   tmp = Spool_Head;
   Spool_Head = Spool_Head->parent;
   SLFREE(tmp);
}

static int spool_fake_active (char *path)
{
   strcpy(Spool_Buf, path);
   *Spool_nBuf='\0';
   Spool_Head=NULL;
   spool_fake_active_in (NULL);
   return 0;
}

static char *spool_fakeactive_read_line(char *line, int len)
{
   struct dirent *ep;
   char *p;
   long l;

   emptydir:

   if (!Spool_Head)
     {
      /* we've reached the end of the road */
	Spool_fFakingActive = 0;
	return NULL;
     }

   /* Scan through all the files, checking the ones with numbers for names */
   while ((ep = readdir(Spool_Head->dp)) != NULL)
     {
	p = ep->d_name;
#ifdef NEED_D_NAMLEN
	p[ep->d_namlen] = 0;
#endif
	if ((0 == spool_is_name_all_digits (p))
	    || ((l = atol (p)) == 0))
	  {
	     if (!(p[0]=='.' && (p[1]=='\0' || (p[1]=='.' && p[2]=='\0'))))
	       {
		  spool_fake_active_in(p);
	       }
	     continue;
	  }
	if (l < Spool_Head->lo)
	  Spool_Head->lo = l;
	if (l > Spool_Head->hi)
	  Spool_Head->hi = l;
     }

   if (Spool_Head->lo == LONG_MAX && Spool_Head->hi==0)
     {
	/* assume all leaf directories are valid groups */
	/* non-leaf directories aren't groups unless they have articles in */
	if (!Spool_Is_LeafDir)
	  {
	     spool_fake_active_out();
	     goto emptydir; /* skip empty "groups" */
	  }
	Spool_Head->lo = 1;
     }

   if (Spool_fakeactive_newsgroups)
     {
      /* newsgroups: alt.foo A group about foo */
	sprintf(line,"%s ?\n",Spool_nBuf); /* !HACK! check len */
     }
   else
     {
      /* active: alt.guitar 0000055382 0000055345 y */
	sprintf(line,"%s %ld %ld y\n",Spool_nBuf,Spool_Head->hi,Spool_Head->lo); /* !HACK! check len */
     }
   spool_fake_active_out();
   return line;
}

/* exactly the same as the nntp.c versions, apart from nntp_ -> spool_ */

/* RFC850: The  required  headers  are
 * Relay-Version,  Posting-Version,  From,  Date, Newsgroups,
 * Subject,  Message-ID,  Path. */

#define MAX_XOVER_SIZE 1024
typedef struct
{
   char name[25];
   unsigned int colon;
   char buf[MAX_XOVER_SIZE + 16];
}
Spool_Header_Xover_Type;

/* These MUST be arranged in XOver format:
 * id|subj|from|date|msgid|refs|bytes|line|misc stuff
 *
 * !!!DO NOT REARRANGE OR ADD ANYTHING WITHOUT UPDATING THE ARRAY REFERENCES!!!
 */

#define SPOOL_NUM_XOVER_HEADERS 7

#if SLRN_HAS_NNTPREAD
extern Spool_Header_Xover_Type Spool_Xover_Headers[]; /* defined in nntp.c */
#else
Spool_Header_Xover_Type Spool_Xover_Headers[SPOOL_NUM_XOVER_HEADERS] =
{
     {"Subject:", 8, ""},
     {"From:", 5, ""},
     {"Date:", 5, ""},
     {"Message-ID:", 11, ""},
     {"References:", 11, ""},
     /* {"Bytes:", 6, ""}, */
     {"Lines:", 6, ""},
     {"Xref:", 5, ""}
};
#endif

/* The rest of these are for scoring/killing */
#define MAX_EXTRA_XOVER_HEADERS 20
Spool_Header_Xover_Type *Spool_Extra_Xover_Headers[MAX_EXTRA_XOVER_HEADERS];
static unsigned int Spool_Next_Extra_Xover_Header;

void spool_open_suspend_xover (void)
{
   Spool_Suspend_Xover_For_Kill = 1;
   Spool_Next_Extra_Xover_Header = 0;
}

void spool_close_suspend_xover (void)
{
   unsigned int i;

   if (Spool_Suspend_Xover_For_Kill == 0) return;
   Spool_Suspend_Xover_For_Kill = 0;
   for (i = 0; i < MAX_EXTRA_XOVER_HEADERS; i++)
     {
	if (Spool_Extra_Xover_Headers[i] != NULL)
	  SLFREE (Spool_Extra_Xover_Headers[i]);
	Spool_Extra_Xover_Headers[i] = NULL;
     }
   Spool_Next_Extra_Xover_Header = 0;
}

char *spool_get_extra_xover_header (char *hdr)
{
   unsigned int i;
   unsigned int colon = strlen (hdr) + 1;
   for (i = 0; i < Spool_Next_Extra_Xover_Header; i++)
     {
	Spool_Header_Xover_Type *xov = Spool_Extra_Xover_Headers[i];
	if ((colon == xov->colon)
	    && (!slrn_case_strcmp ((unsigned char *)xov->name,
				   (unsigned char *)hdr)))
	  return xov->buf;
     }
   return NULL;
}

static char *spool_read_create_xover_line (int id, char *the_buf)
{
   char *hbuf, *b;
   char buf[8 * MAX_XOVER_SIZE];
   unsigned int colon = 0;
   char *p, ch;
   unsigned int group_len;
   int i, headers_not_found;
   Spool_Header_Xover_Type *xov = NULL;

   for (i = 0; i < SPOOL_NUM_XOVER_HEADERS; i++)
     {
	*Spool_Xover_Headers[i].buf = 0;
     }
   Spool_Next_Extra_Xover_Header = 0;

   headers_not_found = SPOOL_NUM_XOVER_HEADERS;

   hbuf = NULL;

   while (NULL != spool_read_line (buf, sizeof(buf)))
     {
	unsigned int maxlen;
	ch = *buf;

	if ((ch == ' ') || (ch == '\t'))
	  {
	     if (hbuf == NULL) continue;
	     if (hbuf - xov->buf >= MAX_XOVER_SIZE - 2)
	       {
		  hbuf = NULL;
		  continue;
	       }

	     *hbuf++ = ' ';	       /* one blank to separate */
	     *hbuf = 0;
	     colon = 0;
	     /* Drop to add continuation */
	  }
	else
	  {
	     if (headers_not_found) i = 0;
	     else i = SPOOL_NUM_XOVER_HEADERS;

	     while (i < SPOOL_NUM_XOVER_HEADERS)
	       {
		  if ((ch == *Spool_Xover_Headers[i].name)
		      && !slrn_case_strncmp ((unsigned char *)Spool_Xover_Headers[i].name,
					     (unsigned char *)buf, Spool_Xover_Headers[i].colon))
		    {
		       headers_not_found--;

		       xov = Spool_Xover_Headers + i;
		       colon = xov->colon;
		       hbuf = xov->buf;
		       break;
		    }
		  i++;
	       }

	     if (i == SPOOL_NUM_XOVER_HEADERS)
	       {
		  if ((!Spool_Suspend_Xover_For_Kill)
		      || (Spool_Next_Extra_Xover_Header == MAX_EXTRA_XOVER_HEADERS))
		    {
		       hbuf = NULL;
		       continue;
		    }

		  xov = Spool_Extra_Xover_Headers[Spool_Next_Extra_Xover_Header];
		  if (xov == NULL)
		    {
		       xov = (Spool_Header_Xover_Type *) SLMALLOC (sizeof(Spool_Header_Xover_Type));
		       if (xov == NULL)
			 {
			    hbuf = NULL;
			    continue;
			 }
		       Spool_Extra_Xover_Headers[Spool_Next_Extra_Xover_Header] = xov;
		    }

		  b = buf;
		  while (*b && (*b != ':')) b++;
		  if (*b != ':')
		    {
		       hbuf = NULL;
		       continue;
		    }
		  *b++ = 0;
		  colon = (b - buf);

		  strncpy (xov->name, buf, sizeof (xov->name) - 1);
		  xov->name[sizeof(xov->name) - 1] = 0;
		  xov->colon = colon;

		  Spool_Next_Extra_Xover_Header += 1;
		  hbuf = xov->buf;
	       }
	  }

	/* trim trailing whitespace */
	(void) slrn_trim_string (buf);

	/* trim leading whitespace */
	b = slrn_skip_whitespace (buf + colon);

	maxlen = (MAX_XOVER_SIZE - (unsigned int)(hbuf - xov->buf)) - 1;
	strncpy (hbuf, b, maxlen);
	hbuf[maxlen] = 0;

	while (*hbuf)
	  {
	     if (*hbuf == '\t') *hbuf = ' ';
	     hbuf++;
	  }
     }

   /* we should try to extract the id from the Xref header if possible */
   if (id == -1)
     {
	char *xref = Spool_Xover_Headers[6].buf;
	if (*xref != 0)
	  {
	     group_len = strlen (Slrn_Current_Group_Name);
	     p = xref;
	     while ((ch = *p) != 0)
	       {
		  if (ch == ' ')
		    {
		       p++;
		       continue;
		    }
		  if (!strncmp (p, Slrn_Current_Group_Name, group_len))
		    {
		       p += group_len;
		       if (*p == ':')
			 {
			    p++;
			    id = atoi (p);
			    break;
			 }
		    }
		  /* skip to next space */
		  while (((ch = *p) != 0) && (ch != ' ')) p++;
	       }
	  }
#if 0
	if ((id == -1) && PROBE_XCMD(Has_XPat, "XPAT"))
	  {
	     char xpatbuf[256];
	     char *msgid = Spool_Xover_Headers[3].buf;
	     if (-1 != spool_xpat_cmd ("Message-Id", Slrn_Server_Min, Slrn_Server_Max, msgid))
	       {
		  /* This should only loop once. */
		  while (spool_read_line (xpatbuf, sizeof (xpatbuf) - 1) != NULL)
		    {
		       id = atoi (xpatbuf);
		    }
	       }
	  }
#endif
     }

   /* put in standard XOVER format:
    * id|subj|from|date|msgid|refs|bytes|line|misc stuff
    */

   sprintf (the_buf, "%d\t%s\t%s\t%s\t%s\t%s\t%s\t%s\tXref: %s",
	    id,
	    Spool_Xover_Headers[0].buf,      /* subject */
	    Spool_Xover_Headers[1].buf,      /* from */
	    Spool_Xover_Headers[2].buf,      /* date */
	    Spool_Xover_Headers[3].buf,      /* msgid */
	    Spool_Xover_Headers[4].buf,      /* references */
	    "",			       /* bytes */
	    Spool_Xover_Headers[5].buf,      /* lines */
	    Spool_Xover_Headers[6].buf       /* xref */
	    );
   return the_buf;
}

char *Slrn_Inn_Root;
char *Slrn_Spool_Root;
char *Slrn_Nov_Root;
char *Slrn_Nov_File;
char *Slrn_Active_File;
char *Slrn_ActiveTimes_File;
char *Slrn_Newsgroups_File;

static int spool_init_objects (void)
{
   Spool_Server_Obj.sv_select_group = spool_select_group;
   Spool_Server_Obj.sv_read_line = spool_read_line;
   Spool_Server_Obj.sv_close = spool_close_server;
   Spool_Server_Obj.sv_initialize = spool_initialize_server;
   Spool_Server_Obj.sv_select_article = spool_select_article;
   Spool_Server_Obj.sv_head_from_msgid = spool_head_from_msgid;
   Spool_Server_Obj.sv_read_xover = spool_read_xover;
   Spool_Server_Obj.sv_open_xover = spool_open_xover;
   Spool_Server_Obj.sv_close_xover = spool_close_xover;
   Spool_Server_Obj.sv_put_server_cmd = spool_put_server_cmd;
   Spool_Server_Obj.sv_xpat_cmd = spool_xpat_cmd;
   Spool_Server_Obj.sv_xhdr_command = spool_xhdr_command;
   Spool_Server_Obj.sv_get_extra_xover_header = spool_get_extra_xover_header;
   Spool_Server_Obj.sv_close_suspend_xover = spool_close_suspend_xover;
   Spool_Server_Obj.sv_open_suspend_xover = spool_open_suspend_xover;
   Spool_Server_Obj.sv_xgtitle_cmd = spool_xgtitle_cmd;
   Spool_Server_Obj.sv_has_xover = 0;
   Spool_Server_Obj.sv_has_cmd = spool_has_cmd;
   Spool_Server_Obj.sv_list_newsgroups = spool_list_newsgroups;
   Spool_Server_Obj.sv_list_active = spool_list_active;
   
   Slrn_Inn_Root = slrn_make_startup_string (SLRN_SPOOL_INNROOT);
   Slrn_Spool_Root = slrn_make_startup_string (SLRN_SPOOL_ROOT);
   Slrn_Nov_Root = slrn_make_startup_string (SLRN_SPOOL_NOV_ROOT);
   Slrn_Nov_File = slrn_make_startup_string (SLRN_SPOOL_NOV_FILE);
   Slrn_Active_File = slrn_make_startup_string (SLRN_SPOOL_ACTIVE);
   Slrn_ActiveTimes_File = slrn_make_startup_string (SLRN_SPOOL_ACTIVETIMES);
   Slrn_Newsgroups_File = slrn_make_startup_string (SLRN_SPOOL_NEWSGROUPS);
   
   return 0;
}

/* This function is used below.  It has a very specific purpose. */
static char *spool_root_dircat (char *file)
{
   char *f;

   if (*file == '/') return file;
   f = spool_dircat (Slrn_Inn_Root, file, 0);
   SLFREE (file);
   return f;
}

static int spool_select_server_object (void)
{
   Slrn_Server_Obj = &Spool_Server_Obj;
   Slrn_Active_File = spool_root_dircat (Slrn_Active_File);
   Slrn_ActiveTimes_File = spool_root_dircat (Slrn_ActiveTimes_File);
   Slrn_Newsgroups_File = spool_root_dircat (Slrn_Newsgroups_File);
   
   if (Spool_Server_Obj.sv_name != NULL)
     SLFREE (Spool_Server_Obj.sv_name);
   Spool_Server_Obj.sv_name = slrn_make_startup_string (Slrn_Spool_Root);
   
   return 0;
}


static void spool_usage (void)
{
   fputs ("--spool options:\n\
",
	  stdout);
   exit (0);
}

static int spool_parse_args (char **argv, int argc)
{
   int i;
   
   for (i = 0; i < argc; i++)
     {
	if (!strcmp (argv[i], "--help"))
	  spool_usage ();
	else break;
     }
   
   return i;
}

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