/* * SpamAssassin Milter version 0.6.1 * For use with the SpamAssassin http://www.spamassassin.org/ ``spamd'' * * Copyright (c) 2002 - 2003 Peter 'Luna' Runestig * All rights reserved. * * Redistribution and use in source and binary forms, with or without modifi- * cation, 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. */ /* * It recognizes the following options: * -p port The port through which the MTA will connect to the filter. * -t sec The timeout value. * -H host The host running spamd (default "localhost"). * -P port The spamd port (default 783). * -U uid Run under this UID (only works if started as root) * -u user The user to connect to spamd as (default is to try to * figure out which local user is the recipient, and use * that user). * -m maxsize Messages greater than maxsize (in bytes) will not be sent * to spamd, but simply be shortcutted. This only works, if * {msg_size} is set by sendmail and added to the * milter-options in sendmail.cf This really speeds up the * whole thing, since spam is normally less < 100kB, but * big pdf etc are normally not spam. More on activating * {msg_size} further down. * -c command Command to spamd (default "PROCESS"). NOT IMPLEMENTED YET! * -a Process *all* messages, not just local delivery. * -d domain Process only messages for specified domain, (implies -a). * -D domainfile Process only messages for specified domain in the file domainfile, (implies -a). * -S domainfile Disable processing of messages for specified domain in the file domainfile, (implies -a). NOT IMPLEMENTED YET * -s Disable Subject Rewriting * -r Disable Report Headers * -F file Use file as list of rewrite domains (default /etc/mail/rewrite_domains) * * This milter doesn't reject any messages, nor does it mess with the message * body, and it only processes messages intended for local delivery. It only * feeds the message's headers and body (only a first part of the boby if it's * big) to the SpamAssassins spamd daemon, reads spamd's reply, and then adds * the extra "X-Spam-..." headers from SpamAssassin to the message. You can * then, later in the deliver process, do what you like with the messages, * based on those extra "X-Spam-..." headers. * * If SpamAssassin considers the message as spam, it also wants to change the * message body, by putting a number of "SPAM: ..." lines on top. Instead, this * milter adds those lines as a "X-Spam-Report" multi-line header. * * As an example, if you are running Cyrus IMAP server, you might want to put * this in your ~/.sieve, to put the spam in your ``spam'' folder: * * require ["fileinto"]; * if header :is "X-Spam-Flag" ["YES"] { * fileinto "INBOX.spam"; * } * * BUGS: * The code that tries to determine which local user to run spamd as, doesn't * handle local aliases, or mailboxes without local accounts. You can however * run the milter as a "fixed" user with the -u switch. * Any existing "X-Spam-..." headers in a message gets "doubled". That isn't * hard to fix, but maybe performance-wise it's better to just live with it? * * Build with: * gcc -o spamassassin_milter spamassassin_milter.c -lmilter -lsm -pthread * [Note 1: '-pthread' only works with some gcc versions, you might have to * use '-lpthread' instead.] * [Note 2: On Solaris, you have to add '-lresolv'.] * [Note 3 from Kristian Koehntopp : We were able to get it * up and running stable even under very high load on Solaris, but we * needed to compile it with the Solaris Compiler as opposed to GNU CC, as * GNU CC seems to have problems with threads in some situations. Also, we * had to apply all pthreads related patches to the Solaris machine.] * Start the filter, preferrably as a non-root user, e.g.: * /path/to/spamassassin_milter -U nobody -p unix:/var/run/whatever-name.sock & * Then, add something like this to 'sendmail.mc': * INPUT_MAIL_FILTER(`SpamAssassinFilter', `S=unix:/var/run/whatever-name.sock') * If you don't want every single message header addition logged: * define(`confMILTER_LOG_LEVEL', 7) * Rebuild 'sendmail.cf' with: * m4 -D_FFR_MILTER ../m4/cf.m4 sendmail.mc > /etc/mail/sendmail.cf * Restart sendmail. * * The default timeout for a milter to respond is 10 seconds. In some cases, that * is too short for SpamAssassin, and sendmail aborts and logs an error: "timeout * before data read". To increase the timeout to e.g. 20 seconds, tweak * 'sendmail.mc' like this: * INPUT_MAIL_FILTER(`SpamAssassinFilter', `S=unix:/var/run/whatever-name.sock, T=R:20s;S:20s') * * See the sendmail/milter documentation on how to activate the '{msg_size}' * macro, but here is a quick example, that preserves the default macros: * define(`confMILTER_MACROS_ENVFROM', `i, {auth_type}, {auth_authen}, {auth_ssf}, {auth_author}, {mail_mailer}, {mail_host}, {mail_addr}, {msg_size}') * Note that in our case (we want to use '{msg_size}' before the whole message * is recieved), '{msg_size}' is only set if the remote MTA gives the SIZE= * parameter in an ESMTP dialogue. This is not always the case; according to my * own tests, far from it! * * Further credits: * Mike Smith * Addition of the '-s' and '-r' command line flags. * More help text output from the usage() function. * * Roland Kaltefleiter * Addition of the '-m' command line flag. * Added syslog'ing. * New Options: * -U username Run under this username(login) (only works if started as * root) * -d domain Process only messages for specified domain, (implies -a). * (default is all domains) * -D domainfile Process only messages for specified domain in the file * domainfile, (implies -a). (default is all domains) * And upcoming (later) * -S domainfile Disable processing of messages for specified domain in * the file domainfile, (implies -a). (default is all * domains) NOT IMPLEMENTED YET! * -U I hate this su - uid .... to run. It make watchdogs more * complicated. So i put in the code a setuid/setgid, if * started as root and -U set. * -d/-D (can be used multiple times) will only send the specified domain * (Envelope-To) to spamd and skip for all others. I needed this on a * shared server. * * Kristian Köhntopp, Peter Karstens, Matthias Lange and Roland Kaltefleiter, all NetUSE AG * Fix leaking of file descriptors under certain conditions. * Use of strtok() (not threadsafe) in a threaded milter will crash * spamassassin_milter under load. * * Chuck Yerkes * Addition of the -R (reject) command line flags. * Only change Subject: if X-Spam-Flag: YES. * Prettying up the code a bit. * */ #ifndef lint static char copyright[] = "@(#) Copyright (c) Peter 'Luna' Runestig 2002 - 2003 .\n"; #endif /* not lint */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include struct _ADDR_REWRITE; typedef struct _ADDR_REWRITE { char *domain; /* domain for which to rewrite */ char *address; /* address to rewrite to */ struct _ADDR_REWRITE *next; /* next addr in the list */ } ADDR_REWRITE; struct _RCPT_ADDR; typedef struct _RCPT_ADDR { char *rcpt; struct _RCPT_ADDR *next; } RCPT_ADDR; typedef struct _SA_SESSION { int fd; /* socket connected to spamd */ char *head; /* the full msg header builds here */ size_t head_len; char *tmp_hl; /* temporary storage for reading multi-line headers */ int sent_to_spamd; char user[30]; /* the user getting the message */ RCPT_ADDR *rcptlist; /* linked list of rcpt addresses */ } SA_SESSION; static char *milter_port = NULL; static char *spamd_host = "localhost"; static int spamd_port = 783; static char *spamd_user = NULL, *spamd_command = NULL; static char **spamd_rcpt_list = NULL; static int spamd_check_all = 0; static long spamd_maxsize = 0, spamd_rcpt_list_size = 0; static int spamd_process, no_subject_change = 0, no_report_change = 0, ScoreToReject = -1, spamd_use_rcpt_list = 0; static char *rewrite_domainfile = "/etc/mail/rewrite_domains"; static ADDR_REWRITE *rewrite_addrs = NULL; static void free_sa_session(SA_SESSION **sess) { if (*sess) { if ((*sess)->fd > -1) close((*sess)->fd); if ((*sess)->head) free((*sess)->head); if ((*sess)->tmp_hl) free((*sess)->tmp_hl); free(*sess); *sess = NULL; } } static int connect_spamd(const char *host, const int port) /* returns the socket if OK, else a custom negative error code */ /* all the FWDX_UNIX_SOCK stuff is left here, just in case one wants to use * local unix sockets to connect to spamd. that code must be tweaked before * it can work though. */ { int s, rv = -1; struct sockaddr_in saddr_in = { AF_INET }; #ifdef FWDX_UNIX_SOCK int family, dpynum, scrnum; struct sockaddr_un saddr_un = { AF_UNIX}; #endif /* FWDX_UNIX_SOCK */ struct hostent *hi; #ifdef FWDX_UNIX_SOCK if (family == FamilyLocal) s = socket(PF_UNIX, SOCK_STREAM, 0); else #endif /* FWDX_UNIX_SOCK */ s = socket(AF_INET, SOCK_STREAM, 0); if (s < 0) { rv = -1; goto cleanup; } /* connect to spamd */ #ifdef FWDX_UNIX_SOCK #ifndef SUN_LEN #define SUN_LEN(ptr) \ (((size_t) (((struct sockaddr_un *) 0)->sun_path) + strlen((ptr)->sun_path))) #endif /* !SUN_LEN */ if (family == FamilyLocal) { char sock_name[30]; snprintf(sock_name, sizeof(sock_name), "/tmp/.X11-unix/X%d", dpynum); strncpy(saddr_un.sun_path, sock_name, sizeof(saddr_un.sun_path)); if (connect(s, (struct sockaddr *) &saddr_un, SUN_LEN(&saddr_un)) < 0) { fwdx_sockets[num_channels] = -1; rv = -2; goto cleanup; } } else { #endif /* FWDX_UNIX_SOCK */ saddr_in.sin_port = htons(port); /* first try if "host" is really a dotted address */ if (!(inet_aton(host, (struct in_addr *) &saddr_in.sin_addr))) { if ((hi = gethostbyname(host))) saddr_in.sin_addr = *(struct in_addr *) hi->h_addr; else { rv = -3; goto cleanup; } } if (connect(s, (struct sockaddr *) &saddr_in, sizeof(saddr_in)) < 0) { rv = -4; goto cleanup; } else rv = s; #ifdef FWDX_UNIX_SOCK } #endif /* FWDX_UNIX_SOCK */ cleanup: return rv; } static int write_all(int fd, void *data, int len) { int n = 0; while (n < len) { int r = write(fd, (char *) data + n, len - n); if (r < 0) { if (errno == EWOULDBLOCK || errno == EINTR) { fd_set wfds; struct timeval tv; FD_ZERO(&wfds); FD_SET(fd, &wfds); tv.tv_sec = 10; /* the time out */ tv.tv_usec = 0; if (select(fd + 1, NULL, &wfds, NULL, &tv) > 0) continue; /* retry */ else return 1; /* time out or error, give up... */ } else return 1; } else n += r; } return 0; } static char *ltrim(char *s) /* removes white space in the beginning */ { char *p = s; while (*p && isspace(*p)) p++; if (p != s) memmove(s, p, strlen(p) + 1); return s; } static char *rtrim(char *s) /* removes trailing white space */ { int si = strlen(s) - 1; while (si >= 0 && isspace(s[si])) s[si--] = '\0'; return s; } static void read_rewrite_domains() { FILE *infile; char buf[512]; int index, len; infile = fopen(rewrite_domainfile, "r"); if(!infile) { /* silently fail if we can't open the file, might not be what we want XXX */ return; } while(fgets(buf, sizeof(buf), infile) != NULL) { ADDR_REWRITE *rw; char *p; if(buf[0] == '\n' || buf[0] == '#') { continue; } rw = malloc(sizeof(ADDR_REWRITE)); if(!rw) { fprintf(stderr, "malloc failed: %s\n", strerror(errno)); exit(EX_SOFTWARE); } index = strcspn(buf, " \t"); if(index == strlen(buf)) { free(rw); continue; } p = buf+index+1; buf[index] = '\0'; while(isspace(*p)) p++; if(p == '\0') { free(rw); continue; } rtrim(p); rw->domain = strdup(buf); if(!rw->domain) { /* out of memory */ exit(EX_SOFTWARE); } rw->address = strdup(p); if(!rw->address) { /* out of memory */ exit(EX_SOFTWARE); } /* add to head of list */ rw->next = rewrite_addrs; rewrite_addrs = rw; printf("rewriting %s to %s\n", rw->domain, rw->address); } } static int split_colonspace(char *str, char **key, char **value) /* splits a string like "key: value" to "key" and "value", and fills the * pointers 'key' and 'value' accordingly */ { char *p = strchr(str, ':'); if (p == NULL) return 1; *key = str; *p = '\0'; *value = p + 1; ltrim(*value); return 0; } /* * Milter Callbacks */ sfsistat mlfi_envrcpt(SMFICTX *ctx, char **argv) { /* here we check if it's a message for local delivery, and in that case, try * to figure out the recipient. */ SA_SESSION *sess; RCPT_ADDR *addr; int ld; char *p, *user = NULL, *rcpt = smfi_getsymval(ctx, "{rcpt_addr}"); /* do we have a list of rcpt-domain ? */ if (spamd_use_rcpt_list && argv[0]) { char *rk_rcpt = argv[0]; char *rk_at; long rk_i; int rk_domain_found; /* find the @ and search in config list */ rk_at = strchr(rk_rcpt, '@'); if (rk_at) { rk_at++; /* if we are NOT in list -> skip, exit on first match */ rk_domain_found = 0; for (rk_i = 0; rk_i < spamd_rcpt_list_size; rk_i++) { if (strncasecmp(spamd_rcpt_list[0], rk_at, strlen(rk_at) - 1) != 0) { rk_domain_found++; break; } } if (rk_domain_found == 0) { syslog(LOG_NOTICE, "mlfi_envrcpt: skip sending to spamd: <%s> not in domainlist\n", rk_at ? rk_at : ""); return SMFIS_CONTINUE; } } } /* if recipient contains '@', we think "not for local delivery" */ ld = strchr(rcpt, '@') ? 0 : 1; if (!spamd_check_all && !ld) return SMFIS_CONTINUE; if (ld && spamd_user == NULL) { /* try to figure out local recipient */ if (!(user = strdup(rcpt))) /* out of memory */ return SMFIS_CONTINUE; /* if rcpt contains a '+', terminate at that */ if ((p = strchr(user, '+'))) *p = '\0'; } sess = (SA_SESSION *) smfi_getpriv(ctx); /* if sess != NULL, we have already done this, and stick to that user. */ if (sess == NULL) { sess = calloc(1, sizeof(SA_SESSION)); if (sess) { sess->fd = -1; if (spamd_user) strncpy(sess->user, spamd_user, sizeof(sess->user)); else if (user) strncpy(sess->user, user, sizeof(sess->user)); else sess->user[0] = '\0'; sess->user[sizeof(sess->user) - 1] = '\0'; smfi_setpriv(ctx, sess); } } if(sess) { /* * if we don't have a session to add the address to, there's * no point in allocating a struct for it */ addr = calloc(1, sizeof(RCPT_ADDR)); if(!addr) { /* out of memory */ goto out; } addr->rcpt = strdup(rcpt); if(!addr->rcpt) { /* out of memory */ free(addr); goto out; } /* add rcpt to head of rcptlist */ addr->next = sess->rcptlist; sess->rcptlist = addr; } out: if (user) free(user); return SMFIS_CONTINUE; } sfsistat mlfi_header(SMFICTX *ctx, char *headerf, char *headerv) { /* here we collect all the message's headers into the sess->head buffer */ SA_SESSION *sess = (SA_SESSION *) smfi_getpriv(ctx); if (sess) { size_t new_len = sess->head_len + strlen(headerf) + strlen(headerv) + 4; /* 5 == ": " + "\r\n" */ if (sess->head) { char *tmp = realloc(sess->head, new_len + 1); if (tmp) { sprintf(tmp + sess->head_len, "%s: %s\r\n", headerf, /*safe*/ headerv); sess->head = tmp; sess->head_len = new_len; } } else { sess->head = malloc(new_len + 1); if (sess->head) { sprintf(sess->head, "%s: %s\r\n", headerf, headerv); /*safe*/ sess->head_len = new_len; } } } /* continue processing */ return SMFIS_CONTINUE; } sfsistat mlfi_body(SMFICTX *ctx, unsigned char *bodyp, size_t len) { /* here we send it all to spamd. actually, if mlfi_body() gets called more * than once, we only send it the first time (the first "message part"), to * reduce the overhead. */ SA_SESSION *sess = (SA_SESSION *) smfi_getpriv(ctx); if (sess == NULL) return SMFIS_CONTINUE; if (sess->sent_to_spamd) /* we only send a body part to spamd once */ return SMFIS_CONTINUE; /* if body > spamd_maxsize, we (probably) do not have spam and skip spamd */ if (spamd_maxsize > 0) { /* try the size of this body part first; it's already available, and the * the '{msg_size}' macro isn't always available. */ if (len > spamd_maxsize) { sess->sent_to_spamd = 1; return SMFIS_CONTINUE; } else { char *msg_size = smfi_getsymval(ctx, "{msg_size}"); long rklong = 0; if (msg_size) rklong = atol(msg_size); else syslog(LOG_NOTICE, "{msg_size} undefined!"); if (rklong > spamd_maxsize) { sess->sent_to_spamd = 1; return SMFIS_CONTINUE; } } } if (sess && sess->head) { sess->fd = connect_spamd(spamd_host, spamd_port); if (sess->fd > -1) { char buf[512]; if (sess->user[0]) snprintf(buf, sizeof(buf), "%s SPAMC/1.2\r\n" "Content-length: %d\r\n" "User: %s\r\n\r\n", spamd_command, sess->head_len + 2 + len, sess->user); else snprintf(buf, sizeof(buf), "%s SPAMC/1.2\r\n" "Content-length: %d\r\n\r\n", spamd_command, sess->head_len + 2 + len); buf[sizeof(buf) - 1] = '\0'; if (write_all(sess->fd, buf, strlen(buf))) /* the write() didn't work, let's move on. * cleanup comes later anyway */ return SMFIS_CONTINUE; if (write_all(sess->fd, (void *) sess->head, sess->head_len)) return SMFIS_CONTINUE; free(sess->head); sess->head = NULL; sess->head_len = 0; if (write_all(sess->fd, (void *) "\r\n", 2)) return SMFIS_CONTINUE; if (write_all(sess->fd, (void *) bodyp, len)) return SMFIS_CONTINUE; shutdown(sess->fd, SHUT_WR); /* shut down our end */ sess->sent_to_spamd = 1; } } return SMFIS_CONTINUE; } #define ISSPACE(x) (x == ' ' || x == '\t') static char *get_next_header(FILE *f, SA_SESSION *s) /* this is to support multi-line headers */ { char buf[512], *rv, *line; line = fgets(buf, sizeof(buf), f); if (line == NULL) return NULL; rtrim(line); if (s->tmp_hl) { /* a line from the previus call exists */ rv = s->tmp_hl; s->tmp_hl = NULL; } else { rv = strdup(line); if (rv == NULL) return NULL; line = fgets(buf, sizeof(buf), f); if (line == NULL) { free(rv); return NULL; } rtrim(line); } /* check if it's a multi-line header */ while (line && ISSPACE(*line)) { char *tmp = realloc(rv, strlen(rv) + strlen(line) + 2); if (tmp == NULL) break; sprintf(tmp + strlen(tmp), "\n%s", line); /* safe */ rv = tmp; line = fgets(buf, sizeof(buf), f); if (line) rtrim(line); } if (line && !ISSPACE(*line)) s->tmp_hl = strdup(line); return rv; } sfsistat mlfi_eom(SMFICTX *ctx) { /* here we read the reply from spamd, and adds headers to the real * message accordingly. */ FILE *spamd = NULL; SA_SESSION *sess = (SA_SESSION *) smfi_getpriv(ctx); ADDR_REWRITE *rw; /* long size; */ int newfd; char *SubjectFromSpamd = NULL; if (sess && sess->sent_to_spamd && sess->fd > -1) { char iobuf[1024], *strtok_buf, *p; int spam_flag = 0, subj_idx = 0; if ((newfd = dup(sess->fd)) < 0) goto cleanup; spamd = fdopen(newfd, "r"); if (spamd == NULL) goto cleanup; if (!fgets(iobuf, sizeof(iobuf), spamd)) goto cleanup; /* now we expect something like this in buf: * SPAMD/1.1 0 EX_OK * ^-- this 0 is importand; without it, something's wrong. */ strtok_r(iobuf, " ", &strtok_buf); p = strtok_r(NULL, " ", &strtok_buf); if (strcmp(p, "0")) goto cleanup; /* read into the processed message */ while ((p = fgets(iobuf, sizeof(iobuf), spamd))) { rtrim(iobuf); if (iobuf[0] == '\0') break; } if (p == NULL) { syslog(LOG_ALERT, "unexpected end of input (1)"); goto cleanup; /* unexpected end of input */ } /* now we're in the processed message's header. look for "X-Spam-..." * headers, and add them to our real message at the MTA. * only change the subject line, which may or may not have been changed * by spamd, if "X-Spam-Flag: YES". */ while ((p = get_next_header(spamd, sess))) { if (*p == '\0') { free(p); break; } /* A X-Spam-Level header looks like this, for level 6.5: * X-Spam-Level: ****** * "X-Spam-Level: " is 14 chars. * If the header's len is that + ScoreToReject, reject it. */ if ((strncmp(p, "X-Spam-Level: ", 14) == 0) && (ScoreToReject > 0) && (strlen(p) >= (14 + ScoreToReject))) { smfi_setreply(ctx, "550", "5.7.1", "Blocked as Spam"); if (sess) { free_sa_session(&sess); smfi_setpriv(ctx, NULL); } return SMFIS_REJECT; } if (strncmp(p, "X-Spam-", 7) == 0) { char *key, *val; if (!split_colonspace(p, &key, &val)) { smfi_addheader(ctx, key, val); if ((strncmp(key, "X-Spam-Flag", 11) == 0) && (strncmp(val, "YES", 3) == 0) ) { spam_flag = 1; } } } else if (!no_subject_change && (strncasecmp(p, "Subject", 7) == 0)) { char *key, *val; subj_idx++; if (!split_colonspace(p, &key, &val)) { if ((SubjectFromSpamd = strdup(val)) == NULL) { syslog(LOG_ALERT, "Out of memory with with %s", p); return -1; } } } free(p); } if (p == NULL) { syslog(LOG_ALERT, "unexpected end of input (2)"); goto cleanup; /* unexpected end of input */ } if (spam_flag == 1) { /* syslog(LOG_ALERT, "Is Spam %s", p); */ smfi_chgheader(ctx, "Subject", subj_idx, SubjectFromSpamd); } else { /* syslog(LOG_ALERT, "NOT Spam %s", p); */ } if(spam_flag) { rw = rewrite_addrs; while(rw) { int found = 0; RCPT_ADDR *rcpt = sess->rcptlist; char *p; while(rcpt) { p = strchr(rcpt->rcpt, '@'); if(!p) continue; p++; if(!strcasecmp(p, rw->domain)) { smfi_delrcpt(ctx, rcpt->rcpt); found = 1; } rcpt=rcpt->next; } if(found) { smfi_addrcpt(ctx, rw->address); } rw = rw->next; } } /* now we're in the processed message's body. if it's spam, add any * "SPAM: ..." lines as a new multi-line header to our real message at * the MTA. that way, we don't have to replace the message body, which * is costly. */ if (!no_report_change && spam_flag && spamd_process) { char *header = NULL; while ((p = fgets(iobuf, sizeof(iobuf), spamd))) { rtrim(iobuf); if (!strncmp(iobuf, "SPAM:", 5)) { char *key, *val; if (!split_colonspace(iobuf, &key, &val) && *val) { /* skip empty lines */ if (header == NULL) { header = malloc(strlen(val) + 1); if (header) strcpy(header, val); /* safe */ } else { size_t sl = strlen(header); char *tmp = realloc(header, sl + strlen(val) + 3); if (tmp) { sprintf(tmp + sl, "\n\t%s", val); /* safe */ header = tmp; } } } } else { if (header) smfi_addheader(ctx, "X-Spam-Report", header); break; /* the "SPAM:" stuff is first in message */ } } if (header) free(header); } } cleanup: if (spamd) fclose(spamd); /* is it OK to later close() the fd now? */ if (sess) { free_sa_session(&sess); smfi_setpriv(ctx, NULL); } if (SubjectFromSpamd) free(SubjectFromSpamd); return SMFIS_CONTINUE; } sfsistat mlfi_abort(SMFICTX *ctx) { SA_SESSION *sess = (SA_SESSION *) smfi_getpriv(ctx); if (sess) { free_sa_session(&sess); smfi_setpriv(ctx, NULL); } return SMFIS_CONTINUE; } struct smfiDesc smfilter = { "SpamAssassinFilter", /* filter name */ SMFI_VERSION, /* version code -- do not change */ SMFIF_CHGHDRS | SMFIF_ADDHDRS | SMFIF_DELRCPT| SMFIF_ADDRCPT, /* 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 */ mlfi_body, /* body block filter */ mlfi_eom, /* end of message */ mlfi_abort, /* message aborted */ NULL, /* connection cleanup */ }; static void usage(char *me) { fprintf(stderr, "Usage: %s [-p socket-addr] [-t timeout] [-H spamd-host] [-P spamd-port] " #ifdef TO_BE_IMPLEMENTED "[-u user] [-c command] [-a]\n", me); #else "[-U uid] [-u user] [-m maxmsgsize] [-d domain] [-D domainfile] [-S domainfile] " "[-a] [-s] [-r] [-R number]\n" "-p port The port through which the MTA will connect to the filter.\n" "-t sec The timeout value.\n" "-H host The host running spamd (default \"localhost\").\n" "-P port The spamd port (default 783).\n" "-U uid Run under this UID (only works if started as root)\n" "-u user The user to connect to spamd as (default is to try to\n" " figure out which local user is the recipient, and use\n" " that user).\n" "-m maxmsgsize Messages larger than this (in bytes) will not be sent to spamd\n" " (default is 0, so all messages will be sent to spamd).\n" "-d domain Process only messages for specified domain, (implies -a).\n" " (default is all domains)\n" "-D domainfile Process only messages for specified domain in the file domainfile, (implies -a).\n" " (default is all domains)\n" "-S domainfile Disable processing of messages for specified domain in the file domainfile, (implies -a).\n" " (default is all domains) NOT IMPLEMENTED YET!\n" "-c command Command to spamd (default \"PROCESS\"). NOT IMPLEMENTED YET!\n" "-a Process *all* messages, not just local delivery.\n" "-s Disable Subject Rewriting.\n" "-r Disable Report Headers.\n" "-F file Use file as list of rewrite domains (default /etc/mail/rewrite_domains)\n" "-R number Reject messages that spamd scores > number.\n" , me); #endif } int main(int argc, char **argv) { char c; FILE *domfd; char rk_line[1024]; struct passwd rk_passwd, *rk_pwretval; char rk_pbuf[2048]; long rk_h1; #ifdef TO_BE_IMPLEMENTED const char *args = "ac:d:D:F:hH:m:p:P:rR:st:U:u:"; #else const char *args = "ad:D:F:hH:m:p:P:rR:st:U:u:"; #endif extern char *optarg; /* Setup for listhandler */ rk_h1 = 128; spamd_rcpt_list = malloc(sizeof(char) * rk_h1); spamd_rcpt_list_size = 0; openlog("spamassassin_milter", LOG_PID, LOG_MAIL); spamd_command = "PROCESS"; /* Process command line options */ while ((c = getopt(argc, argv, args)) != (char) EOF) { switch (c) { case 'p': if (optarg == NULL || *optarg == '\0') { (void) fprintf(stderr, "Illegal conn!\n"); exit(EX_USAGE); } if (!(milter_port = strdup(optarg))) { (void) fprintf(stderr, "Out of memory!\n"); exit(EX_SOFTWARE); } break; case 'U': if (optarg == NULL || *optarg == '\0') { (void) fprintf(stderr, "UID requiered\n"); exit(EX_USAGE); } /* Is this for Solaris?: rk_pwretval = getpwnam_r(optarg, &rk_passwd, rk_pbuf, sizeof(rk_pbuf)); if (!rk_pwretval) { (void) fprintf(stderr, "Not started as root -- runuser (%s) not found\n", optarg); exit(EX_USAGE); } */ if (getpwnam_r(optarg, &rk_passwd, rk_pbuf, sizeof(rk_pbuf), &rk_pwretval) != 0) { (void) fprintf(stderr, "Not started as root -- runuser (%s) not found\n", optarg); exit(EX_USAGE); } if (getuid() == 0) { setgid(rk_passwd.pw_gid); setuid(rk_passwd.pw_uid); syslog(LOG_NOTICE, "Changed runuser to %s (%d/%d)", optarg, getuid(), getgid()); } else { (void) fprintf(stderr, "Not started as root -- cannot change to runuser (%s)\n", optarg); exit(EX_USAGE); } break; case 'm': if (optarg == NULL || *optarg == '\0') { (void) fprintf(stderr, "Illegal maxmsgsize!\n"); exit(EX_USAGE); } spamd_maxsize = atol(optarg); syslog(LOG_NOTICE, "Got maxmsgsize %ld bytes", spamd_maxsize); break; case 'd': if (optarg == NULL || *optarg == '\0') { (void) fprintf(stderr, "missing domainparameter!\n"); exit(EX_USAGE); } spamd_use_rcpt_list = 1; if (rk_h1 == spamd_rcpt_list_size) { rk_h1 = rk_h1 + 128; spamd_rcpt_list = realloc(spamd_rcpt_list, sizeof(char) * rk_h1); if (spamd_rcpt_list == NULL) { (void) fprintf(stderr, "Out of memory!\n"); exit(EX_SOFTWARE); } } spamd_check_all = 1; /* -d implies -a */ spamd_rcpt_list[spamd_rcpt_list_size] = strdup(optarg); spamd_rcpt_list_size++; syslog(LOG_NOTICE, "Only working for domain %s!\n", optarg); break; case 'D': if (optarg == NULL || *optarg == '\0') { (void) fprintf(stderr, "missing domainfileparameter!\n"); exit(EX_USAGE); } spamd_use_rcpt_list = 1; spamd_check_all = 1; /* -d implies -a */ /* read file */ domfd = fopen(optarg,"r"); if (!domfd) { (void) fprintf(stderr, "Cannot open domainfile %s for reading!\n", optarg); exit(EX_USAGE); } while (fgets(rk_line, 1023, domfd) != NULL) { if (rk_h1 == spamd_rcpt_list_size) { rk_h1 = rk_h1 + 128; spamd_rcpt_list = realloc(spamd_rcpt_list, sizeof(char) * rk_h1); if (spamd_rcpt_list == NULL) { (void) fprintf(stderr, "Out of memory!\n"); exit(EX_SOFTWARE); } } if (rk_line[0] != '#' && strlen(rk_line) > 1) { rk_line[strlen(rk_line) - 1] = 0x00; spamd_rcpt_list[spamd_rcpt_list_size] = strdup(rk_line); if (spamd_rcpt_list[spamd_rcpt_list_size] == NULL) { (void) fprintf(stderr, "Out of memory!\n"); exit(EX_SOFTWARE); } syslog(LOG_NOTICE, "Only working for domain %s!\n", rk_line); spamd_rcpt_list_size++; } } fclose(domfd); break; case 't': if (optarg == NULL || *optarg == '\0') { (void) fprintf(stderr, "Illegal timeout!\n"); exit(EX_USAGE); } if (smfi_settimeout(atoi(optarg)) == MI_FAILURE) { (void) fputs("smfi_settimeout failed", stderr); exit(EX_SOFTWARE); } break; case 'H': if (optarg == NULL || *optarg == '\0') { (void) fprintf(stderr, "Illegal host!\n"); exit(EX_USAGE); } if (!(spamd_host = strdup(optarg))) { (void) fprintf(stderr, "Out of memory!\n"); exit(EX_SOFTWARE); } break; case 'P': if (optarg == NULL || *optarg == '\0') { (void) fprintf(stderr, "Illegal port!\n"); exit(EX_USAGE); } spamd_port = atoi(optarg); break; case 'u': if (optarg == NULL || *optarg == '\0') { (void) fprintf(stderr, "Illegal user!\n"); exit(EX_USAGE); } if (!(spamd_user = strdup(optarg))) { (void) fprintf(stderr, "Out of memory!\n"); exit(EX_SOFTWARE); } break; case 'F': if(optarg == NULL || *optarg == '\0') { (void) fprintf(stderr, "Illegal filename!\n"); exit(EX_USAGE); } if (!(rewrite_domainfile = strdup(optarg))) { (void) fprintf(stderr, "Out of memory!\n"); exit(EX_SOFTWARE); } break; #ifdef TO_BE_IMPLEMENTED case 'c': if (optarg == NULL || *optarg == '\0') { (void) fprintf(stderr, "Illegal command!\n"); exit(EX_USAGE); } if (!(spamd_command = strdup(optarg))) { (void) fprintf(stderr, "Out of memory!\n"); exit(EX_SOFTWARE); } break; #endif case 'a': spamd_check_all = 1; break; case 's': no_subject_change = 1; break; case 'r': no_report_change = 1; break; case 'R': /* -R(eject) NUMBER */ if (optarg == NULL || *optarg == '\0') { (void) fprintf(stderr, "-R requires argument!\n"); exit(EX_USAGE); } if (!isdigit(*optarg)) { (void) fprintf(stderr, "-R requires digit!\n"); exit(EX_USAGE); } ScoreToReject = atoi(optarg); if (ScoreToReject == 0 && (strncmp(optarg, "0", 1) != 0)) { (void) fprintf(stderr, "-R requires digit!\n"); exit(EX_USAGE); } break; case 'h': default: usage(argv[0]); exit(1); } } /* while getopt */ /* if ( ! nofork ) { fork here ... blah blah */ syslog(LOG_NOTICE, "Starting..."); if (ScoreToReject != -1) syslog(LOG_ALERT, "Rejecting at %d", ScoreToReject); if (milter_port == NULL) { usage(argv[0]); exit(1); } read_rewrite_domains(); if (smfi_setconn(milter_port) == MI_FAILURE) { fputs("smfi_setconn failed!\n", stderr); exit(EX_SOFTWARE); } /* If we're using a local socket, make sure it doesn't * already exist. */ if (!strncmp(milter_port, "unix:", 5)) unlink(milter_port + 5); else if (!strncmp(milter_port, "local:", 6)) unlink(milter_port + 6); spamd_process = !strcmp(spamd_command, "PROCESS"); if (smfi_register(smfilter) == MI_FAILURE) { fputs("smfi_register failed!\n", stderr); exit(EX_UNAVAILABLE); } return smfi_main(); }