/*
* Copyright (c) 2006-2017 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
#include "pathnames.h"
#include
#include
MPD_Recipient *
MPD_MessageAddRecipient(MPD_Message *msg, const char *addr)
{
MPD_Recipient *rcpt;
if ((rcpt = malloc(sizeof(MPD_Recipient))) == NULL) {
MPD_SetErrorS("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
MPD_MessageInit(MPD_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);
}
MPD_Message *
MPD_MessageNew(void)
{
MPD_Message *msg;
if ((msg = malloc(sizeof(MPD_Message))) == NULL) {
MPD_SetErrorS("Out of memory");
return (NULL);
}
MPD_MessageInit(msg);
return (msg);
}
void
MPD_MessageFree(MPD_Message *msg)
{
MPD_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.
* XXX XXX
*/
int
MPD_DomainForcedFiltering(const char *dom)
{
size_t domLen = strlen(dom);
struct dirent *dent;
DIR *dir;
#if 0
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);
if (s != NULL)
return (1);
#endif
/*
* If the domain has no DKIM key on this server, it is likely
* to be external.
*/
if ((dir = opendir(_PATH_DKIM_CERT)) == NULL) {
return (0);
}
while ((dent = readdir(dir)) != NULL) {
if (strstr(dent->d_name, dom) == &dent->d_name[0] &&
(dent->d_name[domLen] == '.' || /* Ignore selector */
dent->d_name[domLen] == '\0')) {
break;
}
}
closedir(dir);
if (dent != NULL) {
return (0);
} else {
return (1); /* Likely external */
}
}
#ifdef HAVE_SA
/*
* Scan a ruleset for "spam" conditions to determine if we'll need
* a spam check for classification.
*/
static int
NeedFiltering(MPD_Message *msg, MPD_Ruleset *ruleset, int lvl)
{
MPD_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' &&
MPD_DomainForcedFiltering(&dom[1]))
return (1);
}
if (rule->insn[0] == '&') {
MPD_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);
MPD_RulesetFree(pRuleset);
if (rv == 1 || rv == -1) {
return (rv);
}
return (1);
}
}
return (0);
}
/*
* Report source IP address and reported score to a central mailblockd
* server (i.e., running on a border router).
*/
static int
ReportScoreToMBD(MPD_Message *msg)
{
char buf[12+39+1+13];
struct addrinfo hints, *res, *res0;
const char *cause = NULL;
int s, rv;
snprintf(buf, sizeof(buf), "%s %s %f", mbdPass, msg->ip,
msg->status.score);
memset(&hints, 0, sizeof(hints));
hints.ai_family = PF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
if ((rv = getaddrinfo(mbdHost, mbdPort, &hints, &res0)) != 0) {
MPD_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) {
MPD_SetError("%s: %s", cause, strerror(errno));
goto fail;
}
if (QMGR_Write(s, buf, strlen(buf)) == -1) {
goto fail;
}
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
MPD_MessageProcess(MPD_Message *msg, MPD_Recipient *rcpt)
{
MPD_Ruleset *ruleset;
int rv;
if ((ruleset = LOCAL_GetRulesetByRcpt(rcpt->addr)) == NULL) {
MPD_SetError("<%s>: No ruleset defined", rcpt->addr);
return (-1);
}
#ifdef HAVE_SA
if (NeedFiltering(msg, ruleset, 0)) {
if (SPAM_Check(msg, rcpt) == -1) {
syslog(LOG_WARNING, "<%s>: spamcheck failed (%s)",
rcpt->addr, MPD_GetError());
msg->status.score = 0.0;
msg->status.req_score = 6.66;
msg->status.spam_status = 0;
} else {
if (mbdReportEnable)
ReportScoreToMBD(msg);
}
}
#endif /* HAVE_SA */
/* Deliver the message according to user classification rulesets. */
rv = MPD_MessageClassify(msg, rcpt, ruleset, 0);
MPD_RulesetFree(ruleset);
return (rv);
}
static __inline__ int
MPD_MatchRule(MPD_Message *msg, MPD_Recipient *rcpt, MPD_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
MPD_MessageClassify(MPD_Message *msg, MPD_Recipient *rcpt, MPD_Ruleset *ruleset,
int lvl)
{
uid_t default_uid;
gid_t default_gid;
MPD_Rule *rule;
MPD_Ruleset *pRuleset;
int rv;
if (LOCAL_GetDefaultRecipientUID(rcpt->addr, &default_uid, &default_gid) == -1) {
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) &&
!MPD_MatchRule(msg, rcpt, rule)) ||
( (rule->flags&RULE_NEGATE) &&
MPD_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);
#ifdef COMPAT_QMAIL
if (rv == 99)
break;
#endif
if (rv != 0) {
MPD_SetError("FeedToPipe(%s): %s",
&rule->insn[1], MPD_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] == '/') {
if ((rv = LOCAL_DeliverToMaildir(msg, rcpt,
rule->insn, rule->cond, uid, gid)) != 0)
return (rv);
} else {
if ((rv = LOCAL_DeliverToMailbox(msg, rcpt,
rule->insn, rule->cond, uid, gid)) != 0)
return (rv);
}
break;
case '&':
if (lvl >= RULE_MAX_RECURSION_LVL) {
MPD_SetErrorS("Too many levels of recursion");
return (-1);
}
if ((pRuleset = LOCAL_GetRulesetByName(&rule->insn[1]))
== NULL) {
return (-1);
}
if ((rv = MPD_MessageClassify(msg, rcpt, pRuleset,
lvl+1)) != 0) {
MPD_RulesetFree(pRuleset);
return (rv);
}
MPD_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
MPD_ParseRecipientParts(MPD_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;
Uint 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 = (Uint)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. */
MPD_Ruleset *
MPD_RulesetParse(const char *key, char *data)
{
char *data_dup , *pdata, *s;
MPD_Ruleset *rs;
MPD_Rule *r;
if ((data_dup = strdup(data)) == NULL) {
MPD_SetErrorS("Out of memory");
return (NULL);
}
pdata = data_dup;
rs = Malloc(sizeof(MPD_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(MPD_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
MPD_RulesetFree(MPD_Ruleset *rs)
{
MPD_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);
}
}