spamc.c   [plain text]


/*
 * This code is copyright 2001 by Craig Hughes
 * It is licensed under the same license as Perl itself.  The text of this
 * license is included in the SpamAssassin distribution in the file named
 * "License".
 */

#include "config.h"
#include "libspamc.h"
#include "utils.h"

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <syslog.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <netdb.h>
#include <arpa/inet.h>

#ifdef HAVE_SYSEXITS_H
#include <sysexits.h>
#endif
#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif
#ifdef HAVE_SYS_ERRNO_H
#include <sys/errno.h>
#endif
#ifdef HAVE_TIME_H
#include <time.h>
#endif
#ifdef HAVE_SYS_TIME_H
#include <sys/time.h>
#endif
#ifdef HAVE_SIGNAL_H
#include <signal.h>
#endif
#ifdef HAVE_PWD_H
#include <pwd.h>
#endif

/* SunOS 4.1.4 patch from Tom Lipkis <tal@pss.com> */
#if (defined(__sun__) && defined(__sparc__) && !defined(__svr4__)) /* SunOS */ \
     || (defined(__sgi))  /* IRIX */ \
     || (defined(__osf__)) /* Digital UNIX */ \
     || (defined(hpux) || defined(__hpux)) /* HPUX */ \
     || (defined(_WIN32) || defined(__CYGWIN__)) /* CygWin, Win32 */

extern int optind;
extern char *optarg;

#endif

/* safe fallback defaults to on now - CRH */
int flags = SPAMC_RAW_MODE | SPAMC_SAFE_FALLBACK;

/* Aug 14, 2002 bj: global to hold -e command */
char **exec_argv;

static int timeout = 600;

void print_usage(void)
{
  printf("Usage: spamc [options] < message\n\n");
  printf("-B: BSMTP mode - expect input to be a single SMTP-formatted message\n");
  printf("-c: check only - print score/threshold and exit code set to 0 if message is not spam, 1 if spam\n");
  printf("-r: report if spam - print report for spam messages\n");
  printf("-R: report - print report for all messages\n");
  printf("-y: symbols - print only the names of the tests hit\n");
  printf("-d host: specify host to connect to  [default: localhost]\n");
  printf("-e command [args]: Command to output to instead of stdout. MUST BE THE LAST OPTION.\n");
  printf("-f: fallback safely - in case of comms error, dump original message unchanges instead of setting exitcode\n");
  printf("-h: print this help message\n");
  printf("-p port: specify port for connection [default: 783]\n");
  printf("-s size: specify max message size, any bigger and it will be returned w/out processing [default: 250k]\n");
#ifdef SPAMC_SSL
  printf("-S: use SSL to talk to spamd\n");
#endif
  printf("-u username: specify the username for spamd to process this message under\n");
  printf("-x: don't fallback safely - in a comms error, exit with a TEMPFAIL error code\n");
  printf("-t: timeout in seconds to read from spamd. 0 disables. [default: 600]\n\n");
  printf("-H: randomize the IP addresses in the looked-up hostname\n");
  printf("-U path: use UNIX domain socket with path\n");
}

int
read_args(int argc, char **argv, int *max_size, const char **username,
	struct transport *ptrn)
{
  int opt, i, j;

  while(-1 != (opt = getopt(argc,argv,"-BcrRd:e:fhyp:t:s:u:xSHU:")))
  {
    switch(opt)
    {
    case 'H':
      {
        flags |= SPAMC_RANDOMIZE_HOSTS;
        break;
      }
    case 'U':
      {
        ptrn->type       = TRANSPORT_UNIX;
        ptrn->socketpath = optarg;
        break;
      }
    case 'B':
      {
        flags = (flags & ~SPAMC_MODE_MASK) | SPAMC_BSMTP_MODE;
        break;
      }
    case 'c':
      {
        flags |= SPAMC_CHECK_ONLY;
	break;
      }
    case 'r':
      {
        flags |= SPAMC_REPORT_IFSPAM;
	break;
      }
    case 'R':
      {
        flags |= SPAMC_REPORT;
	break;
      }
    case 'y':
      {
        flags |= SPAMC_SYMBOLS;
	break;
      }
    case 'd':
      {
        ptrn->type     = TRANSPORT_TCP;
	ptrn->hostname = optarg;	/* fix the ptr to point to this string */
	break;
      }
    case 'e':
      {
        if((exec_argv=malloc(sizeof(*exec_argv)*(argc-optind+2)))==NULL)
            return EX_OSERR;
        for(i=0, j=optind-1; j<argc; i++, j++){
            exec_argv[i]=argv[j];
        }
        exec_argv[i]=NULL;
        return EX_OK;
      }
    case 'p':
      {
	ptrn->port = atoi(optarg);
	break;
      }
    case 'f':
      {
        flags |= SPAMC_SAFE_FALLBACK;
	break;
      }
    case 'x':
      {
	flags &= (~SPAMC_SAFE_FALLBACK);
	break;
      }
    case 'u':
      {
	*username = optarg;
	break;
      }
    case 's':
      {
	*max_size = atoi(optarg);
	break;
      }
#ifdef SPAMC_SSL
    case 'S':
      {
	flags |= SPAMC_USE_SSL;
	break;
      }
#endif
    case 't':
      {
	timeout = atoi(optarg);
	break;
      }
    case '?': {
      syslog (LOG_ERR, "invalid usage");
      /* NOTE: falls through to usage case below... */
    }
    case 'h':
    case 1:
      {
	print_usage();
	exit(EX_USAGE);
      }
    }
  }
  return EX_OK;
}	

void get_output_fd(int *fd){
    int fds[2];
    pid_t pid;
    
    if(*fd!=-1) return;
    if(exec_argv==NULL){
        *fd=STDOUT_FILENO;
        return;
    }
    if(pipe(fds)){
        syslog(LOG_ERR, "pipe creation failed: %m");
        exit(EX_OSERR);
    }
    pid=fork();
    if(pid<0){
        syslog(LOG_ERR, "fork failed: %m");
        exit(EX_OSERR);
    } else if(pid==0){
        /* child process */
        /* Normally you'd expect the parent process here, however that would
         * screw up an invoker waiting on the death of the parent. So instead,
         * we fork a child to feed the data and have the parent exec the new
         * prog */
        close(fds[0]);
        *fd=fds[1];
        return;
    }
    /* parent process (see above) */
    close(fds[1]);
    if(dup2(fds[0], STDIN_FILENO)){
        syslog(LOG_ERR, "redirection of stdin failed: %m");
        exit(EX_OSERR);
    }
    close(fds[0]); /* no point in leaving extra fds lying around */
    execv(exec_argv[0], exec_argv);
    syslog(LOG_ERR, "exec failed: %m");
    exit(EX_OSERR);
}

int main (int argc, char **argv) {
  int max_size = 250*1024;
  const char *username = NULL;
  int ret;
  struct message m;
  int out_fd;
  struct transport trans;
  int result;

  transport_init(&trans);

#ifdef LIBSPAMC_UNIT_TESTS
  /* unit test support; divert execution.  will not return */
  do_libspamc_unit_tests();
#endif

  openlog ("spamc", LOG_CONS|LOG_PID, LOG_MAIL);
  signal (SIGPIPE, SIG_IGN);

  read_args(argc,argv, &max_size, &username, &trans);

  /*--------------------------------------------------------------------
   * DETERMINE USER
   *
   * If the program's caller didn't identify the user to run as, use the
   * current user for this. Note that we're not talking about UNIX perm-
   * issions, but giving SpamAssassin a username so it can do per-user
   * configuration (whitelists & the like).
   *
   * Since "curr_user" points to static library data, we don't wish to risk
   * some other part of the system overwriting it, so we copy the username
   * to our own buffer - then this won't arise as a problem.
   */
 
  if(NULL == username)
  {
  static char   userbuf[256];
  struct passwd *curr_user;

    curr_user = getpwuid(geteuid());
    if (curr_user == NULL) {
      perror ("getpwuid failed");
            if(flags&SPAMC_CHECK_ONLY) { printf("0/0\n"); return EX_NOTSPAM; } else { return EX_OSERR; }
    }
    memset(userbuf, 0, sizeof userbuf);
    strncpy(userbuf, curr_user->pw_name, sizeof userbuf - 1);
    userbuf[sizeof userbuf - 1] = '\0';
    username = userbuf;
  }

  if ((flags & SPAMC_RANDOMIZE_HOSTS) != 0) {
    /* we don't need strong randomness; this is just so we pick
     * a random host for loadbalancing.
     */
    srand(getpid() ^ time(NULL));
  }

  /*--------------------------------------------------------------------
   * SET UP TRANSPORT
   *
   * This takes the user parameters and digs up what it can about how
   * we connect to the spam daemon. Mainly this involves lookup up the
   * hostname and getting the IP addresses to connect to.
   */
  if ( (ret = transport_setup(&trans, flags)) != EX_OK )
    goto FAIL;


    out_fd=-1;
    m.type    = MESSAGE_NONE;
    m.max_len = max_size;
    m.timeout = timeout;

    ret=message_read(STDIN_FILENO, flags, &m);
    if(ret!=EX_OK) goto FAIL;
    ret=message_filter(&trans, username, flags, &m);
    if(ret!=EX_OK) goto FAIL;
    get_output_fd(&out_fd);

    if(message_write(out_fd, &m)<0) {
      goto FAIL;
    }

    result = m.is_spam;
    if ((flags&SPAMC_CHECK_ONLY) && result != EX_TOOBIG) {
      message_cleanup (&m);
      return result;
    } else {
      message_cleanup (&m);
      return ret;
    }

FAIL:
    get_output_fd(&out_fd);

    result = m.is_spam;
    if((flags&SPAMC_CHECK_ONLY) && result != EX_TOOBIG) {
	/* probably, the write to stdout failed; we can still report exit code */
	message_cleanup (&m);
	return result;

    } else if(flags&SPAMC_CHECK_ONLY || flags&SPAMC_REPORT || flags&SPAMC_REPORT_IFSPAM) {
        full_write(out_fd, "0/0\n", 4);
	message_cleanup (&m);
        return EX_NOTSPAM;

    } else {
        message_dump(STDIN_FILENO, out_fd, &m);
	message_cleanup (&m);
        if (ret == EX_TOOBIG) {
          return 0;
        } else if (flags & SPAMC_SAFE_FALLBACK) {
	  return EX_OK;
	} else {
	  return ret;
	}
    }
}