// 
//
//  $Id: spamass-milter.cpp,v 1.90 2006/03/23 21:41:36 dnelson Exp $
//
//  SpamAss-Milter 
//    - a rather trivial SpamAssassin Sendmail Milter plugin
//
//  for information about SpamAssassin please see
//                        http://www.spamassassin.org
//
//  for information about Sendmail please see
//                        http://www.sendmail.org
//
//  Copyright (c) 2002 Georg C. F. Greve <greve@gnu.org>,
//   all rights maintained by FSF Europe e.V., 
//   Villa Vogelsang, Antonienallee 1, 45279 Essen, Germany
//

// {{{ License, Contact, Notes & Includes 

//   This program is free software; you can redistribute it and/or modify
//   it under the terms of the GNU General Public License 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
//
//   Contact:
//            Michael Brown <michaelb@opentext.com>
//

// Notes:
//
//  The libmilter for sendmail works callback-oriented, so if you have no
//  experience with event-driven programming, the following may be hard for
//  you to understand.
//
//  The code should be reasonably thread-safe. No guarantees, though.
//
//  This program roughly does the following steps:
//
//   1. register filter with libmilter & set up socket
//   2. register the callback functions defined in this file
//    -- wait for mail to show up --
//   3. start spamc client
//   4. assemble mail since libmilter passes it in pieces and put
//      these parts in the output pipe to spamc.
//   5. when the mail is complete, close the pipe.
//   6. read output from spamc, close input pipe and clean up PID
//   7. check for the flags affected by SpamAssassin and set/change
//      them accordingly
//   8. replace the body with the one provided by SpamAssassin if the
//      mail was rated spam, unless -m is specified
//   9. free all temporary data
//   10. tell sendmail to let the mail to go on (default) or be discarded
//    -- wait for mail to show up -- (restart at 3)
//

// Includes  
#include "config.h"

#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <strings.h>
#include <sysexits.h>
#include <unistd.h>
#include <fcntl.h>
#include <syslog.h>
#include <signal.h>
#include <pthread.h>
#ifdef HAVE_POLL_H
#include <poll.h>
#else
#include "subst_poll.h"
#endif
#include <errno.h>

// C++ includes
#include <cstdio>
#include <cstddef>
#include <csignal>
#include <string>
#include <iostream>

#ifdef  __cplusplus
extern "C" {
#endif

#include "libmilter/mfapi.h"
//#include "libmilter/mfdef.h"

#if !HAVE_DECL_STRSEP
char *strsep(char **stringp, const char *delim);
#endif 

#if !HAVE_DECL_DAEMON
int daemon(int nochdir, int noclose);
#endif 

#ifdef  __cplusplus
}
#endif

#include "spamass-milter.h"

#ifdef WITH_DMALLOC
#include "dmalloc.h"
#endif

#ifndef INADDR_LOOPBACK
#define INADDR_LOOPBACK 0x7F000001
#endif

// }}} 

static const char Id[] = "$Id: spamass-milter.cpp,v 1.90 2006/03/23 21:41:36 dnelson Exp $";

struct smfiDesc smfilter =
  {
    "SpamAssassin", // filter name
    SMFI_VERSION,   // version code -- leave untouched
    SMFIF_ADDHDRS|SMFIF_CHGHDRS|SMFIF_CHGBODY,  // flags
    mlfi_connect, // info filter callback
    mlfi_helo, // HELO filter callback
    mlfi_envfrom, // envelope sender filter callback
    mlfi_envrcpt, // envelope recipient filter callback
    mlfi_header, // header filter callback
    mlfi_eoh, // end of header callback
    mlfi_body, // body filter callback
    mlfi_eom, // end of message callback
    mlfi_abort, // message aborted callback
    mlfi_close, // connection cleanup callback
  };

const char *const debugstrings[] = {
	"ALL", "FUNC", "POLL", "UORI", "STR", "MISC", "NET", "SPAMC", "RCPT",
	"COPY",
	NULL
};

int flag_debug = (1<<D_ALWAYS);
bool flag_reject = false;
int reject_score = -1;
bool dontmodifyspam = false;    // Don't modify/add body or spam results headers
bool dontmodify = false;        // Don't add SA headers, ever.
bool flag_sniffuser = false;
char *defaultuser;				/* Username to send to spamc if there are multiple recipients */
char *defaultdomain;			/* Domain to append if incoming address has none */
char *spamdhost;
struct networklist ignorenets;
int spamc_argc;
char **spamc_argv;
bool flag_bucket = false;
bool flag_bucket_only = false;
char *spambucket;
bool flag_full_email = false;		/* pass full email address to spamc */
bool flag_expand = false;	/* alias/virtusertable expansion */
bool warnedmacro = false;	/* have we logged that we couldn't fetch a macro? */

#if defined(__FreeBSD__) /* popen bug - see PR bin/50770 */
static pthread_mutex_t popen_mutex = PTHREAD_MUTEX_INITIALIZER;
#endif

// {{{ main()

int
main(int argc, char* argv[])
{
   int c, err = 0;
   const char *args = "fd:mMp:P:r:u:D:i:b:B:e:x";
   char *sock = NULL;
   bool dofork = false;
   char *pidfilename = NULL;
   FILE *pidfile = NULL;

#ifdef HAVE_VERBOSE_TERMINATE_HANDLER
	std::set_terminate (__gnu_cxx::__verbose_terminate_handler);
#endif

   openlog("spamass-milter", LOG_PID, LOG_MAIL);

	/* Process command line options */
	while ((c = getopt(argc, argv, args)) != -1) {
		switch (c) {
			case 'f':
				dofork = true;
				break;
			case 'd':
				parse_debuglevel(optarg);
				break;
			case 'D':
				spamdhost = strdup(optarg);
				break;
			case 'e':
				flag_full_email = true;
				defaultdomain = strdup(optarg);
				break;
			case 'i':
				debug(D_MISC, "Parsing ignore list");
				parse_networklist(optarg, &ignorenets);
				break;
			case 'm':
				dontmodifyspam = true;
				smfilter.xxfi_flags &= ~SMFIF_CHGBODY;
				break;
			case 'M':
				dontmodify = true;
				dontmodifyspam = true;
				smfilter.xxfi_flags &= ~(SMFIF_CHGBODY|SMFIF_CHGHDRS);
				break;
			case 'p':
				sock = strdup(optarg);
				break;
			case 'P':
				pidfilename = strdup(optarg);
				break;
			case 'r':
				flag_reject = true;
				reject_score = atoi(optarg);
				break;
			case 'u':
				flag_sniffuser = true;
				defaultuser = strdup(optarg);
				break;
			case 'b':
			case 'B':
				if (flag_bucket)
				{
					fprintf(stderr, "Can only have one -b or -B flag\n");
					err = 1;
					break;
				}
				flag_bucket = true;
				if (c == 'b')
				{
					flag_bucket_only = true;
					smfilter.xxfi_flags |= SMFIF_DELRCPT; // May delete recipients
				}
				// we will modify the recipient list; if spamc returns
				// indicating that this mail is spam, the message will be
				// sent to <optarg>@localhost
				smfilter.xxfi_flags |= SMFIF_ADDRCPT; // May add recipients
				// XXX we should probably verify that optarg is vaguely sane
				spambucket = strdup( optarg );
				break;
			case 'x':
				flag_expand = true;
				break;
			case '?':
				err = 1;
				break;
		}
	}

   if (flag_full_email && !flag_sniffuser)
   {
   	  fprintf(stderr, "-e flag requires -u\n");
      err=1;
   }

   /* remember the remainer of the arguments so we can pass them to spamc */
   spamc_argc = argc - optind;
   spamc_argv = argv + optind;

   if (!sock || err) {
      cout << PACKAGE_NAME << " - Version " << PACKAGE_VERSION << endl;
      cout << "SpamAssassin Sendmail Milter Plugin" << endl;
      cout << "Usage: spamass-milter -p socket [-b|-B bucket] [-d xx[,yy...]] [-D host]" << endl;
      cout << "                      [-e defaultdomain] [-f] [-i networks] [-m] [-M]" << endl;
      cout << "                      [-P pidfile] [-r nn] [-u defaultuser] [-x]" << endl;
      cout << "                      [-- spamc args ]" << endl;
      cout << "   -p socket: path to create socket" << endl;
      cout << "   -b bucket: redirect spam to this mail address.  The orignal" << endl;
      cout << "          recipient(s) will not receive anything." << endl;
      cout << "   -B bucket: add this mail address as a BCC recipient of spam." << endl;
      cout << "   -d xx[,yy ...]: set debug flags.  Logs to syslog" << endl;
      cout << "   -D host: connect to spamd at remote host (deprecated)" << endl;
      cout << "   -e defaultdomain: pass full email address to spamc instead of just\n"
              "          username.  Uses 'defaultdomain' if there was none" << endl;
      cout << "   -f: fork into background" << endl;
      cout << "   -i: skip (ignore) checks from these IPs or netblocks" << endl;
      cout << "          example: -i 192.168.12.5,10.0.0.0/8,172.16.0.0/255.255.0.0" << endl;
      cout << "   -m: don't modify body, Content-type: or Subject:" << endl;
      cout << "   -M: don't modify the message at all" << endl;
      cout << "   -P pidfile: Put processid in pidfile" << endl;
      cout << "   -r nn: reject messages with a score >= nn with an SMTP error.\n"
              "          use -1 to reject any messages tagged by SA." << endl;
      cout << "   -u defaultuser: pass the recipient's username to spamc.\n"
              "          Uses 'defaultuser' if there are multiple recipients." << endl;
      cout << "   -x: pass email address through alias and virtusertable expansion." << endl;
      cout << "   -- spamc args: pass the remaining flags to spamc." << endl;
              
      exit(EX_USAGE);
   }

	if (pidfilename)
	{
		unlink(pidfilename);
		pidfile = fopen(pidfilename,"w");
		if (!pidfile)
		{
			fprintf(stderr, "Could not open pidfile: %s\n", strerror(errno));
			exit(1);
		}
		/* leave the file open through the fork, since we don't know our pid
		   yet
		*/
	}


	if (dofork == true) 
	{
		if (daemon(0, 0) == -1)
		{
            fprintf(stderr, "daemon() failed: %s\n", strerror(errno));
            exit(1);
		}
	}
	
	if (pidfile)
	{
		fprintf(pidfile, "%ld\n", (long)getpid());
		fclose(pidfile);
		pidfile = NULL;
	}	
	
   {
      struct stat junk;
      if (stat(sock,&junk) == 0) unlink(sock);
   }

   (void) smfi_setconn(sock);
	if (smfi_register(smfilter) == MI_FAILURE) {
		fprintf(stderr, "smfi_register failed\n");
		exit(EX_UNAVAILABLE);
	} else {
      debug(D_MISC, "smfi_register succeeded");
   }
	debug(D_ALWAYS, "spamass-milter %s starting", PACKAGE_VERSION);
	err = smfi_main();
	debug(D_ALWAYS, "spamass-milter %s exiting", PACKAGE_VERSION);
	if (pidfilename)
		unlink(pidfilename);
	return err;
}

// }}}

/* Update a header if SA changes it, or add it if it is new. */
void update_or_insert(SpamAssassin* assassin, SMFICTX* ctx, string oldstring, t_setter setter, char *header )
{
	string::size_type eoh1 = assassin->d().find("\n\n");
	string::size_type eoh2 = assassin->d().find("\n\r\n");
	string::size_type eoh = ( eoh1 < eoh2 ? eoh1 : eoh2 );

	string newstring;
	string::size_type oldsize;

	debug(D_UORI, "u_or_i: looking at <%s>", header);
	debug(D_UORI, "u_or_i: oldstring: <%s>", oldstring.c_str());

	newstring = retrieve_field(assassin->d().substr(0, eoh), header);
	debug(D_UORI, "u_or_i: newstring: <%s>", newstring.c_str());

	oldsize = callsetter(*assassin,setter)(newstring);
      
	if (!dontmodify)
	{
		if (newstring != oldstring)
		{
			/* change if old one was present, append if non-null */
			char* cstr = const_cast<char*>(newstring.c_str());
			if (oldsize > 0)
			{
				debug(D_UORI, "u_or_i: changing");
				smfi_chgheader(ctx, header, 1, newstring.size() > 0 ? 
					cstr : NULL );
			} else if (newstring.size() > 0)
			{
				debug(D_UORI, "u_or_i: inserting");
				smfi_addheader(ctx, header, cstr);
			}
		} else
		{
			debug(D_UORI, "u_or_i: no change");
		}
	}
}

// {{{ Assassinate

//
// implement the changes suggested by SpamAssassin for the mail.  Returns
// the milter error code.
int 
assassinate(SMFICTX* ctx, SpamAssassin* assassin)
{
  // find end of header (eol in last line of header)
  // and beginning of body
  string::size_type eoh1 = assassin->d().find("\n\n");
  string::size_type eoh2 = assassin->d().find("\n\r\n");
  string::size_type eoh = (eoh1 < eoh2) ? eoh1 : eoh2;
  string::size_type bob = assassin->d().find_first_not_of("\r\n", eoh);

  if (bob == string::npos)
  	bob = assassin->d().size();

  update_or_insert(assassin, ctx, assassin->spam_flag(), &SpamAssassin::set_spam_flag, "X-Spam-Flag");
  update_or_insert(assassin, ctx, assassin->spam_status(), &SpamAssassin::set_spam_status, "X-Spam-Status");

  /* Summarily reject the message if SA tagged it, or if we have a minimum
     score, reject if it exceeds that score. */
  if (flag_reject)
  {
	bool do_reject = false;
	if (reject_score == -1 && !assassin->spam_flag().empty())
		do_reject = true;
	if (reject_score != -1)
	{
		int score, rv;
		const char *spam_status = assassin->spam_status().c_str();
		/* SA 3.0 uses the keyword "score" */
		rv = sscanf(spam_status,"%*s score=%d", &score);
		if (rv != 1)
		{
			/* SA 2.x uses the keyword "hits" */
			rv = sscanf(spam_status,"%*s hits=%d", &score);
		}
		if (rv != 1)
			debug(D_ALWAYS, "Could not extract score from <%s>", spam_status);
		else 
		{
			debug(D_MISC, "SA score: %d", score);
			if (score >= reject_score)
				do_reject = true;
		}
	}
	if (do_reject)
	{
		debug(D_MISC, "Rejecting");
		smfi_setreply(ctx, "550", "5.7.1", "Blocked by SpamAssassin");


		if (flag_bucket)
		{
			/* If we also want a copy of the spam, shell out to sendmail and
			   send another copy.  The milter API will not let you send the
			   message AND return a failure code to the sender, so this is
			   the only way to do it. */
#if defined(__FreeBSD__)
			int rv;
#endif
			
#if defined(HAVE_ASPRINTF)
			char *buf;
#else
			char buf[1024];
#endif
			char *fmt="%s \"%s\"";
			FILE *p;

#if defined(HAVE_ASPRINTF)
			asprintf(&buf, fmt, SENDMAIL, spambucket);
#else
#if defined(HAVE_SNPRINTF)
			snprintf(buf, sizeof(buf)-1, fmt, SENDMAIL, spambucket);
#else
			/* XXX possible buffer overflow here */
			sprintf(buf, fmt, SENDMAIL, spambucket);
#endif
#endif

			debug(D_COPY, "calling %s", buf);
#if defined(__FreeBSD__) /* popen bug - see PR bin/50770 */
			rv = pthread_mutex_lock(&popen_mutex);
			if (rv)
			{
				debug(D_ALWAYS, "Could not lock popen mutex: %s", strerror(rv));
				abort();
			}		
#endif
			p = popen(buf, "w");
			if (!p)
			{
				debug(D_COPY, "popen failed(%s).  Will not send a copy to spambucket", strerror(errno));
			} else
			{
				// Send message provided by SpamAssassin
				fwrite(assassin->d().c_str(), assassin->d().size(), 1, p);
				pclose(p); p = NULL;
			}
#if defined(__FreeBSD__)
			rv = pthread_mutex_unlock(&popen_mutex);
			if (rv)
			{
				debug(D_ALWAYS, "Could not unlock popen mutex: %s", strerror(rv));
				abort();
			}		
#endif
#if defined(HAVE_ASPRINTF)
			free(buf);
#endif 
		}
		return SMFIS_REJECT;
	}
  }

  /* Drop the message into the spam bucket if it's spam */
  if ( flag_bucket ) {
        if (!assassin->spam_flag().empty()) {
          // first, add the spambucket address
          if ( smfi_addrcpt( ctx, spambucket ) != MI_SUCCESS ) {
                throw string( "Failed to add spambucket to recipients" );
          }
          if (flag_bucket_only) {
                // Move recipients to a non-active header, one at a
                // time. Note, this may generate multiple X-Spam-Orig-To
                // headers, but that's okay.
                while( !assassin->recipients.empty()) {
                  if ( smfi_addheader( ctx, "X-Spam-Orig-To", (char *)assassin->recipients.front().c_str()) != MI_SUCCESS ) {
                        throw string( "Failed to save recipient" );
                  }

                  // It's not 100% important that this succeeds, so we'll just warn on failure rather than bailing out.
                  if ( smfi_delrcpt( ctx, (char *)assassin->recipients.front().c_str()) != MI_SUCCESS ) {
                        // throw_error really just logs a warning as opposed to actually throw()ing
                        debug(D_ALWAYS, "Failed to remove recipient %s from the envelope", assassin->recipients.front().c_str() );
                  }
                  assassin->recipients.pop_front();
                }
          }
        }
  }

  update_or_insert(assassin, ctx, assassin->spam_report(), &SpamAssassin::set_spam_report, "X-Spam-Report");
  update_or_insert(assassin, ctx, assassin->spam_prev_content_type(), &SpamAssassin::set_spam_prev_content_type, "X-Spam-Prev-Content-Type");
  update_or_insert(assassin, ctx, assassin->spam_level(), &SpamAssassin::set_spam_level, "X-Spam-Level");
  update_or_insert(assassin, ctx, assassin->spam_checker_version(), &SpamAssassin::set_spam_checker_version, "X-Spam-Checker-Version");

  // 
  // If SpamAssassin thinks it is spam, replace
  //  Subject:
  //  Content-Type:
  //  <Body>
  // 
  //  However, only issue the header replacement calls if the content has
  //  actually changed. If SA didn't change subject or content-type, don't
  //  replace here unnecessarily.
  if (!dontmodifyspam && assassin->spam_flag().size()>0)
    {
	  update_or_insert(assassin, ctx, assassin->subject(), &SpamAssassin::set_subject, "Subject");
	  update_or_insert(assassin, ctx, assassin->content_type(), &SpamAssassin::set_content_type, "Content-Type");

      // Replace body with the one SpamAssassin provided
      string::size_type body_size = assassin->d().size() - bob;
      string body=assassin->d().substr(bob, string::npos);
      if ( smfi_replacebody(ctx, (unsigned char *)body.c_str(), body_size) == MI_FAILURE )
	throw string("error. could not replace body.");
      
    }

  return SMFIS_CONTINUE;
}

// retrieve the content of a specific field in the header
// and return it.
string
old_retrieve_field(const string& header, const string& field)
{
  // look for beginning of content
  string::size_type pos = find_nocase(header, "\n" + field + ": ");

  // return empty string if not found
  if (pos == string::npos)
  {
    debug(D_STR, "r_f: failed");
    return string("");
  }

  // look for end of field name
  pos = find_nocase(header, " ", pos) + 1;
  
  string::size_type pos2(pos);

  // is field empty? 
  if (pos2 == find_nocase(header, "\n", pos2))
    return string("");

  // look for end of content
  do {

    pos2 = find_nocase(header, "\n", pos2+1);

  }
  while ( pos2 < string::npos &&
	  isspace(header[pos2+1]) );

  return header.substr(pos, pos2-pos);

}

// retrieve the content of a specific field in the header
// and return it.
string
retrieve_field(const string& header, const string& field)
{
  // Find the field
  string::size_type field_start = string::npos;
  string::size_type field_end = string::npos;
  string::size_type idx = 0;

  while( field_start == string::npos ) {
	idx = find_nocase( header, field + ":", idx );

	// no match
	if ( idx == string::npos ) {
	  return string( "" );
	}

	// The string we've found needs to be either at the start of the
	// headers string, or immediately following a "\n"
	if ( idx != 0 ) {
	  if ( header[ idx - 1 ] != '\n' ) {
		idx++; // so we don't get stuck in an infinite loop
		continue; // loop around again
	  }
	}

	field_start = idx;
  }

  // A mail field starts just after the header. Ideally, there's a
  // space, but it's possible that there isn't.
  field_start += field.length() + 1;
  if ( field_start < ( header.length() - 1 ) && header[ field_start ] == ' ' ) {
	field_start++;
  }

  // See if there's anything left, to shortcut the rest of the
  // function.
  if ( field_start == header.length() - 1 ) {
	return string( "" );
  }

  // The field continues to the end of line. If the start of the next
  // line is whitespace, then the field continues.
  idx = field_start;
  while( field_end == string::npos ) {
	idx = header.find( "\n", idx );

	// if we don't find a "\n", gobble everything to the end of the headers
	if ( idx == string::npos ) {
	  field_end = header.length();
	} else {
	  // check the next character
	  if (( idx + 1 ) < header.length() && ( isspace( header[ idx + 1 ] ))) {
		idx ++; // whitespace found, so wrap to the next line
	  } else {
		field_end = idx;
	  }
	}
  }

  /* if the header line ends in \r\n, don't return the \r */
  if (header[field_end-1] == '\r')
  	field_end--;

  string data = header.substr( field_start, field_end - field_start );
  
  /* Replace all CRLF pairs with LF */
  idx = 0;
  while ( (idx = data.find("\r\n", idx)) != string::npos )
  {
  	data.replace(idx,2,"\n");
  }

  return data;
}


// }}}

// {{{ MLFI callbacks

//
// Gets called once when a client connects to sendmail
//
// gets the originating IP address and checks it against the ignore list
// if it isn't in the list, store the IP in a structure and store a 
// pointer to it in the private data area.
//
sfsistat 
mlfi_connect(SMFICTX * ctx, char *hostname, _SOCK_ADDR * hostaddr)
{
	struct context *sctx;
	int rv;

	debug(D_FUNC, "mlfi_connect: enter");

	/* allocate a structure to store the IP address (and SA object) in */
	sctx = (struct context *)malloc(sizeof(*sctx));
	if (!hostaddr)
	{
		/* not a socket; probably a local user calling sendmail directly */
		/* set to 127.0.0.1 */
		sctx->connect_ip.s_addr = htonl(INADDR_LOOPBACK);
	} else
	{
		sctx->connect_ip = ((struct sockaddr_in *) hostaddr)->sin_addr;
	}
	sctx->assassin = NULL;
	sctx->helo = NULL;
	
	/* store a pointer to it with setpriv */
	rv = smfi_setpriv(ctx, sctx);
	if (rv != MI_SUCCESS)
	{
		debug(D_ALWAYS, "smfi_setpriv failed!");
		return SMFIS_TEMPFAIL;
	}
	/* debug(D_ALWAYS, "ZZZ set private context to %p", sctx); */

	if (ip_in_networklist(sctx->connect_ip, &ignorenets))
	{
		debug(D_NET, "%s is in our ignore list - accepting message",
		    inet_ntoa(sctx->connect_ip));
		debug(D_FUNC, "mlfi_connect: exit ignore");
		return SMFIS_ACCEPT;
	}
	
	// Tell Milter to continue
	debug(D_FUNC, "mlfi_connect: exit");

	return SMFIS_CONTINUE;
}

//
// Gets called on every "HELO"
//
// stores the result in the context structure
//
sfsistat mlfi_helo(SMFICTX * ctx, char * helohost)
{
	struct context *sctx = (struct context*)smfi_getpriv(ctx);
	if (sctx->helo)
		free(sctx->helo);
	sctx->helo = strdup(helohost);

	return SMFIS_CONTINUE;
}

//
// Gets called first for all messages
//
// creates SpamAssassin object and makes pointer to it
// private data of this filter process
//
sfsistat
mlfi_envfrom(SMFICTX* ctx, char** envfrom)
{
  SpamAssassin* assassin;
  struct context *sctx = (struct context *)smfi_getpriv(ctx);
  char *queueid;

  if (sctx == NULL)
  {
    debug(D_ALWAYS, "smfi_getpriv failed!");
    return SMFIS_TEMPFAIL;
  }
  /* debug(D_ALWAYS, "ZZZ got private context %p", sctx); */

  debug(D_FUNC, "mlfi_envfrom: enter");
  try {
    // launch new SpamAssassin
    assassin=new SpamAssassin;
  } catch (string& problem)
    {
      throw_error(problem);
      return SMFIS_TEMPFAIL;
    };
  
  assassin->set_connectip(string(inet_ntoa(sctx->connect_ip)));

  // Store a pointer to the assassin object in our context struct
  sctx->assassin = assassin;

  // remember the MAIL FROM address
  assassin->set_from(string(envfrom[0]));
  
  queueid=smfi_getsymval(ctx,"i");
  if (!queueid)
  {
    queueid="unknown";
    warnmacro("i", "ENVFROM");
  }
  assassin->queueid = queueid;

  debug(D_MISC, "queueid=%s", queueid);

  // tell Milter to continue
  debug(D_FUNC, "mlfi_envfrom: exit");

  return SMFIS_CONTINUE;
}

//
// Gets called once for each recipient
//
// stores the first recipient in the spamassassin object and
// stores all addresses and the number thereof (some redundancy)
//


sfsistat
mlfi_envrcpt(SMFICTX* ctx, char** envrcpt)
{
	struct context *sctx = (struct context*)smfi_getpriv(ctx);
	SpamAssassin* assassin = sctx->assassin;
	FILE *p;
#if defined(__FreeBSD__)
	int rv;
#endif

	debug(D_FUNC, "mlfi_envrcpt: enter");

	if (flag_expand)
	{
		/* open a pipe to sendmail so we can do address expansion */

		char buf[1024];
		char *fmt="%s -bv \"%s\" 2>&1";

#if defined(HAVE_SNPRINTF)
		snprintf(buf, sizeof(buf)-1, fmt, SENDMAIL, envrcpt[0]);
#else
		/* XXX possible buffer overflow here */
		sprintf(buf, fmt, SENDMAIL, envrcpt[0]);
#endif

		debug(D_RCPT, "calling %s", buf);

#if defined(__FreeBSD__) /* popen bug - see PR bin/50770 */
		rv = pthread_mutex_lock(&popen_mutex);
		if (rv)
		{
			debug(D_ALWAYS, "Could not lock popen mutex: %s", strerror(rv));
			abort();
		}		
#endif

		p = popen(buf, "r");
		if (!p)
		{
			debug(D_RCPT, "popen failed(%s).  Will not expand aliases", strerror(errno));
			assassin->expandedrcpt.push_back(envrcpt[0]);
		} else
		{
			while (fgets(buf, sizeof(buf), p) != NULL)
			{
				int i = strlen(buf);
				/* strip trailing EOLs */
				while (i > 0 && buf[i - 1] <= ' ')
					i--;
				buf[i] = '\0';
				debug(D_RCPT, "sendmail output: %s", buf);
				/*	From a quick scan of the sendmail source, a valid email
					address gets printed via either
					    "deliverable: mailer %s, host %s, user %s"
					or  "deliverable: mailer %s, user %s"
				*/
				if (strstr(buf, "... deliverable: mailer "))
				{
					char *p=strstr(buf,", user ");
					/* anything after ", user " is the email address */
					debug(D_RCPT, "user: %s", p+7);
					assassin->expandedrcpt.push_back(p+7);
				}
			}
			pclose(p); p = NULL;
		}
#if defined(__FreeBSD__)
		rv = pthread_mutex_unlock(&popen_mutex);
		if (rv)
		{
			debug(D_ALWAYS, "Could not unlock popen mutex: %s", strerror(rv));
			abort();
		}		
#endif
	} else
	{
		assassin->expandedrcpt.push_back(envrcpt[0]);
	}	
	debug(D_RCPT, "Total of %d actual recipients", (int)assassin->expandedrcpt.size());

	if (assassin->numrcpt() == 0)
	{
		/* Send the envelope headers as X-Envelope-From: and
		   X-Envelope-To: so that SpamAssassin can use them in its
		   whitelist checks.  Also forge as complete a dummy
		   Received: header as possible because SA gets a lot of
		   info from it.
		   
			HReceived: $?sfrom $s $.$?_($?s$|from $.$_)
				$.$?{auth_type}(authenticated$?{auth_ssf} bits=${auth_ssf}$.)
				$.by $j ($v/$Z)$?r with $r$. id $i$?{tls_version}
				(version=${tls_version} cipher=${cipher} bits=${cipher_bits} verify=${verify})$.$?u
				for $u; $|;
				$.$b$?g
				(envelope-from $g)$.
		   
		*/
		const char *macro_b, *macro_i, *macro_j, *macro_r,
		           *macro_s, *macro_v, *macro_Z, *macro__;
		char date[32];

		/* RFC 822 date. */
		macro_b = smfi_getsymval(ctx, "b");
		if (!macro_b)                                  
		{
			time_t tval;
			time(&tval);
			strftime(date, sizeof(date), "%a, %d %b %Y %H:%M:%S %z", localtime(&tval));
			macro_b = date;
			warnmacro("b", "ENVRCPT");
		}

		/* queue ID */
		macro_i = smfi_getsymval(ctx, "i");
		if (!macro_i)
		{
			macro_i = "unknown";
			warnmacro("i", "ENVRCPT");
		}

		/* FQDN of this site */
		macro_j = smfi_getsymval(ctx, "j");
		if (!macro_j)
		{
			macro_j = "localhost";
			warnmacro("j", "ENVRCPT");
		}

		/* Protocol used to receive the message */
		macro_r = smfi_getsymval(ctx, "r");
		if (!macro_r)
		{
			macro_r = "SMTP";
			warnmacro("r", "ENVRCPT");
		}
			
		/* Sendmail currently cannot pass us the {s} macro, but
		   I do not know why.  Leave this in for the day sendmail is
		   fixed.  Until that day, use the value remembered by
		   mlfi_helo()
		*/
		macro_s = smfi_getsymval(ctx, "s");
		if (!macro_s)
			macro_s = sctx->helo;
		if (!macro_s)
			macro_s = "nohelo";

		/* Sendmail binary version */
		macro_v = smfi_getsymval(ctx, "v");
		if (!macro_v)
		{
			macro_v = "8.13.0";
			warnmacro("v", "ENVRCPT");
		}

		/* Sendmail .cf version */
		macro_Z = smfi_getsymval(ctx, "Z");
		if (!macro_Z)
		{
			macro_Z = "8.13.0";
			warnmacro("Z", "ENVRCPT");
		}

		/* Validated sending site's address */
		macro__ = smfi_getsymval(ctx, "_");
		if (!macro__)
		{
			macro__ = "unknown";
			warnmacro("_", "ENVRCPT");
		}

		assassin->output((string)"X-Envelope-From: "+assassin->from()+"\r\n");
		assassin->output((string)"X-Envelope-To: "+envrcpt[0]+"\r\n");

		assassin->output((string)
			"Received: from "+macro_s+" ("+macro__+")\r\n\t"+
			"by "+macro_j+"("+macro_v+"/"+macro_Z+") with "+macro_r+" id "+macro_i+"\r\n\t"+
			macro_b+"\r\n\t"+
			"(envelope-from "+assassin->from()+"\r\n");

	} else
		assassin->output((string)"X-Envelope-To: "+envrcpt[0]+"\r\n");

	/* increment RCPT TO: count */
	assassin->set_numrcpt();

	/* If we expanded to at least one user and we haven't recorded one yet,
	   record the first one */
	if (!assassin->expandedrcpt.empty() && (assassin->rcpt().size() == 0))
	{
		debug(D_RCPT, "remembering %s for spamc", assassin->expandedrcpt.front().c_str());
		assassin->set_rcpt(assassin->expandedrcpt.front());
	}

	debug(D_RCPT, "remembering recipient %s", envrcpt[0]);
	assassin->recipients.push_back( envrcpt[0] ); // XXX verify that this worked

	debug(D_FUNC, "mlfi_envrcpt: exit");

	return SMFIS_CONTINUE;
}

//
// Gets called repeatedly for all header fields
//
// assembles the headers and passes them on to the SpamAssassin client
// through the pipe.
//
// only exception: SpamAssassin header fields (X-Spam-*) get suppressed
// but are being stored in the SpamAssassin element.
//
// this function also starts the connection with the SPAMC program the
// first time it is called.
//

sfsistat
mlfi_header(SMFICTX* ctx, char* headerf, char* headerv)
{
  SpamAssassin* assassin = ((struct context *)smfi_getpriv(ctx))->assassin;
  debug(D_FUNC, "mlfi_header: enter");

  // Check if the SPAMC program has already been run, if not we run it.
  if ( !(assassin->connected) )
     {
       try {
         assassin->connected = 1; // SPAMC is getting ready to run
         assassin->Connect();
       } 
       catch (string& problem) {
         throw_error(problem);
         ((struct context *)smfi_getpriv(ctx))->assassin=NULL;
         delete assassin;
         debug(D_FUNC, "mlfi_header: exit error connect");
         return SMFIS_TEMPFAIL;
       };
     }

  // Is it a "X-Spam-" header field?
  if ( cmp_nocase_partial("X-Spam-", headerf) == 0 )
    {
      int suppress = 1;
      // memorize content of old fields

      if ( cmp_nocase_partial("X-Spam-Status", headerf) == 0 )
	assassin->set_spam_status(headerv);
      else if ( cmp_nocase_partial("X-Spam-Flag", headerf) == 0 )
	assassin->set_spam_flag(headerv);
      else if ( cmp_nocase_partial("X-Spam-Report", headerf) == 0 )
	assassin->set_spam_report(headerv);
      else if ( cmp_nocase_partial("X-Spam-Prev-Content-Type", headerf) == 0 )
	assassin->set_spam_prev_content_type(headerv);
      else if ( cmp_nocase_partial("X-Spam-Level", headerf) == 0 )
	assassin->set_spam_level(headerv);
      else if ( cmp_nocase_partial("X-Spam-Checker-Version", headerf) == 0 )
	assassin->set_spam_checker_version(headerv);
      else
      {
      	/* Hm. X-Spam header, but not one we recognize.  Pass it through. */
      	suppress = 0;
      }
      
      if (suppress)
      {
	debug(D_FUNC, "mlfi_header: suppress");
	return SMFIS_CONTINUE;
      }
    }

  // Content-Type: will be stored if present
  if ( cmp_nocase_partial("Content-Type", headerf) == 0 )
    assassin->set_content_type(headerv);

  // Subject: should be stored
  if ( cmp_nocase_partial("Subject", headerf) == 0 )
    assassin->set_subject(headerv);

  // assemble header to be written to SpamAssassin
  string header = string(headerf) + ": " + headerv + "\r\n";
 
  try {
    // write to SpamAssassin client
    assassin->output(header.c_str(),header.size());
  } catch (string& problem)
    {
      throw_error(problem);
      ((struct context *)smfi_getpriv(ctx))->assassin=NULL;
      delete assassin;
      debug(D_FUNC, "mlfi_header: exit error output");
      return SMFIS_TEMPFAIL;
    };
  
  // go on...
  debug(D_FUNC, "mlfi_header: exit");

  return SMFIS_CONTINUE;
}

// 
// Gets called once when the header is finished.
//
// writes empty line to SpamAssassin client to separate
// headers from body.
//
sfsistat
mlfi_eoh(SMFICTX* ctx)
{
  SpamAssassin* assassin = ((struct context *)smfi_getpriv(ctx))->assassin;

  debug(D_FUNC, "mlfi_eoh: enter");

  // Check if the SPAMC program has already been run, if not we run it.
  if ( !(assassin->connected) )
     {
       try {
         assassin->connected = 1; // SPAMC is getting ready to run
         assassin->Connect();
       } 
       catch (string& problem) {
         throw_error(problem);
         ((struct context *)smfi_getpriv(ctx))->assassin=NULL;
         delete assassin;

         debug(D_FUNC, "mlfi_eoh: exit error connect");
         return SMFIS_TEMPFAIL;
       };
     }

  try {
    // add blank line between header and body
    assassin->output("\r\n",2);
  } catch (string& problem)
    {
      throw_error(problem);
      ((struct context *)smfi_getpriv(ctx))->assassin=NULL;
      delete assassin;
  
      debug(D_FUNC, "mlfi_eoh: exit error output");
      return SMFIS_TEMPFAIL;
    };
  
  // go on...

  debug(D_FUNC, "mlfi_eoh: exit");
  return SMFIS_CONTINUE;
}

//
// Gets called repeatedly to transmit the body
//
// writes everything directly to SpamAssassin client
//
sfsistat
mlfi_body(SMFICTX* ctx, u_char *bodyp, size_t bodylen)
{
  debug(D_FUNC, "mlfi_body: enter");
  SpamAssassin* assassin = ((struct context *)smfi_getpriv(ctx))->assassin;

 
  try {
    assassin->output(bodyp, bodylen);
  } catch (string& problem)
    {
      throw_error(problem);
      ((struct context *)smfi_getpriv(ctx))->assassin=NULL;
      delete assassin;
      debug(D_FUNC, "mlfi_body: exit error");
      return SMFIS_TEMPFAIL;
    };

  // go on...
  debug(D_FUNC, "mlfi_body: exit");
  return SMFIS_CONTINUE;
}

//
// Gets called once at the end of mail processing
//
// tells SpamAssassin client that the mail is complete
// through EOF and then modifies the mail accordingly by
// calling the "assassinate" function
//
sfsistat
mlfi_eom(SMFICTX* ctx)
{
  SpamAssassin* assassin = ((struct context *)smfi_getpriv(ctx))->assassin;
  int milter_status;
 
  debug(D_FUNC, "mlfi_eom: enter");
  try {

    // close output pipe to signal EOF to SpamAssassin
    assassin->close_output();

    // read what the Assassin is telling us
    assassin->input();

    milter_status = assassinate(ctx, assassin);

    // now cleanup the element.
    ((struct context *)smfi_getpriv(ctx))->assassin=NULL;
    delete assassin;

  } catch (string& problem)
    {
      throw_error(problem);
      ((struct context *)smfi_getpriv(ctx))->assassin=NULL;
      delete assassin;
      debug(D_FUNC, "mlfi_eom: exit error");
      return SMFIS_TEMPFAIL;
    };
  
  // go on...
  debug(D_FUNC, "mlfi_eom: exit");
  return milter_status;
}

//
// Gets called on session-basis. This keeps things nice & quiet.
//
sfsistat
mlfi_close(SMFICTX* ctx)
{
  struct context *sctx;
  debug(D_FUNC, "mlfi_close");
  
  sctx = (struct context*)smfi_getpriv(ctx);
  if (sctx == NULL)
    return SMFIS_ACCEPT;

  if (sctx->helo)
  	free(sctx->helo);
  free(sctx);
  smfi_setpriv(ctx, NULL);
  
  return SMFIS_ACCEPT;
}

//
// Gets called when things are being aborted.
//
// kills the SpamAssassin object, its destructor should
// take care of everything.
//
sfsistat
mlfi_abort(SMFICTX* ctx)
{
  SpamAssassin* assassin = ((struct context *)smfi_getpriv(ctx))->assassin;

  debug(D_FUNC, "mlfi_abort");
  ((struct context *)smfi_getpriv(ctx))->assassin=NULL;
  delete assassin;

  return SMFIS_ACCEPT;
}

// }}}

// {{{ SpamAssassin Class

//
// This is a new constructor for the SpamAssassin object.  It simply 
// initializes two variables.  The original constructor has been
// renamed to Connect().
//
SpamAssassin::SpamAssassin():
  error(false),
  running(false),
  connected(false),
  _numrcpt(0)
{
}


SpamAssassin::~SpamAssassin()
{ 
	if (connected) 
	{
		// close all pipes that are still open
		if (pipe_io[0][0] > -1)	close(pipe_io[0][0]);
		if (pipe_io[0][1] > -1)	close(pipe_io[0][1]);
		if (pipe_io[1][0] > -1)	close(pipe_io[1][0]);
		if (pipe_io[1][1] > -1)	close(pipe_io[1][1]);

		// child still running?
		if (running)
		{
			// make sure the pid is valid
			if (pid > 0) {
				// slaughter child
				kill(pid, SIGKILL);

				// wait for child to terminate
				int status;
				waitpid(pid, &status, 0);
			}
		}
    }

	// Clean up the recip list. Might be overkill, but it's good housekeeping.
	while( !recipients.empty()) 
	{
		recipients.pop_front();
	}
	// Clean up the recip list. Might be overkill, but it's good housekeeping.
	while( !expandedrcpt.empty()) 
	{
		expandedrcpt.pop_front();
	}
}

//
// This is the old SpamAssassin constructor.  It has been renamed Connect(),
// and is now called at the beginning of the mlfi_header() function.
//

void SpamAssassin::Connect()
{
  // set up pipes for in- and output
  if (pipe(pipe_io[0]))
    throw string(string("pipe error: ")+string(strerror(errno)));
  if (pipe(pipe_io[1]))
    throw string(string("pipe error: ")+string(strerror(errno)));

  // now execute SpamAssassin client for contact with SpamAssassin spamd

  // start child process
  switch(pid = fork())
    {
    case -1:
      // forking trouble. throw error.
      throw string(string("fork error: ")+string(strerror(errno)));
      break;
    case 0:
      // +++ CHILD +++
      
      // close unused pipes
      close(pipe_io[1][0]);
      close(pipe_io[0][1]);

      // redirect stdin(0), stdout(1) and stderr(2)
      dup2(pipe_io[0][0],0);
      dup2(pipe_io[1][1],1);
      dup2(pipe_io[1][1],2);

      closeall(3);

      // execute spamc 
      // absolute path (determined in autoconf) 
      // should be a little more secure
      // XXX arbitrary 100-argument max
      int argc = 0;
      char** argv = (char**) malloc(100*sizeof(char*));
      argv[argc++] = SPAMC;
      if (flag_sniffuser) 
      {
        argv[argc++] = "-u";
        if ( expandedrcpt.size() != 1 )
        {
          // More (or less?) than one recipient, so we pass the default
          // username to SPAMC.  This way special rules can be defined for
          // multi recipient messages.
          debug(D_RCPT, "%d recipients; spamc gets default username %s", (int)expandedrcpt.size(), defaultuser);
          argv[argc++] = defaultuser; 
        } else
        { 
          // There is only 1 recipient so we pass the username
          // (converted to lowercase) to SPAMC.  Don't worry about 
          // freeing this memory as we're exec()ing anyhow.
          if (flag_full_email)
            argv[argc] = strlwr(strdup(full_user().c_str())); 
          else
            argv[argc] = strlwr(strdup(local_user().c_str())); 

          debug(D_RCPT, "spamc gets %s", argv[argc]);
         
          argc++;
        }
      }
      if (spamdhost) 
      {
        argv[argc++] = "-d";
        argv[argc++] = spamdhost;
      }
      if (spamc_argc)
      {
      	memcpy(argv+argc, spamc_argv, spamc_argc * sizeof(char *));
      	argc += spamc_argc;
      }
      argv[argc++] = 0;

      execvp(argv[0] , argv); // does not return!

      // execution failed
      throw_error(string("execution error: ")+string(strerror(errno)));
      _exit(1);
      break;
    }

  // +++ PARENT +++

  // close unused pipes
  close(pipe_io[0][0]);
  close(pipe_io[1][1]);
  pipe_io[0][0]=-1;
  pipe_io[1][1]=-1;

  // mark the pipes non-blocking
  if(fcntl(pipe_io[0][1], F_SETFL, O_NONBLOCK) == -1)
     throw string(string("Cannot set pipe01 nonblocking: ")+string(strerror(errno)));
#if 0  /* don't really need to make the sink pipe nonblocking */
  if(fcntl(pipe_io[1][0], F_SETFL, O_NONBLOCK) == -1)
     throw string(string("Cannot set pipe10 nonblocking: ")+string(strerror(errno)));
#endif

  // we have to assume the client is running now.
  running=true;

  /* If we have any buffered output, write it now. */
  if (outputbuffer.size())
  {
    output(outputbuffer);
    outputbuffer="";
  }
}

// write to SpamAssassin
void
SpamAssassin::output(const void* buffer, long size)
{
  debug(D_FUNC, "::output enter");

  debug(D_SPAMC, "output \"%*.*s\"", (int)size, (int)size, (char *)buffer);

  // if there are problems, fail.
  if (error)
    throw string("tried output despite problems. failed.");

  /* If we haven't launched spamc yet, just store the data */
  if (!connected)
  {
	/* Silly C++ can't tell the difference between 
		(const char*, string::size_type) and
		(string::size_type, char), so we have to cast the parameters.
	*/
  	outputbuffer.append((const char *)buffer,(string::size_type)size);
  	debug(D_FUNC, "::output exit1");
  	return;
  }

  // send to SpamAssassin
  long total = 0;
  long wsize = 0;
  string reason;
  int status;
  do {
	struct pollfd fds[2];
	int nfds = 2, nready;
	fds[0].fd = pipe_io[0][1];
	fds[0].events = POLLOUT;
	fds[1].fd = pipe_io[1][0];
	fds[1].events = POLLIN;

	debug(D_POLL, "polling fds %d and %d", pipe_io[0][1], pipe_io[1][0]);
	nready = poll(fds, nfds, 1000);
	if (nready == -1)
		throw("poll failed");

	debug(D_POLL, "poll returned %d, fd0=%d, fd1=%d", nready, fds[0].revents, fds[1].revents);

	if (fds[1].revents & (POLLERR|POLLNVAL|POLLHUP))
	{
		throw string("poll says my read pipe is busted");
	}

	if (fds[0].revents & (POLLERR|POLLNVAL|POLLHUP))
	{
		throw string("poll says my write pipe is busted");
	}

	if (fds[1].revents & POLLIN)
	{
		debug(D_POLL, "poll says I can read");
		read_pipe();
	}

	if (fds[0].revents & POLLOUT)
	{
		debug(D_POLL, "poll says I can write");
		switch(wsize = write(pipe_io[0][1], (char *)buffer + total, size - total))
		{
		  case -1:
			if (errno == EAGAIN)
				continue;
			reason = string(strerror(errno));

			// close the pipes
			close(pipe_io[0][1]);
			close(pipe_io[1][0]);
			pipe_io[0][1]=-1;	
			pipe_io[1][0]=-1;	

			// Slaughter child
			kill(pid, SIGKILL);

			// set flags
			error = true;
			running = false;
	
			// wait until child is dead
			waitpid(pid, &status, 0);

			throw string(string("write error: ")+reason);	
			break;
	      default:
			total += wsize;
			debug(D_POLL, "wrote %ld bytes", wsize);
			break;
		}
	}
  } while ( total < size );

  debug(D_FUNC, "::output exit2");
}

void SpamAssassin::output(const void* buffer)
{
	output(buffer, strlen((const char *)buffer));
}

void SpamAssassin::output(string buffer)
{
	output(buffer.c_str(), buffer.size());
}

// close output pipe
void
SpamAssassin::close_output()
{
  if(close(pipe_io[0][1]))
    throw string(string("close error: ")+string(strerror(errno)));
  pipe_io[0][1]=-1;
}

void
SpamAssassin::input()
{
  debug(D_FUNC, "::input enter");
  // if the child has exited or we experienced an error, return
  // immediately.
  if (!running || error)
  {
    debug(D_FUNC, "::input exit1");
    return;
  }

  // keep reading from input pipe until it is empty
  empty_and_close_pipe();
  
  // that's it, we're through
  running = false;

  // wait until child is dead
  int status;
  if(waitpid(pid, &status, 0)<0)
    {
      error = true;
      throw string(string("waitpid error: ")+string(strerror(errno)));
    }; 
	debug(D_FUNC, "::input exit2");
}

//
// return reference to mail
//
string& 
SpamAssassin::d()
{
  return mail;
}

//
// get values of the different SpamAssassin fields
//
string& 
SpamAssassin::spam_status()
{
  return x_spam_status;
}

string& 
SpamAssassin::spam_flag()
{
  return x_spam_flag;
}

string& 
SpamAssassin::spam_report()
{
  return x_spam_report;
}

string& 
SpamAssassin::spam_prev_content_type()
{
  return x_spam_prev_content_type;
}

string& 
SpamAssassin::spam_checker_version()
{
  return x_spam_checker_version;
}

string& 
SpamAssassin::spam_level()
{
  return x_spam_level;
}

string& 
SpamAssassin::content_type()
{
  return _content_type;
}

string& 
SpamAssassin::subject()
{
  return _subject;
}

string&
SpamAssassin::rcpt()
{
  return _rcpt;
}

string&
SpamAssassin::from()
{
  return _from;
}

string&
SpamAssassin::connectip()
{
  return _connectip;
}


string
SpamAssassin::local_user()
{
  // assuming we have a recipient in the form: <username@somehost.somedomain>
  // (angle brackets optional) we return 'username'
  if (_rcpt[0] == '<')
    return _rcpt.substr(1, _rcpt.find_first_of("@+")-1);
  else
  	return _rcpt.substr(0, _rcpt.find_first_of("@+"));
}

string
SpamAssassin::full_user()
{
  string name;
  // assuming we have a recipient in the form: <username@somehost.somedomain>
  // (angle brackets optional) we return 'username@somehost.somedomain'
  if (_rcpt[0] == '<')
    name = _rcpt.substr(1, _rcpt.find('>')-1);
  else
  	name = _rcpt;
  if(name.find('@') == string::npos)
  {
    /* if the name had no domain part (local delivery), append the default one */
    name = name + "@" + defaultdomain;
  }
  return name;
}

int
SpamAssassin::numrcpt()
{
  return _numrcpt;
}

int
SpamAssassin::set_numrcpt()
{
  _numrcpt++;
  return _numrcpt;
}

int
SpamAssassin::set_numrcpt(const int val)
{
  _numrcpt = val;
  return _numrcpt;
}

//
// set the values of the different SpamAssassin
// fields in our element. Returns former size of field
//
string::size_type
SpamAssassin::set_spam_status(const string& val)
{
  string::size_type old = x_spam_status.size();
  x_spam_status = val;
  return (old);
}

string::size_type
SpamAssassin::set_spam_flag(const string& val)
{
  string::size_type old = x_spam_flag.size();
  x_spam_flag = val;
  return (old);
}

string::size_type
SpamAssassin::set_spam_report(const string& val)
{
  string::size_type old = x_spam_report.size();
  x_spam_report = val;
  return (old);
}

string::size_type
SpamAssassin::set_spam_prev_content_type(const string& val)
{
  string::size_type old = x_spam_prev_content_type.size();
  x_spam_prev_content_type = val;
  return (old);
}

string::size_type
SpamAssassin::set_spam_checker_version(const string& val)
{
  string::size_type old = x_spam_checker_version.size();
  x_spam_checker_version = val;
  return (old);
}

string::size_type
SpamAssassin::set_spam_level(const string& val)
{
  string::size_type old = x_spam_level.size();
  x_spam_level = val;
  return (old);
}

string::size_type
SpamAssassin::set_content_type(const string& val)
{
  string::size_type old = _content_type.size();
  _content_type = val;
  return (old);
}

string::size_type
SpamAssassin::set_subject(const string& val)
{
  string::size_type old = _subject.size();
  _subject = val;
  return (old);
}

string::size_type
SpamAssassin::set_rcpt(const string& val)
{
  string::size_type old = _rcpt.size();
  _rcpt = val;
  return (old);  
}

string::size_type
SpamAssassin::set_from(const string& val)
{
  string::size_type old = _from.size();
  _from = val;
  return (old);  
}

string::size_type
SpamAssassin::set_connectip(const string& val)
{
  string::size_type old = _connectip.size();
  _connectip = val;
  return (old);  
}

//
// Read available output from SpamAssassin client
//
int
SpamAssassin::read_pipe()
{
	long size;
	int  status;
	char iobuff[1024];
	string reason;

	debug(D_FUNC, "::read_pipe enter");

	if (pipe_io[1][0] == -1)
	{
		debug(D_FUNC, "::read_pipe exit - shouldn't have been called?");
		return 0;
	}

	size = read(pipe_io[1][0], iobuff, 1024);

	if (size < 0)
    {
		// Error. 
		reason = string(strerror(errno));
		
		// Close remaining pipe.
		close(pipe_io[1][0]);
		pipe_io[1][0] = -1;
	
		// Slaughter child
		kill(pid, SIGKILL);
	
		// set flags
		error = true;
		running = false;
	
		// wait until child is dead
		waitpid(pid, &status, 0);
	
		// throw the error message that caused this trouble
		throw string(string("read error: ")+reason);
	} else if ( size == 0 )
	{

		// EOF. Close the pipe
		if(close(pipe_io[1][0]))
			throw string(string("close error: ")+string(strerror(errno)));
		pipe_io[1][0] = -1;
	
	} else
	{ 
		// append to mail buffer 
		mail.append(iobuff, size);
		debug(D_POLL, "read %ld bytes", size);
		debug(D_SPAMC, "input  \"%*.*s\"", (int)size, (int)size, iobuff);
	}
	debug(D_FUNC, "::read_pipe exit");
	return size;
}

//
// Read all output from SpamAssassin client
// and close the pipe
//
void
SpamAssassin::empty_and_close_pipe()
{
	debug(D_FUNC, "::empty_and_close_pipe enter");
	while (read_pipe())
		;
	debug(D_FUNC, "::empty_and_close_pipe exit");
}

// }}}

// {{{ Some small subroutines without much relation to functionality

// output error message to syslog facility
void
throw_error(const string& errmsg)
{
  if (errmsg.c_str())
    syslog(LOG_ERR, "Thrown error: %s", errmsg.c_str());
  else
    syslog(LOG_ERR, "Unknown error");
}

/* Takes a comma or space-delimited string of debug tokens and sets the
   appropriate bits in flag_debug.  "all" sets all the bits.
*/
void parse_debuglevel(char* string)
{
	char *token;

	/* make a copy so we don't overwrite argv[] */
	string = strdup(string);

	/* handle the old numeric values too */
	switch(atoi(string))
	{
		case 3:
			flag_debug |= (1<<D_UORI) | (1<<D_STR);
		case 2:
			flag_debug |= (1<<D_POLL);
		case 1:
			flag_debug |= (1<<D_MISC) | (1<<D_FUNC);
			debug(D_ALWAYS, "Setting debug level to 0x%0x", flag_debug);
			free(string);
			return;
		default:
			break;
	}

	while ((token = strsep(&string, ", ")))
	{
		int i;
		for (i=0; debugstrings[i]; i++)
		{
			if(strcasecmp(token, "ALL")==0)
			{
				flag_debug = (1<<D_MAX)-1;
				break;
			}
			if(strcasecmp(token, debugstrings[i])==0)
			{
				flag_debug |= (1<<i);
				break;
			}
		}

		if (!debugstrings[i])
		{
			fprintf(stderr, "Invalid debug token \"%s\"\n", token);
			exit(1);
		}
	}
	debug(D_ALWAYS, "Setting debug level to 0x%0x", flag_debug);
	free(string);
}

/*
   Output a line to syslog using print format, but only if the appropriate
   debug level is set.  The D_ALWAYS level is always enabled.
*/
void debug(enum debuglevel level, const char* fmt, ...)
{
	if ((1<<level) & flag_debug)
	{
#if defined(HAVE_VSYSLOG)
		va_list vl;
		va_start(vl, fmt);
		vsyslog(LOG_ERR, fmt, vl);
		va_end(vl);
#else
#if defined(HAVE_VASPRINTF)
		char *buf;
#else
		char buf[1024];
#endif
		va_list vl;
		va_start(vl, fmt);
#if defined(HAVE_VASPRINTF)
		vasprintf(&buf, fmt, vl);
#else
#if defined(HAVE_VSNPRINTF)
		vsnprintf(buf, sizeof(buf)-1, fmt, vl);
#else
		/* XXX possible buffer overflow here; be careful what you pass to debug() */
		vsprintf(buf, fmt, vl);
#endif
#endif
		va_end(vl);
		syslog(LOG_ERR, "%s", buf);
#if defined(HAVE_VASPRINTF)
		free(buf);
#endif 
#endif /* vsyslog */
	}
}

// case-insensitive search 
string::size_type 
find_nocase(const string& array, const string& pattern, string::size_type start)
{
  string::size_type pos(start);

  while (pos < array.size())
    {
      string::size_type ctr = 0;

      while( (pos+ctr) < array.size() &&
	     toupper(array[pos+ctr]) == toupper(pattern[ctr]) )
	{
	  ++ctr;
	  if (ctr == pattern.size())
	  {
	    debug(D_STR, "f_nc: <%s><%s>: hit", array.c_str(), pattern.c_str());
	    return pos;
	  }
	};
      
      ++pos;
    };

  debug(D_STR, "f_nc: <%s><%s>: nohit", array.c_str(), pattern.c_str());
  return string::npos;
}

// compare case-insensitive
int
cmp_nocase_partial(const string& s, const string& s2)
{
  string::const_iterator p=s.begin();
  string::const_iterator p2=s2.begin();

  while ( p != s.end() && p2 <= s2.end() ) {
    if (toupper(*p) != toupper(*p2))
    {
      debug(D_STR, "c_nc_p: <%s><%s> : miss", s.c_str(), s2.c_str());
      return (toupper(*p) < toupper(*p2)) ? -1 : 1;
    }
    ++p;
    ++p2;
  };

  debug(D_STR, "c_nc_p: <%s><%s> : hit", s.c_str(), s2.c_str());
  return 0;

}

/* closeall() - close all FDs >= a specified value */ 
void closeall(int fd) 
{
	int fdlimit = sysconf(_SC_OPEN_MAX); 
	while (fd < fdlimit) 
		close(fd++); 
}

void parse_networklist(char *string, struct networklist *list)
{
	char *token;

	/* make a copy so we don't overwrite argv[] */
	string = strdup(string);

	while ((token = strsep(&string, ", ")))
	{
		char *tnet = strsep(&token, "/");
		char *tmask = token;
		struct in_addr net, mask;

		if (list->num_nets % 10 == 0)
			list->nets = (struct net*)realloc(list->nets, sizeof(*list->nets) * (list->num_nets + 10));

		if (!inet_aton(tnet, &net))
		{
			fprintf(stderr, "Could not parse \"%s\" as a network\n", tnet);
			exit(1);
		}

		if (tmask)
		{
			if (strchr(tmask, '.') == NULL)
			{
				/* CIDR */
				unsigned int bits;
				int ret;
				ret = sscanf(tmask, "%u", &bits);
				if (ret != 1 || bits > 32)
				{
					fprintf(stderr,"%s: bad CIDR value", tmask);
					exit(1);
				}
				mask.s_addr = htonl(~((1L << (32 - bits)) - 1) & 0xffffffff);
			} else if (!inet_aton(tmask, &mask))
			{
				fprintf(stderr, "Could not parse \"%s\" as a netmask\n", tmask);
				exit(1);
			}
		} else
			mask.s_addr = 0xffffffff;

		{
			char *snet = strdup(inet_ntoa(net));
			debug(D_MISC, "Adding %s/%s to network list", snet, inet_ntoa(mask));
			free(snet);
		}

		net.s_addr = net.s_addr & mask.s_addr;
		list->nets[list->num_nets].network = net;
		list->nets[list->num_nets].netmask = mask;
		list->num_nets++;
	}
	free(string);
}

int ip_in_networklist(struct in_addr ip, struct networklist *list)
{
	int i;

	if (list->num_nets == 0)
		return 0;
		
	debug(D_NET, "Checking %s against:", inet_ntoa(ip));
	for (i = 0; i < list->num_nets; i++)
	{
		debug(D_NET, "%s", inet_ntoa(list->nets[i].network));
		debug(D_NET, "/%s", inet_ntoa(list->nets[i].netmask));
		if ((ip.s_addr & list->nets[i].netmask.s_addr) == list->nets[i].network.s_addr)
        {
        	debug(D_NET, "Hit!");
			return 1;
		}
	}

	return 0;
}

char *strlwr(char *str)
{
    char *s = str;
    while (*s)
    {
        *s = tolower(*s);
        s++;
    }
    return str;
}

/* Log a message about missing milter macros, but only the first time */
void warnmacro(char *macro, char *scope)
{
	if (warnedmacro)
		return;
	debug(D_ALWAYS, "Could not retrieve sendmail macro \"%s\"!.  Please add it to confMILTER_MACROS_%s for better spamassassin results",
		macro, scope);
	warnedmacro = true;
}

// }}}
// vim6:ai:noexpandtab


syntax highlighted by Code2HTML, v. 0.9.1