Download | Plain Text | Line Numbers


/*
 * 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <errno.h>
#include <mysql.h>
 
#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;
}