/*
  post.c

  $Id: post.c 316 2001-10-31 11:44:53Z bears $
*/

#if HAVE_CONFIG_H
#include <config.h>
#endif

#include <errno.h>
#include <pwd.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include "common.h"
#include "configfile.h"
#include "content.h"
#include "control.h"
#include "database.h"
#include "group.h"
#include "itemlist.h"
#include "log.h"
#include "outgoing.h"
#include "over.h"
#include "portable.h"
#include "post.h"
#include "protocol.h"
#include "util.h"

#define	BEGIN_SIG	"-- "
#define	SIG_FILE	"/.signature"

struct OverInfo
{
    Str subject;
    Str from;
    Str date;
    Str msgId;
    Str ref;
    size_t bytes;
    size_t lines;
};

struct Article
{
    DynStr *text;	   /* Processed article text */
    ItemList *newsgroups;  /* Newsgroups for dispatch */
    ItemList *control;	   /* Control message? NULL if not */
    Bool approved;	   /* Has Approved: header? */
    Bool posted;	   /* Has it been put in the article database? */
    int flags;             /* Posting flags */
    struct OverInfo over;
};

static struct Article article = { NULL, NULL, NULL, FALSE, FALSE, 0,
				  { "", "", "", "", "", 0, 0 } };

/* Add the article to a group. */
static Bool
addToGroup( const char * grp )
{
    Over * over;
    const char *msgId;

    over = new_Over( article.over.subject,
		     article.over.from,
		     article.over.date,
		     article.over.msgId,
		     article.over.ref,
		     article.over.bytes,
		     article.over.lines );
    
    msgId = article.over.msgId;
    
    Cont_read( grp );
    Cont_app( over );
    Log_dbg( LOG_DBG_POST, "Added message '%s' to group '%s'.", msgId, grp );

    if ( !article.posted )
    {
        Log_inf( "Added '%s' to database.", msgId );
        if ( ! Db_prepareEntry( over, Cont_grp(), Cont_last() )
	     || ! Db_storeArt ( msgId, DynStr_str( article.text ) ) )
	    return FALSE;
	article.posted = TRUE;
    }
    else
    {
	Str t;
	const char *xref;

	xref = Db_xref( msgId );
	Log_dbg( LOG_DBG_POST, "Adding '%s' to Xref of '%s'", grp, msgId );
	snprintf( t, MAXCHAR, "%s %s:%i", xref, grp, Ov_numb( over ) );
	Db_setXref( msgId, t );
    }
    
    Cont_write();
    Grp_setFirstLast( Cont_grp(), Cont_first(), Cont_last() );
    Grp_setLastPostTime( Cont_grp() );
    return TRUE;
}

static Bool
checkPostableNewsgroup( void )
{
    const char * grp;
    Bool knownGrp = FALSE;
    Bool postAllowedGrp = TRUE;
    Bool local;

    /*
     * Check all known groups are writeable, and there is
     * at least one known group.
     */
    for( grp = Itl_first( article.newsgroups );
	 postAllowedGrp && grp != NULL;
	 grp = Itl_next( article.newsgroups ) )
    {
	if ( Grp_exists( grp ) )
	{
	    local = Grp_local( grp );
	    knownGrp = TRUE;
	    switch( Grp_postAllow( grp ) )
	    {
	    case 'n':
		postAllowedGrp = FALSE;
		break;
	    case 'y':
		break;
	    case 'm':
		/*
		 * Can post to moderated groups if *either*
		 * 1. Group is local and article approved, or
		 * 2. Group is external
		 */
		postAllowedGrp = 
		    ! local ||
		    article.approved;
		break;
	    default:
		/*
		 * Unknown mode for local groups. Forward
		 * to server for external groups; presumably the
		 * server knows what to do.
		 */
		postAllowedGrp = ! local;
		break;
	    }
	}
    }
	    
    if ( ! knownGrp )
    {
	Log_err( "No known group in Newsgroups header field" );
	return FALSE;
    }
    else if ( ! postAllowedGrp )
    {
	Log_err( "A group does not permit posting" );
	return FALSE;
    }

    return TRUE;
}

/* Get article text, check for validity & build overview. */
static Bool
getArticleText( const char *p )
{
    DynStr * s;
    Str line, field, value;
    Bool replyToFound, pathFound, orgFound;
    time_t t;
    int sigLines;

    s = new_DynStr( 10000 );
    article.text = s;

    memset( &article.over, 0, sizeof( article.over ) );
    replyToFound = pathFound = orgFound = FALSE;
    
    /* Grab header lines first, getting overview info as we go. */
    while ( ( p = Utl_getHeaderLn( line, p ) ) != NULL
	    && line[ 0 ] != '\0'
	    && Prt_getField( field, value, line ) )
    {
	/* Look for headers we need to stash. */
	if ( strcmp( field, "subject" ) == 0 )
	{
	    Utl_cpyStr( article.over.subject, value );
	    DynStr_appLn( s, line );
	}
	else if ( strcmp ( field, "from" ) == 0 )
	{
	    Utl_cpyStr( article.over.from, value );
	    DynStr_appLn( s, line );
	}
	else if ( strcmp ( field, "date" ) == 0 )
	    Utl_cpyStr( article.over.date, value );
	else if ( strcmp ( field, "references" ) == 0 )
	{
	    Utl_cpyStr( article.over.ref, value );
	    DynStr_appLn( s, line );
	}
	else if ( strcmp ( field, "message-id" ) == 0 )
	    Utl_cpyStr( article.over.msgId, value );
	else if ( strcmp ( field, "newsgroups" ) == 0 )
	{
	    article.newsgroups = new_Itl( value, " ,\n\t" );
	    DynStr_appLn( s, line );
	}
	else if ( strcmp ( field, "control" ) == 0 )
	{
	    article.control = new_Itl( value, " " );
	    DynStr_appLn( s, line );
	}
	else if ( strcmp ( field, "reply-to" ) == 0 )
	{
	    replyToFound = TRUE;
	    DynStr_appLn( s, line );
	}
	else if ( strcmp ( field, "approved" ) == 0 )
	{
	    article.approved = TRUE;
	    DynStr_appLn( s, line );
	}
	else if ( strcmp ( field, "path" ) == 0 )
	{
	    pathFound = TRUE;
	    DynStr_appLn( s, line );
	}
	else if ( strcmp ( field, "organization" ) == 0 )
	{
	    orgFound = TRUE;
	    DynStr_appLn( s, line );
	}
	else if ( strcmp ( field, "x-sender" ) == 0 )
	{
	    DynStr_app( s, "X-NOFFLE-X-Sender: " );
	    DynStr_appLn( s, value );
	}
	else if ( strcmp ( field, "xref" ) == 0 )
	    Log_inf( "Xref header in post ignored" );
	else
	    DynStr_appLn( s, line );
    }

    /* Now sort header-related issues */
    if ( article.over.from[ 0 ] == '\0' )
    {
	if ( article.flags & POST_ADD_FROM )
	{
	    Log_dbg( LOG_DBG_POST, "Adding From field to posted message." );
	    DynStr_app( s, "From: " );
	    if ( ! Prt_genFromHdr( article.over.from ) )
	    {
		Log_err( "Can't generate From field" );
		return FALSE;
	    }
	    DynStr_appLn( s, article.over.from );
	}
	else
	{
	    Log_err( "Posted message has no From field" );
	    return FALSE;
	}
    }
    if ( article.over.subject[ 0 ] == '\0' )
    {
	Log_err( "Posted message has no Subject field" );
	return FALSE;
    }
    if ( article.newsgroups == NULL || Itl_count( article.newsgroups) == 0 )
    {
	Log_err( "Posted message has no valid Newsgroups field" );
	return FALSE;
    }

    /* Ensure correctly formatted date */
    t = Utl_parseNewsDate( article.over.date );
    if ( t == (time_t) -1 )
    {
	time( &t );
	Utl_newsDate( t, article.over.date );
    }
    DynStr_app( s, "Date: " );
    DynStr_appLn( s, article.over.date );

    /* Ensure Message ID is present and valid */
    if ( Cfg_replaceMsgId() )
    {
	Prt_genMsgId( article.over.msgId, article.over.from, "NOFFLE" );
	Log_dbg( LOG_DBG_POST,
		 "Replacing Message-ID with '%s'",
		 article.over.msgId );
    }
    else if ( article.over.msgId[ 0 ] == '\0' )
    {
	Prt_genMsgId( article.over.msgId, article.over.from, "NOFFLE" );
	Log_inf( "Adding missing Message-ID '%s'", article.over.msgId );
    }
    DynStr_app( s, "Message-ID: " );
    DynStr_appLn( s, article.over.msgId );

    /* Ensure Path header */
    if ( ! pathFound )
    {
	Str path;
	
	Log_dbg( LOG_DBG_POST, "Adding Path field to posted message." );
	DynStr_app( s, "Path: " );
	Utl_cpyStr( path, Cfg_pathHeader() );
	if ( path[ 0 ] == '\0' )
	    Prt_genPathHdr( path, article.over.from );
	DynStr_appLn( s, path );
    }

    /* Ensure Reply-To header */
    if ( ! replyToFound )
    {
	Log_dbg( LOG_DBG_POST, "Adding Reply-To field to posted message." );
	DynStr_app( s, "Reply-To: " );
	DynStr_appLn( s, article.over.from );
    }

    /* Ensure Organization header if required */
    if ( ( ! orgFound ) && ( article.flags & POST_ADD_ORG ) )
    {
	Str org;

	Utl_cpyStr( org, Cfg_organization() );
	if ( org[ 0 ] != '\0' )
	{
	    Log_dbg( LOG_DBG_POST,
		     "Adding Organization field to posted message." );
	    DynStr_app( s, "Organization: " );
 	    DynStr_appLn( s, org );
	}
    }

    /* OK, header ready to roll. Something to accompany it? */
    if ( p == NULL || p[ 0 ] == '\0' )
    {
	Log_err( "Posted  message has no body" );
	return FALSE;
    }

    /* Add the empty line separating header and body */
    DynStr_appLn( s, "" );

    /* Now pop on the rest of the body */
    DynStr_app( s, p );

    /* Add a signature if requested to do so and if one found. */
    sigLines = 0;
    if ( article.flags & POST_ADD_SIG )
    {
	Str sigfile;
	struct passwd *pwd;
	FILE *f;

	/* Generate sig file path */
	pwd = getpwuid( getuid() );
	Utl_cpyStr( sigfile, pwd->pw_dir );
	Utl_catStr( sigfile, SIG_FILE );

	f = fopen( sigfile, "r" );
	if ( f == NULL )
	{
	    /* If err is ENOENT, file doesn't exist. This is OK. */
	    if ( errno != ENOENT )
	    {
		Log_err( "Can't access .signature file (%s), "
			 "article not posted.",
			 strerror( errno ) );
		return FALSE;
	    }
	}
	else
	{
	    /* OK, try to add it. */
	    Str sline;
	    
	    Log_dbg( LOG_DBG_POST, "Adding .signature to posted message." );

	    DynStr_appLn( s, BEGIN_SIG );
	    sigLines++;
	    while ( Prt_getLn( sline, f, 0 ) )
	    {
		DynStr_appLn( s, sline );
		sigLines++;
	    }

	    if ( ferror( f ) )
	    {
		Log_err( "Error reading .signature file (%s), "
			 "article not posted.",
			 strerror( errno ) );
		fclose( f );
		return FALSE;
	    }

	    fclose( f );
	}
    }

    /*
     * Count the lines & bytes. This counts the original number of
     * lines in the supplied body, so add in the number of signature
     * lines added, including the separator.
     */
    for ( p++, article.over.lines = sigLines; *p != '\0'; p++ )
	if ( *p == '\n' )
	    article.over.lines++;
    article.over.bytes = DynStr_len( s );

    return TRUE;
}

/* Add article to outgoing if needs be */
static Bool
postExternal( void )
{
    const char * grp;
    Str serversSeen;
    Bool err;

    /*
     * For each external group, send to that group's server if it has
     * not seen the post already.
     */
    serversSeen[ 0 ] = '\0';
    err = FALSE;
    
    for ( grp = Itl_first( article.newsgroups );
	  grp != NULL;
	  grp = Itl_next( article.newsgroups ) )
    {
	if ( Grp_exists( grp ) && ! Grp_local( grp ) )
	{
	    const char * servName = Grp_server( grp );

	    if ( strstr( serversSeen, servName ) != NULL )
		continue;

	    if ( ! Out_add( servName, article.over.msgId, article.text ) )
	    {
		Log_err( "Cannot add posted article to outgoing directory" );
		err = TRUE;
	    }
	    
	    Utl_catStr( serversSeen, " " );
	    Utl_catStr( serversSeen, servName );
	}
    }

    return err;
}

/* Cancel and return TRUE if need to send cancel message on to server. */
static Bool
controlCancel( const char *cancelId )
{
    return ( Ctrl_cancel( cancelId ) == CANCEL_NEEDS_MSG );
}

/*
  It's a control message. Currently we only know about 'cancel'
  messages; others are passed on for outside groups, and logged
  as ignored for local groups.
 */
static Bool
handleControl( void )
{
    const char *grp;
    const char *op;

    op = Itl_first( article.control );
    if ( op == NULL )
    {
	Log_err( "Malformed control line." );
	return TRUE;
    }
    else if ( strcasecmp( op, "cancel" ) == 0 )
    {
	if ( ! controlCancel( Itl_next( article.control ) ) )
	    return TRUE;	/* Handled entirely locally */
    }
    else
    {
	/* Log 'can't do' for internal groups. */
	for( grp = Itl_first( article.newsgroups );
	     grp != NULL;
	     grp = Itl_next( article.newsgroups ) )
	{
	    if ( Grp_exists( grp ) && Grp_local( grp ) )
		Log_inf( "Ignoring control '%s' for '%s'.", op, grp );
	}
    }

    return postExternal();
}

static Bool
postArticle( void )
{
    const char *grp;
    Bool err;
    Bool local;
    Bool postLocal;

    err = FALSE;
    postLocal = Cfg_postLocal();

    /*
     * Run round first doing all local groups.
     * Remember, we've already checked it is OK to post to them all.
     */ 
    for( grp = Itl_first( article.newsgroups );
	 grp != NULL;
	 grp = Itl_next( article.newsgroups ) )
    {
	local = Grp_local( grp );

	/*
	 * Only post locally to external group if that group's post
	 * status is 'y'. Otherwise retrieve from upstream server -
	 * for example, we don't want to immediately post locally articles
	 * destined for the moderator of a moderated group.
	 */
	if ( ! local && ( ! postLocal || Grp_postAllow( grp ) != 'y' ) )
	    continue;
	err = addToGroup( grp ) && err;
    }

    return postExternal() && err;
}

/* Register an article for posting. */
Bool
Post_open( const char * text, unsigned flags )
{
    if ( article.text != NULL )
    {
	Log_err( "Busy article in Post_open." );
	return FALSE;
    }

    article.flags = flags;
    
    if ( ! getArticleText( text ) )
	return FALSE;

    if ( Db_contains( article.over.msgId ) )
    {
	Post_close();
	Log_err( "Duplicate article %s.", article.over.msgId );
	return FALSE;
    }

    return TRUE;
}

/* Process the posting */
Bool
Post_post( void )
{
    if ( article.flags & POST_DEBUG )
    {
	fputs( DynStr_str( article.text ), stdout );
	return TRUE;
    }

    if ( ! checkPostableNewsgroup() )
	return FALSE;
    
    return ( article.control == NULL )
	? ! postArticle()
	: ! handleControl();
}
   
/* Done with article - tidy up. */
void
Post_close( void )
{
    if ( article.text != NULL )
    {
	del_DynStr( article.text );
	article.text = NULL;
    }
    if ( article.newsgroups != NULL )
    {
	del_Itl( article.newsgroups );
	article.newsgroups = NULL;
    }
    if ( article.control != NULL )
    {
	del_Itl( article.control );
	article.control = NULL;
    }
    article.approved = FALSE;
    article.posted = FALSE;
}


