/*
 * slurp - a passive nntp news client
 *
 * Copyright (C) 1992/93 Stephen Hebditch. All rights reserved.
 * TQM Communications, BCM Box 225, London, WC1N 3XX.
 * steveh@orbital.demon.co.uk  +44 836 825962
 *
 * See README for more information and disclaimers
 *
 * This is the main routine for slurp together with the routines to
 * handle the configuration files and command line arguments.
 *
 *
 * $Id: slurp.c,v 1.5 1993/03/01 17:51:33 root Exp $
 *
 * $Log: slurp.c,v $
 * Revision 1.5  1993/03/01  17:51:33  root
 * read_sys can cope with lines longer than BUFSIZ.
 * report when attempting to load unretrieved message ids.
 * Move sublist to a parameter after the hostname, separate by a slash.
 * Changed some system error checking.
 *
 * Revision 1.4  1993/02/14  14:57:43  root
 * Added support for simple authorisation protocol.
 * Added support for INN's 'MODE READER' command.
 * Re-arranged command line options.
 * Rewrote read_sys and added flags and authorisation options to it.
 * Rewrote get_ntime and set_ntime to use a filename of slurp.<hostname>
 * instead of slurp.tim, solving lack of locking and allowing the file
 * to contain a list of unretrieved message ids on the lines following
 * the time.
 * Don't care if slurp.<hostname> doesn't exist already.
 * If RNEWS is not defined, then change to INDIR for writing out batch
 * files.
 *
 * Revision 1.3  1992/12/15
 * Open syslog *before* we start doing things that might write to it.
 * Informational messages logged as LOG_INFO.
 * Assorted minor tidy-ups.
 *
 * Revision 1.2  1992/12/07
 * Corrected test for 4.2/4.3 BSD syslog open.
 *
 * Revision 1.1  1992/12/06
 * Made no_time_flag global.
 * Fixed null dereferencing of nn_distributions.
 *
 * Revision 1.0  1992/08/07
 * Initial coding.
 *
 */

#include "slurp.h"
#include <sys/stat.h>

char *hostname = NULL;
char *pname;

int debug_flag = FALSE;
int no_time_flag = FALSE;
int no_id_load_flag = FALSE;
static int local_time_flag = FALSE;
static int mode_reader_flag = FALSE;

int  dupart = 0;
int  misart = 0;
int  newart = 0;
long totalsize = 0;

char *nn_newsgroups    = NULL;
char *nn_time          = NULL;
char *nn_distributions = NULL;

static char *sublist = NULL;
static char *ai_username = NULL;
static char *ai_password = NULL;

struct mnode *root = NULL;
int entries = 0;

static long newdate, newtime;


/*
 * test_time - Check NEWNEWS time string is in the right format (ish)
 */

	static int
test_time ()
	{
	return (!(isdigit (nn_time [0]) &&
	          isdigit (nn_time [1]) &&
	          isdigit (nn_time [2]) &&
	          isdigit (nn_time [3]) &&
	          isdigit (nn_time [4]) &&
	          isdigit (nn_time [5]) &&
	          isspace (nn_time [6]) &&
	          isdigit (nn_time [7]) &&
	          isdigit (nn_time [8]) &&
	          isdigit (nn_time [9]) &&
	          isdigit (nn_time [10]) &&
	          isdigit (nn_time [11]) &&
	          isdigit (nn_time [12])));
	}


/*
 * parse_args - Parse the command line arguments. Returns 1 if there is
 * an error, otherwise returns 0.
 */

	static int
parse_args (int argc, char **argv)
	{
	int c;
	extern int optind;
	extern char *optarg;
	char *pos;
	
	while ((c = getopt (argc, argv, "a:g:t:dilrw")) != EOF)
		switch (c)
			{
			case 'a':	/* Do an authinfo */
				if (pos = strchr (optarg, '/'))
					{
					ai_username = optarg;
					*pos++ = '\0';
					ai_password = pos;
					}
				else
					{
					(void) fprintf (stderr, "Invalid authinfo username/password");
					return (1);
					}
				break;
			case 'g':	/* Newsgroups list */
				if (pos = strchr (optarg, '/'))
					{
					*pos++ = '\0';
					nn_distributions = pos;
					}
				else
					nn_distributions = "";
				nn_newsgroups = optarg;
				no_time_flag++;
				break;
			case 't':	/* Start time */
				nn_time = optarg;
				break;
			case 'd':	/* Debugging on */
				debug_flag++;
				break;
			case 'i':	/* Don't load unprocessed ids */
				no_id_load_flag++;
				break;
			case 'l':	/* Use local time */
				local_time_flag++;
				break;
			case 'r':	/* Do a 'MODE READER' */
				mode_reader_flag++;
				break;
			case 'w':	/* Don't set next time */
				no_time_flag++;
				break;
			default:
				return (1);
			}

	/* Get server name */
	if (optind < argc)
		{
		hostname = argv [optind];
		if (pos = strchr (hostname, '/'))
			{
			*pos++ = '\0';
			sublist = pos;
			}
		}
	else
		{
		(void) fprintf (stderr, "No server name supplied\n");
		return (1);
		}

	/* If groups are specified, then must have a time */
	if ((nn_newsgroups != NULL) && (nn_time == NULL))
		{
		(void) fprintf (stderr, "Time must be specified for -g option\n");
		return (1);
		}

	/* Verify that the time is in something like the right format */
	if (nn_time)
		if (test_time ())
			{
			(void) fprintf (stderr, "Invalid time specification - should be 'YYMMDD HHMMSS'\n");
			return (1);
			}

	return (0);
	}


/*
 * read_sys_line - Read a line from the slurp.sys file, skipping lines
 * which are blank or all comments, truncating lines at comments.
 * If the line has not yet all been read or the continued-on-next-line
 * token '\' is present, then returns 1; if eof then returns -1, otherwise
 * returns 0.
 */

	static int
read_sys_line (char *line, int size, FILE *sysfp)
	{
	int status;
	char *pos;

	for (;;)
		{
		status = 0;

		(void) fgets (line, size, sysfp);
		if (feof (sysfp))
			return (-1);
		if (ferror (sysfp))
			log_sys ("read_sys: Error reading %s", SYSFILE);

		if (pos = strchr (line, '\n'))
			*pos = '\0';
		else
			status = 1;

		if (pos = strchr (line, '\\'))
			{
			*pos = '\0';
			status = 1;
			}

		if (pos = strchr (line, '#'))
			*pos = '\0';

		if (strlen (line))
			return (status);
		}
	}


/*
 * read_sys - Read in the appropriate entry from the slurp.sys file
 * for the specified hostname. Stores the relevant newsgroups for that
 * host in nn_newsgroups and the relevant distribution in nn_distributions.
 * Returns 0 if an appropriate entry for the current host is found, 
 * otherwise returns 1.
 */

	static int
read_sys ()
	{
	FILE *sysfp;
	char buf [BUFSIZ];
	char searchname [BUFSIZ];
	struct stat stbuf;
	size_t tlen;
	char *pos;
	char *mbuf;
	int status;
	int object;

	/* Attempt to open the sys file */
	if ((sysfp = fopen (SYSFILE, "r")) == NULL)
		log_sys ("read_sys: Error opening %s", SYSFILE);

	/* Allocate memory block for storing the details */
	if (fstat (fileno (sysfp), &stbuf) < 0)
		log_sys ("read_sys: Can't fstat %s", SYSFILE);
	if (stbuf.st_size == 0)
		{
		log_msg ("read_sys: Host %s not found in %s", hostname, SYSFILE);
		return (1);
		}
	if ((mbuf = (char *) malloc (stbuf.st_size)) == NULL)
		log_sys ("read_sys: malloc %d bytes", stbuf.st_size);
	
	/* Create pattern to search for in the sys file */
	(void) strcpy (searchname, hostname);
	if (sublist)
		{
		(void) strcat (searchname, "/");
		(void) strcat (searchname, sublist);
		}
	(void) strcat (searchname, ":");
	tlen = strlen (searchname);

	/* Read in file until we find hostname */
	for (;;)
		{
		if ((status = read_sys_line (buf, sizeof (buf), sysfp)) == -1)
			{
			log_msg ("read_sys: Host %s not found in %s",
					 hostname, SYSFILE);
			return (1);
			}
		if (strncmp (buf, searchname, tlen) == 0)
			break;
		}

	/* Strip off hostname stuff from front of line */
	(void) strcpy (buf, buf + tlen);
	object = 1;
	nn_newsgroups = mbuf;

	/* Loop through entry */
	for (;;)
		{
		/* Currently adding newsgroups */
		if (object == 1)
			{
			if (pos = strchr (buf, ':'))		/* Flags next */
				{
				*pos++ = '\0';
				object = 3;
				(void) strcpy (mbuf, buf);
				mbuf += strlen (buf) + 1;
				ai_username = mbuf;
				(void) strcpy (buf, pos);
				}
			else if (pos = strchr (buf, '/'))	/* Distributions next */
				{
				*pos++ = '\0';
				object = 2;
				(void) strcpy (mbuf, buf);
				mbuf += strlen (buf) + 1;
				nn_distributions = mbuf;
				(void) strcpy (buf, pos);
				}
			else
				{
				(void) strcpy (mbuf, buf);		/* Nothing else this line */
				mbuf += strlen (buf);
				}
			}

		/* Currently adding distributions */
		if (object == 2)
			{
			if (pos = strchr (buf, ':'))		/* Flags next */
				{
				*pos++ = '\0';
				object = 3;
				(void) strcpy (mbuf, buf);
				mbuf += strlen (buf) + 1;
				ai_username = mbuf;
				(void) strcpy (buf, pos);
				}
			else								/* Nothing else this line */
				{
				(void) strcpy (mbuf, buf);
				mbuf += strlen (buf);
				}
			}

		/* Currently setting flags */
		if (object == 3)
			{
			if (pos = strchr (buf, ':'))		/* authinfo user next */
				*pos++ = '\0';
			if (strchr (buf, 'i'))
				no_id_load_flag++;
			if (strchr (buf, 'l'))
				local_time_flag++;
			if (strchr (buf, 'r'))
				mode_reader_flag++;
			if (pos)
				{
				(void) strcpy (buf, pos);
				object = 4;
				}
			}

		/* Currently setting username */
		if (object == 4)
			{
			if (pos = strchr (buf, '/'))		/* authinfo pass next */
				{
				*pos++ = '\0';
				object = 5;
				(void) strcpy (mbuf, buf);
				mbuf += strlen (buf) + 1;
				ai_password = mbuf;
				(void) strcpy (buf, pos);
				}
			else
				{
				(void) strcpy (mbuf, buf);
				break;
				}
			}

		/* Currently setting password */
		if (object == 5)
			{
			(void) strcpy (mbuf, buf);
			mbuf += strlen (buf) +1;
			}

		if (status != 1)
			break;

		status = read_sys_line (buf, sizeof (buf), sysfp);
		}

	(void) fclose (sysfp);

	if (nn_distributions == NULL)
		nn_distributions = "";

	return (0);
	}


/*
 * get_ntime - Get the start time for this NEWNEWS for system. Returns 0
 * if an appropriate entry for the current host is found, otherwise 1.
 */

	static int
get_ntime ()
	{
	FILE *timefp;
	char buf [BUFSIZ];
	char filename [PATH_MAX];
	char *pos;

	/* Attempt to open the time file */
	(void) strcpy (filename, TIMFILE);
	(void) strcat (filename, hostname);
	if (sublist)
		{
		(void) strcat (filename, ".");
		(void) strcat (filename, sublist);
		}
	if ((timefp = fopen (filename, "r")) == NULL)
		log_sys ("get_ntime: error opening %s", filename);

	/* Read in the time and store it */
	nn_time = (char *) malloc (14);
	(void) fgets (nn_time, 14, timefp);
	if (ferror (timefp))
		log_sys ("get_ntime: Error reading %s", filename);
	
	/* Return if time doesn't look ok */
	if (test_time ())
		return (1);

	/* Load in any message ids following */
	if (!no_id_load_flag)
		{
		if (debug_flag)
			(void) fprintf (stderr, "Loading any unretrieved message IDs\n");
		for (;;)
			{
			(void) fgets (buf, sizeof (buf), timefp);
			if (feof (timefp))
				break;
			if (ferror (timefp))
				log_sys ("get_ntime: Error reading %s", filename);
			if (pos = strchr (buf, '\n'))
				*pos = '\0';
			if (strlen (buf))
				if ((buf [0] == '<') && (buf [strlen (buf) - 1] == '>'))
					process_id (buf);
			}
		}

	(void) fclose (timefp);
	return (0);
	}


/*
 * write_tree - Traverse the tree writing out ids of articles that were
 * not successfully retrieved.
 */

	static void
write_tree (struct mnode *p, FILE *timefp)
	{
	if (p != NULL)	
    	{
		write_tree (p->left, timefp);
		if (!p->used)
			{
			(void) fprintf (timefp, "%s\n", p->msgid);
			if (ferror (timefp))
				log_sys ("write_tree: Error writing ids");
			}
		write_tree (p->right, timefp);
    	}
	}


/*
 * set_ntime - Set the start time for the next NEWNEWS for system
 */

	void
set_ntime ()
	{
	FILE *timefp;
	char filename [PATH_MAX];
	char backup [PATH_MAX];

	/* Copy the file to a backup */
	(void) strcpy (filename, TIMFILE);
	(void) strcat (filename, hostname);
	if (sublist)
		{
		(void) strcat (filename, ".");
		(void) strcat (filename, sublist);
		}
	(void) strcpy (backup, filename);
	(void) strcat (backup, ".o");
	if (rename (filename, backup))
		if (errno != ENOENT)
			{
			log_ret ("set_ntime: Error renaming %s to %s", filename, backup);
			exit (1);
			}

	/* Open new file */
	if ((timefp = fopen (filename, "w")) == NULL)
		{
		log_ret ("get_ntime: Error opening %s", filename);
		exit (1);
		}

	/* Write the new time for current host */
	(void) fprintf (timefp, "%06ld %06ld\n", newdate, newtime);
	if (ferror (timefp))
		{
		log_ret ("set_ntime: Error writing %s", filename);
		exit (1);
		}

	/* Write out any message ids not read in */
	write_tree (root, timefp);

	(void) fclose (timefp);
	}


/*
 * do_authinfo - Check in the authinfo username and password with the
 * server.
 */

	static void
do_authinfo ()
	{
	char buf [NNTP_STRLEN];

	/* Send the username to the server */
	(void) sprintf (buf, "AUTHINFO USER %s", ai_username);
	if (debug_flag)
		(void) fprintf (stderr, "<- %s\n", buf);
	put_server (buf);

	/* Get the response and check it's okay */
	get_server (buf, sizeof (buf));
	if (debug_flag)
		(void) fprintf (stderr, "-> %s\n", buf);
	if (atoi (buf) != NEED_AUTHDATA)
		{
		log_msg ("do_authinfo: NNTP protocol error: got '%s'", buf);
		exit (4);
		}
	                
	/* Send the password to the server */
	(void) sprintf (buf, "AUTHINFO PASS %s", ai_password);
	if (debug_flag)
		(void) fprintf (stderr, "<- %s\n", buf);
	put_server (buf);

	/* Get the response and check it's okay */
	get_server (buf, sizeof (buf));
	if (debug_flag)
		(void) fprintf (stderr, "-> %s\n", buf);
	if (atoi (buf) != OK_AUTH)
		{
		log_msg ("do_authinfo: NNTP protocol error: got '%s'", buf);
		exit (4);
		}
	}


/*
 * do_mode_reader - Send mode reader command to INN to switch to nnrpd
 * so we can do a NEWNEWS.
 */

	static void
do_mode_reader ()
	{
	char buf [NNTP_STRLEN];

	/* Send the command to the server */
	if (debug_flag)
		(void) fprintf (stderr, "<- MODE reader\n");
	put_server ("MODE READER");

	/* Get the response and check it's okay */
	get_server (buf, sizeof (buf));
	if (debug_flag)
		(void) fprintf (stderr, "-> %s\n", buf);
	switch (atoi (buf))
		{
		case OK_CANPOST :
		case OK_NOPOST :
			break;
		default :
			log_msg ("do_authinfo: NNTP protocol error: got '%s'", buf);
			exit (4);
		}
	}


/*
 * MAIN PROCEDURE
 */

	int
main (int argc, char **argv)
	{
    int ret;
	time_t clock, starttime, endtime;
	struct tm *now;

	/* Set the name of the program and parse the args */
	pname = (pname = (char *) strrchr (argv [0], '/')) ? pname + 1 : argv [0];
	if (parse_args (argc, argv))
		{
		(void) fprintf (stderr, "Usage: %s [-g newsgroups/distribution] [-t time] [-a username/password]\n", pname);
		(void) fprintf (stderr, "       [-d] [-i] [-l] [-r] [-w] server[/sublist]\n");
		exit (2);
		}

	/* Open syslog if required with appropriate BSD 4.2/4.3 call */
#ifdef SYSLOG
#ifdef LOG_AUTH
	openlog(pname, LOG_PID, SYSLOG);
#else
	openlog(pname, LOG_PID);
#endif
#endif

	/* If groups not supplied in args, then get from slurp.sys file */
	if (nn_newsgroups == NULL)
		if (read_sys ())
			exit (2);

	/* If start time not supplied in args, then get from slurp.tim file */
	if (nn_time == NULL)
		if (get_ntime ())
			exit (2);

	if (debug_flag)
		{
		(void) fprintf (stderr, "server: %s\n", hostname);
		(void) fprintf (stderr, "time: %s\n", nn_time);
		(void) fprintf (stderr, "newsgroups: '%s'\n", nn_newsgroups);
		(void) fprintf (stderr, "distributions: '%s'\n", nn_distributions);
		}

	/* Unless don't write flag set, get time for next NEWNEWS */
	if (!no_time_flag)
		{
		if (local_time_flag)
			clock = time ((time_t *) 0);
		else
			if ((clock = server_time (hostname)) == 0)
				exit (3);

		now = gmtime (&clock);
		newdate = (now->tm_year * 10000) +
				 ((now->tm_mon + 1) * 100) +
				   now->tm_mday;
		newtime = (now->tm_hour * 10000) +
				  (now->tm_min * 100) +
				   now->tm_sec;
		}

	/* Open the history file */
	if (open_history ())
		log_sys ("Can't open history file %s", HISTORY_FILE);

#ifndef RNEWS
	/* Change to the incoming batch directory */
	if (chdir (INDIR))
		log_sys ("Can't change directory to %s", INDIR);
#endif

	/* Set up the connection to the server */
	switch (ret = server_init (hostname))
		{
		case -1 :
			exit (3);
		case OK_CANPOST :
		case OK_NOPOST :
			break;
		default :
			log_msg ("Can't talk to %s: got response code %d", hostname, ret);
			exit (4);
		}

	/* If authinfo details supplied, then use 'em */
	if (ai_username)
		do_authinfo ();

	/* Switch INN to nnrpd instead of innd if needed */
	if (mode_reader_flag)
		do_mode_reader ();

	/* Get a list of the new articles */
	get_ids ();

	/* Now get the actual articles */
	starttime = time ((time_t *) 0);
	if (entries > 0)
		get_articles ();
	endtime = time ((time_t *) 0);

	/* Time to say goodbye */
	close_server ();
	close_history ();

	/* Submit the remaining batch, if present */
	enqueue_batch ();

	/* do we want to update the timestamp file? */
	if (!no_time_flag)
		set_ntime ();

#ifdef SYSLOG
	if (!debug_flag)
		syslog (LOG_INFO,"Processed %d new, %d duplicate, %d missing articles",
				newart, dupart, misart);
	else
#endif
		(void) fprintf (stderr, "Processed %d new, %d duplicate, %d missing articles\n",
						newart, dupart, misart);

#ifdef SPEEDSTATS
  #ifdef SYSLOG
	if (!debug_flag)
		syslog (LOG_INFO, "Average transfer speed %ld cps",
				totalsize / (starttime == endtime ? 1 : endtime - starttime));
	else
  #endif
		(void) fprintf (stderr, "Average transfer speed %ld cps\n",
				totalsize / (starttime == endtime ? 1 : endtime - starttime));
#endif

	exit (0);
	}

/* END-OF-FILE */
