#define HASSNPRINTF 1
#ifndef __P
#define __P(proto) proto
#endif

#include <ctype.h>
#include <fcntl.h>
#ifdef linux
#include <getopt.h>
#include <sys/ioctl.h>
#endif
#include <stdio.h>
#include <string.h>
#include <sysexits.h>
#include <termios.h>
#include <unistd.h>
#ifdef __FreeBSD__
#include <stdlib.h>
#include <netdb.h>
#include <unistd.h>
#endif
#include "libmilter/mfapi.h"

/*
 * Al Smith <Al.Smith@aeschi.ch.eu.org>  May 9 2000
 *
 * Enhancements submitted by Sven Nielsen <dalvenjah@dal.net>  May 13 2000
 * Inline rejection suggested by Paul Yeh <Paul.Yeh@abc.com>   May 24 2000
 *
 * Please note that this code requires sendmail-8.11.0 or newer.
 *
 * This code is distributed under the GNU General Public License.
 * Share and enjoy.
 */

/*
 * HISTORY
 * February 6 2004 -- vbsfilter-1.15
 * Executable name could be in Quoted-Printable format.
 * Thanks to David Chapeau for reporting this.
 *
 * June 26 2003 -- vbsfilter-1.14
 * New version of sobig, using the .zi extension. Block this.
 * Thanks to David F. Russell for reporting this.
 *
 * January 30 2003
 * Implement some suggestions from David F. Russell.
 *
 * January 14 2003 -- vbsfilter-1.11
 * Recognise quoted filenames with missing end quote.
 * Problem (and fix!) supplied by Ian j Hart <ianjhart@ntlworld.com>
 * and Andrea Adams <andrea@vividimage.com> in response to a virus
 * termed W32.Sobig.A@mm. Thanks!
 *
 * November 12 2002 -- vbsfilter-1.10
 * Use ioctl(TIOCNOTTY) on /dev/tty.
 *
 * Herbert Straub <h.straub@aon.at>
 * close STDIN, STDOUT and STDERR to avoid hanging processes on exit.
 *
 * June 12 2002 -- vbsfilter-1.9
 * Ross Bergman <rbergman@vividusa.com> noted that some malformed endings,
 * for example "foo.exe.". Also that a file can have a CLSID as a file
 * extension, again hidden by windows, and Windows will use the CLSID to
 * determine how to open the attachment.
 *
 * January 30 2002 -- vbsfilter-1.8
 * Updated vbsfilter to detect uuencoded attachments.
 * Thanks to Matthew Wong <matthew_wong@aelhk.com.hk> for noting
 * that the "MyParty" virus spreads itself in this way.
 *
 * August 6 2001 -- vbsfilter-1.7
 * Various fixes for the case where headers and/or body are empty.
 * Patch sent in by Sergiy Zhuk <serge@yahoo-inc.com>
 *
 * Update "dangerous extension" list.
 * Noted by David F. Russell <David.F.Russell@ncmail.net>
 *
 * July 25 2001 -- vbsfilter-1.6
 * Also recognise 'name=blah' as well as the more usual 'name="blah"'.
 * Patch sent in by Andrea Adams <andrea@vividimage.com>
 *
 * July 9 2001 -- vbsfilter-1.5
 * Add support to reject arbitrary extensions.
 *
 * March 31 2001 -- vbsfilter-1.4
 * Update for sendmail-8.12
 *
 * February 19 2001 -- vbsfilter-1.3a
 * Non-zero terminated string error, noted by
 * Dirk Meyer <dirk.meyer@dinoex.sub.org>
 *
 * July 21 2000 -- vbsfilter-1.3
 * Update for sendmail-8.11
 *
 * June 20 2000 -- vbsfilter-1.2
 * Add recognition of .SHS attachments.
 *
 * May 24 2000 -- vbsfilter-1.1
 * Add recognition of inline VBS attachments.
 * Libmilter doesn't let us modify the headers, so such mails
 * must be rejected.
 *
 * May  9 2000 -- vbsfilter-1.0
 * Filter incoming visual basic script attachments into text attachments,
 * by changing the .vbs filename suffix into .txt.
 */

/*
 * TODO
 *
 * detect any mime type, such as ms/vb-script or whathaveyou and
 * also change that into something harmless like text/plain
 */

struct mlfiPriv {
	int   len;
	u_char *body;
	int   addver;
};

/*
 * Microsoft considers these extensions dangerous:
 * http://support.microsoft.com/support/kb/articles/Q262/6/17.ASP
 */
static char *exts[] = { "ade", "adp", "bas", "bat", "chm", "cmd", "com",
	"cpl", "crt", "exe", "hlp", "hta", "inf", "ins", "isp", "js", "jse",
	"lnk", "mdb", "mde", "msc", "msi", "msp", "mst", "pcd", "pif", "reg",
	"scr", "sct", "shs", "shb", "url", "vb", "vbe", "vbs", "wsc", "wsf",
	"wsh", "zi", NULL };

#define VERTEXT "X-Filter-Version"
#define VERSION "1.15"

#define MLFIPRIV ((struct mlfiPriv *) smfi_getpriv(ctx))

extern sfsistat mlfi_cleanup(SMFICTX *, sfsistat);

static int nocase_strncmp(const char *s1, const char *s2, int len) {
	register int n = len;

	for (; n; s1++, s2++, n--) {
		if (tolower(*s1) != tolower(*s2)) return (1);
	}
	return(n > 0);
}

static char * nocase_strstr(const char *s1, const char *s2) {
	const char *p, *q, *r;

	for(p = s1; *p; p++) {
		for(q = s2, r = p; *q && *r; q++, r++) {
			if (tolower(*q) != tolower(*r)) break;
		}
		if (!*q) {
			return((char *) p);
		} else if (!*r) {
			return(NULL);
		}
	}
	return(NULL);
}

static int detect_header(u_char *body, char *header, char *subfield, char **exts, char *ext2) {
	char *p, *q, *r;
	register int i;
	int lfcnt, rc = 0, quoted;
	char **ext;

	for(p = (char *) body; p && (p = nocase_strstr(p, header)); p++) {
		for(lfcnt = 0, q = p+strlen(header); *q && lfcnt < 3; q++) {
			if (*q == '\n') {
				lfcnt++;
			} else {
				if (!nocase_strncmp(q, subfield, strlen(subfield))) {
					r=q+strlen(subfield);
					quoted = (*r == '"');
					if (quoted) r++;
					for(; *r != '\r' && *r != '\n' && *r != '\0'; r++) {
						if (quoted && *r == '"') break;
					}
					if ((quoted && *r == '"') || (*r == '\n' || *r == '\r')) {
						if (*(r-1) == '}' && r-q > 39 && *(r-38) == '{' && *(r-39) == '.') {
							for(i = 39; i > 0; i--) {
								*(r-i) = ' ';
							}
							rc++;
						}

						/* virus.exe.?= */
						if (*(r-7) == '.' && *(r-3) == '.' && *(r-2) == '?' && *(r-1) == '=') {
							for(ext = exts; *ext; ext++) {
								if (strlen(*ext) != 3) continue;
								if (	tolower(*(r-6)) == (*ext)[0] &&
									tolower(*(r-5)) == (*ext)[1] &&
									tolower(*(r-4)) == (*ext)[2]) {

									*(r-6) = ext2[0];
									*(r-5) = ext2[1];
									*(r-4) = ext2[2];
									rc++;
								}
							}
						}

						/* virus.exe?= */
						if (*(r-6) == '.' && *(r-2) == '?' && *(r-1) == '=') {
							for(ext = exts; *ext; ext++) {
								if (strlen(*ext) != 3) continue;
								if (	tolower(*(r-5)) == (*ext)[0] &&
									tolower(*(r-4)) == (*ext)[1] &&
									tolower(*(r-3)) == (*ext)[2]) {

									*(r-5) = ext2[0];
									*(r-4) = ext2[1];
									*(r-3) = ext2[2];
									rc++;
								}
							}
						}

						/* virus.exe. */
						if (*(r-5) == '.' && *(r-1) == '.') {
							for(ext = exts; *ext; ext++) {
								if (strlen(*ext) != 3) continue;
								if (	tolower(*(r-4)) == (*ext)[0] &&
									tolower(*(r-3)) == (*ext)[1] &&
									tolower(*(r-2)) == (*ext)[2]) {

									*(r-4) = ext2[0];
									*(r-3) = ext2[1];
									*(r-2) = ext2[2];
									rc++;
								}
							}
						}

						/* virus.exe */
						if (*(r-4) == '.') {
							for(ext = exts; *ext; ext++) {
								if (strlen(*ext) != 3) continue;
								if (	tolower(*(r-3)) == (*ext)[0] &&
									tolower(*(r-2)) == (*ext)[1] &&
									tolower(*(r-1)) == (*ext)[2]) {

									*(r-3) = ext2[0];
									*(r-2) = ext2[1];
									*(r-1) = ext2[2];
									rc++;
								}
							}
						}

						/* virus.js */
						if (*(r-3)  == '.') {
							for(ext = exts; *ext; ext++) {
								if (strlen(*ext) != 2) continue;
								if (	tolower(*(r-2)) == (*ext)[0] &&
									tolower(*(r-1)) == (*ext)[1]) {

									*(r-2) = ext2[0];
									*(r-1) = ext2[1];
									rc++;
								}
							}
						}
					}
				}
			}
		}
	}
	return(rc);
}

sfsistat mlfi_envfrom(SMFICTX *ctx, char **envfrom) {
	struct mlfiPriv *priv;

	priv = (struct mlfiPriv *) malloc(sizeof *priv);
	if (!priv) {
		/* can't accept this message right now */
		return SMFIS_TEMPFAIL;
	}
	priv->body = (u_char *) strdup("\n");
	priv->len  = 1;
	priv->addver = 1;

	if (!priv->body) {
		/* can't accept this message right now */
		return SMFIS_TEMPFAIL;
	}

	/* save the private data */
	smfi_setpriv(ctx, priv);

	/* continue processing */
	return SMFIS_CONTINUE;
}

sfsistat mlfi_header(SMFICTX *ctx, char *headerf, char *headerv) {
	struct mlfiPriv *priv = MLFIPRIV;
	int found = 0;

	if (!strcmp(headerf, VERTEXT)) {
		priv->addver = 0;
	}

	if (!nocase_strncmp(headerf, "Content-Disposition", strlen("Content-Disposition"))) {
		found = detect_header((u_char *) headerv, "", "filename=", exts, "txt");
	}
	if (!nocase_strncmp(headerf, "Content-Type", strlen("Content-Type"))) {
		found += detect_header((u_char *) headerv, "", "name=", exts, "txt");
	}
	if (found) {
		smfi_setreply(ctx, "554", "5.6.1", "Rejecting inline executable attachment");
		return mlfi_cleanup(ctx, SMFIS_REJECT);
	}
	return SMFIS_CONTINUE;
}

sfsistat mlfi_eoh(SMFICTX *ctx) {
	return SMFIS_CONTINUE;
}

sfsistat mlfi_body(SMFICTX *ctx, u_char *bodyp, size_t bodylen) {
	struct mlfiPriv *priv = MLFIPRIV;
	
	priv->body = (u_char *) realloc(priv->body, priv->len + bodylen + 1);

	if (!priv->body) {
		/* can't accept this message right now */
		return SMFIS_TEMPFAIL;
	}

	memcpy((u_char*)(priv->body+priv->len), bodyp, bodylen);
	priv->len += bodylen;
	priv->body[priv->len] = '\0';

	/* continue processing */
	return SMFIS_CONTINUE;
}

sfsistat mlfi_eom(SMFICTX *ctx) {
	struct mlfiPriv *priv = MLFIPRIV;
	int found_disp = 0, found_type = 0, found_uuencode = 0;
	int found;
	char buf[1024];
	char host[512];

	if (!priv->body) {
		return mlfi_cleanup(ctx, SMFIS_CONTINUE);
	}

	/*
	 * here's an example of what netscape sends out:
	 *
	 * Content-Type: application/octet-stream; name="test.vbs"
	 * Content-Transfer-Encoding: base64
	 * Content-Disposition: inline; filename="test.vbs"
	 */
	found_disp = detect_header(priv->body, "\nContent-Disposition", "filename=", exts, "txt");
	found_type = detect_header(priv->body, "\nContent-Type", "name=", exts, "txt");
	found_uuencode = detect_header(priv->body, "\nbegin ", " ", exts, "txt");

	found = (found_type > found_disp) ? found_type : found_disp;
	found = (found_uuencode > found) ? found_uuencode : found;

	if (gethostname(host, sizeof(host)) < 0) {
		strncpy(host, "localhost", sizeof host);
	}

	sprintf(buf, "%s (%s)", VERSION, host);

	if (priv->addver) smfi_addheader(ctx, VERTEXT, buf);
	if (found) {
		sprintf(buf, "%d attachment%s changed to TXT",
			found, (found == 1) ? "" : "s");
		smfi_addheader(ctx, "X-Filter", buf);
		smfi_replacebody (ctx, priv->body+1, priv->len-1);
	}

	return mlfi_cleanup(ctx, SMFIS_CONTINUE);
}

sfsistat mlfi_close(SMFICTX *ctx) {
	return mlfi_cleanup(ctx, SMFIS_ACCEPT);
}

sfsistat mlfi_abort(SMFICTX *ctx) {
	return mlfi_cleanup(ctx, SMFIS_CONTINUE);
}

sfsistat mlfi_cleanup(SMFICTX *ctx, sfsistat rc) {
	struct mlfiPriv *priv = MLFIPRIV;

	if (priv) {
		if (priv->body) free(priv->body);
		free(priv);
		smfi_setpriv(ctx, NULL);
	}

	return(rc);
}

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


int main(int argc, char *argv[]) {
	int c, fd;
	const char *args = "p:";

	/* Process command line options */
	while ((c = getopt(argc, argv, args)) != -1) {
		switch (c) {
		  case 'p':
			if (optarg == NULL || *optarg == '\0') {
				(void) fprintf(stderr, "Illegal conn: %s\n",
					       optarg);
				exit(EX_USAGE);
			}
			(void) smfi_setconn(optarg);
			break;

		}
	}

	if (smfi_register(smfilter) == MI_FAILURE) {
		fprintf(stderr, "smfi_register failed\n");
		exit(EX_UNAVAILABLE);
	}

	if (fork() == 0) {
		close(STDIN_FILENO);
		close(STDOUT_FILENO);
		close(STDERR_FILENO);

		fd = open("/dev/tty", O_RDWR);
		if (fd >= 0) {
			ioctl(fd, TIOCNOTTY, 0);
			close(fd);
		}

		return smfi_main();
	}
	return 0;
}



syntax highlighted by Code2HTML, v. 0.9.1