/* babel - News transport agent for STiK
 *
 * group.c - Routines to process a group
 *
 * (c)1996 Mark Baker. Distributable under the terms of the GNU
 *                     general public licence
 *
 * $Id: group.c,v 1.12 1996/10/05 14:16:08 mnb20 Exp $
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include "babel.h"

/* String lengths */
#define MAXLINELENGTH 255
#define MAXTIMELENGTH 18

/* Structure used to store header information */
typedef struct _hdr_entry
{
  struct _hdr_entry *next ;

  char *message_id ;
  char *in_reply_to ;

  int offset ;

  struct _hdr_flags
  {
    int new:1 ;         /* N Message unread (set on new messages) */
    int replied:1 ;     /* R User has replied (babel doesn't touch) */
    int deleted:1 ;     /* D Message deleted (superseded messages) */
    int keep:1 ;        /* K Never delete message */
    int outgoing:1 ;    /* O User created message (babel sends it) */
    int header_only:1 ; /* H Message is header only */
    int requested:1 ;   /* Q Body should be retrieved */
  } flags ;
} hdr_entry ;

/* Pointer to first header entry */
hdr_entry *headers ;

/* Pointer to first header of as-yet unfetched message */
hdr_entry **new_headers ;
hdr_entry **next ;

/*
 * do_group() does everything necessary for processing a group.
 */

void do_group( char *group, char *filename, int headers_only, char *date )
{
  char command[MAXLINELENGTH] ;
  FILE *messages, *tempfile ;

  hdr_entry *header, *new_header ;
  char inputline[MAXLINELENGTH] ;

  /* Read group status line */
  sprintf( command, strings.readgroup, group ) ;
  status_line( command ) ;

  /* Read header file into memory */
  read_header( filename ) ;

  /* Get a list of new articles */
  new_articles( group, date ) ;

  /* Open .msg file */
  messages = open_messages( filename ) ;

  /* Now fetch the text of all those that aren't dupes */
  read_article_text( messages, headers_only ) ;
 
  /* Request articles in headers-only groups */
  get_requested_articles( messages, headers_only ) ;

  /* Sending group status line */
  sprintf( command, strings.sendgroup, group ) ;
  status_line( command ) ;

  /* Now send messages */
  send_messages( messages, group ) ;

  /* Close files */
  close_messages( messages, tempfile, filename ) ;

  write_header( filename ) ;
  free_header() ;
}

/*
 * read_header() reads in a header file and stores the data in
 *    memory.
 */

void read_header( char *filename )
{
  FILE *hdr ;
  char line[MAXLINELENGTH] ;
  char *token ;
  char *id ;
  hdr_entry *header ;

  /* Initialise to first header */
  next = &headers ;
  
  /* Open file to read from */
  hdr = open_group_header( filename, "r" ) ;

  /* Exit now if file doesn't exist */
  if( hdr == NULL )
    {
      new_headers = next ;
      return ;
    }

  while( fgets( line, MAXLINELENGTH, hdr ), !feof( hdr ) )
    {
      /* Read message id */
      token = strtok( line, " \t\n" ) ;

      /* Check for blank line - shouldn't happen but you never know */
      if( token == NULL )
	continue ;

      /* Malloc space for header */
      *next = (hdr_entry *) chk_malloc( sizeof ( hdr_entry ) ) ;
      header = *next ;
      next = &(header->next) ;
      *next = NULL ;

      /* Store message id in a dynamically allocated string */
      id = chk_malloc( strlen( token ) + 1 ) ;
      strcpy( id, token ) ;
      header->message_id = id ;

      /* Read and store offset */
      token = strtok( NULL, " \t\n" ) ;
      header->offset = atoi( token ) ;

      /* Clear flags */
      header->flags.new = 0 ;
      header->flags.replied = 0 ;
      header->flags.deleted = 0 ;
      header->flags.keep = 0 ;
      header->flags.outgoing = 0 ;
      header->flags.header_only = 0 ;
      header->flags.requested = 0 ;

      /* Read flags */
      token = strtok( NULL, " \t\n" ) ;

      /* If it's null, there's no flags and no message ID either */
      if( token == NULL )
	{
	  header->in_reply_to = NULL ;
	  continue ;
	}
	  
      /* If no flags, first character will be a message ID */
      if( token[0] != '<' )
	{
	  /* loop through flags string */
	  while( *token != '\0' ) 
	    {
	      switch( *token++ )
		{
		case 'n' :
		case 'N' :
		  header->flags.new = 1 ;
		  break ;
		case 'r' :
		case 'R' :
		  header->flags.replied = 1 ;
		  break ;
		case 'd' :
		case 'D' :
		  header->flags.deleted = 1 ;
		  break ;
		case 'k' :
		case 'K' :
		  header->flags.keep = 1 ;
		  break ;
		case 'o' :
		case 'O' :
		  header->flags.outgoing = 1 ;
		  break ;
		case 'h' :
		case 'H' :
		  header->flags.header_only = 1 ;
		  break ;
		case 'q' :
		case 'Q' :
		  header->flags.requested = 1 ;
		  break ;
		}
	    }

	  /* Read reply ID */
	  token = strtok( NULL, " \t\n" ) ;
	}

      /* If there is a reply ID */
      if( token != NULL )
	{
	  /* Store it in a dynamically allocated string */
	  id = chk_malloc( strlen( token ) + 1 ) ;
	  strcpy( id, token ) ;
	  header->in_reply_to = id ;
	}
      else
	header->in_reply_to = NULL ;
    }

  new_headers = next ;

  fclose( hdr ) ;
}

/*
 * write_header() writes a header back to disc
 */

void write_header( char *filename )
{
  FILE *hdr ;
  hdr_entry *header ;

  /* Open header for writing */
  hdr = open_group_header( filename, "w" ) ;

  /* If there are any headers to save */
  if( headers != NULL )
    {
      header = headers ;

      do
	{
	  /* Write message ID and offset */
	  fprintf( hdr, "%s %d ", header->message_id, header->offset ) ;
	  
	  /* Write flags, or - where a flag is not set */
	  fputc( header->flags.new ? 'N' : '-', hdr ) ;
	  fputc( header->flags.replied ? 'R' : '-', hdr ) ;
	  fputc( header->flags.deleted ? 'D' : '-', hdr ) ;
	  fputc( header->flags.keep ? 'K' : '-', hdr ) ;
	  fputc( header->flags.outgoing ? 'O' : '-', hdr ) ;
	  fputc( header->flags.header_only ? 'H' : '-', hdr ) ;
	  fputc( header->flags.requested ? 'Q' : '-', hdr ) ;
	  
	  /* Write in-reply-to ID if there is one */
	  if( header->in_reply_to != NULL )
	    fprintf( hdr, " %s\n", header->in_reply_to ) ;
	  else
	    fputc( '\n', hdr ) ;
	}
      while( ( header = header->next ) != NULL ) ;
    }

  fclose( hdr ) ;
}

/*
 * free_header() removes the header information, in particularly the
 *    message IDs which are dynamically allocated, from memory
 */

void free_header( void )
{
  hdr_entry *header ;
  hdr_entry *next ;

  if( headers != NULL )
    {
      header = headers ;

      do
	{
	  next = header->next ;

	  free( header->message_id ) ;
	  if( header->in_reply_to != NULL )
	    free( header->in_reply_to ) ;

	  free( header ) ;
	}
      while( ( header = next ) != NULL ) ;
    }

  headers = NULL ;
}

/*
 * new_articles() reads in new article IDs. It returns a pointer to the
 *     first.
 */

void new_articles( char *group, char *date )
{
  char command[MAXLINELENGTH] ;
  char inputline[MAXLINELENGTH] ;
  int isdup ;
  hdr_entry *header ;

  sprintf( command, "newnews %s %s\n", group, date ) ;
  write_string( command ) ;
  
  /* Get 230 reply */
  read_line( inputline, MAXLINELENGTH ) ;
  inputline[3] = '\0' ;
  
  if( strcmp( inputline, "230" ) )
    {
      alert( strings.unexpected ) ;
      byebye(1) ;
    }
  
  while( 1 )
    {
      /* Read a message id from socket */
      read_line( inputline, MAXLINELENGTH ) ;
      
      /* . terminates list */
      if( inputline[0] == '.' )
	break ;
      
      isdup = 0 ;
      
      /* Search through existing messages */
      header = headers ;
      while( header != NULL )
	{
	  /* If ID matches then it's a dupe */
	  if( !strcmp( inputline, header->message_id ) )
	    isdup = 1 ; 
	  header = header->next ;
	}

      /* Store header if it isn't a dupe */
      if( !isdup )
	{
	  /* Malloc space for header */
	  *next = (hdr_entry *) chk_malloc( sizeof ( hdr_entry ) ) ;
	  header = *next ;
	  next = &(header->next) ;
	  *next = NULL ;

	  /* Store message ID */
	  header->message_id = chk_malloc( strlen( inputline ) + 1 ) ;
	  strcpy( header->message_id, inputline ) ;
	}
    }
}

/*
 * read_article_text() reads in the text for new articles
 */

void read_article_text( FILE *messages, int headers_only )
{
  char command[MAXLINELENGTH] ;
  char inputline[MAXLINELENGTH] ;
  int inheader ;
  hdr_entry *header ;
  char *token, *lasttoken ;

  while( *new_headers != NULL )
    {
      /* next message */
      header = *new_headers ;
      
      /* Store offset into messages file of new message */
      header->offset = ftell( messages ) ;
      
      /* Set flags */
      header->flags.new = 1 ;
      header->flags.replied = 0 ;
      header->flags.deleted = 0 ;
      header->flags.keep = 0 ;
      header->flags.outgoing = 0 ;
      header->flags.header_only = headers_only ? 1 : 0 ;
      header->flags.requested = 0 ;
      
      /* No reply to ID known yet */
      header->in_reply_to = NULL ;
      
      /* Request article */
      if( headers_only )
	sprintf( command, "head %s\n", header->message_id ) ;
      else
	sprintf( command, "article %s\n", header->message_id ) ;
      
      write_string( command ) ;
      
      /* Get 220 or 221 reply */
      read_line( inputline, MAXLINELENGTH ) ;
      inputline[3] = '\0' ;
      if( !strcmp( inputline, "430" ) )
	{
	  /* No such article... weird. Oh well */
	  /* Unallocate and unlink this rogue header */
	  *new_headers = header->next ;
	  free( header->message_id ) ;
	  free( header ) ;
	  continue ;
	}
      if( inputline[0] == '5' )
	{
	  /* Some kind of error... probably 502, permission denied */
	  /* Write a dummy message */
	  fprintf( messages, "From: Babel\n" ) ;
	  fprintf( messages, "Subject: %s %s\n", inputline, inputline + 4 ) ;
	  fprintf( messages, "Msg-Id: %s\n\n", header->message_id ) ;
	  fprintf( messages, ".\n" ) ;
	}
      if( strcmp( inputline, "220" ) && strcmp( inputline, "221" ) )
	{
	  alert( strings.unexpected ) ;
	  byebye(1) ;
	}

      /* Flag to keep track of whether we're receiving a header */
      inheader = 1 ;

      while( 1 )
	{
	  /* Read a line of the message from socket */
	  read_line( inputline, MAXLINELENGTH ) ;

	  /* And write it to the file */
	  fprintf( messages, "%s\n", inputline ) ;

	  /* blank line marks end of header */
	  if( inputline[0] == '\0' )
	    inheader = 0 ;

	  /* . on it's own terminates message */
	  if( !strcmp( inputline, "." ) )
	    break ;

	  if( inheader )
	    {
	      /* If it's a references header */
	      if( !strcmp( "References:", strtok( inputline, " \t\n" ) ) )
		{
		  /* Step through words until none left */
		  while( token = strtok( NULL, " \t\n" ) )
		    lasttoken = token ;

		  /* Now lasttoken points to last word on line - which
		     is the message id this is in reply to */
		  header->in_reply_to = chk_malloc( strlen( lasttoken ) + 1 ) ;
		  strcpy( header->in_reply_to, lasttoken ) ;
		}
	    }
	}
      /* Update new headers pointer */
      new_headers = &(header->next) ;
    }
}

/*
 * get_requested_articles() requests the full text of headers only
 *    articles that have been requested.
 */

void get_requested_articles( FILE *messages, int headers_only ) 
{
  char command[MAXLINELENGTH] ;
  char inputline[MAXLINELENGTH] ;
  hdr_entry *header, *new_header ;

  if( headers_only && headers != NULL )
    {
      header = headers ;

      /* Loop through all messages */
      do
	{
	  /* If they're requested */
	  if( header->flags.requested && !header->flags.deleted )
	    {
	      /* Requesting message */
	      sprintf( command, strings.request, header->message_id ) ;
	      status_line( command ) ;

	      /* Mark header-only message as deleted, and make sure
		 it isn't requested again */
	      header->flags.deleted = 1 ;
	      header->flags.requested = 0 ;

	      /* Malloc space for header for complete message */
	      *next = (hdr_entry *) chk_malloc( sizeof ( hdr_entry ) ) ;
	      new_header = *next ;

	      /* Store message ID */
	      new_header->message_id = chk_malloc( strlen( header->message_id )
						   + 1 ) ;
	      strcpy( new_header->message_id, header->message_id ) ;

	      /* I can't be bothered to look for reply to ID - maybe
		 I'll add it later. It's not terribly useful IMO */
	      new_header->in_reply_to = NULL ;

	      /* Store offset into messages file of new message */
	      new_header->offset = ftell( messages ) ;

	      /* Set flags */
	      new_header->flags.new = 1 ;
	      new_header->flags.replied = 0 ;
	      new_header->flags.deleted = 0 ;
	      new_header->flags.keep = 0 ;
	      new_header->flags.outgoing = 0 ;
	      new_header->flags.header_only = 0 ;
	      new_header->flags.requested = 0 ;

	      sprintf( command, "article %s\n", header->message_id ) ;
	      write_string( command ) ;

	      /* Get 220 reply */
	      read_line( inputline, MAXLINELENGTH ) ;
	      inputline[3] = '\0' ;
	      if( !strcmp( inputline, "430" ) )
		{
		  /* Message doesn't exist. Not so weird here I guess */
		  /* Free and unlink header */
		  free( new_header->message_id ) ;
		  free( new_header ) ;
		  *next = NULL ;

		  /* Maybe we'd better not delete the header? */
		  header->flags.deleted = 0 ;

		  continue ;
		}
	      if( strcmp( inputline, "220" ) )
		{
		  alert( strings.unexpected ) ;
		  byebye(1) ;
		}
	      
	      while( 1 )
		{
		  /* Read a line of the message from socket */
		  read_line( inputline, MAXLINELENGTH ) ;
		  
		  /* And write it to the file */
		  fprintf( messages, "%s\n", inputline ) ;
		  
		  /* . on it's own terminates message */
		  if( !strcmp( inputline, "." ) )
		    break ;
		  
		}
	      /* Update next message pointer */
	      next = &(new_header->next) ;
	      *next = NULL ;
	    }
	}
      while( ( header = header->next ) != NULL ) ;
    }
}

/*
 * send_messages() posts unsent articles
 */

void send_messages( FILE *messages, char *group )
{
  char inputline[MAXLINELENGTH] ;
  hdr_entry *header ;

  if( headers != NULL )
    {
      header = headers ;

      /* Loop through all messages */
      do
	{
	  /* If they're outgoing messages */
	  if( header->flags.outgoing )
	    {
	      /* Don't send it again! */
	      header->flags.outgoing = 0 ;

	      write_string( "post\n" ) ;

	      /* Get 340 reply */
	      read_line( inputline, MAXLINELENGTH ) ;
	      inputline[3] = '\0' ;
	      if( !strcmp( inputline, "440" ) )
		{
		  /* No posting allowed! Skip all posting */
		  break ;
		}
	      if( strcmp( inputline, "340" ) )
		{
		  alert( strings.unexpected ) ;
		  byebye(1) ;
		}

	      /* Find start of message */
	      if( fseek( messages, header->offset, SEEK_SET ) )
		{
		  alert( strings.poserror ) ;
		  byebye(1) ;
		}
	      do
		{
		  /* Read a line of message */
		  fgets( inputline, MAXLINELENGTH, messages ) ;

		  /* Squirt it down socket */
		  write_string( inputline ) ;

		  /* This shouldn't happen... */
		  if( feof( messages ) )
		    {
		      write_string( ".\n" ) ;
		      break ;
		    }
		}
	      /* Until we get . on it's own */
	      while( strcmp( inputline, ".\n" ) ) ;

	      /* Get 240 reply */
	      read_line( inputline, MAXLINELENGTH ) ;
	      inputline[3] = '\0' ;
	      if( strcmp( inputline, "240" ) )
		{
		  alert( strings.unexpected ) ;
		  byebye(1) ;
		}
	    }
	}
      while( ( header = header->next ) != NULL ) ;
    }
}

/*
 * chk_malloc() does a malloc() and aborts if there is an error
 */

void *chk_malloc( size_t size )
{
  void *ptr ;

  ptr = malloc( size );

  if( ptr == 0 )
    {
      alert( strings.memory ) ;
      byebye(1) ;
    }

  return ptr ;
}
