Mercurial > noffle
view src/post.c @ 500:614a3177b15c noffle tip
Add mail-from option.
Some modern mail systems will try and ensure the sender email is a legitimate
address. Which will fail if there isn't such an address.
author | Jim Hague <jim.hague@acm.org> |
---|---|
date | Wed, 14 Aug 2013 12:04:39 +0100 |
parents | 372f8b55506e |
children |
line wrap: on
line source
/* post.c $Id: post.c 645 2006-07-12 19:26:41Z 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 ); /* Cont modules owns ov after this */ 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 ); } if ( Cont_write() ) { Grp_setFirstLast( Cont_grp(), Cont_first(), Cont_last() ); Grp_setLastPostTime( Cont_grp() ); return TRUE; } else return FALSE; } 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 dateFound, fromFound, msgIdFound, subjectFound; Bool newsgroupsFound, pathFound; Bool replyToFound, orgFound; Bool continuation; time_t t; int sigLines; Bool addMsgIdIfMissing; Bool processMsgId = TRUE; addMsgIdIfMissing = Cfg_addMsgIdIfMissing(); s = new_DynStr( 10000 ); article.text = s; /* RFC says only one of these headers. */ dateFound = fromFound = msgIdFound = subjectFound = newsgroupsFound = pathFound = FALSE; /* Stuff we might want to add. */ replyToFound = orgFound = FALSE; field[ 0 ] = '\0'; /* * Grab header lines first, getting overview info as we go. * Note that a line may be a continuation line, hence we always * cat the information into the destination. */ while ( ( p = Utl_getHeaderLn( line, p ) ) != NULL && line[ 0 ] != '\0' && Prt_getField( field, value, &continuation, line ) ) { if ( field [ 0 ] == '\0' ) { /* Error! Continuation without preceding header. */ Log_err( "First header line started with white space" ); return FALSE; } /* Look for headers we need to stash. */ if ( strcmp( field, "subject" ) == 0 ) { if ( !continuation && subjectFound ) { Log_err( "Duplicate Subject: header" ); return FALSE; } Utl_catStr( article.over.subject, value ); DynStr_appLn( s, line ); subjectFound = TRUE; } else if ( strcmp ( field, "from" ) == 0 ) { if ( !continuation && fromFound ) { Log_err( "Duplicate From: header" ); return FALSE; } Utl_catStr( article.over.from, value ); DynStr_appLn( s, line ); fromFound = TRUE; } else if ( strcmp ( field, "date" ) == 0 ) { if ( !continuation && dateFound ) { Log_err( "Duplicate Date: header" ); return FALSE; } Utl_catStr( article.over.date, value ); dateFound = TRUE; } else if ( strcmp ( field, "references" ) == 0 ) { Utl_catStr( article.over.ref, value ); DynStr_appLn( s, line ); } else if ( strcmp ( field, "message-id" ) == 0 ) { if ( !continuation && msgIdFound ) { Log_err( "Duplicate Message-Id: header" ); return FALSE; } Utl_catStr( article.over.msgId, value ); msgIdFound = TRUE; } else if ( strcmp ( field, "newsgroups" ) == 0 ) { if ( !continuation && newsgroupsFound ) { Log_err( "Duplicate Newsgroups: header" ); return FALSE; } article.newsgroups = new_Itl( value, " ,\n\t" ); DynStr_appLn( s, line ); newsgroupsFound = TRUE; } 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 ) { if ( !continuation && pathFound ) { Log_err( "Duplicate Path: header" ); return FALSE; } 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 ( ! subjectFound ) { 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 ( article.over.msgId[ 0 ] == '\0' ) { Prt_genMsgId( article.over.msgId, article.over.from, "NOFFLE" ); Log_inf( "Adding missing Message-ID '%s'", article.over.msgId ); if ( ! addMsgIdIfMissing ) processMsgId = FALSE; } else if ( ! Prt_isValidMsgId( article.over.msgId ) || Cfg_replaceMsgId() ) { Prt_genMsgId( article.over.msgId, article.over.from, "NOFFLE" ); Log_dbg( LOG_DBG_POST, "Replacing Message-ID with '%s'", article.over.msgId ); } if ( processMsgId ) { DynStr_app( s, "Message-ID: " ); DynStr_appLn( s, article.over.msgId ); } else Log_inf( "Not storing Message-ID '%s' in message.", 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 configuration demands it */ if ( ! replyToFound && Cfg_appendReplyTo() ) { 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' ) ) err = addToGroup( grp ) && err; } return postExternal() && err; } static void clearArticleInfo( void ) { article.text = NULL; article.newsgroups = NULL; article.control = NULL; article.approved = FALSE; article.posted = FALSE; article.flags = 0; memset( &article.over, 0, sizeof( article.over ) ); } /* 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; } clearArticleInfo(); 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; } }