/*
* $Id$
* Copyright (C) 2002 Olafur Osvaldsson
*
* 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.
*/
/* The following defines must be externally defined to have any effect:
*
* ADD_HEADER - add a X-Filter header to the message
*
* LOG_ENVFROM - when a mail msg is rejected, log the envelope from address
* (otherwise just the attachment filename and pattern are logged).
*
* DEBUG - attempt to create a copy of each mail msg in /tmp, and remove
* it when this filter terminates normally. If this filter
* crashes, the triggering (presumed) msg is available for debugging.
*
* DMALLOC - When DEBUG is also defined, use a debugging malloc, so as to
* detect memory leaks.
*
* MUST_ARCHIVE - When DEBUG is also defined, abort this filter with TEMPFAIL,
* if we can't create and open a msg archive file in /tmp. If
* undefined (the default) we just syslog the inability to have
* an archive copy.
*/
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sysexits.h>
#include <errno.h>
#include <unistd.h>
#include <ctype.h>
#include <signal.h>
#include <regex.h>
#include <syslog.h>
#if defined(DEBUG) && defined(DMALLOC)
#include <dmalloc.h>
#endif /* DEBUG && DMALLOC */
#ifdef MFAPI_H
#include <mfapi.h>
#else
#include <libmilter/mfapi.h>
#endif /* MFAPI_H */
#include "rfc2047.h"
#include "rfc2231.h"
#if HAVE_CONFIG_H
#include "config.h"
#endif
#define CRLF "\r\n"
#define PATTERN_HASH_SIZE 1024
#define MAX_LINE_LENGTH 1024
#define OVERLAP_SIZE MAX_LINE_LENGTH
#define BUFFER_SIZE (MILTER_CHUNK_SIZE+OVERLAP_SIZE)
#define MAX_ADDRESS_SIZE 63
#ifndef true
#ifndef SMFIF_QUARANTINE
#ifndef __bool_true_false_are_defined
typedef int bool; /* kludge: typedef unneeded in 8.13.0, when
* quarantining was introduced */
#endif
#endif
#define false 0
#define true 1
#endif /* ! true */
static const char rcsid[] = "$Id$";
char *progname;
struct mlfiPriv {
u_char buffer[BUFFER_SIZE + 1];
u_char overlap[OVERLAP_SIZE + 1];
#ifdef LOG_ENVFROM
u_char from_address[MAX_ADDRESS_SIZE + 1];
#endif
#ifdef DEBUG
char *mlfi_fname;
FILE *mlfi_fp;
#endif /* DEBUG */
};
#define MLFIPRIV ((struct mlfiPriv *) smfi_getpriv(ctx))
extern sfsistat mlfi_cleanup(SMFICTX *, bool);
char *patterns[PATTERN_HASH_SIZE]; /* patterns */
int pattern_count = 0;
/*
* Check if the pointer is NULL and free it otherwise
*/
void
safe_free(void *p)
{
if (p) {
free(p);
p = 0;
}
}
#if !HAVE_STRCASESTR
/*
* This is the same as strstr function just ignores case
* I got this from the ht://Dig package
*/
char *
strcasestr(const char *s, const char *pattern)
{
int length = strlen(pattern);
while (*s) {
if (strncasecmp(s, pattern, length) == 0)
return (char *)s;
s++;
}
return 0;
}
#endif /* !HAVE_STRCASESTR */
/*
* Match string against the extended regular expression in pattern, treating
* errors as no match.
*
* return 1 for match, 0 for no match
*/
int
match(string, pattern)
const char *string;
char *pattern;
{
int status;
regex_t re;
int cflags = 0;
char *pat = NULL;
char *args = NULL;
char *tmp_pat;
tmp_pat = pattern;
/*
* Here we need to parse a string wich looks like "/pattern/args" args
* can be empty or a series of letters, only 'i' supported now
*/
switch (*pattern) {
case '/':
pattern++;
pat = pattern;
break;
default:
/* Exit with error if the string doesn't start with a slash */
safe_free(tmp_pat);
return 0;
}
while (*pattern) {
if (*pattern == '/') {
args = (pattern + 1);
*pattern = '\0';
break;
}
pattern++;
}
while (*args) {
switch (*args) {
case 'i':
cflags |= REG_ICASE;
break;
}
args++;
}
if (regcomp(&re, pat, cflags | REG_EXTENDED | REG_NOSUB) != 0) {
safe_free(tmp_pat);
return (0); /* report error */
}
status = regexec(&re, string, (size_t) 0, NULL, 0);
regfree(&re);
if (status != 0) {
safe_free(tmp_pat);
return (0); /* report error */
}
safe_free(tmp_pat);
return (1);
}
/* Return: 0 => no match, 1 => match, so reject msg, -1 => error, temp-fail msg */
int
check_line(ctx, line)
SMFICTX *ctx;
const char *line;
{
char *namestr; /* Filename string (filename.vbs) */
char *end_quote; /* The search for the second quote */
char *encoded_name; /* Encoded filename */
char *decoded_name; /* Decoded filename */
char m [MAX_LINE_LENGTH + 1]; /* Error message to be
* returned */
char charset[128]; /* The encoding charset */
int got_name = 0; /* Set to 1 if we've found a name to
* check */
int i;
decoded_name = NULL;
if (strncasecmp(line, "Content-", 8) == 0) {
namestr = strcasestr(line, "name");
if ((namestr != NULL) && (strlen(namestr) > 4)) {
namestr += 4; /* move closer to the filename */
/* Some progs put * in front of the = */
if (*namestr == '*')
namestr++;
/* Some progs put space in front of the = */
while (*namestr == ' ')
namestr++;
/* This we don't need */
if (*namestr == '=')
namestr++;
/* Some progs put space behind the = */
while (*namestr == ' ')
namestr++;
/* Get rid of the first quote if its there */
if (*namestr == '\"') {
namestr++;
/* There should be a second quote if everything is ok */
end_quote = strchr(namestr, '\"');
if (end_quote == NULL) {
/* None found, something is wrong, just give it an OK */
return 0;
}
else {
/* Get rid of the quote */
*end_quote = '\0';
}
}
if (strncasecmp(namestr, "=?", 2) == 0) {
/* This should return the same string if not encoded */
decoded_name = rfc2047_decode_simple(namestr);
}
else {
encoded_name = rfc2231_get_charset(namestr, charset, sizeof(charset));
if ((decoded_name = malloc(strlen(encoded_name) + 1)) == NULL) {
return -1;
}
rfc2231_decode(decoded_name, encoded_name);
}
got_name = 1;
}
}
else if (strncasecmp(line, "begin ", 6) == 0) {
/* Find the first whitespace */
namestr = strchr(line, ' ');
/* If we don't find a whitespace, we are in the wrong place */
if (namestr == NULL)
return 0;
/* Move ahead one char */
namestr++;
/* Find the next whitespace */
namestr = strchr(namestr, ' ');
/* If we don't find a whitespace, we are in the wrong place */
if (namestr == NULL)
return 0;
/* Move ahead one char */
namestr++;
/* Now we should have our filename from here to the end of line */
/* Get rid of the first quote if its there */
if (*namestr == '\"') {
namestr++;
/* There should be a second quote if everything is ok */
end_quote = strchr(namestr, '\"');
if (end_quote == NULL) {
/* None found, something is wrong, just give it an OK */
return 0;
}
else {
/* Get rid of the quote */
*end_quote = '\0';
}
}
if (strncasecmp(namestr, "=?", 2) == 0) {
/* This should return the same string if not encoded */
decoded_name = rfc2047_decode_simple(namestr);
}
else {
encoded_name = rfc2231_get_charset(namestr, charset, sizeof(charset));
if ((decoded_name = malloc(strlen(encoded_name) + 1)) == NULL) {
return -1;
}
rfc2231_decode(decoded_name, encoded_name);
}
got_name = 1;
}
else {
return 0;
}
if (got_name == 1) {
for (i = 0; i < pattern_count; i++) {
if (match(decoded_name, strdup(patterns[i])) == 1) {
/*
* trim long decode_name; print the end: we expect patterns
* to match suffixes
*/
char *start_of_output = decoded_name;
int length = strlen(decoded_name);
if (length > 100) {
start_of_output = decoded_name + length - 90;
strncpy(start_of_output, "...", 3);
}
#ifdef LOG_ENVFROM
syslog(LOG_INFO, "rejecting attachment \"%.100s\" from %s on pattern %d: \"%.30s\"",
start_of_output, MLFIPRIV->from_address, i, patterns[i]);
#else
syslog(LOG_INFO, "rejecting attachment \"%.100s\" on pattern %d: \"%.30s\"",
start_of_output, i, patterns[i]);
#endif
sprintf(m, "Sorry, I can't accept messages with proscribed attachments. (%.100s)", start_of_output);
smfi_setreply(ctx, "554", "5.6.1", m);
safe_free(decoded_name);
return 1;
}
}
}
safe_free(decoded_name);
return 0;
}
int
read_pattern_file(void)
{
FILE *pat_file = NULL; /* Input file */
char line [255]; /* Input line */
char *result; /* result from fgets */
int count = 0;
int i;
/*
* Empty the list of patterns
*/
for (i = 0; i < pattern_count; i++) {
safe_free(patterns[i]);
patterns[i] = NULL;
}
/*
* Read in data
*/
pat_file = fopen(PAT_FILE, "r");
if (pat_file == NULL) {
fprintf(stderr, "Unable to open pattern file\n");
return (8);
}
while (1) {
result = fgets(line, sizeof(line), pat_file);
if (result == NULL) {
if (!feof(pat_file)) {
fprintf(stderr,
"Error while reading pattern file!\n");
return (8);
}
else {
break;
}
}
if (line[0] != '#') {
if (strlen(line) > 1) {
while ((line[strlen(line) - 1] == '\n') ||
(line[strlen(line) - 1] == '\r')) {
line[strlen(line) - 1] = '\0';
}
patterns[count] = strdup(line);
count++;
}
}
}
pattern_count = count;
fclose(pat_file);
syslog(LOG_INFO, "Read %d patterns from file %s\n", count, PAT_FILE);
return (0);
}
void
handler(sig)
int sig;
{
read_pattern_file();
}
int
write_pid_file(void)
{
FILE *pid_file; /* output file */
static pid_t pid = 0; /* Our PID */
pid_file = fopen(PID_FILE, "w");
if (pid_file == NULL) {
fprintf(stderr, "Unable to open pid file\n");
exit(8);
}
pid = getpid();
fprintf(pid_file, "%d", (int)pid);
fclose(pid_file);
return (0);
}
void
usage(program_name)
char *program_name;
{
char *real_name = NULL;
char *s;
real_name = strdup(program_name);
while (1) {
s = strchr(real_name, '/');
if (s == '\0')
break;
else
real_name = (s + 1);
}
fprintf(stderr, "Usage is: %s [options]\n", real_name);
fprintf(stderr, "\n");
fprintf(stderr, "Options:\n");
fprintf(stderr, " -h Usage Information\n");
fprintf(stderr, " -n Don't fork\n");
fprintf(stderr, " -p <portinfo> Port to listen to\n");
fprintf(stderr, "\n");
fprintf(stderr, "Examples:\n");
fprintf(stderr,
" %s -p inet:26@localhost (Listen to ipv4 port 26 on localhost)\n",
real_name);
//fprintf(stderr,
//" %s -p inet6:26@localhost (Listen to ipv6 port 26 on localhost)\n",
//real_name);
fprintf(stderr,
" %s -p local:/var/run/noattach.sock (Listen on a unix socket)\n",
real_name);
fprintf(stderr, "\n");
exit(8);
}
sfsistat
mlfi_envfrom(ctx, envfrom)
SMFICTX *ctx;
char **envfrom;
{
struct mlfiPriv *priv;
#ifdef DEBUG
int fd = -1;
#ifdef DMALLOC
dmalloc_debug(0x20403);
#endif
#endif /* DEBUG */
/* allocate some private memory */
if ((priv = malloc(sizeof *priv)) == NULL) {
/* can't accept this message right now */
(void)mlfi_cleanup(ctx, false);
return SMFIS_TEMPFAIL;
}
memset(priv, '\0', sizeof *priv);
strcpy(priv->buffer, "");
strcpy(priv->overlap, "");
#ifdef LOG_ENVFROM
if (envfrom != NULL && envfrom[0] != NULL) {
strncpy(priv->from_address, envfrom[0], MAX_ADDRESS_SIZE);
priv->from_address[MAX_ADDRESS_SIZE] = '\0'; /* in case maximal len */
}
else
strcpy(priv->from_address, "");
#endif
#ifdef DEBUG
priv->mlfi_fp = NULL;
/* try to open a file to store this message */
priv->mlfi_fname = strdup("/tmp/noattach.XXXXXXXX");
if (priv->mlfi_fname == NULL) {
#ifdef MUST_ARCHIVE
(void)mlfi_cleanup(ctx, false);
safe_free(priv);
return SMFIS_TEMPFAIL;
#else
syslog(LOG_INFO, "Cannot allocate memory for debug filename");
#endif
}
else if ((fd = mkstemp(priv->mlfi_fname)) < 0 ||
(priv->mlfi_fp = fdopen(fd, "w+")) == NULL) {
if (fd >= 0)
(void)close(fd);
safe_free(priv->mlfi_fname);
#ifdef MUST_ARCHIVE
(void)mlfi_cleanup(ctx, false);
safe_free(priv);
return SMFIS_TEMPFAIL;
#else
syslog(LOG_INFO, "Cannot create/open debug filename");
#endif
}
#endif /* DEBUG */
/* save the private data */
smfi_setpriv(ctx, priv);
/* continue processing */
return SMFIS_CONTINUE;
}
sfsistat
mlfi_header(ctx, headerf, headerv)
SMFICTX *ctx;
char *headerf;
char *headerv;
{
#ifdef DEBUG
/* write the header to the log file */
if (MLFIPRIV->mlfi_fp != NULL)
fprintf(MLFIPRIV->mlfi_fp, "%s: %s\r\n", headerf, headerv);
#endif /* DEBUG */
/* continue processing */
return SMFIS_CONTINUE;
}
sfsistat
mlfi_eoh(ctx)
SMFICTX *ctx;
{
#ifdef DEBUG
/* output the blank line between the header and the body */
if (MLFIPRIV->mlfi_fp != NULL)
fprintf(MLFIPRIV->mlfi_fp, "\r\n");
#endif /* DEBUG */
/* continue processing */
return SMFIS_CONTINUE;
}
sfsistat
mlfi_body(ctx, bodyp, bodylen)
SMFICTX *ctx;
u_char *bodyp;
size_t bodylen;
{
struct mlfiPriv *priv = MLFIPRIV;
int max_lines = 1024; /* Default maximum, will be increased */
u_char **lines; /* The lines we are working with */
u_char *line; /* Current line we are working with */
u_char *last_line = NULL; /* The last line we got */
char *last; /* Last line found with strtok_r() */
int num_lines = 0; /* Number of lines */
int line_len = 0; /* Length of line for realloc */
int i , cl_err;
if ((ctx == NULL) || (bodyp == NULL))
return SMFIS_CONTINUE;
if (priv->overlap != NULL) {
strncpy(priv->buffer, priv->overlap, BUFFER_SIZE);
strncat(priv->buffer, bodyp, bodylen);
}
else {
size_t copy_size = (bodylen < BUFFER_SIZE ? bodylen : BUFFER_SIZE);
strncpy(priv->buffer, bodyp, copy_size);
}
#ifdef DEBUG
/* output body block to log file */
if (priv->mlfi_fp != NULL && fwrite(bodyp, bodylen, 1, priv->mlfi_fp) <= 0) {
/* write failed */
(void)mlfi_cleanup(ctx, false);
return SMFIS_TEMPFAIL;
}
#endif /* DEBUG */
if ((lines = malloc(max_lines * sizeof(u_char *))) == NULL) {
/* can't accept this message right now */
(void)mlfi_cleanup(ctx, false);
return SMFIS_TEMPFAIL;
}
for (line = (u_char *) strtok_r(priv->buffer, CRLF, &last);
line != NULL;
line = (u_char *) strtok_r(NULL, CRLF, &last)) {
if (num_lines >= max_lines) {
max_lines += 1024;
if ((lines = realloc(lines,
(max_lines * sizeof(u_char *)))) == NULL) {
/* can't accept this message right now */
for (i = 0; i < num_lines; i++)
safe_free(lines[i]);
safe_free(lines);
(void)mlfi_cleanup(ctx, false);
return SMFIS_TEMPFAIL;
}
}
if ((lines[num_lines] = malloc(MAX_LINE_LENGTH + 1)) == NULL) {
/* can't accept this message right now */
for (i = 0; i < num_lines; i++)
safe_free(lines[i]);
safe_free(lines);
(void)mlfi_cleanup(ctx, false);
return SMFIS_TEMPFAIL;
}
strncpy(lines[num_lines], line, MAX_LINE_LENGTH);
lines[num_lines][MAX_LINE_LENGTH] = '\0'; /* ensure NULL for
* maximal lines */
num_lines++;
}
for (i = 0; i < num_lines; i++) {
line = lines[i];
if ((last_line != NULL) && isspace(*line) &&
(strncasecmp(last_line, "Content-", 8) == 0)) {
/*
* This looks like a continued MIME header line So we append it
* to the previous line
*/
line_len = ((strlen(last_line) + 1) + 1 + (strlen(line) + 1));
if ((last_line = realloc(last_line, line_len)) == NULL) {
/* can't accept this message right now */
for (; i < num_lines; i++)
safe_free(lines[i]);
safe_free(lines);
(void)mlfi_cleanup(ctx, false);
return SMFIS_TEMPFAIL;
}
sprintf(last_line, "%s %s", last_line, line);
safe_free(line);
continue;
}
else {
if ((last_line != NULL) && (strlen(last_line) > 1)) {
/* Check the line */
cl_err = check_line(ctx, last_line);
if (cl_err == 1) {
safe_free(last_line);
for (; i < num_lines; i++)
safe_free(lines[i]);
safe_free(lines);
(void)mlfi_cleanup(ctx, true);
return SMFIS_REJECT;
}
else if (cl_err == -1) {
/* can't accept this message right now */
safe_free(last_line);
for (; i < num_lines; i++)
safe_free(lines[i]);
safe_free(lines);
(void)mlfi_cleanup(ctx, false);
return SMFIS_TEMPFAIL;
}
}
}
safe_free(last_line);
last_line = line;
}
safe_free(lines);
/*
* if we get a full 64k buffer the last line is probably continued in the
* next buffer
*/
if ((last_line != NULL) && (bodylen == MILTER_CHUNK_SIZE)) {
strncpy(priv->overlap, last_line, OVERLAP_SIZE);
}
else if (last_line != NULL) {
/* We need to check the last line */
cl_err = check_line(ctx, last_line);
if (cl_err == 1) {
safe_free(last_line);
(void)mlfi_cleanup(ctx, true);
return SMFIS_REJECT;
}
else if (cl_err == -1) {
/* can't accept this message right now */
safe_free(last_line);
(void)mlfi_cleanup(ctx, false);
return SMFIS_TEMPFAIL;
}
}
safe_free(last_line);
/* continue processing */
return SMFIS_CONTINUE;
}
sfsistat
mlfi_eom(ctx)
SMFICTX *ctx;
{
return mlfi_cleanup(ctx, true);
}
sfsistat
mlfi_close(ctx)
SMFICTX *ctx;
{
return SMFIS_ACCEPT;
}
sfsistat
mlfi_abort(ctx)
SMFICTX *ctx;
{
return mlfi_cleanup(ctx, false);
}
sfsistat
mlfi_cleanup(ctx, ok)
SMFICTX *ctx;
bool ok;
{
/*
* If we get here everything must have succeeded so we finish up and
* delete the file if needed
*/
sfsistat rstat = SMFIS_CONTINUE;
struct mlfiPriv *priv = MLFIPRIV;
#if defined(ADD_HEADER)
char host [512];
char hbuf [1024];
#endif
if (priv == NULL)
return rstat;
#ifdef DEBUG
/* close the archive file */
if (priv->mlfi_fp != NULL && fclose(priv->mlfi_fp) == EOF) {
/* failed; we have to wait until later */
rstat = SMFIS_TEMPFAIL;
(void)unlink(priv->mlfi_fname);
}
else if (ok) {
/* we only keep those that crashed us -- delete the archive file */
(void)unlink(priv->mlfi_fname);
}
else {
/* message was aborted -- delete the archive file */
(void)unlink(priv->mlfi_fname);
}
/* release private memory */
safe_free(priv->mlfi_fname);
#endif /* DEBUG */
#if defined(ADD_HEADER)
/* add a header to the message announcing our presence */
if (gethostname(host, sizeof host) < 0)
snprintf(host, sizeof host, "localhost");
snprintf(hbuf, sizeof hbuf, "%s (%s) on %s", PACKAGE_NAME, PACKAGE_VERSION, host);
smfi_addheader(ctx, "X-Filter", hbuf);
#endif
/* release private memory */
free(priv);
smfi_setpriv(ctx, NULL);
#if defined(DEBUG) && defined(DMALLOC)
dmalloc_shutdown();
#endif /* DEBUG && DMALLOC */
/* return status */
return rstat;
}
struct smfiDesc smfilter =
{
"noattach-Filter", /* filter name */
SMFI_VERSION, /* version code -- do not change */
SMFIF_ADDHDRS, /* 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 */
};
char cmd_line [1024];
int
main(argc, argv)
int argc;
char *argv[];
{
int c;
int return_code;/* Return code from smfi_main */
int lets_fork = 1; /* Should we fork or not */
const char *args = "hnp:";
struct sigaction act;
struct sigaction oldact;
act.sa_handler = &handler;
sigfillset(&act.sa_mask);
act.sa_flags = SA_RESTART;
if (sigaction(SIGUSR1, &act, &oldact) == -1) {
perror("Error from sigaction() - SIGUSR1 cannot be used to reread pattern file");
}
progname = strrchr(argv[0], '/');
if (progname)
progname++;
else
progname = argv[0];
progname = strdup(progname);
/* Process command line options */
while ((c = getopt(argc, argv, args)) != -1) {
switch (c) {
case 'n':
lets_fork = 0;
break;
case 'p':
if (optarg == NULL || *optarg == '\0') {
(void)fprintf(stderr, "Illegal conn: %s\n\n",
optarg);
usage(argv[0]);
exit(EX_USAGE);
}
if (smfi_setconn(optarg) != MI_SUCCESS) { /* sets errno? */
fprintf(stderr, "%s: connection %s: %s\n", progname,
optarg, strerror(errno));
exit(EX_USAGE);
}
break;
case 'h':
usage(argv[0]);
default:
usage(argv[0]);
}
}
if (lets_fork == 1) {
switch (fork()) {
case -1:
fprintf(stderr, "ERROR: fork()\n");
exit(3);
case 0:
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
if (setsid() == -1)
exit(4);
break;
default:
return 0;
}
/* Don't write the pid file unless we are forking */
write_pid_file();
}
openlog(progname, LOG_PID, LOG_MAIL);
syslog(LOG_INFO, "starting");
if (smfi_register(smfilter) == MI_FAILURE) {
fprintf(stderr, "smfi_register failed\n");
if (lets_fork == 1)
unlink(PID_FILE);
exit(EX_UNAVAILABLE);
}
return_code = read_pattern_file();
if (return_code > 0) {
if (lets_fork == 1)
unlink(PID_FILE);
exit(return_code);
}
return_code = smfi_main();
if (lets_fork == 1)
unlink(PID_FILE);
return return_code;
}
syntax highlighted by Code2HTML, v. 0.9.1