/*
 *                   Envelope Milter version 0.1.1
 *
 * Rewrite envelope recipients based on the presence of a trigger header.  All
 * existing envelope recipients are removed and replaced with a configurable
 * 'trap' recipient.
 *
 * Copyright (c) 2002,2004 Richard Smith <richard@chaos.org.uk>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   o  Redistributions of source code must retain the above copyright notice,
 *      this list of conditions and the following disclaimer.
 *
 *   o  Redistributions in binary form must reproduce the above copyright no-
 *      tice, this list of conditions and the following disclaimer in the do-
 *      cumentation and/or other materials provided with the distribution.
 *
 *   o  The names of the contributors may not be used to endorse or promote
 *      products derived from this software without specific prior written
 *      permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LI-
 * ABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUEN-
 * TIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEV-
 * ER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABI-
 * LITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
 * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Based on the Sendmail distribution's "A Sample Filter",
 * Copyright (c) 2000 Sendmail, Inc. and its suppliers. All rights reserved.
 *
 * Based on "SpamAssassin Milter",
 * Copyright (c) 2002 Peter 'Luna' Runestig <peter@runestig.com>
 * All rights reserved.
 * 
 */

/*
 * Changelog
 *
 * 0.1.1
 * Allow string to match a substring in the right hand side of header.
 * Add option to set milter name
 * Add a changelog :)
 * 
 * Previous versions have existed as 0.0.1 and 0.1.0
 */

/*
 * Envelope Milter recognises the following command line options:
 *
 * 	-b		Run in the background.  Default is to run in forground.
 * 	-d		Debug mode.  Log extra debuging information.
 * 	-m header	Name of header to match.  Match is case insensitive.
 * 			Default is "X-Spam-Status".
 *	-n name		Name of Milter to pass to sendmail.  Each milter must
 *			have a unique name.  Default is "SpamRedirectFilter".
 * 	-p port		Name of Milter port. Default is "unix:env-milter.sock"
 * 			in the current directory.
 * 	-r recipient	Valid email address of recipient if match header is
 * 			found.  Default is "root@localhost".
 * 	-s string	Right hand side of header to match.  Match is case
 * 			insensitive.  Default is "Yes".
 * 	-t timeout	Time to wait for connection from Sendmail.  Default is
 * 			is configured via sendmail (typically 1800 seconds).
 * 	-x header	Header to add for each removed recipient.  Default is
 * 			"X-Env-Recipient".  If set to an empty string, no
 * 			header will be added.
 *
 *
 * Compile with something like:
 * gcc -o env-milter env-milter.c -lmilter -lsm -pthread
 *
 */

static char copyright[] = "Copyright (c) Richard Smith 2002 <richard@chaos.org.uk>.\n";

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sysexits.h>
#include <getopt.h>
#include <unistd.h>
#include <syslog.h>

#include <libmilter/mfapi.h>

/* Static variables for config defaults, etc. */
static char *refheader = "X-Spam-Status";
static char *refstring = "Yes";
static int reflen = 3;
static char *refrcpt = "root@localhost";
static char *ref_x_header = "X-Env-Recipient";
static char *refconn = "unix:env-milter.sock";
static int reftimeout = -1;
static int refdaemon = 0;
static int refdebug = 0;

/* structures */

/* Recipient address structure */

struct S_ENVRCPT {
    char *rcpt;			/* recipient address */
    struct S_ENVRCPT *next;	/* pointer to next recipient */
};

typedef struct S_ENVRCPT ENVRCPT;

/* Private data structure */

typedef struct S_ENVSESS {
    int isspam;		/* set to one if header match is found */
    ENVRCPT *envrcpt;	/* pointer to first recipient */
} ENVSESS;


/* Private functions */

/* Add a recipient address to the list.  Pointer to the first element of
 * the list is in the private data structure.
 */

sfsistat add_rcpt( ENVRCPT **envrcpt, char *rcpt ) {

    *envrcpt = malloc( sizeof( ENVRCPT ) );
    /* Decline recipient if memory could not be allocated */
    if( *envrcpt == NULL )
	return SMFIS_TEMPFAIL;
    if( refdebug )
	syslog( LOG_DEBUG, "Allocated ENVRCPT" );
    (*envrcpt)->next = NULL;
    (*envrcpt)->rcpt = strdup( rcpt );
    /* Decline recipient if memory could not be allocated */
    if( (*envrcpt)->rcpt == NULL )
	return SMFIS_TEMPFAIL;
    if( refdebug )
	syslog( LOG_DEBUG, "Allocated string" );
    return SMFIS_CONTINUE;
}

/* Release private storage space. These functions die horribly if arg is null */

/* Release recipient address string and ENVRCPT structure containing it.
 * Expects r->next to be saved elsewhere, if the value of r->next is still
 * allocated.
 */

void free_one( ENVRCPT **r ) {

    if( (*r)->rcpt != NULL ) {
	free( (*r)->rcpt );
	if( refdebug )
	    syslog( LOG_DEBUG, "Freed string" );
    }
    free( *r );
    if( refdebug )
	syslog( LOG_DEBUG, "Freed ENVRCPT" );
    *r = NULL;
}

/* Release storage of each recipient address in turn, then release private
 * data structure.
 */

void free_all( SMFICTX *ctx, ENVSESS **s ) {
    ENVRCPT *o = NULL;

    while( (*s)->envrcpt != NULL ) {
	if( (*s)->envrcpt->next != NULL ) {
	    /* Free up the first in the list and link to the second */
	    o = (*s)->envrcpt->next;
	    free_one( &(*s)->envrcpt );
	    (*s)->envrcpt = o;
	} else {
	    /* This causes the while loop to terminate */
	    free_one( &(*s)->envrcpt );
	}
    }
    free( *s );
    if( refdebug )
	syslog( LOG_DEBUG, "Freed ENVSESS" );
    *s = NULL;
    smfi_setpriv( ctx, NULL );
}

/* Callback functions */

/* Called once for each recipient of a message.  Builds a singly linked
 * list of recipients.
 */

sfsistat mlfi_envrcpt( SMFICTX *ctx, char **argv ) {
    ENVSESS *s = (ENVSESS *) smfi_getpriv( ctx );
    char *rcpt = smfi_getsymval( ctx, "{rcpt_addr}" );
    ENVRCPT *o = NULL;
    sfsistat retval = SMFIS_CONTINUE;

    if( s == NULL ) {
	/* First call - need to set up private data structure */
	s = malloc( sizeof( ENVSESS ) );
	/* Decline recipient if memory could not be allocated */
	if( s == NULL )
	    return SMFIS_TEMPFAIL;
	if( refdebug )
	    syslog( LOG_DEBUG, "Allocated ENVSESS" );
	/* Save pointer to private data stucture in context */
	smfi_setpriv( ctx, s );
	s->isspam = 0;
	s->envrcpt = NULL;
    }

    /* Insert at begining of existing recipient list */
    /* Save pointer to next structure (may be NULL) */
    o = s->envrcpt;
    /* Add recipient to private data structure */
    if( (retval = add_rcpt( &s->envrcpt, rcpt )) == SMFIS_CONTINUE ) {
	/* Restore saved pointer to existing list (may be NULL) */
	s->envrcpt->next = o;
    } else if( s->envrcpt == NULL ) {
	/* Allocation of ENVRCPT structure failed */
	s->envrcpt = o;
    } else {
	/* Must have been the strdup that failed, free the ENVRCPT structure */
	free( s->envrcpt );
	if( refdebug )
	    syslog( LOG_DEBUG, "Freed ENVRCPT - string allocation failed" );
	s->envrcpt = o;
    }
    return retval;
}

/* Called once for each header.  Looks for a match with refheader and
 * refstring.
 */

sfsistat mlfi_header( SMFICTX *ctx, char *headerf, char *headerv ) {
    ENVSESS *s = (ENVSESS *) smfi_getpriv( ctx );
    int i;
    int len = strlen(headerv);

    if( s != NULL ) {
	/* Check for matching header name */
	if( len >= reflen && strcasecmp( refheader, headerf ) == 0 ) {
	    /* Check for string in value of matching header name */
	    for( i=0; i<=(len-reflen); i++ ) {
		if( strncasecmp( refstring, headerv+i, reflen ) == 0 ) {
		    /* Set isspam flag and exit the loop on match */
		    s->isspam = 1;
		    break;
		}
	    }
	}
    }
    return SMFIS_CONTINUE;
}

/* Called once for each message.  This is where the recipients are changed
 * and the 'X-' header (if any) is added.
 */

sfsistat mlfi_eom( SMFICTX *ctx ) {
    ENVSESS *s = (ENVSESS *) smfi_getpriv( ctx );
    ENVRCPT *i = NULL;

    /* Only need to do anything if private data structure exists */
    if( s != NULL ) {
	/* Is the spam flag set? */
	if( s->isspam == 1 ) {
	    /* Delete existing envelope recipients */
	    for( i = s->envrcpt; i != NULL; i = i->next ) {
		if( i->rcpt != NULL && *i->rcpt != '\0' ) {
		    if( smfi_delrcpt( ctx, i->rcpt ) != MI_SUCCESS ) {
			free_all( ctx, &s );
			return( SMFIS_TEMPFAIL );
		    }
		    if( ref_x_header != NULL ) {
			if( smfi_addheader( ctx, ref_x_header, i->rcpt ) != MI_SUCCESS ) {
			    free_all( ctx, &s );
			    return( SMFIS_TEMPFAIL );
			}
		    }
		}
	    }
	    /* Add new recipient */
	    if( smfi_addrcpt( ctx, refrcpt ) != MI_SUCCESS ) {
		free_all( ctx, &s );
		return( SMFIS_TEMPFAIL );
	    }
	}
	/* Free private data */
	free_all( ctx, &s );
    }
    return SMFIS_CONTINUE;
}

/* Called if message is aborted for any reason.
 */

sfsistat mlfi_abort( SMFICTX *ctx ) {
    ENVSESS *s = (ENVSESS *) smfi_getpriv( ctx );

    /* Only need to do anything if private data structure exists */
    if( s != NULL ) {
	/* Free private data */
	free_all( ctx, &s );
    }
    return SMFIS_CONTINUE;
}

struct smfiDesc smfilter = {
    "SpamRedirectFilter",		/* filter name */
    SMFI_VERSION,	/* version code -- do not change */
    SMFIF_ADDRCPT | SMFIF_DELRCPT | SMFIF_ADDHDRS,	/* flags */
    NULL,		/* connection info filter */
    NULL,		/* SMTP HELO command filter */
    NULL,		/* envelope sender filter */
    mlfi_envrcpt,	/* envelope recipient filter */
    mlfi_header,	/* header filter */
    NULL,		/* end of header */
    NULL,		/* body block filter */
    mlfi_eom,		/* end of message */
    mlfi_abort,		/* message aborted */
    NULL,		/* connection cleanup */
};

int main(int argc, char **argv) {
    int c;
    const char *args = ":bdm:n:p:r:s:t:x:";
    pid_t pid;

    openlog( "env-milter", LOG_PID, LOG_MAIL );
    syslog( LOG_NOTICE, "Starting" );
    /* File perm mask for milter socket */
    umask( 0177 );
    /* Process commandline options */
    while( ( c = getopt( argc, argv, args ) ) != -1 ) {
	switch (c) {
	    case 'b':
		refdaemon = 1;
		break;
	    case 'd':
		refdebug = 1;
		break;
	    case 'm':
		if( optarg == NULL || *optarg == '\0' ) {
		    (void) fprintf( stderr, "Unspecified match header.\n" );
		    exit( EX_USAGE );
		}
		if( !( refheader = strdup( optarg ) ) ) {
		    (void) fprintf( stderr, "Out of memory.\n" );
		    exit( EX_OSERR );
		}
		break;
	    case 'n':
		if( optarg == NULL || *optarg == '\0' ) {
		    (void) fprintf( stderr, "Unspecified milter name.\n" );
		    exit( EX_USAGE );
		}
		if( !( smfilter.xxfi_name = strdup( optarg ) ) ) {
		    (void) fprintf( stderr, "Out of memory.\n" );
		    exit( EX_OSERR );
		}
		break;
	    case 'p':
		if( optarg == NULL || *optarg == '\0' ) {
		    (void) fprintf( stderr, "Unspecified Milter port.\n" );
		    exit( EX_USAGE );
		}
		if( !( refconn = strdup( optarg ) ) ) {
		    (void) fprintf( stderr, "Out of memory.\n" );
		    exit( EX_OSERR );
		}
		break;
	    case 'r':
		if( optarg == NULL || *optarg == '\0' ) {
		    (void) fprintf( stderr, "Unspecified trap recipient.\n" );
		    exit( EX_USAGE );
		}
		if( !( refrcpt = strdup( optarg ) ) ) {
		    (void) fprintf( stderr, "Out of memory.\n" );
		    exit( EX_OSERR );
		}
		break;
	    case 's':
		if( optarg == NULL || *optarg == '\0' ) {
		    (void) fprintf( stderr, "Unspecified match text.\n" );
		    exit( EX_USAGE );
		}
		if( !( refstring = strdup( optarg ) ) ) {
		    (void) fprintf( stderr, "Out of memory.\n" );
		    exit( EX_OSERR );
		}
		reflen = strlen( refstring );
		break;
	    case 't':
		if( optarg == NULL || *optarg == '\0' ) {
		    (void) fprintf( stderr, "Unspecified timeout.\n" );
		    exit( EX_USAGE );
		}
		reftimeout = atoi( optarg );
		break;
	    case 'x':
		if( optarg == NULL || *optarg == '\0' ) {
		    ref_x_header = NULL;
		} else if( !( ref_x_header = strdup( optarg ) ) ) {
		    (void) fprintf( stderr, "Out of memory.\n" );
		    exit( EX_OSERR );
		}
		break;
	    case '?':
		(void) fprintf( stderr, "Unrecognised option: '%c'.\n", optopt );
		exit( EX_USAGE );
		break;
	    case ':':
		(void) fprintf( stderr, "Option '%c' requires an argument.\n", optopt );
		exit( EX_USAGE );
		break;
	}
    }

    /* Minimal backgrounding */
    if( refdaemon ) {
	if( !freopen( "/dev/null", "r", stdin ) )
	    exit( EX_OSERR );
	if( !freopen( "/dev/null", "w", stdout ) )
	    exit( EX_OSERR );
	if( !freopen( "/dev/null", "w", stderr ) )
	    exit( EX_OSERR );
	if( ( pid = fork() ) == -1 )
	    exit( EX_OSERR );
	if( pid != 0 )
	    exit( EX_OK );
    }

    /* Register the filter */
    if( smfi_register( smfilter ) != MI_SUCCESS )
	exit( EX_UNAVAILABLE );

    /* Specify the socket to use */
    if( smfi_setconn( refconn ) != MI_SUCCESS )
	exit( EX_UNAVAILABLE );

    /* Optionally set a timeout */
    if( reftimeout >= 0 ) {
	if( smfi_settimeout( reftimeout ) != MI_SUCCESS )
	    exit( EX_UNAVAILABLE );
    }

    /* Hand control to libmilter */
    if( smfi_main() != MI_SUCCESS )
	exit( EX_UNAVAILABLE );
    syslog( LOG_NOTICE, "Stopping" );
    closelog();
    exit( EX_OK );
}

