// newsd -- A simple news server - erco@3dsite.com
//
// Copyright 2003-2004 Michael Sweet
// Copyright 2002 Greg Ercolano
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public Licensse as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
//
// This program was designed based on the RFCs:
//
//	rfc1036.txt
//	rfc2980.txt
//	rfc977.txt
//
// TODO:
//      Should probably reorganize both NNTP headers and mail headers
//      so when browsing messages, headers don't jump around
//
//      Cleanup the whole mail gateway posting/mail vs regular posting/mail;
//      lots of redundant code.
//
//      IPv6 support!
//

#define _BSD_SIGNALS 1			/* IRIX */
#define _USE_BSD			/* LINUX */
#define _BSD				/* OSF1 */

// Convenience macro...
#define ISHEAD(a)	(strncasecmp(header[t].c_str(), (a), strlen(a))==0)

#include "Server.H"

// Global configuration data...
Configuration G_conf;

// Number of child processes...
static unsigned G_numclients = 0;

// Overview data headers...
static const char *overview[] =
{
    "Subject:",
    "From:",
    "Date:",
    "Message-ID:",
    "References:",
    "Bytes:",
    "Lines:",
    "Xref:full",
    "Reply-To:",
    NULL
};

// HANDLE SIGCHLD
//    Daemon reaps child processes to keep count.
//
void sigcld_handler(int)
{
    // REAPER
    //    for() limits #children reaped at a time mainly to prevent 
    //    possible infinite loop.
    //
    pid_t	pid;			// Process ID
    int		status;			// Exit status

    for (int t = 0; t < 100 && (pid = waitpid(-1, &status, WNOHANG)) > 0; t ++)
    {
        if ( pid > 0 && G_numclients > 0 ) 
	    G_numclients --;
    }
}

void HelpAndExit()
{
    fputs("newsd - a simple news daemon (V " VERSION ")\n"
          "        See LICENSE file packaged with newsd for license/copyright info.\n"
          "\n"
	  "Usage:\n"
          "    newsd [-c configfile] [-d] [-f] -- start server\n"
	  "    newsd -mailgateway <group>      -- used in /etc/aliases\n"
	  "    newsd -newgroup                 -- used to create new groups\n"
	  "    newsd -rotate                   -- force log rotation\n",
	  stderr);
    exit(1);
}

// RUN AS A PARTICULAR USER
int RunAs()
{
    int err = 0;

    if (chdir("/")) { perror("newsd: chdir(\"/\")"); err = 1; }

    if (G_conf.BadUser()) err = 1;
    else if (!geteuid())
    {
        gid_t	gid = G_conf.GID();

        setgroups(1, &gid);

	if ( setgid(gid) < 0 ) { perror("newsd: setgid()"); err = 1; }
	if ( setuid(G_conf.UID()) < 0 ) { perror("newsd: setuid()"); err = 1; }
    }

    return(err);
}

// CREATE DEAD LETTER FILE, CHOWN ACCORDINGLY
int CreateDeadLetter()
{
    string filename = G_conf.SpoolDir();
    filename += "/.deadletters";
    fprintf(stderr, "(Creating %s)\n", filename.c_str());
    FILE *fp = fopen(filename.c_str(), "a");
    if ( fp == NULL ) { perror(filename.c_str()); return(-1); }
    fchmod(fileno(fp), 0600);
    fchown(fileno(fp), G_conf.UID(), G_conf.GID());
    fclose(fp);

    return(0);
}

// WRITE A MESSAGE (header+body) TO DEAD LETTER FILE
void DeadLetter(const char *errmsg, vector<string>&head, vector<string>&body)
{
    string filename = G_conf.SpoolDir();
    filename += "/.deadletters";
    FILE *fp = fopen(filename.c_str(), "a");
    fchmod(fileno(fp), 0600);
    fchown(fileno(fp), G_conf.UID(), G_conf.GID());
    if ( fp == NULL )
        { perror(filename.c_str()); return; }
    for ( uint t=0; t<head.size(); t++ )
        fprintf(fp, "%s\n", head[t].c_str());
    fprintf(fp, "\n");
    for ( uint t=0; t<body.size(); t++ )
        fprintf(fp, "%s\n", body[t].c_str());
    fprintf(fp, "\n");
    fclose(fp);
}

// HANDLE GATEWAYING MAIL INTO THE NEWSGROUP
//    Reads email message from stdin.
//
int MailGateway(const char *groupname)
{
    Group group;
    if ( group.Load(groupname) < 0 )
    {
	fprintf(stderr, "newsd: Unknown group \"%s\": %s\n", groupname,
                group.Errmsg());
	return(1);
    }

    // COLLECT EMAIL FROM STDIN
    int linechars = 0,
	linecount = 0,
	toolong = 0;
    int eom = 0;
    char c;
    string msg;

    // CONVERT EMAIL HEADER -> NEWSGROUP HEADER
    {
	// Newsgroups:
	msg = "Newsgroups: ";
	msg += groupname;
	msg += "\r\n";

	// X-News-Gateway:
	//    I pulled this outta my ass; need some way to indicate
	//    msg passed through a mail -> news gateway.
	//
	msg += "X-Mail-To-News-Gateway: via newsd ";
	msg += VERSION;
	msg += "\r\n";
    }

    while (read(0, &c, 1) == 1 )
    {
	// KEEP TRACK OF #LINES
	//    Lines longer than 80 chars count as multiple lines.
	//    If posting too long, stop accumulating message in ram,
	//    but keep reading until they've sent the terminating "."
	//
	++linechars;
	if ( linechars > 80 || c == '\n' )
	    { linechars = 0; linecount++; }
	if ( group.PostLimit() > 0 && linecount > group.PostLimit() )
	    { toolong = 1; continue; }

	if ( c == '\n' ) { msg += '\r'; }		// SMTP "\n" -> NNTP "\r\n"
	msg += c;

	// PARSE FOR END OF MESSAGE
	//    Cute little state machine to maintain end of message
	//    parse over buffer boundaries. The states:
	//
	//          0 - not yet at eom
	//          1 - \n before the dot
	//          2 - the dot
	//          3 - \r (or \n) after the dot
	//
	if (c == '\n' || c == '\r')
	{
	    if (eom == 0)
	        eom = 1;
	    else if (eom == 2)
	    {
		eom = 3;
		break;
	    }
	}
	else if ( eom == 1 && ( c == '.' ) )
	    eom = 2;
	else if ( eom != 0 )
	    eom = 0;
    }

    // POSTING TOO LONG? FAIL
    if ( toolong )
    {
	fprintf(stderr, "newsd: Article not posted to %s: longer than %d lines.\n",
	        groupname, group.PostLimit());
	return(1);
    }

    // BLESS ARTICLE -- VERIFY HEADER INTEGRITY, ADD NEEDED HEADERS
    vector<string> header;
    vector<string> body;
    if ( group.ParseArticle(msg, header, body) < 0 )
    {
	fprintf(stderr, "newsd: Article not posted to %s: %s.\n",
	        groupname, group.Errmsg());
	return(1);
    }

    // UPDATE 'Path:'
    group.UpdatePath(header);

    // CHECK FOR LOOPS, MASSAGE HEADERS
    for ( uint t=0; t<header.size(); t++ )
    {
	// CONVERT RFC822 "From .." -> "X-Original-From: .."
	if ( ISHEAD("From ") )
	{
	    string newfrom = header[t];
	    newfrom.replace(0, strlen("From "), "X-Original-From: ");
	    header[t] = newfrom;
	}
	// SHORT CIRCUIT LOOP DELIVERY
	//     Example: "X-Loop: outOfSpace"
	//
	else if ( ISHEAD("X-Newsd-Loop:") || ISHEAD("X-Loop:") )
	{
	    string errmsg = "newsd: -mailgateway ";
	    errmsg += groupname;
	    errmsg += ": NOT POSTED: '";
	    errmsg += header[t];
	    errmsg += "' mail loop detected: message dropped to "
	              SPOOL_DIR "/.deadletters";
	    DeadLetter(errmsg.c_str(), header, body);
	    fprintf(stderr, "%s\n", 
	        (const char*)errmsg.c_str());
	    return(1);
	}
    }

    // POST ARTICLE
    //    Don't affect 'current group' or 'current article'.
    //
    if ( group.Post(overview, header, body, "localhost", true) < 0 )
    {
	fprintf(stderr, "newsd: Article not posted to %s: %s.\n",
	        groupname, group.Errmsg());
	return(1);
    }

    // CC MESSAGE TO MAIL ADDRESS?
    if ( group.IsCCPost() )
    {
	string from = "Anonymous",
	       subject = "-";

	// HANDLE PRESERVING FIELDS FROM NNTP POSTING -> SMTP
	string preserve;
	int pflag = 0;
	for ( unsigned t=0; t<header.size(); t++ )
	{
	    char c = header[t].c_str()[0];

	    // CONTINUATION OF HEADER LINE?
	    if ( c == ' ' || c == 9 )
	    {
		// CONTINUATION OF PREVIOUS PRESERVED HEADER LINE?
		if ( pflag )
		    { preserve += header[t]; preserve += "\n"; }
		continue;
	    }

	    // ZERO OUT PRESERVE -- NO MORE CONTINUATIONS
	    pflag = 0;

	    // CHECK FOR PRESERVE FIELDS
	    if ( ISHEAD("From: ") ||			// must
		 ISHEAD("Subject: ") ||			// must
		 ISHEAD("References: ") ||		// needed to preserve threading
		 ISHEAD("Xref: ") ||			// ?
		 ISHEAD("Path: ") ||			// RFC 1036 2.1.6 (STR #15)
		 ISHEAD("Content-Type: ") ||		// mime related
		 ISHEAD("MIME-Version: ") ||		// mime related
		 ISHEAD("Message-ID: ") )		// needed to preserve threading
	    {
		pflag = 1;
		preserve += header[t];
		preserve += "\n";
	    }
	}

        G_conf.LogMessage(L_DEBUG, "popen(%s,\"w\")..", 
	    (const char*)G_conf.SendMail());

	FILE *fp = popen(G_conf.SendMail(), "w");
	if ( ! fp )
	{
            G_conf.LogMessage(L_ERROR, 
	        "mailgateway: ccpost popen() can't execute '%s': %s",
                (const char*)G_conf.SendMail(),
                (const char*)strerror(errno));
	}
	else
	{
	    fprintf(fp, "To: %s\n", (const char*)group.VoidEmail());
	    fprintf(fp, "Bcc: %s\n", (const char*)group.CCPost());
	    fprintf(fp, "%s", preserve.c_str());

	    // Reply-To: Needed for mail gateway
	    if ( group.IsReplyTo() )
		fprintf(fp, "Reply-To: %s\n", (const char*)group.ReplyTo());

	    // Errors-To: advised so admin hears about problems, in addition
	    //            to the real person who sent the message.
	    //
	    fprintf(fp, "Errors-To: %s\n", (const char*)group.Creator());
	    fprintf(fp, "\n");
	    fprintf(fp, "[posted to %s]\n\n", (const char*)group.Name());
	    for (unsigned t = 0; t < body.size(); t ++ )
		fprintf(fp, "%s\n", body[t].c_str());
	    if (pclose(fp) < 0)
                G_conf.LogMessage(L_ERROR, 
		    "mailgateway: ccpost pclose() failed for '%s': %s",
                    (const char*)G_conf.SendMail(),
                    (const char*)strerror(errno));
	}
    }
    
    return(0);
}

int main(int argc, const char *argv[])
{

    // HANDLE SIGCHLD, IGNORE SIGPIPE + SIGALRM
    signal(SIGCHLD, sigcld_handler);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGALRM, SIG_IGN);
    umask(022);		// enforce rw-r--r-- perms

    Server server;
    const char *conffile = CONFIG_FILE;
    const char *mailgateway = NULL;
    int newgroup = 0;
    int dodebug = 0,
        dofork = 1,
        dorotate = 0;

    // Scan command-line...
    for (int t = 1; t < argc; t ++)
    {
        if (!strcmp(argv[t], "-c"))
	{
	    if (++t >= argc)
	    {
	        fputs("newsd: Expected filename after \"-c\"!\n", stderr);
		HelpAndExit();
	    }

            conffile = argv[t];
	}
	else if (!strcmp(argv[t], "-d"))
	    { dodebug = 1; dofork = 0; }
	else if (!strcmp(argv[t], "-f"))
	    { dofork = 0; }
	else if (!strncmp(argv[t], "-h", 2))
	    { HelpAndExit(); }
        else if (!strcmp(argv[t], "-mailgateway"))
	{
	    if (++t >= argc)
	    {
	        fputs("newsd: Expected groupname after \"-mailgateway\"!\n", stderr);
		HelpAndExit();
	    }

            mailgateway = argv[t];
	    dofork      = 0;
	}
        else if (!strcmp(argv[t], "-newgroup"))
	    { newgroup = 1; dofork = 0; }
        else if (!strcmp(argv[t], "-rotate"))
	    { dorotate = 1; dofork = 0; }
	else
	    { fprintf(stderr, "newsd: Unknown argument '%s'\n", argv[t]); HelpAndExit(); }
    }

    // Load global config data...
    G_conf.Load(conffile);

    if (dodebug)
    {
        G_conf.LogLevel(L_DEBUG);
	G_conf.ErrorLog("stderr");
    }

    // Do stuff...
    if (dorotate)
    {
        G_conf.InitLog();	// open log (it isn't yet)
        G_conf.LogLock();	// lock while rotating
	G_conf.Rotate(true);	// force rotation
	G_conf.LogUnlock();
	exit(0);
    }
    else if (mailgateway)
    {
	if (RunAs()) return(1);

	return(MailGateway(mailgateway));
    }
    else if (newgroup)
    {
	if (RunAs()) return(1);

	// CREATE DEAD LETTER FILE, IF NONE
	if ( CreateDeadLetter() < 0 )
	    { fprintf(stderr, "news: CreateDeadLetter(): failed\n"); return(1); }

	Group tmp;
	return(tmp.NewGroup());
    }

    // Start logging...
    G_conf.InitLog();

    // Log server information...
    G_conf.LogMessage(L_INFO, "-- newsd started --");
    G_conf.LogMessage(L_INFO, "-- start config summary --");
    G_conf.LogSelf(L_INFO);
    G_conf.LogMessage(L_INFO, "-- end config summary --");

    if (server.Listen() < 0)
    {
        G_conf.LogMessage(L_ERROR, "Unable to listen for connections: %s",
	                  server.Errmsg());
        return(1);
    }

    // RUN AS THE USER 'NEWS'
    //     Now that we've opened the reserved port, 
    //     we no longer need to be root.
    //
    if (RunAs())
        return(1);

    // Fork into the background...
    if (dofork)
    {
	pid_t pid = fork();
	switch ( pid )
	{
	    case -1: // ERROR
	        G_conf.LogMessage(L_ERROR, "daemonize fork(): %s (exiting)", 
		                  strerror(errno));
		return(1);

	    case 0:  // CHILD
        	// "Daemonize" our application so it is no longer connected to
		// the current terminal session...
        	close(0);
		open("/dev/null", O_RDONLY);
		close(1);
		open("/dev/null", O_WRONLY);
		close(2);
		open("/dev/null", O_WRONLY);

	        if ( setsid() < 0 )
		    G_conf.LogMessage(L_ERROR, "setsid() failed (ignored): %s", 
				      strerror(errno));
		break;
		
	    default: // PARENT
		return(0);
	}
    }

    // ACCEPT NEW CONNECTIONS LOOP
    for (;;)
    {
        if (server.Accept() < 0)
	{
	    G_conf.LogMessage(L_ERROR, "Unable to accept new connection: %s",
	                      server.Errmsg());
	    sleep(10);
	    continue;
        }

	// TOO MANY CHILDREN?
	if (G_conf.MaxClients() != 0 && G_numclients >= G_conf.MaxClients())
	{
	    server.Send("400 Server has too many connections open -- try again later");
	    close(server.MsgSock());
	    continue;
	}

	// FORK A CHILD TO HANDLE CONNECTION
	//    News readers can keep a connection open for the entire
	//    duration of the news reading session.
	//
	pid_t pid;
	while ((pid = fork()) == -1)
	{
	    G_conf.LogMessage(L_ERROR, "Unable to fork handler process: %s",
	                      strerror(errno));
	    sleep(10);
        }

	G_numclients ++;

	switch (pid)
	{
	    case 0 :	// CHILD
	        G_conf.ErrorLog(G_conf.ErrorLog());
		server.CommandLoop(overview);
		exit(0);

	    default :	// PARENT
		close(server.MsgSock());
	        break;
	}
    }
    //NOTREACHED
}