/* * 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); } }