view src/post.c @ 191:28488e0e3630 noffle

[svn] * src/group.h,src/group.c,src/noffle.c,src/server.c: Grp_setLastAccess is only ever called with last param as time(NULL), so remove it and call time() inside the implementation of Grp_setLastAccess. * src/client.c,src/group.h,src/group.c,src/noffle.c,src/post.c: Groups are automatically unsubscribed when the last access to the group is older than a particular threshold. However, for very low traffic groups, the last access may exceed the threshold simply because there has been no new article posted. In this case, rather than unsubscribe, update the group last access time. This means that groups are now only unsubscribed if the last access exceeds the threshold AND articles have arrived in the group since. Add Grp_setLastPostTime() to track the last time an article arrived in the group.
author bears
date Sat, 20 Oct 2001 14:23:46 +0100
parents fed1334d766b
children 24d4cd032da5
line wrap: on
line source

/*
  post.c

  $Id: post.c 310 2001-10-20 13:23:46Z 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 "post.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 "protocol.h"
#include "util.h"
#include "portable.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;
}