/* * $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 #include #include #include #include #include #include #include #include #include #include #if defined(DEBUG) && defined(DMALLOC) #include #endif /* DEBUG && DMALLOC */ #ifdef MFAPI_H #include #else #include #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 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; }