/* * Copyright (C) 2006 Manuel Mausz (manuel@mausz.at) * Origin code copyright (c) mjd@digitaleveryware.com 2003 * (http://www.digitaleveryware.com/projects/greylisting/) * * 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. */ #include #include #include #include #include #include #define SQLCMDSIZE 2048 #define RET_NOTFOUND 0 #define RET_ACCEPT 1 #define RET_REJECT 2 #define RET_TEMPREJECT 3 #define CMD_TEMPREJECT "E451 temporary failure (#4.3.0)\n" #define CMD_REJECT "E553 sorry, your envelope sender has been denied (#5.7.1)\n" #define LOGLEVEL_FATAL 1 #define LOGLEVEL_ERROR 2 #define LOGLEVEL_WARN 3 #define LOGLEVEL_INFO 4 #define LOGLEVEL_DEBUG 5 #define MAXCONFIGLINESIZE 1024 // maybe change this to dynamic allocation sometime #if MYSQL_VERSION_ID >= 50003 # define QUERYSIZE 500 #else # define QUERYSIZE 700 #endif static char *configfile = "control/greylisting"; static char *mysql_host = NULL; static char *mysql_user = NULL; static char *mysql_pass = NULL; static char *mysql_db = NULL; unsigned int mysql_port = 3306; unsigned int block_expire = 55; unsigned int record_expire = 500; unsigned int record_expire_good = 36; static char *relay_ip; static char *mail_from; static char *rcpt_to; static int loglevel = LOGLEVEL_WARN; void gllog(unsigned int level, char* format, ...) { va_list args; if (level > loglevel) return; va_start(args, format); vfprintf(stderr, format, args); va_end(args); } int load_config(void) { char *tmp, *delim, *atpos; FILE *config; char buf[MAXCONFIGLINESIZE]; int i; unsigned int userlen; /* first check for logging var */ tmp = getenv("GLLOGLEVEL"); if (tmp) loglevel = atoi(tmp); /* check if greylisting is enabled */ if (!getenv("GREYLISTING") || getenv("RELAYCLIENT")) { gllog(LOGLEVEL_DEBUG, "greylisting: greylisting is not enabled\n"); return 0; } /* basic environment variables needed */ relay_ip = getenv("TCPREMOTEIP"); mail_from = getenv("SMTPMAILFROM"); rcpt_to = getenv("SMTPRCPTTO"); if (!relay_ip || !mail_from || !rcpt_to) { gllog(LOGLEVEL_FATAL, "greylisting: one of the following envvars is undefined: TCPREMOTEIP, SMTPMAILFROM, SMTPRCPTTO\n"); return 0; } /* check for BATV ("prvs=X=u@d.t" minimum) */ if (strlen(mail_from) > 11 && mail_from[0] == 'p' && mail_from[1] == 'r' && mail_from[2] == 'v' && mail_from[3] == 's' && mail_from[4] == '=') { /* BATV: prvs=HASH=user@domain.tld */ if ((delim = strchr(mail_from + 5, '='))) mail_from = delim + 1; /* BATV: prvs=user/HASH@domain.tld */ else if ((delim = strchr(mail_from + 5, '/')) && (atpos = strchr(delim, '@'))) { userlen = delim - mail_from - 5; memmove(atpos - userlen, mail_from + 5, userlen); mail_from = atpos - userlen; } } /* avoid buffer overflows (max. query is ~410 chars long) */ if (strlen(relay_ip) + strlen(mail_from) + strlen(rcpt_to) > SQLCMDSIZE - QUERYSIZE) { gllog(LOGLEVEL_FATAL, "greylisting: buffer overflow protection occurs\n"); return 0; } /* fetch config file path */ tmp = getenv("GLCONFIGFILE"); if (tmp) configfile = tmp; /* fetch config file content */ gllog(LOGLEVEL_DEBUG, "greylisting: configfile=%s\n", configfile); config = fopen(configfile, "r"); if (!config) gllog(LOGLEVEL_DEBUG, "greylisting: configfile error: %s\n", strerror(errno)); else { while((tmp = fgets(buf, sizeof(buf), config))) { if (buf[0] == '#' || buf[0] == ';') continue; for(i = 0; i < strlen(buf) && buf[i] != '\r' && buf[i] != '\n'; i++); buf[i] = 0; if (strstr(tmp, "mysql_host=") == tmp) { free(mysql_host); mysql_host = strdup(tmp + strlen("mysql_host=")); } else if (strstr(tmp, "mysql_port=") == tmp) mysql_port = atoi(tmp + strlen("mysql_port=")); else if (strstr(tmp, "mysql_user=") == tmp) { free(mysql_user); mysql_user = strdup(tmp + strlen("mysql_user=")); } else if (strstr(tmp, "mysql_pass=") == tmp) { free(mysql_pass); mysql_pass = strdup(tmp + strlen("mysql_pass=")); } else if (strstr(tmp, "mysql_db=") == tmp) { free(mysql_db); mysql_db = strdup(tmp + strlen("mysql_db=")); } else if (strstr(tmp, "block_expire=") == tmp) block_expire = atoi(tmp + strlen("block_expire=")); else if (strstr(tmp, "record_expire=") == tmp) record_expire = atoi(tmp + strlen("record_expire=")); else if (strstr(tmp, "record_expire_good=") == tmp) record_expire_good = atoi(tmp + strlen("record_expire_good=")); else if (strstr(tmp, "loglevel=") == tmp && !getenv("GLLOGLEVEL")) loglevel = atoi(tmp + strlen("loglevel=")); } fclose(config); } /* environment variables */ tmp = getenv("GLMYSQLHOST"); if (tmp) { free(mysql_host); mysql_host = strdup(tmp); } tmp = getenv("GLMYSQLPORT"); if (tmp) mysql_port = atoi(tmp); tmp = getenv("GLMYSQLUSER"); if (tmp) { free(mysql_user); mysql_user = strdup(tmp); } tmp = getenv("GLMYSQLPASS"); if (tmp) { free(mysql_pass); mysql_pass = strdup(tmp); } tmp = getenv("GLMYSQLDB"); if (tmp) { free(mysql_db); mysql_db = strdup(tmp); } tmp = getenv("GLBLOCKEXPIRE"); if (tmp) block_expire = atoi(tmp); tmp = getenv("GLRECORDEXPIRE"); if (tmp) record_expire = atoi(tmp); tmp = getenv("GLRECORDEXPIREGOOD"); if (tmp) record_expire_good = atoi(tmp); /* logging */ gllog(LOGLEVEL_DEBUG, "greylisting: mysql: host=%s, port=%d, user=%s, pass=******\n", mysql_host, mysql_port, mysql_user); gllog(LOGLEVEL_DEBUG, "greylisting: block_expire=%d, record_expire=%d, record_expire_good=%d\n", block_expire, record_expire, record_expire_good); return 1; } void cleanup() { free(mysql_host); free(mysql_user); free(mysql_pass); } int mysql_query_wrapper(MYSQL *mysql, char *query) { int result; result = mysql_query(mysql, query); gllog(LOGLEVEL_DEBUG, "greylisting: mysql: %s - ret=%d\n", query, result); return result; } /* check if relay_ip or rcpt_to is white-/blacklisted */ int check_listed(MYSQL *mysql) { MYSQL_RES *res; MYSQL_ROW row; char query[SQLCMDSIZE]; int found = RET_NOTFOUND; char *rcpt_to_esc = NULL; char *domain_esc = NULL; char *domain = NULL; domain = strrchr(rcpt_to, '@'); /* fallback to full rcpt_to if there's no domain */ domain = (domain) ? domain + 1 : rcpt_to; domain_esc = malloc(strlen(domain)*2 + 1); mysql_real_escape_string(mysql, domain_esc, domain, strlen(domain)); rcpt_to_esc = malloc(strlen(rcpt_to)*2 + 1); mysql_real_escape_string(mysql, rcpt_to_esc, rcpt_to, strlen(rcpt_to)); sprintf(query, "SET @uipaddr = inet_aton('%s'), @udomain = '%s', @urcpt_to = '%s'; ", relay_ip, domain_esc, rcpt_to_esc); if (mysql_query_wrapper(mysql, query)) { gllog(LOGLEVEL_ERROR, "greylisting: mysql: %s\n", mysql_error(mysql)); return 0; } free(domain_esc); free(rcpt_to_esc); #if MYSQL_VERSION_ID >= 50003 sprintf(query, "SELECT `id`, `block_expires` >= UTC_TIMESTAMP(), `block_expires` < UTC_TIMESTAMP() " "FROM `greylisting_lists` " "WHERE `record_expires` > UTC_TIMESTAMP() " "AND ( " "( " "`rcpt_to` IS NULL " "AND `ipaddr_start` <= @uipaddr " "AND @uipaddr <= `ipaddr_end` " ") OR ( " "`ipaddr` IS NULL " "AND ( " "`rcpt_to` = @udomain " "OR `rcpt_to` = @urcpt_to " ") " ") " ") " "ORDER BY (`ipaddr_end` - `ipaddr_start`) ASC " "LIMIT 1"); #else sprintf(query, "SELECT `id`, `block_expires` >= UTC_TIMESTAMP(), `block_expires` < UTC_TIMESTAMP() " "FROM `greylisting_lists` " "WHERE `record_expires` > UTC_TIMESTAMP() " "AND ( " "( " "`rcpt_to` IS NULL " "AND (@base := IF(INSTR(`ipaddr`, '.'), 32, 128)) " "AND IF( " "INSTR(`ipaddr`, '/'), " "(@ipaddr_start := inet_aton(substring_index(`ipaddr`, '/', 1))) " "AND (@ipaddr_count := POW(2, @base - substring_index(`ipaddr`, '/', -1))) " "AND (@ipaddr_end := @ipaddr_start + @ipaddr_count - 1), " "(@ipaddr_start := inet_aton(`ipaddr`)) " "AND (@ipaddr_end := @ipaddr_start) " ") " "AND @ipaddr_start <= @uipaddr " "AND @uipaddr <= @ipaddr_end " ") OR ( " "`ipaddr` IS NULL " "AND ( " "`rcpt_to` = @udomain " "OR `rcpt_to` = @urcpt_to " ") " ") " ") " "ORDER BY (@ipaddr_end - @ipaddr_start) ASC " "LIMIT 1"); #endif if (mysql_query_wrapper(mysql, query) || !(res = mysql_store_result(mysql))) { gllog(LOGLEVEL_ERROR, "greylisting: mysql: %s\n", mysql_error(mysql)); return 0; } if ((row = mysql_fetch_row(res))) { if (atoi(row[1])) { found = RET_REJECT; gllog(LOGLEVEL_INFO, "greylisting: %s/%s is blacklisted (id=%s) - rejecting\n", relay_ip, domain, row[0]); } else if (atoi(row[2])) { found = RET_ACCEPT; gllog(LOGLEVEL_INFO, "greylisting: %s/%s is whitelisted (id=%s) - accepting\n", relay_ip, domain, row[0]); } } mysql_free_result(res); return found; } int check_greylisted(MYSQL *mysql) { MYSQL_RES *res; MYSQL_ROW row; char query[SQLCMDSIZE]; char *mail_from_esc = NULL; char *rcpt_to_esc = NULL; char *relay_ip_sub = NULL; char *ptr; char ipdelimeter; int ret = RET_NOTFOUND; mail_from_esc = malloc(strlen(mail_from)*2 + 1); rcpt_to_esc = malloc(strlen(rcpt_to)*2 + 1); mysql_real_escape_string(mysql, mail_from_esc, mail_from, strlen(mail_from)); mysql_real_escape_string(mysql, rcpt_to_esc, rcpt_to, strlen(rcpt_to)); /* * 0 ... query matches anything in the same /24 subnet * 1 ... query does an exact ip match */ if (0) { sprintf(query, "SELECT `id`, `block_expires` < UTC_TIMESTAMP() " "FROM `greylisting_data` " "WHERE `record_expires` > UTC_TIMESTAMP() " "AND `relay_ip` = '%s' " "AND `mail_from` = '%s' " "AND `rcpt_to` = '%s' " "LIMIT 1", relay_ip, mail_from_esc, rcpt_to_esc); } else { /* strip off the last octet */ ipdelimeter = '.'; if (!strchr(relay_ip, '.')) ipdelimeter = ':'; relay_ip_sub = strdup(relay_ip); ptr = strrchr(relay_ip_sub, ipdelimeter); if (ptr) *ptr = '\0'; sprintf(query, "SELECT `id`, `block_expires` < UTC_TIMESTAMP() " "FROM `greylisting_data` " "WHERE `record_expires` > UTC_TIMESTAMP() " "AND `relay_ip` LIKE '%s%c%%' " "AND `mail_from` = '%s' " "AND `rcpt_to` = '%s' " "LIMIT 1", relay_ip_sub, ipdelimeter, mail_from_esc, rcpt_to_esc); free(relay_ip_sub); } if (mysql_query_wrapper(mysql, query) || !(res = mysql_store_result(mysql))) { gllog(LOGLEVEL_ERROR, "greylisting: mysql: %s\n", mysql_error(mysql)); return RET_NOTFOUND; } if ((row = mysql_fetch_row(res))) { if (atoi(row[1])) { sprintf(query, "UPDATE `greylisting_data` " "SET `record_expires` = UTC_TIMESTAMP() + INTERVAL %u DAY, `passed_count` = `passed_count` + 1 " "WHERE `id` = '%s'", record_expire_good, row[0]); ret = RET_ACCEPT; gllog(LOGLEVEL_INFO, "greylisting: %s (%s -> %s) exists (id=%s) - accepting\n", relay_ip, mail_from, rcpt_to, row[0]); } else { sprintf(query, "UPDATE `greylisting_data` " "SET `blocked_count` = `blocked_count` + 1 " "WHERE `id` = '%s'", row[0]); ret = RET_TEMPREJECT; gllog(LOGLEVEL_INFO, "greylisting: %s (%s -> %s) is blocked (id=%s) - temp. rejecting\n", relay_ip, mail_from, rcpt_to, row[0]); } } else { sprintf(query, "INSERT INTO `greylisting_data` " "VALUES (0, '%s', '%s', '%s', UTC_TIMESTAMP() + INTERVAL %u MINUTE, UTC_TIMESTAMP() + INTERVAL %u MINUTE, 1, 0, 0, UTC_TIMESTAMP(), UTC_TIMESTAMP())", relay_ip, mail_from_esc, rcpt_to_esc, block_expire, record_expire); ret = RET_TEMPREJECT; gllog(LOGLEVEL_INFO, "greylisting: %s (%s -> %s) doesn't exist. - temp. rejecting\n", relay_ip, mail_from, rcpt_to); } mysql_free_result(res); if (mysql_query_wrapper(mysql, query)) { gllog(LOGLEVEL_ERROR, "greylisting: mysql: %s\n", mysql_error(mysql)); return RET_NOTFOUND; } free(mail_from_esc); free(rcpt_to_esc); return ret; } int main() { int ret = 1; int greylisted = 0; MYSQL *mysql = NULL; /* load config */ if (ret && !load_config()) ret = 0; /* connect to mysql */ if (ret) { mysql_library_init(-1, NULL, NULL); mysql = mysql_init(NULL); if (!mysql_real_connect(mysql, mysql_host, mysql_user, mysql_pass, mysql_db, mysql_port, NULL, 0)) { gllog(LOGLEVEL_FATAL, "greylisting: mysql: %s\n", mysql_error(mysql)); ret = 0; } } /* greylisting checks */ if (ret && !greylisted) { greylisted = check_listed(mysql); if (greylisted == RET_NOTFOUND) greylisted = check_greylisted(mysql); } /* print smtp error code */ if (ret) { switch(greylisted) { case RET_REJECT: printf(CMD_REJECT); break; case RET_TEMPREJECT: printf(CMD_TEMPREJECT); break; } } /* cleanup stuff */ gllog(LOGLEVEL_DEBUG, "greylisting: exiting\n"); if (mysql) mysql_close(mysql); mysql_library_end(); cleanup(); return !ret; }