#include "global.h"
#include "md5.h"
#include <errno.h>
#include <pwd.h>
#include <stdio.h>
#include <unistd.h>
#include <libgen.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <string.h>
#include <stdlib.h>
#if defined(SYSLOG)
#include <syslog.h>
#define ENV_REMOTEHOST	"TCPREMOTEHOST"
#define ENV_REMOTEIP	"TCPREMOTEIP"
#endif

#ifndef LINE_MAX
#define LINE_MAX 256
#endif
unsigned char up[513];
int uplen;

#ifndef USERAPOPFILE
# define USERAPOPFILE	".apop"
#endif
#ifndef APOPOPEN
# define APOPOPEN	"/usr/local/sbin/deapop"
#endif
#ifndef DASH
# define DASH		"-"
#endif
#define PATHSIZ		MAXPATHLEN
#ifndef QMAILCONTROL
# define QMAILCONTROL	"/var/qmail/control"
#endif
static char pathname[MAXPATHLEN];
static char *qmailcontrol = QMAILCONTROL;


void abortwithmsg(char *msg)
{
  fprintf(stderr, "%s\n", msg);
  _exit(3);
}
void nomem()
{
  abortwithmsg("Out of memory");
}


/* memo:
   \0 can be dropped by strncpy.
   \0 never be dropped by snprintf.
 */

char* conv_virtualdomain(char *mailname)
{
  char *dom = strchr(mailname, '@'), *p;
  char vd[PATHSIZ+1], previous[PATHSIZ+1];
  static char rewrite[PATHSIZ+1];
  FILE *vdfd;
  int match=0;
  if (!dom) return mailname;
  
  snprintf(vd, PATHSIZ, "%s/%s", qmailcontrol, "virtualdomains");
  dom++;                /* set position of domain part beginning */
  if (dom && NULL != (vdfd = fopen (vd, "r"))) {
    char buf[PATHSIZ+1], *s;
    int l = strlen(dom);
    int L = strlen(mailname);
    while ((s=fgets(buf, PATHSIZ, vdfd))) {
      if ((p=strchr(s, '#')))
        *p = '\0';                      /* zap comments */
      if (!strchr(buf, ':'))
        continue;
      while (s && (strrchr(s, '\n') || strrchr(s, '\r') || strrchr(s, ' ')))
        s[strlen(s)-1] = '\0';
      if (!strncmp(mailname, s, L) && s[L] == ':' && s[L+1]) { /* user matches */
        match = 3;
        snprintf(rewrite, PATHSIZ, "%s%s%s", s+L+1, DASH, mailname);
        break;
      }
      if (!strncmp(dom, s, l) && s[l] == ':' && s[l+1]) { /* domain matches */
        match = 2;
        snprintf(rewrite, PATHSIZ, "%s%s%s", s+l+1, DASH, mailname);
        continue;
      }
      if (match < 2 && s[0] == '.') { /* if domain described in wildcard */
        if ((p=strchr(s, ':'))) {
          *p = '\0';
          if (!strcmp(dom+(strlen(dom)-strlen(s)), s)) {
            if (match == 0 || strlen(previous) < strlen(s)) {
              match = 1;
              strncpy(previous, s, PATHSIZ);
              snprintf(rewrite, PATHSIZ, "%s%s%s", p+1, DASH, mailname);
            }
          }
        }
      }
    }
    fclose(vdfd);
    if (match) {
      p = strchr(rewrite, '@');
      if (p) {
        *p = '\0';
      }
      memset(vd, 0, sizeof(vd));
      memset(previous, 0, sizeof(previous));
      return rewrite;
    }
  }
  return mailname;            /* no @ marks, return itself */
}

typedef union mixval {
  int   i;
  char	c;
  char*	cp;
  FILE*	fp;
} mix;

mix* assign(char* job, mix arg)
{
  static mix rval;
  return &rval;
}

typedef struct {
  int  n;
  char **line;
} Assign;

static Assign ag[2] = {{0, 0}, {0, 0}};
/* ag[0][] is for users/assign '=' type,
 * ag[1][] is for users/assign '+' type.
 */
void init_assign()
{
  char assign[MAXPATHLEN];
  char *buf, *p;
  char *qcdir;
  FILE *fp;
  struct stat st;
  int i;
  if (ag[0].line && ag[1].line) return;
  qcdir = strdup(qmailcontrol);
  if (!qcdir) _exit(2);
  snprintf(pathname, sizeof pathname, "%s", dirname(qcdir));
  free(qcdir);
  snprintf(assign, sizeof assign, "%s/%s", pathname, "users/assign");
  if (stat(assign, &st) < 0)
    return;
  if (NULL == (buf=(char*)malloc(st.st_size))) nomem();
  if (NULL == (fp=fopen(assign, "r")))
    return;

  for (i=0; i<2; i++) {
    if (NULL == (ag[i].line = (char**)malloc(sizeof(char**)))) nomem();
    ag[i].line[0] = NULL;
  }
  
  while (fgets(buf, st.st_size, fp)) {
    int atype, n;
    for (p=buf; strchr(" \t", *p); p++) ;
    if (*p == '#') continue;
    atype = *p=='+' ? 1 : -(*p!='='); /* '='=0, '+'=1, other=-1 */
    if (atype==-1) continue;
    n = ag[atype].n;
    if (NULL == (ag[atype].line[n]=(char*)malloc(strlen(p)+1))) nomem();
    strncpy(ag[atype].line[n], p, strlen(p));
    ag[atype].line = (char**)realloc(ag[atype].line, (++n+1)*sizeof(char**));
    if (NULL == ag[atype].line)
      _exit(3);
    ag[atype].n = n;
    ag[atype].line[n] = NULL;
  }
#ifdef DEBUG1
  int j;
  for (i=0; i<2; i++) {
    for (j=0; j<ag[i].n; j++)
      printf("%c%4d: %s", "=+"[i], j, ag[i].line[j]);
  }
#endif
}

/* Get Nth field from LINE regarding DELIM as delimiter.
 * Return the newly allocated string.  Free it afterwards.
 */
char* getfieldn(char *line, int n, char *delim)
{
  int i, dlen=strlen(delim), flen;
  char *field, *p, *q = NULL;
  for (i=0, p=line; i<n-1 && *p; i++) {
    for (q=p; *q && strncmp(delim, q, dlen) && *q != '\n'; q++);
    p = q+dlen;
  }
  if (i==n-1 && *p) {
    for (q=p; *q && strncmp(delim, q, dlen) && *q != '\n'; q++);
    flen = q-p;
    if (NULL == (field=(char*)malloc(flen+1))) nomem();
    memset(field, 0, flen+1);
    strncpy(field, p, flen);
    return field;
  }
  return NULL;
}
void setuidgidhome(char *line, struct passwd *pw)
{
  char *delim = ":";
  char *p;
  if ((p=getfieldn(line, 3, delim))) {
    pw->pw_uid = (uid_t)atoi(p);
    free(p);
  }
  if ((p=getfieldn(line, 4, delim))) {
    pw->pw_gid = (uid_t)atoi(p);
    free(p);
  }
  if ((p=getfieldn(line, 5, delim))) {
    pw->pw_dir = p;             /* Don't free here */
  }
}

static char* substr(char *s, char *e)
{
  char *news;
  int len = e-s+1;
  if (NULL == (news=(char*)malloc(len))) nomem();
  strncpy(news, s, len);
  return news;
}

typedef struct {
  struct passwd *p;
  char *dotapop;
} pwapop;

static pwapop* lookupassign(char *user)
{
  int type, j, len, maxlen;
  char *l, *r, *dash, *ext, *pre, *field;
  static pwapop pa;
  static struct passwd p;
  init_assign();
  pa.p = &p;
  type=0;                       /* check against strict assignment */
  for (j=0; j<ag[type].n; j++) {
    l = ag[type].line[j]+1;
    for (r = l; *r && *r != ':'; r++);
    if (!*r) continue;
    len = r-l;
    if (!strncasecmp(user, l, len)) {
      setuidgidhome(l, &p);
      // fprintf(stderr, "%d %d %s\n", p.pw_uid, p.pw_gid, p.pw_dir);
      dash = getfieldn(l, 6, ":");
      ext = getfieldn(l, 7, ":");
      len = strlen(p.pw_dir)+1+5+strlen(dash)+strlen(ext)+1;
      /*       $HOME        /.apop   $DASH       $EXT    \0  */

      if (NULL == (pa.dotapop=malloc(len))) nomem();
      snprintf(pa.dotapop, len, "%s/.apop%s%s", p.pw_dir, dash, ext);
      // fprintf(stderr, "l=%d[%s]\n", len, pa.dotapop);
      free(dash);
      free(ext);
      return &pa;
    }
  }
  type=1;                       /* check against wildcard assignment */
  maxlen = -1;
  for (j=0; j<ag[type].n; j++) {
    l = ag[type].line[j]+1;
    field = getfieldn(l, 1, ":");
    len = strlen(field);
    if (!strncasecmp(user, l, len) && maxlen < len) { /* longest match? */
      maxlen = len;
      if (p.pw_dir) free(p.pw_dir); /* for 2nd loop or later */
      setuidgidhome(l, &p);
      ext = substr(user+len, user+strlen(user));
      dash = getfieldn(l, 6, ":");
      pre = getfieldn(l, 7, ":");
      len = strlen(p.pw_dir)+1+5+strlen(dash)+strlen(pre)+strlen(ext)+1;
      if (pa.dotapop) free(pa.dotapop); /* for 2nd loop or later */
      if (NULL == (pa.dotapop=malloc(len))) nomem();
      snprintf(pa.dotapop, len, "%s/.apop%s%s%s", p.pw_dir, dash, pre, ext);
      // fprintf(stderr, "l=%d[%s]\n", len, pa.dotapop);
      free(ext); free(dash); free(pre);
    }
    if (field) {
      free(field);
      field = NULL;
    }
  }
  if (maxlen > 0 && p.pw_dir && pa.dotapop)
    return &pa;
  return NULL;
}

FILE* getapopfd(char *user)
{
   FILE *fp;
   struct passwd *pw;
   struct stat stbuf;
   char apopfile[PATHSIZ], aprog[PATHSIZ*2];
   char *suffix = NULL;
   pwapop *pa;
   int dashlen = strlen(DASH);
   uid_t uid;

   //fprintf(stderr, "%s\n", user);
   user=conv_virtualdomain(user);
   //fprintf(stderr, "%s\n", user);
   if (!user) _exit(2);
   /* First, lookup users/assign */
   if ((pa=lookupassign(user))) {
     strncpy(apopfile, pa->dotapop, PATHSIZ);
     free(pa->dotapop);
     pa->dotapop = NULL;
     uid = pa->p->pw_uid;
   } else {
     /* Then, try to get real user from passwd entries */
     pw = getpwnam(user);
     if (!pw && strstr(user, DASH)) {
       /* Try again after removing extension - Check qmail alias name */
       char tmp[PATHSIZ];
       char *s = user;
       memset(tmp, 0, PATHSIZ);
       while (*s && s && !pw) {
         s = strstr(s, DASH);     /* Search from left. */
         if (!s) break;
         strncpy(tmp, user, s-user); /* copy upto '-' */
         s += dashlen;
         pw = getpwnam(tmp);
       }
       if (pw) {
         /* s points to the next of DASH */
         if (NULL == (suffix=(char*)malloc(strlen(s)+dashlen+1))) nomem();
         memcpy(suffix, s-dashlen, strlen(s)+dashlen+1); /* including \0 */
         user[strlen(tmp)] = '\0'; /* zap suffix */
       }
     }
     if (!pw) _exit(2);           /* If no user entry here, abort. */
     uid = pw->pw_uid;
     snprintf(apopfile, sizeof apopfile, "%s/%s", pw->pw_dir, USERAPOPFILE);

     if (suffix)
       strncat(apopfile, suffix, (sizeof apopfile)-strlen(apopfile)-1);
     /* Do not care if path name truncated.  Just skip it. */

   }
   if (setuid(uid) != 0) _exit(2);
   if (stat(apopfile, &stbuf) < 0)
     return (FILE*)-2;          /* no password file, exit with error */
   if (stbuf.st_mode & ~(S_IFREG | S_IREAD | S_IWRITE))
     return (FILE*)-3;      /* if readable by others, exit with error */
   if (stat(APOPOPEN, &stbuf) >= 0) {
     snprintf(aprog, sizeof aprog, "%s %s", APOPOPEN, apopfile);
     fp = popen(aprog, "r");
   } else {
     fp = fopen(apopfile, "r");
   }
   return fp;
}

int doit(unsigned char *testlogin, unsigned char *response, unsigned char *challenge)
{
  static char line[LINE_MAX + 1];
  int found_user= 0;
  char *password = NULL;
  char *digest;
  FILE *fp;
  int result;
  memset(line, 0, sizeof line);

  if (0 >= (fp=getapopfd((char*)testlogin)))
    _exit(2);

  if (fp) {
    fgets(line, sizeof line, fp);
    fclose(fp);
    if (line[0]) {
      int p = strlen(line);
      while (p > 0 && (line[p-1] == '\n' || line[p-1] == '\r')) {
        line[--p] = '\0'; /* zap trailing newlines */
      }
      password = line;
      found_user=1;
    }
  }

  if (!found_user) {
    memset(line, 0, sizeof line);
    return(1);
  }
//fprintf(stderr, "challenge=[%s](%d), password=[%s](%d)\n", challenge, strlen(challenge), password, strlen(password));
  digest = hmac_md5((char*)challenge, strlen((char*)challenge), (char*)password, strlen((char*)password));

  result = (strcmp(digest,(char*)response) && strcmp(password,(char*)response));
  memset(line, 0, sizeof line);
  return result;
}

int main(int argc,char **argv)
{
  unsigned char *login;
  unsigned char *response;
  unsigned char *challenge;
  int r;
  int i;
  int accepted;
  memset(up, 0, sizeof up);
 
  if (!argv[1]) _exit(2);
  if (getenv("QMAILCONTROL"))
    qmailcontrol = getenv("QMAILCONTROL");
 
  uplen = 0;
  for (;;) {
    do
      r = read(3,up + uplen,sizeof(up) - uplen);
    while ((r == -1) && (errno == EINTR));
    if (r == -1) _exit(111);
    if (r == 0) break;
    uplen += r;
    if (uplen >= sizeof(up)) _exit(1);
  }

  close(3);

  i = 0;
  login = up + i;
  while (up[i++]) if (i == uplen) _exit(2);
  response = up + i;
  if (i == uplen) _exit(2);
  while (up[i++]) if (i == uplen) _exit(2);
  challenge = up + i;

#ifdef QMAIL_SMTPD_AUTH_031	/* It sends 2params in reverse order */
  accepted=doit(login, challenge, response);
#else
  accepted=doit(login, response, challenge);
#endif
  
#if defined(SYSLOG) && defined(LOG_MAIL)
    syslog(LOG_INFO|LOG_MAIL, "Auth %s user=%s host=%s remoteip=%s",
           accepted ? "FAIL" : "success",
           login,
           getenv(ENV_REMOTEHOST) ? getenv(ENV_REMOTEHOST) :"unknown",
           getenv(ENV_REMOTEIP) ? getenv(ENV_REMOTEIP) :"unknownIP");
#endif
  for (i = 0;i < sizeof(up);++i) up[i] = 0;
 
  if (accepted) _exit(1);
  execvp(argv[1], argv + 1);
  _exit(111);
}
