Import ezmlm-idx 0.40
[ezmlm] / ezmlm-warn.c
index 3140976..7d601e3 100644 (file)
@@ -1,3 +1,5 @@
+/*$Id: ezmlm-warn.c,v 1.27 1999/08/07 20:47:26 lindberg Exp $*/
+/*$Name: ezmlm-idx-040 $*/
 #include <sys/types.h>
 #include <sys/stat.h>
 #include "direntry.h"
@@ -6,6 +8,7 @@
 #include "substdio.h"
 #include "stralloc.h"
 #include "slurp.h"
+#include "sgetopt.h"
 #include "getconf.h"
 #include "byte.h"
 #include "error.h"
 #include "fmt.h"
 #include "cookie.h"
 #include "qmail.h"
+#include "errtxt.h"
+#include "mime.h"
+#include "idx.h"
+#include "subscribe.h"
 
 #define FATAL "ezmlm-warn: fatal: "
-void die_usage() { strerr_die1x(100,"ezmlm-warn: usage: ezmlm-warn dir"); }
-void die_nomem() { strerr_die2x(111,FATAL,"out of memory"); }
+void die_usage()
+{
+  strerr_die1x(100,"ezmlm-warn: usage: ezmlm-warn -dD -l secs -t days dir");
+}
+
+void die_nomem() { strerr_die2x(111,FATAL,ERR_NOMEM); }
 
 stralloc key = {0};
 stralloc outhost = {0};
 stralloc outlocal = {0};
 stralloc mailinglist = {0};
+stralloc digdir = {0};
+stralloc charset = {0};
+char boundary[COOKIE];
+
+substdio ssout;
+char outbuf[16];
 
 unsigned long when;
 char *dir;
+char *workdir;
+int flagdig = 0;
+char flagcd = '\0';            /* default: don't use transfer encoding */
 stralloc fn = {0};
+stralloc bdname = {0};
+stralloc fnlasth = {0};
+stralloc fnlastd = {0};
+stralloc lasth = {0};
+stralloc lastd = {0};
 struct stat st;
+void *psql = (void *) 0;
 
-void die_read() { strerr_die6sys(111,FATAL,"unable to read ",dir,"/",fn.s,": "); }
+void die_read() { strerr_die4sys(111,FATAL,ERR_READ,fn.s,": "); }
+
+void makedir(s)
+char *s;
+{
+  if (mkdir(s,0755) == -1)
+    if (errno != error_exist)
+      strerr_die4x(111,FATAL,ERR_CREATE,s,": ");
+}
 
 char inbuf[1024];
 substdio ssin;
@@ -46,6 +80,7 @@ char hash[COOKIE];
 stralloc fnhash = {0};
 stralloc quoted = {0};
 stralloc line = {0};
+stralloc qline = {0};
 
 struct qmail qq;
 int qqwrite(fd,buf,len) int fd; char *buf; unsigned int len;
@@ -58,35 +93,29 @@ substdio ssqq = SUBSTDIO_FDBUF(qqwrite,-1,qqbuf,sizeof(qqbuf));
 struct datetime dt;
 char date[DATE822FMT];
 
-void copy(fn)
-char *fn;
+void code_qput(s,n)
+char *s;
+unsigned int n;
 {
-  int fd;
-  int match;
-
-  fd = open_read(fn);
-  if (fd == -1)
-    strerr_die4sys(111,FATAL,"unable to open ",fn,": ");
-
-  substdio_fdbuf(&sstext,read,fd,textbuf,sizeof(textbuf));
-  for (;;) {
-    if (getln(&sstext,&line,&match,'\n') == -1)
-      strerr_die4sys(111,FATAL,"unable to read ",fn,": ");
-    if (!match)
-      break;
-    qmail_put(&qq,line.s,line.len);
-  }
-
-  close(fd);
+    if (!flagcd)
+      qmail_put(&qq,s,n);
+    else {
+      if (flagcd == 'B')
+        encodeB(s,n,&qline,0,FATAL);
+      else
+        encodeQ(s,n,&qline,FATAL);
+      qmail_put(&qq,qline.s,qline.len);
+    }
 }
 
 void doit(flagw)
 int flagw;
 {
-  int i;
+  unsigned int i;
   int fd;
   int match;
   int fdhash;
+  char *err;
   datetime_sec msgwhen;
 
   fd = open_read(fn.s);
@@ -95,29 +124,42 @@ int flagw;
 
   if (getln(&ssin,&addr,&match,'\0') == -1) die_read();
   if (!match) { close(fd); return; }
-
-  if (!issub(addr.s)) { close(fd); /*XXX*/unlink(fn.s); return; }
-
+  if (!issub(workdir,addr.s,(char *) 0,FATAL)) { close(fd);
+                        /*XXX*/unlink(fn.s); return; }
   cookie(hash,"",0,"",addr.s,"");
-  if (!stralloc_copys(&fnhash,"bounce/h")) die_nomem();
-  if (!stralloc_catb(&fnhash,hash,COOKIE)) die_nomem();
+  if (!stralloc_copys(&fnhash,workdir)) die_nomem();
+  if (!stralloc_cats(&fnhash,"/bounce/h/")) die_nomem();
+  if (!stralloc_catb(&fnhash,hash,1)) die_nomem();
+  if (!stralloc_cats(&fnhash,"/h")) die_nomem();
+  if (!stralloc_catb(&fnhash,hash+1,COOKIE-1)) die_nomem();
   if (!stralloc_0(&fnhash)) die_nomem();
 
-  if (qmail_open(&qq) == -1)
-    strerr_die2sys(111,FATAL,"unable to run qmail-queue: ");
+  if (qmail_open(&qq, (stralloc *) 0) == -1)
+    strerr_die2sys(111,FATAL,ERR_QMAIL_QUEUE);
 
   msgwhen = now();
   qmail_puts(&qq,"Mailing-List: ");
   qmail_put(&qq,mailinglist.s,mailinglist.len);
+  if (getconf_line(&line,"listid",0,FATAL,dir)) {
+    qmail_puts(&qq,"\nList-ID: ");
+    qmail_put(&qq,line.s,line.len);
+  }
   qmail_puts(&qq,"\nDate: ");
   datetime_tai(&dt,msgwhen);
   qmail_put(&qq,date,date822fmt(date,&dt));
-  qmail_puts(&qq,"Message-ID: <");
-  qmail_put(&qq,strnum,fmt_ulong(strnum,(unsigned long) msgwhen));
-  qmail_puts(&qq,".");
-  qmail_put(&qq,strnum,fmt_ulong(strnum,(unsigned long) getpid()));
-  qmail_puts(&qq,".ezmlm-warn@");
-  qmail_put(&qq,outhost.s,outhost.len);
+  if (!stralloc_copys(&line,"Message-ID: <")) die_nomem();
+  if (!stralloc_catb(&line,strnum,fmt_ulong(strnum,(unsigned long) msgwhen)))
+               die_nomem();
+  if (!stralloc_cats(&line,".")) die_nomem();
+  if (!stralloc_catb(&line,strnum,fmt_ulong(strnum,(unsigned long) getpid())))
+               die_nomem();
+  if (!stralloc_cats(&line,".ezmlm-warn@")) die_nomem();
+  if (!stralloc_catb(&line,outhost.s,outhost.len)) die_nomem();
+  qmail_put(&qq,line.s,line.len);
+  if (flagcd) {
+    if (!stralloc_0(&line)) die_nomem();
+    cookie(boundary,"",0,"",line.s,"");        /* universal MIME boundary */
+  }
   qmail_puts(&qq,">\nFrom: ");
   if (!quote(&quoted,&outlocal)) die_nomem();
   qmail_put(&qq,quoted.s,quoted.len);
@@ -126,34 +168,84 @@ int flagw;
   qmail_puts(&qq,"\nTo: ");
   if (!quote2(&quoted,addr.s)) die_nomem();
   qmail_put(&qq,quoted.s,quoted.len);
-  qmail_puts(&qq,flagw ? "\nSubject: ezmlm probe\n\n" : "\nSubject: ezmlm warning\n\n");
-
-  copy("text/top");
-  copy(flagw ? "text/bounce-probe" : "text/bounce-warn");
+  if (flagcd) {                        /* to accomodate transfer-encoding */
+    qmail_puts(&qq,"\nMIME-Version: 1.0\n");
+    qmail_puts(&qq,"Content-Type: multipart/mixed; boundary=");
+    qmail_put(&qq,boundary,COOKIE);
+  } else {
+    qmail_puts(&qq,"\nContent-type: text/plain; charset=");
+    qmail_puts(&qq,charset.s);
+  }
+  qmail_puts(&qq,flagw ? "\nSubject: ezmlm probe\n" : "\nSubject: ezmlm warning\n");
+
+  if (flagcd) {                        /* first part for QP/base64 multipart msg */
+    qmail_puts(&qq,"\n\n--");
+    qmail_put(&qq,boundary,COOKIE);
+    qmail_puts(&qq,"\nContent-Type: text/plain; charset=");
+    qmail_puts(&qq,charset.s);
+    qmail_puts(&qq,"\nContent-Transfer-Encoding: ");
+    if (flagcd == 'Q')
+      qmail_puts(&qq,"Quoted-printable\n\n");
+    else
+      qmail_puts(&qq,"base64\n\n");
+  } else
+    qmail_puts(&qq,"\n");
+
+  copy(&qq,"text/top",flagcd,FATAL);
+  copy(&qq,flagw ? "text/bounce-probe" : "text/bounce-warn",flagcd,FATAL);
 
   if (!flagw) {
-    fdhash = open_read(fnhash.s);
-    if (fdhash == -1) {
-      if (errno != error_noent)
-        strerr_die6sys(111,FATAL,"unable to open ",dir,"/",fnhash.s,": ");
-    }
-    else {
-      copy("text/bounce-num");
-      substdio_fdbuf(&sstext,read,fdhash,textbuf,sizeof(textbuf));
-      if (substdio_copy(&ssqq,&sstext) < 0)
-        strerr_die6sys(111,FATAL,"unable to read ",dir,"/",fnhash.s,": ");
+    if (flagdig)
+      copy(&qq,"text/dig-bounce-num",flagcd,FATAL);
+    else
+      copy(&qq,"text/bounce-num",flagcd,FATAL);
+    if (!flagcd) {
+      fdhash = open_read(fnhash.s);
+      if (fdhash == -1) {
+        if (errno != error_noent)
+          strerr_die4sys(111,FATAL,ERR_OPEN,fnhash.s,": ");
+      } else {
+        substdio_fdbuf(&sstext,read,fdhash,textbuf,sizeof(textbuf));
+        for(;;) {
+          if (getln(&sstext,&line,&match,'\n') == -1)
+            strerr_die4sys(111,FATAL,ERR_READ,fnhash.s,": ");
+          if (!match) break;
+          code_qput(line.s,line.len);
+        }
+      }
       close(fdhash);
+    } else {
+      if (!stralloc_copys(&line,"")) die_nomem();      /* slurp adds! */
+      if (slurp(fnhash.s,&line,256) < 0)
+        strerr_die4sys(111,FATAL,ERR_OPEN,fnhash.s,": ");
+      code_qput(line.s,line.len);
     }
   }
 
-  copy("text/bounce-bottom");
+  copy(&qq,"text/bounce-bottom",flagcd,FATAL);
+  if (flagcd) {
+    if (flagcd == 'B') {
+      encodeB("",0,&line,2,FATAL);
+      qmail_put(&qq,line.s,line.len);  /* flush */
+    }
+    qmail_puts(&qq,"\n\n--");
+    qmail_put(&qq,boundary,COOKIE);
+    qmail_puts(&qq,"\nContent-Type: message/rfc822\n\n");
+  }
   if (substdio_copy(&ssqq,&ssin) < 0) die_read();
   close(fd);
 
+  if (flagcd) {                                /* end multipart/mixed */
+    qmail_puts(&qq,"\n--");
+    qmail_put(&qq,boundary,COOKIE);
+    qmail_puts(&qq,"--\n");
+  }
+
   strnum[fmt_ulong(strnum,when)] = 0;
   cookie(hash,key.s,key.len,strnum,addr.s,flagw ? "P" : "W");
   if (!stralloc_copy(&line,&outlocal)) die_nomem();
-  if (!stralloc_cats(&line,flagw ? "-return-probe-" : "-return-warn-")) die_nomem();
+  if (!stralloc_cats(&line,flagw ? "-return-probe-" : "-return-warn-"))
+       die_nomem();
   if (!stralloc_cats(&line,strnum)) die_nomem();
   if (!stralloc_cats(&line,".")) die_nomem();
   if (!stralloc_catb(&line,hash,COOKIE)) die_nomem();
@@ -170,8 +262,8 @@ int flagw;
   qmail_from(&qq,line.s);
 
   qmail_to(&qq,addr.s);
-  if (qmail_close(&qq) != 0)
-    strerr_die2x(111,FATAL,"temporary qmail-queue error");
+  if (*(err = qmail_close(&qq)) != '\0')
+    strerr_die3x(111,FATAL,ERR_TMP_QMAIL_QUEUE, err + 1);
 
   strnum[fmt_ulong(strnum,qmail_qp(&qq))] = 0;
   strerr_warn2("ezmlm-warn: info: qp ",strnum,0);
@@ -179,76 +271,252 @@ int flagw;
   if (!flagw) {
     if (unlink(fnhash.s) == -1)
       if (errno != error_noent)
-        strerr_die6sys(111,FATAL,"unable to remove ",dir,"/",fnhash.s,": ");
+        strerr_die4sys(111,FATAL,ERR_DELETE,fnhash.s,": ");
   }
   if (unlink(fn.s) == -1)
-    strerr_die6sys(111,FATAL,"unable to remove ",dir,"/",fn.s,": ");
+    strerr_die4sys(111,FATAL,ERR_DELETE,fn.s,": ");
 }
 
 void main(argc,argv)
 int argc;
 char **argv;
 {
-  DIR *bouncedir;
-  direntry *d;
+  DIR *bouncedir, *bsdir, *hdir;
+  direntry *d, *ds;
   unsigned long bouncedate;
-  int fdlock;
-
-  umask(022);
+  unsigned long bouncetimeout = BOUNCE_TIMEOUT;
+  unsigned long lockout = 0L;
+  unsigned long ld;
+  unsigned long ddir,dfile;
+  int fdlock,fd;
+  char *err;
+  int opt;
+  char ch;
+
+  (void) umask(022);
   sig_pipeignore();
   when = (unsigned long) now();
-
-  dir = argv[1];
+  while ((opt = getopt(argc,argv,"dDl:t:vV")) != opteof)
+    switch(opt) {
+      case 'd': flagdig = 1; break;
+      case 'D': flagdig = 0; break;
+      case 'l':
+                if (optarg) {  /* lockout in seconds */
+                  (void) scan_ulong(optarg,&lockout);
+                }
+                break;
+      case 't':
+                if (optarg) {  /* bouncetimeout in days */
+                  (void) scan_ulong(optarg,&bouncetimeout);
+                  bouncetimeout *= 3600L * 24L;
+                }
+                break;
+      case 'v':
+      case 'V': strerr_die2x(0,
+               "ezmlm-warn version: ezmlm-0.53+",EZIDX_VERSION);
+      default:
+       die_usage();
+    }
+  dir = argv[optind];
   if (!dir) die_usage();
-
   if (chdir(dir) == -1)
-    strerr_die4sys(111,FATAL,"unable to switch to ",dir,": ");
+    strerr_die4sys(111,FATAL,ERR_SWITCH,dir,": ");
+  if (flagdig) {
+    if (!stralloc_copys(&digdir,dir)) die_nomem();
+    if (!stralloc_cats(&digdir,"/digest")) die_nomem();
+    if (!stralloc_0(&digdir)) die_nomem();
+    workdir = digdir.s;
+  } else
+    workdir = dir;
+
+  if (!stralloc_copys(&fnlastd,workdir)) die_nomem();
+  if (!stralloc_cats(&fnlastd,"/bounce/lastd")) die_nomem();
+  if (!stralloc_0(&fnlastd)) die_nomem();
+  if (slurp(fnlastd.s,&lastd,16) == -1)                /* last time d was scanned */
+      strerr_die4sys(111,FATAL,ERR_READ,fnlastd.s,": ");
+  if (!stralloc_0(&lastd)) die_nomem();
+  (void) scan_ulong(lastd.s,&ld);
+  if (!lockout)
+    lockout = bouncetimeout / 50;              /* 5.6 h for default timeout */
+  if (ld + lockout > when && ld < when)
+    _exit(0);          /* exit silently. Second check is to prevent lockup */
+                       /* if lastd gets corrupted */
+
+  if (!stralloc_copy(&fnlasth,&fnlastd)) die_nomem();
+  fnlasth.s[fnlasth.len - 2] = 'h';            /* bad, but feels good ... */
 
   switch(slurp("key",&key,32)) {
     case -1:
-      strerr_die4sys(111,FATAL,"unable to read ",dir,"/key: ");
+      strerr_die4sys(111,FATAL,ERR_READ,dir,"/key: ");
     case 0:
-      strerr_die3x(100,FATAL,dir,"/key does not exist");
+      strerr_die4x(100,FATAL,dir,"/key",ERR_NOEXIST);
   }
   getconf_line(&outhost,"outhost",1,FATAL,dir);
   getconf_line(&outlocal,"outlocal",1,FATAL,dir);
+  if (flagdig)
+    if (!stralloc_cats(&outlocal,"-digest")) die_nomem();
   getconf_line(&mailinglist,"mailinglist",1,FATAL,dir);
+  if (getconf_line(&charset,"charset",0,FATAL,dir)) {
+    if (charset.len >= 2 && charset.s[charset.len - 2] == ':') {
+      if (charset.s[charset.len - 1] == 'B' ||
+               charset.s[charset.len - 1] == 'Q') {
+        flagcd = charset.s[charset.len - 1];
+        charset.s[charset.len - 2] = '\0';
+      }
+    }
+  } else
+    if (!stralloc_copys(&charset,TXT_DEF_CHARSET)) die_nomem();
+  if (!stralloc_0(&charset)) die_nomem();
+
+  set_cpoutlocal(&outlocal);   /* for copy */
+  set_cpouthost(&outhost);     /* for copy */
+  ddir = when / 10000;
+  dfile = when - 10000 * ddir;
 
-  fdlock = open_append("lockbounce");
+  if (!stralloc_copys(&line,workdir)) die_nomem();
+  if (!stralloc_cats(&line,"/lockbounce")) die_nomem();
+  if (!stralloc_0(&line)) die_nomem();
+  fdlock = open_append(line.s);
   if (fdlock == -1)
-    strerr_die4sys(111,FATAL,"unable to open ",dir,"/lockbounce: ");
+    strerr_die4sys(111,FATAL,ERR_OPEN,line.s,": ");
   if (lock_ex(fdlock) == -1)
-    strerr_die4sys(111,FATAL,"unable to lock ",dir,"/lockbounce: ");
+    strerr_die4sys(111,FATAL,ERR_OBTAIN,line.s,": ");
 
-  bouncedir = opendir("bounce");
+  if (!stralloc_copys(&line,workdir)) die_nomem();
+  if (!stralloc_cats(&line,"/bounce/d")) die_nomem();
+  if (!stralloc_0(&line)) die_nomem();
+  bouncedir = opendir(line.s);
   if (!bouncedir)
-    strerr_die4sys(111,FATAL,"unable to open ",dir,"/bounce: ");
+    if (errno != error_noent)
+      strerr_die4sys(111,FATAL,ERR_OPEN,line.s,": ");
+    else
+      _exit(0);                /* no bouncedir - no bounces! */
 
-  while (d = readdir(bouncedir)) {
+  while ((d = readdir(bouncedir))) {           /* dxxx/ */
     if (str_equal(d->d_name,".")) continue;
     if (str_equal(d->d_name,"..")) continue;
 
-    if (!stralloc_copys(&fn,"bounce/")) die_nomem();
-    if (!stralloc_cats(&fn,d->d_name)) die_nomem();
-    if (!stralloc_0(&fn)) die_nomem();
-
-    if (stat(fn.s,&st) == -1) {
-      if (errno == error_noent) continue;
-      strerr_die6sys(111,FATAL,"unable to stat ",dir,"/",fn.s,": ");
+    scan_ulong(d->d_name,&bouncedate);
+       /* since we do entire dir, we do files that are not old enough. */
+       /* to not do this and accept a delay of 10000s (2.8h) of the oldest */
+       /* bounce we add to bouncedate. We don't if bouncetimeout=0 so that */
+       /* that setting still processes _all_ bounces. */
+    if (bouncetimeout) ++bouncedate;
+    if (when >= bouncedate * 10000 + bouncetimeout) {
+      if (!stralloc_copys(&bdname,workdir)) die_nomem();
+      if (!stralloc_cats(&bdname,"/bounce/d/")) die_nomem();
+      if (!stralloc_cats(&bdname,d->d_name)) die_nomem();
+      if (!stralloc_0(&bdname)) die_nomem();
+      bsdir = opendir(bdname.s);
+      if (!bsdir) {
+       if (errno != error_notdir)
+         strerr_die4sys(111,FATAL,ERR_OPEN,bdname.s,":y ");
+       else {                          /* leftover nnnnn_dmmmmm file */
+         if (unlink(bdname.s) == -1)
+           strerr_die4sys(111,FATAL,ERR_DELETE,bdname.s,": ");
+         continue;
+       }
+      }
+      while ((ds = readdir(bsdir))) {                  /* dxxxx/yyyy */
+       if (str_equal(ds->d_name,".")) continue;
+       if (str_equal(ds->d_name,"..")) continue;
+       if (!stralloc_copy(&fn,&bdname)) die_nomem();   /* '\0' at end */
+         fn.s[fn.len - 1] = '/';
+       if (!stralloc_cats(&fn,ds->d_name)) die_nomem();
+       if (!stralloc_0(&fn)) die_nomem();
+       if ((ds->d_name[0] == 'd') || (ds->d_name[0] == 'w'))
+         doit(ds->d_name[0] == 'w');
+        else                           /* other stuff is junk */
+         if (unlink(fn.s) == -1)
+           strerr_die4sys(111,FATAL,ERR_DELETE,fn.s,": ");
+      }
+      closedir(bsdir);
+      if (rmdir(bdname.s) == -1)       /* the directory itself */
+      if (errno != error_noent)
+          strerr_die4sys(111,FATAL,ERR_DELETE,bdname.s,": ");
     }
+  }
+  closedir(bouncedir);
 
-    if (when > st.st_mtime + 3000000)
-      if (unlink(fn.s) == -1)
-        strerr_die6sys(111,FATAL,"unable to remove ",dir,"/",fn.s,": ");
-
-    if ((d->d_name[0] == 'd') || (d->d_name[0] == 'w')) {
-      scan_ulong(d->d_name + 1,&bouncedate);
-      if (when > bouncedate + 1000000)
-       doit(d->d_name[0] == 'w');
+  if (!stralloc_copy(&line,&fnlastd)) die_nomem();
+  line.s[line.len - 2] = 'D';
+  fd = open_trunc(line.s);                     /* write lastd. Do safe */
+                                               /* since we read before lock*/
+  if (fd == -1) strerr_die4sys(111,FATAL,ERR_OPEN,line.s,": ");
+  substdio_fdbuf(&ssout,write,fd,outbuf,sizeof(outbuf));
+  if (substdio_put(&ssout,strnum,fmt_ulong(strnum,when)) == -1)
+    strerr_die4sys(111,FATAL,ERR_WRITE,line.s,": ");
+  if (substdio_put(&ssout,"\n",1) == -1)       /* prettier */
+    strerr_die4sys(111,FATAL,ERR_WRITE,line.s,": ");
+  if (substdio_flush(&ssout) == -1)
+    strerr_die4sys(111,FATAL,ERR_FLUSH,line.s,": ");
+  if (fsync(fd) == -1)
+    strerr_die4sys(111,FATAL,ERR_SYNC,line.s,": ");
+  if (close(fd) == -1)
+    strerr_die4sys(111,FATAL,ERR_CLOSE,line.s,": ");
+
+  if (rename(line.s,fnlastd.s) == -1)
+    strerr_die4sys(111,FATAL,ERR_MOVE,fnlastd.s,": ");
+
+                               /* no need to do h dir cleaning more than */
+                               /* once per 1-2 days (17-30 days for all) */
+  if (stat(fnlasth.s,&st) == -1) {
+    if (errno != error_noent)
+      strerr_die4sys(111,FATAL,ERR_STAT,fnlasth.s,": ");
+  } else if (when < st.st_mtime + 100000 && when > st.st_mtime)
+    _exit(0);                  /* 2nd comp to guard against corruption */
+
+  if (slurp(fnlasth.s,&lasth,16) == -1)                /* last h cleaned */
+      strerr_die4sys(111,FATAL,ERR_READ,fnlasth.s,": ");
+  if (!stralloc_0(&lasth)) die_nomem();
+  ch = lasth.s[0];                              /* clean h */
+  if (ch >= 'a' && ch <= 'o')
+    ++ch;
+  else
+    ch = 'a';
+  lasth.s[0] = ch;
+  if (!stralloc_copys(&line,workdir)) die_nomem();
+  if (!stralloc_cats(&line,"/bounce/h/")) die_nomem();
+  if (!stralloc_catb(&line,lasth.s,1)) die_nomem();
+  if (!stralloc_0(&line)) die_nomem();
+  hdir = opendir(line.s);              /* clean ./h/xxxxxx */
+
+  if (!hdir) {
+    if (errno != error_noent)
+    strerr_die4sys(111,FATAL,ERR_OPEN,line.s,": ");
+  } else {
+
+    while ((d = readdir(hdir))) {
+      if (str_equal(d->d_name,".")) continue;
+      if (str_equal(d->d_name,"..")) continue;
+      if (!stralloc_copys(&fn,line.s)) die_nomem();
+      if (!stralloc_append(&fn,"/")) die_nomem();
+      if (!stralloc_cats(&fn,d->d_name)) die_nomem();
+      if (!stralloc_0(&fn)) die_nomem();
+      if (stat(fn.s,&st) == -1) {
+       if (errno == error_noent) continue;
+       strerr_die4sys(111,FATAL,ERR_STAT,fn.s,": ");
+      }
+      if (when > st.st_mtime + 3 * bouncetimeout)
+       if (unlink(fn.s) == -1)
+          strerr_die4sys(111,FATAL,ERR_DELETE,fn.s,": ");
     }
+    closedir(hdir);
   }
 
-  closedir(bouncedir);
-
+  fd = open_trunc(fnlasth.s);                  /* write lasth */
+  if (fd == -1) strerr_die4sys(111,FATAL,ERR_OPEN,fnlasth.s,": ");
+  substdio_fdbuf(&ssout,write,fd,outbuf,sizeof(outbuf));
+  if (substdio_put(&ssout,lasth.s,1) == -1)
+    strerr_die4sys(111,FATAL,ERR_OPEN,fnlasth.s,": ");
+  if (substdio_put(&ssout,"\n",1) == -1)       /* prettier */
+    strerr_die4sys(111,FATAL,ERR_OPEN,fnlasth.s,": ");
+  if (substdio_flush(&ssout) == -1)
+    strerr_die4sys(111,FATAL,ERR_OPEN,fnlasth.s,": ");
+  (void) close(fd);            /* no big loss. No reason to flush/sync */
+                               /* See check of ld above to guard against */
+                               /* it being corrupted and > when */
+
+  closesql();
   _exit(0);
 }