/*
* Copyright (c) 2006-2007 Hypertriton, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 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 AUTHOR OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* 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.
*/
#include
#include
#include "mailprocd.h"
#include
#include
#include
#include
#include
#include
#include
#include "pathnames.h"
#include
#include
SF_Recipient *
SF_MessageAddRecipient(SF_Message *msg, const char *addr)
{
SF_Recipient *rcpt;
if ((rcpt = malloc(sizeof(SF_Recipient))) == NULL) {
SF_SetError("Out of memory");
return (NULL);
}
if (addr != NULL) {
Strlcpy(rcpt->addr, addr, sizeof(rcpt->addr));
} else {
rcpt->addr[0] = '\0';
}
rcpt->user_part[0] = '\0';
rcpt->domain_part[0] = '\0';
rcpt->ml = NULL;
TAILQ_INSERT_TAIL(&msg->rcpts, rcpt, rcpts);
return (rcpt);
}
void
SF_MessageInit(SF_Message *msg)
{
msg->mail_from[0] = '\0';
msg->text = NULL;
msg->text_len = 0;
msg->status.score = 0.0;
msg->status.req_score = 100.0;
msg->status.spam_status = 0;
#ifdef HAVE_SA
msg->svParsed = NULL;
msg->svRewrite = NULL;
msg->status.sv = NULL;
#endif
msg->ip[0] = '\0';
TAILQ_INIT(&msg->rcpts);
}
SF_Message *
SF_MessageNew(void)
{
SF_Message *msg;
if ((msg = malloc(sizeof(SF_Message))) == NULL) {
SF_SetError("Out of memory for message");
return (NULL);
}
SF_MessageInit(msg);
return (msg);
}
void
SF_MessageFree(SF_Message *msg)
{
SF_Recipient *rcpt, *rcptNext;
#ifdef HAVE_SA
SA_FinishMessage(msg);
#endif
if (msg->text != NULL) {
free(msg->text);
}
for (rcpt = TAILQ_FIRST(&msg->rcpts);
rcpt != TAILQ_END(&msg->rcpts);
rcpt = rcptNext) {
rcptNext = TAILQ_NEXT(rcpt,rcpts);
if (rcpt->ml != NULL) {
free(rcpt->ml);
}
free(rcpt);
}
free(msg);
}
/* Check if a domain requires unconditional filtering. */
int
SF_DomainForcedFiltering(const char *dom)
{
char *fdomains, *pdomains, *s;
if ((fdomains = strdup(forceFilterDomains)) == NULL) {
return (0);
}
pdomains = fdomains;
while ((s = strsep(&pdomains, ",")) != NULL) {
if (strcasecmp(dom, s) == 0)
break;
}
free(fdomains);
return (s != NULL);
}
#ifdef HAVE_SA
/*
* Scan a ruleset for "spam" conditions to determine if we'll need
* a spam check for classification.
*/
static int
NeedFiltering(SF_Message *msg, SF_Ruleset *ruleset, int lvl)
{
SF_Rule *rule;
char *dom;
TAILQ_FOREACH(rule, &ruleset->rules, rules) {
if (rule->flags & RULE_SMTP) {
continue;
}
if (strncmp(rule->cond, "spam", 4) == 0) {
return (1);
}
if (rule->insn[0] != '/' &&
ValidEmailAddress(rule->insn)) {
if ((dom = strchr(rule->insn, '@')) != NULL &&
*dom != '\0' &&
SF_DomainForcedFiltering(&dom[1]))
return (1);
}
if (rule->insn[0] == '&') {
SF_Ruleset *pRuleset;
int rv;
if (lvl >= RULE_MAX_RECURSION_LVL) {
continue;
}
if ((pRuleset = LOCAL_GetRulesetByName(&rule->insn[1]))
== NULL) {
continue;
}
rv = NeedFiltering(msg, pRuleset, lvl+1);
SF_RulesetFree(pRuleset);
if (rv == 1 || rv == -1) {
return (rv);
}
return (1);
}
}
return (0);
}
static int
ReportScoreToMBD(SF_Message *msg)
{
char buf[12+39+1+13];
struct addrinfo hints, *res, *res0;
const char *cause = NULL;
ssize_t nWrote, rv;
size_t len;
int s;
snprintf(buf, sizeof(buf), "%s %s %f", mbdPass, msg->ip,
msg->status.score);
len = strlen(buf);
memset(&hints, 0, sizeof(hints));
hints.ai_family = PF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
if ((rv = getaddrinfo(mbdHost, mbdPort, &hints, &res0)) != 0) {
SF_SetError("%s:%s: %s", mbdHost, mbdPort, gai_strerror(rv));
return (-1);
}
for (s = -1, res = res0;
res != NULL;
res = res->ai_next) {
s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (s < 0) {
cause = "socket";
continue;
}
if (connect(s, res->ai_addr, res->ai_addrlen) < 0) {
cause = "connect";
close(s);
s = -1;
continue;
}
break;
}
if (s == -1) {
SF_SetError("%s: %s", cause, strerror(errno));
goto fail;
}
for (nWrote = 0; nWrote < len; ) {
rv = write(s, &buf[nWrote], len-nWrote);
if (rv == -1) {
if (errno == EINTR) {
continue;
} else {
SF_SetError("%s", strerror(errno));
goto fail;
}
} else if (rv == 0) {
SF_SetError("EOF");
goto fail;
}
nWrote += rv;
}
close(s);
freeaddrinfo(res0);
return (0);
fail:
if (s != -1) { close(s); }
freeaddrinfo(res0);
return (-1);
}
#endif /* HAVE_SA */
/*
* Main message delivery routine. Returns 0 on success, -1 on permanent
* error and 1 on temporary error.
*/
int
SF_MessageProcess(SF_Message *msg, SF_Recipient *rcpt)
{
SF_Ruleset *ruleset;
int rv;
if ((ruleset = LOCAL_GetRulesetByRcpt(rcpt->addr)) == NULL) {
SF_SetError("<%s>: No ruleset defined", rcpt->addr);
return (-1);
}
#ifdef HAVE_SA
if (NeedFiltering(msg, ruleset, 0)) {
if (SPAM_Check(msg, rcpt) == -1) {
#if 0
syslog(LOG_ERR, "<%s>: spamcheck failed: %s",
rcpt->addr, SF_GetError());
#endif
msg->status.score = 0.0;
msg->status.req_score = 6.66;
msg->status.spam_status = 0;
} else {
if (mbdReportEnable &&
ReportScoreToMBD(msg) == -1) {
syslog(LOG_ERR, "Failed to report to MBD: %s",
SF_GetError());
}
}
}
#endif /* HAVE_SA */
/* Deliver the message according to user classification rulesets. */
rv = SF_MessageClassify(msg, rcpt, ruleset, 0);
SF_RulesetFree(ruleset);
return (rv);
}
static __inline__ int
SF_MatchRule(SF_Message *msg, SF_Recipient *rcpt, SF_Rule *rule)
{
if (strcmp(rule->cond, "any") == 0) {
return (1);
}
if (strncmp(rule->cond, "spam", 4) == 0) {
float score;
char *ep;
if (rule->cond[4] == '\0' || rule->cond[5] == '\0' ||
rule->cond[6] == '\0') {
return (1);
}
score = strtod(&rule->cond[6], &ep);
if (*ep != '\0') {
syslog(LOG_ERR, "<%s>: Bad rule syntax: `%s'",
rcpt->addr, rule->cond);
return (0);
}
return (rule->cond[4] == '<' ? (msg->status.score <= score):
(msg->status.score >= score));
}
if (strncmp(rule->cond, "size", 4) == 0 && rule->cond[5] != '\0') {
size_t size;
char *ep;
if (rule->cond[4] == '\0' || rule->cond[5] == '\0' ||
rule->cond[6] == '\0') {
return (0);
}
size = (size_t)strtoul(&rule->cond[6], &ep, 10);
if (*ep != '\0') {
syslog(LOG_ERR, "<%s>: Bad rule syntax: `%s'",
rcpt->addr, rule->cond);
return (0);
}
return (rule->cond[4] == '<' ? (msg->text_len <= size) :
(msg->text_len >= size));
}
return (0);
}
/*
* Classify a message according to a ruleset. Rulesets are processed
* recursively when the "&ref" action syntax is used. Returns 0 on
* success, 1 on temporary failure and -1 on permanent failure.
*/
int
SF_MessageClassify(SF_Message *msg, SF_Recipient *rcpt, SF_Ruleset *ruleset,
int lvl)
{
uid_t default_uid;
gid_t default_gid;
SF_Rule *rule;
SF_Ruleset *pRuleset;
int rv;
if (LOCAL_GetDefaultRecipientUID(rcpt->addr,
&default_uid, &default_gid) == -1) {
SF_SetError("Cannot get default UID for %s", rcpt->addr);
return (-1);
}
TAILQ_FOREACH(rule, &ruleset->rules, rules) {
uid_t uid = (rule->uid != 0) ? rule->uid : default_uid;
gid_t gid = (rule->gid != 0) ? rule->gid : default_gid;
if (getpwuid(uid) == NULL || getgrgid(gid) == NULL) {
syslog(LOG_ERR, "<%s>: Bad UID %d:%d; ignored rule",
rcpt->addr, uid, gid);
continue;
}
if (rule->flags & RULE_SMTP) {
continue; /* Ignore */
}
if ((!(rule->flags&RULE_NEGATE) &&
!SF_MatchRule(msg, rcpt, rule)) ||
( (rule->flags&RULE_NEGATE) &&
SF_MatchRule(msg, rcpt, rule)))
continue;
Debug("Rule: [%s] %s (as %d:%d)", rule->cond, rule->insn,
(int)uid, (int)gid);
if (rule->insn[0] == '|') {
rv = LOCAL_FeedToPipe(msg, rcpt, &rule->insn[1],
rule->cond, uid, gid, 1);
#ifdef COMPAT_QMAIL
if (rv == 99)
break;
#endif
if (rv != 0) {
SF_SetError("FeedToPipe(%s): %s",
&rule->insn[1], SF_GetError());
return (rv);
}
continue;
}
switch (rule->insn[0]) {
case '/':
if (rule->insn[1] == 'd' &&
strcmp(rule->insn, "/dev/null") == 0) {
/* Silently drop */
return (0);
}
if (rule->insn[strlen(rule->insn)-1] == '/') {
Debug("Delivering to maildir: %s", rule->insn);
if ((rv = LOCAL_DeliverToMaildir(msg, rcpt,
rule->insn, rule->cond, uid, gid)) != 0)
return (rv);
} else {
Debug("Delivering to mbox: %s", rule->insn);
if ((rv = LOCAL_DeliverToMailbox(msg, rcpt,
rule->insn, rule->cond, uid, gid)) != 0)
return (rv);
}
break;
case '&':
if (lvl >= RULE_MAX_RECURSION_LVL) {
SF_SetError("Too many levels of recursion");
return (-1);
}
if ((pRuleset = LOCAL_GetRulesetByName(&rule->insn[1]))
== NULL) {
return (-1);
}
if ((rv = SF_MessageClassify(msg, rcpt, pRuleset,
lvl+1)) != 0) {
SF_RulesetFree(pRuleset);
return (rv);
}
SF_RulesetFree(pRuleset);
break;
default:
if ((rv = LOCAL_DeliverToAddress(msg, rcpt, rule->insn,
uid, gid)) != 0)
return (rv);
}
Debug("Delivery successful: %s => %s", rule, rule->insn);
}
return (0);
}
/* Extract user/domain parts from an address. */
int
SF_ParseRecipientParts(SF_Recipient *rcpt)
{
const char *c;
int i;
/* Parse the local part. */
for (i = 0, c = &rcpt->addr[0];
i < (ADDRESS_MAX-2) && *c != '\0' && *c != '@';
i++, c++) {
rcpt->user_part[i] = *c;
}
if (i == 0 || *c == '\0' || i == (ADDRESS_MAX-2)) {
return (-1);
}
rcpt->user_part[i] = '\0';
c++;
/* Parse the domain part. */
for (i = 0;
i < (ADDRESS_MAX-2) && *c != '\0';
i++, c++) {
rcpt->domain_part[i] = *c;
}
if (i == 0 || i == (ADDRESS_MAX-2)) {
return (-1);
}
rcpt->domain_part[i] = '\0';
return (0);
}
static char *
UnescapeRule(char *rule)
{
char hex[3];
const char *sp;
char *dst, *dp;
unsigned int n;
dp = dst = Malloc(strlen(rule)+2);
hex[2] = '\0';
for (sp = rule; *sp != '\0'; dp++, sp++) {
if (sp[0] == '\\' && isxdigit(sp[1]) && isxdigit(sp[2])) {
hex[0] = sp[1];
hex[1] = sp[2];
n = (unsigned int)strtoul(hex, NULL, 16);
if (n == '\0') {
*dp = '_';
} else {
*dp = n;
}
sp += 2;
} else if (sp[0] == '+') {
*dp = ' ';
} else {
*dp = sp[0];
}
}
*dp = '\0';
return (dst);
}
/* Parse a ruleset and return a ruleset structure. */
SF_Ruleset *
SF_RulesetParse(const char *key, char *data)
{
char *data_dup , *pdata, *s;
SF_Ruleset *rs;
SF_Rule *r;
if ((data_dup = strdup(data)) == NULL) {
SF_SetError("Out of memory");
return (NULL);
}
pdata = data_dup;
rs = Malloc(sizeof(SF_Ruleset));
Strlcpy(rs->key, key, sizeof(rs->key));
TAILQ_INIT(&rs->rules);
while ((s = strsep(&pdata, ",")) != NULL) {
char *cond = strsep(&s, "|");
char *insn = strsep(&s, "|");
char *uid = strsep(&s, "|");
char *gid = strsep(&s, "|");
if (cond == NULL || cond[0] == '\0' ||
insn == NULL || insn[0] == '\0') {
continue;
}
for (; isspace(*cond); cond++) ;;
for (; isspace(*insn); insn++) ;;
r = Malloc(sizeof(SF_Rule));
r->flags = 0;
if (cond[0] == '!') {
r->flags |= RULE_NEGATE;
cond++;
} else if (cond[0] == '>') {
r->flags |= RULE_SMTP;
}
r->cond = UnescapeRule(cond);
r->insn = UnescapeRule(insn);
if (uid != NULL && uid[0] != '\0') {
r->uid = (uid_t)atoi(uid);
} else {
r->uid = 0;
}
if (gid != NULL && gid[0] != '\0') {
r->gid = (uid_t)atoi(gid);
} else {
r->gid = 0;
}
TAILQ_INSERT_TAIL(&rs->rules, r, rules);
}
free(data_dup);
return (rs);
}
void
SF_RulesetFree(SF_Ruleset *rs)
{
SF_Rule *r1, *r2;
for (r1 = TAILQ_FIRST(&rs->rules);
r1 != TAILQ_END(&rs->rules);
r1 = r2) {
r2 = TAILQ_NEXT(r1, rules);
free(r1->cond);
free(r1->insn);
free(r1);
}
}