#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "byte.h"
#include "case.h"
#include "config.h"
#include "cookie.h"
#include "date822fmt.h"
#include "datetime.h"
#include "die.h"
#include "error.h"
#include "messages.h"
#include "fmt.h"
#include "getln.h"
#include "idx.h"
#include "lock.h"
#include "log.h"
#include "makehash.h"
#include "open.h"
#include "qmail.h"
#include "readwrite.h"
#include "scan.h"
#include "slurp.h"
#include "str.h"
#include "stralloc.h"
#include "strerr.h"
#include "sub_std.h"
#include "subhash.h"
#include "substdio.h"
#include "subdb.h"
#include "uint32.h"
#include "wrap.h"

static stralloc addr = {0};
static stralloc lcaddr = {0};
static stralloc fn = {0};
static stralloc fnnew = {0};
static stralloc fnlock = {0};
static stralloc outline = {0};

static char ssbuf[512];
static char inbuf[512];
static char ssnewbuf[256];

static datetime_sec when;

static substdio ss;
static substdio ssin;
static substdio ssnew;

static int fdnew;

static void die_read(void)
{
  strerr_die2sys(111,FATAL,MSG1(ERR_READ,fn.s));
}

static void die_write(const char *name)
{
  strerr_die2sys(111,FATAL,MSG1(ERR_WRITE,name));
}

static void _closesub(struct subdbinfo *info)
{
  (void)info;
}

static const char *_opensub(struct subdbinfo *info)
{
  return 0;
  (void)info;
}

/* Checks that the hash of seed matches hash and returns success
 * (NULL). If not match returns "". If error, returns error string */
static const char *_checktag(struct subdbinfo *info,
			     unsigned long num,	/* message number */
			     unsigned long listno,
			     const char *action,
			     const char *seed,	/* cookie base */
			     const char *hash)	/* cookie */
{
  char strnum[FMT_ULONG];			/* message number as sz */
  char newcookie[COOKIE];

  if (!seed) return (char *) 0;			/* no data - accept */

  strnum[fmt_ulong(strnum,num)] = '\0';		/* message nr ->string*/

  cookie(newcookie,key.s,key.len,strnum,seed,action);
  if (byte_diff(hash,COOKIE,newcookie)) return "";
  else return (char *) 0;

  (void)info;
  (void)listno;
}

static int _issub(struct subdbinfo *info,
		  const char *subdir,
		  const char *userhost,
		  stralloc *recorded)
{
  static stralloc line = {0};

  int fd;
  unsigned int j;
  char ch,lcch;
  int match;

    if (!stralloc_copys(&addr,"T")) die_nomem();
    if (!stralloc_cats(&addr,userhost)) die_nomem();

    j = byte_rchr(addr.s,addr.len,'@');
    if (j == addr.len) return 0;
    case_lowerb(addr.s + j + 1,addr.len - j - 1);
    if (!stralloc_copy(&lcaddr,&addr)) die_nomem();
    case_lowerb(lcaddr.s + 1,j - 1);	/* totally lc version of addr */

    /* make hash for both for backwards comp */
    ch = 64 + subhashsa(&addr);
    lcch = 64 + subhashsa(&lcaddr);

    if (!stralloc_0(&addr)) die_nomem();
    if (!stralloc_0(&lcaddr)) die_nomem();
    makepath(&fn,subdir,"/subscribers/",lcch);

    fd = open_read(fn.s);
    if (fd == -1) {
      if (errno != error_noent)
        strerr_die2sys(111,FATAL,MSG1(ERR_OPEN,fn.s));
    } else {
      substdio_fdbuf(&ss,read,fd,ssbuf,sizeof(ssbuf));

      for (;;) {
        if (getln(&ss,&line,&match,'\0') == -1)
          strerr_die2sys(111,FATAL,MSG1(ERR_READ,fn.s));
        if (!match) break;
        if (line.len == lcaddr.len)
          if (!case_diffb(line.s,line.len,lcaddr.s)) {
	    close(fd);
	    if (recorded)
	      if (!stralloc_copyb(recorded,line.s+1,line.len-1))
		die_nomem();
	    return 1;
	  }
      }

      close(fd);
    }
	/* here if file not found or (file found && addr not there) */

    if (ch == lcch) return 0;

	/* try case sensitive hash for backwards compatibility */
    fn.s[fn.len - 2] = ch;
    fd = open_read(fn.s);
    if (fd == -1) {
      if (errno != error_noent)
        strerr_die2sys(111,FATAL,MSG1(ERR_OPEN,fn.s));
      return 0;
    }
    substdio_fdbuf(&ss,read,fd,ssbuf,sizeof(ssbuf));

    for (;;) {
      if (getln(&ss,&line,&match,'\0') == -1)
        strerr_die2sys(111,FATAL,MSG1(ERR_READ,fn.s));
      if (!match) break;
      if (line.len == addr.len)
        if (!case_diffb(line.s,line.len,addr.s)) {
	  close(fd);
	  if (recorded)
	    if (!stralloc_copyb(recorded,line.s+1,line.len-1))
	      die_nomem();
	  return 1;
	}
    }

    close(fd);

    return 0;

    (void)info;
}

static const char *_logmsg(struct subdbinfo *info,
			   unsigned long msgnum,
			   unsigned long listno,
			   unsigned long subs,
			   int done)
{
  return 0;
  (void)info;
  (void)msgnum;
  (void)listno;
  (void)subs;
  (void)done;
}

/* Outputs all userhostesses in 'subdir' through subwrite. 'subdir' is
 * the subdirectory name.  subwrite must be a function returning >=0 on
 * success, -1 on error, and taking arguments (char* string, unsigned
 * int length). It will be called once per address and should take care
 * of newline or whatever needed for the output form. */
static unsigned long _putsubs(struct subdbinfo *info,
			      const char *subdir,
			      unsigned long hash_lo,
			      unsigned long hash_hi,
			      int subwrite())	/* write function. */
{
  static stralloc line = {0};

  unsigned int i;
  int fd;
  unsigned long no = 0L;
  int match;
  unsigned int hashpos;

    makepath(&fn,subdir,"/subscribers/",'?');
    hashpos = fn.len - 2;
    if (hash_lo > 52) hash_lo = 52;
    if (hash_hi > 52) hash_hi = 52;
    if (hash_hi < hash_lo) hash_hi = hash_lo;

    for (i = hash_lo;i <= hash_hi;++i) {
      fn.s[hashpos] = 64 + i;	/* hash range 0-52 */
      fd = open_read(fn.s);
      if (fd == -1) {
        if (errno != error_noent)
	  strerr_die2sys(111,FATAL,MSG1(ERR_READ,fn.s));
      } else {
        substdio_fdbuf(&ssin,read,fd,inbuf,sizeof(inbuf));
        for (;;) {
          if (getln(&ssin,&line,&match,'\0') == -1)
            strerr_die2sys(111,FATAL,MSG1(ERR_READ,fn.s));
          if (!match)
            break;
          if (subwrite(line.s + 1,line.len - 2) == -1) die_write("stdout");
          no++;
        }
        close(fd);
      }
    }
    return no;

    (void)info;
}

static void lineout(const stralloc *line, int subwrite())
{
  struct datetime dt;
  char date[DATE822FMT];

  (void) scan_ulong(line->s,&when);
  datetime_tai(&dt,when);		/* there is always at least a '\n' */
  if (!stralloc_copyb(&outline,date,date822fmt(date,&dt) - 1))
	die_nomem();
  if (!stralloc_cats(&outline,": ")) die_nomem();
  if (!stralloc_catb(&outline,line->s,line->len - 1)) die_nomem();
  if (subwrite(outline.s,outline.len) == -1)
	strerr_die2sys(111,FATAL,MSG1(ERR_WRITE,"output"));
  return;
}

/* opens dir/Log, and outputs via subwrite(s,len) any line that matches
 * search. A '_' is search is a wildcard. Any other non-alphanum/'.'
 * char is replaced by a '_' */
static void _searchlog(struct subdbinfo *info,
		       const char *subdir,
		       char *search,		/* search string */
		       int subwrite())		/* output fxn */
{
  static stralloc line = {0};

  unsigned char x;
  unsigned char y;
  unsigned char *cp;
  unsigned char *cpsearch;
  unsigned char *cps;
  unsigned char ch;
  unsigned char *cplast, *cpline;
  unsigned int searchlen;
  int fd,match;

  searchlen = str_len(search);
  case_lowerb(search,searchlen);
  cps = (unsigned char *) search;
  while ((ch = *(cps++))) {		/* search is potentially hostile */
    if (ch >= 'a' && ch <= 'z') continue;
    if (ch >= '0' && ch <= '9') continue;
    if (ch == '.' || ch == '_') continue;
    *(cps - 1) = '_';			/* will [also] match char specified */
  }

  makepath(&line,subdir,"/Log",0);
  fd = open_read(line.s);
  if (fd == -1) {
    if (errno != error_noent)
      strerr_die2sys(111,FATAL,MSG1(ERR_OPEN,line.s));
    else
      strerr_die3x(100,FATAL,line.s,MSG(ERR_NOEXIST));
  }
  substdio_fdbuf(&ssin,read,fd,inbuf,sizeof(inbuf));

  for (;;) {
    if (getln(&ssin,&line,&match,'\n') == -1)
      strerr_die2sys(111,FATAL,MSG(ERR_READ_INPUT));
    if (!match) break;
    if (!searchlen) {
      lineout(&line,subwrite);
    } else {		/* simple case-insensitive search */
      cpline = (unsigned char *) line.s - 1;
      cplast = cpline + line.len - searchlen; /* line has \0 at the end */
      while ((cp = ++cpline) <= cplast) {
	cpsearch = (unsigned char *) search;
	for (;;) {
	  x = *cpsearch++;
	  if (!x) break;
	  y = *cp++ - 'A';
	  if (y <= 'Z' - 'A') y += 'a'; else y += 'A';
	  if (x != y && x != '_') break;		/* '_' = wildcard */
	}
	if (!x) {
	  lineout(&line,subwrite);
	  break;
	}
      }
    }
  }
  close(fd);

  (void)info;
}

/* Add (flagadd=1) or remove (flagadd=0) userhost from the
 * subscr. Comment is e.g. the subscriber from line or name. It is added
 * to the log. Event is the action type, e.g. "probe", "manual",
 * etc. The direction (sub/unsub) is inferred from flagadd. Returns 1 on
 * success, 0 on failure. If forcehash is >=0 it is used in place of the
 * calculated hash. This makes it possible to add addresses with a hash
 * that does not exist. forcehash has to be 0..99.  for unsubscribes,
 * the address is only removed if forcehash matches the actual
 * hash. This way, ezmlm-manage can be prevented from touching certain
 * addresses that can only be removed by ezmlm-unsub. Usually, this
 * would be used for sublist addresses (to avoid removal) and sublist
 * aliases (to prevent users from subscribing them (although the cookie
 * mechanism would prevent the resulting duplicate message from being
 * distributed. */
static int _subscribe(struct subdbinfo *info,
		      const char *subdir,
		      const char *userhost,
		      int flagadd,
		      const char *comment,
		      const char *event,
		      int forcehash)
{
  static stralloc line = {0};

  int fdlock;

  unsigned int j;
  unsigned char ch,lcch;
  int match;
  int flagwasthere;
  int fd;

  if (userhost[str_chr(userhost,'\n')])
    strerr_die2x(100,FATAL,MSG(ERR_ADDR_NL));

    if (!stralloc_copys(&addr,"T")) die_nomem();
    if (!stralloc_cats(&addr,userhost)) die_nomem();
    if (addr.len > 401)
      strerr_die2x(100,FATAL,MSG(ERR_ADDR_LONG));

    j = byte_rchr(addr.s,addr.len,'@');
    if (j == addr.len)
      strerr_die2x(100,FATAL,MSG(ERR_ADDR_AT));
    case_lowerb(addr.s + j + 1,addr.len - j - 1);
    if (!stralloc_copy(&lcaddr,&addr)) die_nomem();
    case_lowerb(lcaddr.s + 1,j - 1);	/* make all-lc version of address */

    if (forcehash >= 0 && forcehash <= 52) {
      ch = lcch = 64 + (unsigned char) forcehash;
    } else {
      ch = 64 + subhashsa(&addr);
      lcch = 64 + subhashsa(&lcaddr);
    }

    if (!stralloc_0(&addr)) die_nomem();
    if (!stralloc_0(&lcaddr)) die_nomem();
    makepath(&fn,subdir,"/subscribers/",lcch);
    makepath(&fnlock,subdir,"/lock",0);

    if (!stralloc_copyb(&fnnew,fn.s,fn.len-1)) die_nomem();
	/* code later depends on fnnew = fn + 'n' */
    if (!stralloc_cats(&fnnew,"n")) die_nomem();
    if (!stralloc_0(&fnnew)) die_nomem();

    fdlock = lockfile(fnlock.s);

				/* do lower case hashed version first */
    fdnew = open_trunc(fnnew.s);
    if (fdnew == -1) die_write(fnnew.s);
    substdio_fdbuf(&ssnew,write,fdnew,ssnewbuf,sizeof(ssnewbuf));

    flagwasthere = 0;

    fd = open_read(fn.s);
    if (fd == -1) {
      if (errno != error_noent) { close(fdnew); die_read(); }
    }
    else {
      substdio_fdbuf(&ss,read,fd,ssbuf,sizeof(ssbuf));

      for (;;) {
        if (getln(&ss,&line,&match,'\0') == -1) {
	  close(fd); close(fdnew); die_read();
        }
        if (!match) break;
        if (line.len == addr.len)
          if (!case_diffb(line.s,line.len,addr.s)) {
	    flagwasthere = 1;
	    if (!flagadd)
	      continue;
	  }
        if (substdio_bput(&ssnew,line.s,line.len) == -1) {
	  close(fd); close(fdnew); die_write(fnnew.s);
        }
      }

      close(fd);
    }

    if (flagadd && !flagwasthere)
      if (substdio_bput(&ssnew,addr.s,addr.len) == -1) {
        close(fdnew); die_write(fnnew.s);
      }

    if (substdio_flush(&ssnew) == -1) { close(fdnew); die_write(fnnew.s); }
    if (fsync(fdnew) == -1) { close(fdnew); die_write(fnnew.s); }
    close(fdnew);

    wrap_rename(fnnew.s,fn.s);

    if ((ch == lcch) || flagwasthere) {
      close(fdlock);
      if (flagadd ^ flagwasthere) {
        if (!stralloc_0(&addr)) die_nomem();
        logaddr(subdir,event,addr.s+1,comment);
        return 1;
      }
      return 0;
    }

			/* If unsub and not found and hashed differ, OR */
			/* sub and not found (so added with new hash) */
			/* do the 'case-dependent' hash */

    fn.s[fn.len - 2] = ch;
    fnnew.s[fnnew.len - 3] = ch;
    fdnew = open_trunc(fnnew.s);
    if (fdnew == -1) die_write(fnnew.s);
    substdio_fdbuf(&ssnew,write,fdnew,ssnewbuf,sizeof(ssnewbuf));

    fd = open_read(fn.s);
    if (fd == -1) {
      if (errno != error_noent) { close(fdnew); die_read(); }
    } else {
      substdio_fdbuf(&ss,read,fd,ssbuf,sizeof(ssbuf));

      for (;;) {
        if (getln(&ss,&line,&match,'\0') == -1)
          { close(fd); close(fdnew); die_read(); }
        if (!match) break;
        if (line.len == addr.len)
          if (!case_diffb(line.s,line.len,addr.s)) {
            flagwasthere = 1;
            continue;	/* always want to remove from case-sensitive hash */
          }
        if (substdio_bput(&ssnew,line.s,line.len) == -1)
          { close(fd); close(fdnew); die_write(fnnew.s); }
      }

      close(fd);
    }

    if (substdio_flush(&ssnew) == -1) { close(fdnew); die_write(fnnew.s); }
    if (fsync(fdnew) == -1) { close(fdnew); die_write(fnnew.s); }
    close(fdnew);

    wrap_rename(fnnew.s,fn.s);

    close(fdlock);
    if (flagadd ^ flagwasthere) {
      if (!stralloc_0(&addr)) die_nomem();
      logaddr(subdir,event,addr.s+1,comment);
      return 1;
    }
    return 0;

    (void)info;
}

static void _tagmsg(struct subdbinfo *info,
		    unsigned long msgnum,
		    const char *hashout,
		    unsigned long bodysize,
		    unsigned long chunk)
{
  (void)info;
  (void)msgnum;
  (void)hashout;
  (void)bodysize;
  (void)chunk;
}

static const char *_mktab(struct subdbinfo *info)
{
  if (mkdir("subscribers", 0777) < 0
      && errno != error_exist)
    return strerror(errno);
  if (mkdir("allow/subscribers", 0777) < 0
      && errno != error_exist)
    return strerror(errno);
  if (mkdir("deny/subscribers", 0777) < 0
      && errno != error_exist)
    return strerror(errno);
  if (mkdir("digest/subscribers", 0777) < 0
      && errno != error_exist)
    return strerror(errno);
  if (mkdir("mod/subscribers", 0777) < 0
      && errno != error_exist)
    return strerror(errno);
  return 0;
  (void)info;
}

static const char *rmsubs(const char *subdir)
{
  int ch;
  for (ch = 0; ch < 53; ++ch) {
    makepath(&fn,subdir,"/subscribers/",ch + 64);
    unlink(fn.s);
  }
  makepath(&fn,subdir,"/subscribers",0);
  if (rmdir(fn.s) < 0
      && errno != error_noent)
    return strerror(errno);
  return 0;
}

static const char *_rmtab(struct subdbinfo *info)
{
  const char *r;
  if ((r = rmsubs(".")) != 0) return r;
  if ((r = rmsubs("allow")) != 0) return r;
  if ((r = rmsubs("deny")) != 0) return r;
  if ((r = rmsubs("digest")) != 0) return r;
  if ((r = rmsubs("mod")) != 0) return r;
  return 0;
  (void)info;
}

struct sub_plugin sub_plugin = {
  SUB_PLUGIN_VERSION,
  _checktag,
  _closesub,
  _issub,
  _logmsg,
  _mktab,
  _opensub,
  _putsubs,
  _rmtab,
  _searchlog,
  _subscribe,
  _tagmsg,
};
