view src/post.c @ 150:1c7303c71f66 noffle

[svn] * src/protocol.c: Fix bug in Prt_getLn if we should read a line starting with '\0' - according to the leafnode mailing list, this has been seen in the wild. * docs/inews.1,docs/noffle.1,docs/noffle.conf.5, packages/redhat/noffle.spec,src/configfile.h,src/configfile.c, src/noffle.c,src/post.h,src/post.c: Removed use of getopt_long, and added inews mode - the Noffle executable behaves as inews is invoked as inews. This includes adding From: and Organization: headers if necessary - add configs to override defaults for the From: domain and specify the organization. For all my fellow trn-heads out there, and users of any other ageing newsreader that expects inews. Updated RPM spec to create inews link to noffle on install.
author bears
date Thu, 26 Oct 2000 22:21:13 +0100
parents 55ba957023f9
children ca9769519c96
line wrap: on
line source

/*
  post.c

  $Id: post.c 227 2000-10-26 21:21:13Z 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( "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( "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() );
    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( "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( "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( "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( "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( "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( "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 );
	if ( ! ( postLocal || local ) )
	    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;
}