/* smtp-delay 
   Copyright 2005 Jon Lewis <jlewis@lewis.org>

    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.
-
    For a copy of the GPL, see http://www.fsf.org/licensing/licenses/gpl.html
   
   This little number can be used to introduce smtp banner delays for qmail. 
   When run between tcpserver and rblsmtpd, it'll do a reverse lookup of the
   connecting IP, compare that PTR to a regex, and then apply long banner
   delays if there was no PTR or if the PTR matches the "dialup" regex.  The
   program depends on the fact that tcpserver will set TCPREMOTEIP, and will
   take advantage of TCPREMOTEHOST if it's set.  If the client tries to
   pipeline (ram SMTP commands down our throat before we show them an SMTP
   banner), RBLSMTPD is set, notifying rblsmtpd to refuse their mail.
   
   0.1: 20050614
   
   0.2: 20050614 A bunch of minor code cleanups suggested by der Mouse 
   <mouse@Rodents.Montreal.QC.CA>
   
   0.3: 20050615 made delays configurable via getopt
   
   0.4: 20050615 won't blow away the RBLSMTPD env var if it already exists
   i.e. if it's set to "" to allow some broken host through, they'll still 
   be allowed through even if they do pipeline.
   
   0.5: 20050615 check and use TCPREMOTEHOST which tcpserver may have set.
   If it's there, great.  If it's not, do our own reverse lookup.

   0.6: 20050618 added -S switch for standalone mode...just print a 451 message
   and exit without calling rblsmtpd (or whatever is next) if they pipeline and 
   RBLSMTPD was not already set.  The message "logged" to stderr may not work
   properly (the time waited) on OS's other than Linux.  
   Fixed a problem with the DYNPAT regex and added dhcp to it.
   Altered program flow such that pretty much everything is skipped if RBLSMTPD
   was already set.
  
   0.7 20050707 differentiate between pipeliners and quitters.  
   
   0.8 20050707 If they pipeline, log up to 99 chars of whatever they sent.
   Minor code cleanups.
   
   0.9 20050708 A few additions to the dynamic regex
   
   0.10 20050715 Feature request: allow built-in and command line supplied 
   delays to be overridden by environment variables that may be set by 
   tcpserver via settings in its cdb.  
 
   0.11 20050722 DYNPAT regex update.  Display DYNPAT as part of -v.

   0.12 20050816 remove newline from end of pipeline if it's there...keeps
   from logging an blank line after each log entry for which a short amount 
   of pipelined data was logged.     

   0.13 20051007 if RBLSMTPD is set to SMTPDELAY, smtp-delay will still do 
   the smtp-delay logic, but will then set RBLSMTPD="".  This only really 
   makes sense with smtp-delay running in standalone mode, so it can issue a 
   4xx to pipeliners but treat the connection as if rblsmtpd would have been 
   skipped.  i.e. you want to apply smtp-delay to your own dialup users, 
   but you wouldn't normally subject them to dnsbl blocking.

   0.14 20051017 added -N option which allows smtp-delay to reject mail from
   hosts with no rDNS, but only do so with the -N supplied probability.  This
   can be used to give remote hosts a sort of wakeup call on their lack of rDNS.
   With a small probability set, any mail blocked should eventually make it 
   through...eventually.
   Added error checking to make sure numeric args really are.

   0.15 20051018 minor code optimization.
   
   0.16 20051219 I've realized that using the RBLSMTPD env var for smtp-delay
   (the feature added in 0.13) was a dangerous thing to do.  Change the order 
   of execution and forget to modify tcp.smtp, and you'll reject all mail.  So 
   this feature has been redone.  Set SMTPDELAY="JUSTDELAY" now instead of 
   RBLSMTPD="SMTPDELAY" if you want smtp-delay to skip rblsmtpd but still
   do banner-delay/anti-pipelining.

   0.17 20051230 Minor tweaks to JUSTDELAY.     

   0.18 20060320 Added DYNAMICHELO env var setting for when the connecting IP 
   looks dynamic.  Intention is qmail-smtpd can compare DYNAMICHELO (if set)
   to what the host helo's as, and if they match, reject them.
   Minor tweak to DYNPAT to try to avoid some FPs.
   Set TCPREMOTEHOST if we look up the PTR and it wasn't previously set.
   Add a second "extra dynamic" looking regex.  Put PTR in DYNAMICHELO
   if it matches this second regex.  
   
   0.19 20090902 Tweak to DYNIPPAT to try to reduce FPs triggered by oddly
   named mail servers and/or numbered servers in numeric domains.  i.e. 
   samx1w27m3.etrade.com or m15-29.126.com

   0.20 20111230 Added the DYNFPPAT, a regex to try to avoid FPs on "poorly" named 
   mail servers with otherwise dynamic looking PTRs.  I'm sure this regex will be a 
   work-in-progress
      
*/

#include <unistd.h>
#include <netdb.h>
#include <stdlib.h>
#include <stdio.h>
#include <regex.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define VERSION "0.20"
#define SOMESSAGE "Rejected due to illegal pipelining" 
#define SORDNS "Rejected due to missing rDNS/PTR for IP: "
#define DYNPAT "((ppp|slip|pool|dyn|dinamic|guest|user|dial|dhcp|modem|port|cable|catv|cabo|docsis|kabel|kctv|ktv|acceso|televiso|dorm|newres|resnet|rescomp|reshall|residence|mobile|wireless|wlan|wless|wls|wrless|students|unassigned|unlabeled|unknown|unprovisioned|unused).*\\.[^.]+\\.[^.]+)|[0-9]+[^0-9][0-9]+[^0-9][0-9]+[^0-9]|[0-9]{6}"
#define DYNIPPAT "([^a-z]|^)(ppp|adsl|dhcp|modem|unassigned|unlabeled|unprovisioned)[^a-z].*\\.[^.]+\\.[^.]+|[0-9]+\\.[0-9]+\\.[0-9]+|[0-9]+-[0-9]+-[0-9]+"
#define DYNFPPAT "[^a-z0-9]static[^a-z0-9]|^(e-?)?mail|mx|smtp"
#define SDS 0
#define SDUS 250000
#define LDS 16
#define LDUS 0
#define PDS 26
#define PDUS 0
#define PIPELEN 100
#define SDENV "SMTPDELAYd"
#define LDENV "SMTPDELAYD"
#define PDENV "SMTPDELAYP"
#define JUSTDELAY "JUSTDELAY"

/****************************************************************************
* Print usage info and exit.                                                *
****************************************************************************/
void usage(void) {
  printf("\nsmtp-delay version %s\n\n",VERSION);
  printf("Usage:\nsmtp-delay [-d N] [-D N] [-P N] [-N N] [-S] [-v]\n\n");
  printf("  -d N - delay to use for hosts with non-dynamic looking rDNS\n");
  printf("  -D N - delay to use for hosts with dynamic looking rDNS\n");
  printf("  -P N - delay to use for hosts with no rDNS\n");
  printf("  Any of these delays can be supplied as either an integer number of\n");
  printf("  seconds or a decimal number of [seconds].[fractionalseconds].\n");  
  printf("  If values are not specified via command line arguments, defaults of:\n");
  printf("  -d %f -D %f -P %f will be used.\n", SDS+SDUS/1000000.0, LDS+LDUS/1000000.0, PDS+PDUS/1000000.0);
  printf("  These delays can also be set via the environment variables %s,\n",SDENV);
  printf("  %s, and %s.\n", LDENV, PDENV);
  printf("  -N - probability for which IPs with no PTR should be turned away with a\n");
  printf("  421 message\n");
  printf("  -S - standalone mode...will give the client a 451 message and terminate\n");
  printf("  without execing whatever was to come next if they pipeline.\n"); 
  printf("  -v - prints this message and exits\n\n");
  printf("  \"Dynamic looking\" regex is currently:\n");
  printf("  %s\n",DYNPAT); 
  printf("  \"Extra Dynamic looking\" regex is currently:\n");
  printf("  %s\n",DYNIPPAT);
  printf("  \"Anti-FP\" regex is currently:\n");
  printf("  %s\n\n",DYNFPPAT);
  exit(EXIT_SUCCESS);
}

/****************************************************************************
* Get PTR of supplied IP.  Get it from TCPREMOTEHOST if set by tcpserver... *
* otherwise do it ourself.                                                  *
****************************************************************************/
char *getrev(struct in_addr ip) {
  char *host_env;
  const struct hostent *hent;
  
  host_env = getenv("TCPREMOTEHOST");
  if (host_env)
    return(host_env);
  hent = gethostbyaddr((char *)&ip,sizeof(ip), AF_INET);
  if(hent) { /* may as well have this in the env for smtpd logging purposes
  		For some reason, sometimes tcpserver's query will fail, but
  		smtp-delay's will succeed. */
    setenv("TCPREMOTEHOST",hent->h_name,1);
    return(hent->h_name);
  }
  else
    return(0);
}

/****************************************************************************
* Get override delays from environment variables which may have been set in *
* tcp.smtp.                                                                 *
****************************************************************************/
int get_ord(char *envvar, struct timeval *timeout) {
  float td;
  char *env_delay;
  
  env_delay = getenv(envvar);
  if (env_delay) {
    timeout->tv_sec = atoi(env_delay);
    td = atof(env_delay);
    timeout->tv_usec = 1000000 * (td - timeout->tv_sec);
    return(1);
  }
  return(0);
}

/***************************************************************************
* Return true or false based on whether or not the passed in number is >=  *
* a randomly generated number between 1 and 100.                           *
***************************************************************************/
int chance(int probability) {
  int j;
  
  if (probability == 0 || probability > 99)
    return(probability);
  srand(time(NULL)*getpid());
  j = 1 + (int)(100.0*rand()/(RAND_MAX+1.0)); /* make a rand 1..100 */
  return(probability >= j);
}

int main (int argc, char **argv) {
  struct in_addr ip;
  fd_set rfds, efds;
  struct timeval timeout, otimeout;
  regex_t dynreg, dynipreg, dynfpreg;
  char *ptrname, *ip_env, *rbl_env, pipeline[PIPELEN] = "", *errmsg;
  char *smtpdelay_env;
  int oc, lds, ldus, sds, sdus, pds, pdus, so, justdelay, noptrrej;
  float td;
  extern char *optarg;
  extern int optind, opterr, optopt;
  
  lds = ldus = sds = sdus = pds = pdus = -1;
  so = justdelay = noptrrej = 0;
  
  while (1) {
    oc = getopt(argc, argv, "+D:d:N:P:Sv");
    if (oc == -1) 
      break;
    switch(oc) {
      case 'd':
        if (optarg[0] < '0' || optarg[0] > '9')
          usage();
        sds = atoi(optarg);
        td = atof(optarg);
        sdus = 1000000 * (td - sds);
        break;
      case 'D':
        if (optarg[0] < '0' || optarg[0] > '9')
          usage();
        lds = atoi(optarg);
        td = atof(optarg);
        ldus = 1000000 * (td - lds);
        break;
      case 'N':
        if (optarg[0] < '0' || optarg[0] > '9')
          usage();
        noptrrej = atoi(optarg);
        break;
      case 'P':
        if (optarg[0] < '0' || optarg[0] > '9')
          usage();
        pds = atoi(optarg);
        td = atof(optarg);
        pdus = 1000000 * (td - pds);
        break;
      case 'S':
        so = 1;
        break;
      case '?':
      case 'v':
        usage();
        break;
      }
  }
  if ((smtpdelay_env = getenv("SMTPDELAY")) != NULL)
    if (strncmp(smtpdelay_env,JUSTDELAY,strlen(JUSTDELAY)) == 0)
      justdelay = 1;
  if ((rbl_env = getenv("RBLSMTPD")) == NULL) { 
  /* 
     if RBLSMTPD is non NULL, we're blocking anyway, so don't bother with
     any of this.
  */
    ip_env = getenv("TCPREMOTEIP");
    if (ip_env) {
      inet_aton(ip_env,&ip);  
      ptrname = getrev(ip);
    }
    else {
      ptrname = NULL;
    }  
    
    if (ptrname) { /* ptr exists */
      regcomp(&dynreg, DYNPAT, REG_EXTENDED|REG_ICASE|REG_NOSUB);
      regcomp(&dynfpreg, DYNFPPAT, REG_EXTENDED|REG_ICASE|REG_NOSUB);
      if (regexec(&dynreg, ptrname, 0, NULL, 0) || !regexec(&dynfpreg, ptrname, 0, NULL, 0)) { /* doesn't match dynamic regex, or matches the anti-fp regex*/
        if (! get_ord(SDENV, &timeout)) {
          timeout.tv_sec = (sds < 0) ? SDS : sds;
          timeout.tv_usec = (sdus < 0) ? SDUS : sdus;
        }
      }
      else { /* ptr exists, but is dynamic looking */
        if (!justdelay) {
          regcomp(&dynipreg, DYNIPPAT, REG_EXTENDED|REG_NOSUB);
          if (! regexec(&dynipreg, ptrname, 0, NULL, 0)) /* matches */
            setenv("DYNAMICHELO",ptrname,1);
        }
        if (! get_ord(LDENV, &timeout)) {
          timeout.tv_sec = (lds < 0) ? LDS : lds;
          timeout.tv_usec = (ldus < 0) ? LDUS : ldus;
        }
      }
    }
    else { /* no ptr */
      if (chance(noptrrej) && (!justdelay)) {
        if ((errmsg = malloc(strlen(SORDNS) + strlen(ip_env) + 1)) == NULL) {
          fprintf(stderr,"smtp-delay: %s pid %d: malloc failed\n", ip_env, getpid());
          exit(EXIT_FAILURE);
        }
        sprintf(errmsg, "%s%s", SORDNS, ip_env);
        if (so) {
          printf("421 %s\r\n",errmsg);
          fprintf(stderr,"smtp-delay: %s pid %d: rejected for no rDNS\n", ip_env, getpid());
          exit(EXIT_SUCCESS);
        }
        else {
          setenv("RBLSMTPD",errmsg,1);
        }
      }
      if (! get_ord(PDENV, &timeout)) {
        timeout.tv_sec = (pds < 0) ? PDS : pds;
        timeout.tv_usec = (pdus < 0) ? PDUS : pdus;
      }
    }
    FD_ZERO(&rfds);
    FD_ZERO(&efds);
    FD_SET(0, &rfds);
    FD_SET(0, &efds);
    FD_SET(1, &efds);
    memcpy(&otimeout, &timeout, sizeof(timeout));
    if (select(2, &rfds, 0, &efds, &timeout) > 0) {
      if (so) {
        printf("451 %s\r\n",SOMESSAGE);
        if (fgets(pipeline,PIPELEN,stdin)) {
          if (pipeline[strlen(pipeline) - 1] == '\n') 
            pipeline[strlen(pipeline) - 1] = 0;
          fprintf(stderr,"smtp-delay: %s pid %d: waited %0.2f %s\n", ip_env, getpid(), ((otimeout.tv_sec + otimeout.tv_usec / 1000000.0)-(timeout.tv_sec + timeout.tv_usec / 1000000.0)),pipeline);
        }
        else
          fprintf(stderr,"smtp-delay: %s pid %d: quit %0.2f\n", ip_env, getpid(), ((otimeout.tv_sec + otimeout.tv_usec / 1000000.0)-(timeout.tv_sec + timeout.tv_usec / 1000000.0)));
        exit(EXIT_SUCCESS);
      }
      if (!justdelay)
        setenv("RBLSMTPD",SOMESSAGE,1);
    }
  }
  if (justdelay)
    setenv("RBLSMTPD","",1);
  if (argv[optind])
    execvp(argv[optind], argv + optind);
  return 0;
}
