Import ezmlm-idx 0.40
[ezmlm] / ezmlm-cron.c
diff --git a/ezmlm-cron.c b/ezmlm-cron.c
new file mode 100644 (file)
index 0000000..e35f7ea
--- /dev/null
@@ -0,0 +1,507 @@
+#include <sys/types.h>
+#include <pwd.h>
+#include "strerr.h"
+#include "stralloc.h"
+#include "sgetopt.h"
+#include "substdio.h"
+#include "error.h"
+#include "str.h"
+#include "fmt.h"
+#include "fork.h"
+#include "wait.h"
+#include "readwrite.h"
+#include "auto_qmail.h"
+#include "auto_cron.h"
+#include "errtxt.h"
+#include "idx.h"
+
+#define FATAL "ezmlm-cron: fatal: "
+
+void die_usage()
+{
+ strerr_die2x(100,FATAL,
+  "usage: ezmlm-cron [-cCdDlLvV] [-w dow] [-t hh:mm] [-i hrs] listadr code");
+}
+
+void die_dow()
+{
+  strerr_die2x(100,FATAL,ERR_DOW);
+}
+
+void die_nomem() { strerr_die2x(111,FATAL,ERR_NOMEM); }
+
+unsigned long deltah = 24L;    /* default interval 24h */
+unsigned long hh = 4L;         /* default time 04:12 */
+unsigned long mm = 12L;
+char *dow = "*";               /* day of week */
+char *qmail_inject = "/bin/qmail-inject ";
+char strnum[FMT_ULONG];
+unsigned long uid,euid;
+
+stralloc line = {0};
+stralloc rp = {0};
+stralloc addr = {0};
+stralloc user = {0};
+stralloc euser = {0};
+stralloc dir = {0};
+stralloc listaddr = {0};
+
+struct passwd *ppasswd;
+
+int opt,match;
+int hostmatch;
+int localmatch;
+unsigned long dh,t;
+int founduser = 0;
+int listmatch = 0;
+int flagconfig = 0;
+int flagdelete = 0;
+int flaglist = 0;
+int flagdigit = 0;
+int flagours;
+int foundlocal;
+int foundmatch = 0;
+int nolists = 0;
+int maxlists;
+unsigned int pos,pos2,poslocal,len;
+unsigned int lenhost,lenlocal;
+unsigned int part0start,part0len;
+int fdlock,fdin,fdout;
+
+char *local = (char *) 0;      /* list = local@host */
+char *host = (char *) 0;
+char *code = (char *) 0;       /* digest code */
+char *cp;
+
+void die_syntax()
+{
+  if (!stralloc_0(&line)) die_nomem();
+  strerr_die5x(100,FATAL,TXT_EZCRONRC," ",ERR_SYNTAX,line.s);
+}
+
+void die_argument()
+{
+  strerr_die2x(100,FATAL,ERR_NOT_CLEAN);
+}
+
+int isclean(addr,flagaddr)
+       /* assures that addr has only letters, digits, "-_" */
+       /* also checks allows single '@' if flagaddr = 1 */
+       /* returns 1 if clean, 0 otherwise */
+  char *addr;
+  int flagaddr;                /* 1 for addresses with '@', 0 for other args */
+{
+  unsigned int pos;
+  register char ch;
+  register char *cp;
+  if (flagaddr) {              /* shoud have one '@' */
+    pos = str_chr(addr,'@');
+    if (!pos || !addr[pos])
+      return 0;                        /* at least 1 char for local */
+    if (!addr[pos+1])
+      return 0;                        /* host must be at least 1 char */
+    pos++;
+    case_lowerb(addr+pos,str_len(addr)-pos);
+  } else
+    pos = 0;
+  pos +=  str_chr(addr + pos,'@');
+  if (addr[pos])               /* but no more */
+    return 0;
+  cp = addr;
+  while ((ch = *(cp++)))
+    if (!(ch >= 'a' && ch <= 'z') &&
+        !(ch >= 'A' && ch <= 'Z') &&
+        !(ch >= '0' && ch <= '9') &&
+        ch != '.' && ch != '-' && ch != '_' && ch != '@')
+      return 0;
+  return 1;
+}
+
+char inbuf[512];
+substdio ssin;
+
+char outbuf[512];
+substdio ssout;
+
+void main(argc,argv)
+int argc;
+char **argv;
+
+{
+  int child;
+  char *sendargs[4];
+  int wstat;
+
+  (void) umask(077);
+  sig_pipeignore();
+
+  while ((opt = getopt(argc,argv,"cCdDi:lLt:w:vV")) != opteof)
+    switch (opt) {
+      case 'c': flagconfig = 1; break;
+      case 'C': flagconfig = 0; break;
+      case 'd': flagdelete = 1; break;
+      case 'D': flagdelete = 0; break;
+      case 'i': scan_ulong(optarg,&deltah); break;
+      case 'l': flaglist = 1; break;
+      case 'L': flaglist = 0; break;
+      case 't':
+                pos = scan_ulong(optarg,&hh);
+                if (!optarg[pos++] == ':') die_usage();
+                pos = scan_ulong(optarg + pos,&mm);
+                break;
+      case 'w':
+                dow = optarg;
+                cp = optarg - 1;
+                while (*(++cp)) {
+                  if (*cp >= '0' && *cp <= '7') {
+                    if (flagdigit) die_dow();
+                    flagdigit = 1;
+                  } else if (*cp == ',') {
+                    if (!flagdigit) die_dow();
+                    flagdigit = 0;
+                  } else
+                    die_dow();
+                }
+                break;
+      case 'v':
+      case 'V': strerr_die2x(100,"ezmlm-cron version: ",EZIDX_VERSION);
+      default:
+                die_usage();
+    }
+  if (flaglist + flagdelete + flagconfig > 1)
+    strerr_die2x(100,FATAL,ERR_EXCLUSIVE);
+  uid = getuid();
+  if (uid && !(euid = geteuid()))
+    strerr_die2x(100,FATAL,ERR_SUID);
+  if (!(ppasswd = getpwuid(uid)))
+    strerr_die2x(100,FATAL,ERR_UID);
+  if (!stralloc_copys(&user,ppasswd->pw_name)) die_nomem();
+  if (!stralloc_0(&user)) die_nomem();
+  if (!(ppasswd = getpwuid(euid)))
+    strerr_die2x(100,FATAL,ERR_EUID);
+  if (!stralloc_copys(&dir.s,ppasswd->pw_dir)) die_nomem();
+  if (!stralloc_0(&dir)) die_nomem();
+  if (!stralloc_copys(&euser,ppasswd->pw_name)) die_nomem();
+  if (!stralloc_0(&euser)) die_nomem();
+
+  if (chdir(dir.s) == -1)
+    strerr_die4sys(111,FATAL,ERR_SWITCH,dir.s,": ");
+
+  local = argv[optind++];      /* list address, optional for -c & -l */
+  if (!local) {
+    if (!flagconfig && !flaglist)
+      die_usage();
+    lenlocal = 0;
+    lenhost = 0;
+  } else {
+    if (!stralloc_copys(&listaddr,local)) die_nomem();
+    if (!isclean(local,1))
+      die_argument();
+    pos = str_chr(local,'@');
+    lenlocal = pos;
+    local[pos] = '\0';
+    host = local + pos + 1;
+    lenhost = str_len(host);
+    code = argv[optind];
+    if (!code) {               /* ignored for -l, -c, and -d */
+      if (flagdelete || flaglist || flagconfig)
+                               /* get away with not putting code for delete */
+        code = "a";    /* a hack - so what! */
+      else
+        die_usage();
+    } else
+      if (!isclean(code,0))
+        die_argument();
+  }
+  if ((fdin = open_read(TXT_EZCRONRC)) == -1)
+    strerr_die6sys(111,FATAL,ERR_OPEN,dir.s,"/",TXT_EZCRONRC,": ");
+       /* first line is special */
+  substdio_fdbuf(&ssin,read,fdin,inbuf,sizeof(inbuf));
+  if (getln(&ssin,&line,&match,'\n') == -1)
+    strerr_die6sys(111,FATAL,ERR_READ,dir.s,"/",TXT_EZCRONRC,": ");
+
+  if (!match)
+    strerr_die6sys(111,FATAL,ERR_READ,dir.s,"/",TXT_EZCRONRC,": ");
+       /* (since we have match line.len has to be >= 1) */
+  line.s[line.len - 1] = '\0';
+  if (!isclean(line.s,0))       /* host for bounces */
+    strerr_die4x(100,ERR_CFHOST,dir.s,"/",TXT_EZCRONRC);
+  if (!stralloc_copys(&rp,line.s)) die_nomem();
+
+  match = 1;
+  for(;;) {
+    if (!match) break;         /* to allow last line without '\n' */
+    if (getln(&ssin,&line,&match,'\n') == -1)
+    strerr_die6sys(111,FATAL,ERR_READ,dir.s,"/",TXT_EZCRONRC,": ");
+    if (!line.len)
+      break;
+    line.s[line.len-1] = '\0';
+    if (!case_startb(line.s,line.len,user.s))
+      continue;
+    pos = user.len - 1;
+    if (pos >= line.len || line.s[pos] != ':')
+      continue;
+    founduser = 1;              /* got user line */
+    break;
+  }
+  close(fdin);
+  if (!founduser)
+    strerr_die2x(100,FATAL,ERR_BADUSER);
+  
+  if (flagconfig) {
+    line.s[line.len-1] = '\n'; /* not very elegant ;-) */
+    substdio_fdbuf(&ssout,write,1,outbuf,sizeof(outbuf));
+    if (substdio_put(&ssout,line.s,line.len) == -1)
+      strerr_die3sys(111,FATAL,ERR_WRITE,"stdout: ");
+    if (substdio_flush(&ssout) == -1)
+      strerr_die3sys(111,FATAL,ERR_WRITE,"stdout: ");
+    _exit(0);
+  }
+  ++pos;                               /* points to first ':' */
+  len = str_chr(line.s+pos,':');       /* second ':' */
+    if (!line.s[pos + len])
+      die_syntax();
+  if (!local) {                                /* only -d and std left */
+    localmatch = 1;
+    hostmatch = 1;
+  } else {
+    hostmatch = 0;
+    if (len <= str_len(local))
+      if (!str_diffn(line.s+pos,local,len))
+        localmatch = 1;
+  }
+  pos += len + 1;
+  len = str_chr(line.s + pos,':');     /* third */
+  if (!line.s[pos + len])
+    die_syntax();
+  if (local) {                         /* check host */
+    if (len == 0)                      /* empty host => any host */
+      hostmatch = 1;
+    else
+      if (len == str_len(host))
+        if (!case_diffb(line.s+pos,len,host))
+          hostmatch = 1;
+  }
+  pos += len + 1;
+  pos += scan_ulong(line.s+pos,&maxlists);
+  if (line.s[pos]) {                   /* check additional lists */
+    if (line.s[pos] != ':')
+      die_syntax();
+    if (line.s[pos+1+str_chr(line.s+pos+1,':')])
+      die_syntax();    /* reminder lists are not separated by ':'  */
+                       /* otherwise a ':' or arg miscount will die */
+                       /* silently */
+    if (local) {
+      while (++pos < line.len) {
+        len = str_chr(line.s + pos,'@');
+        if (len == lenlocal && !str_diffn(line.s + pos,local,len)) {
+          pos += len;
+          if (!line.s[pos]) break;
+          pos++;
+          len = str_chr(line.s+pos,',');
+            if (len == lenhost && !case_diffb(line.s+pos,len,host)) {
+              listmatch = 1;
+              break;
+            }
+        }
+        pos += len;
+      }
+    }
+  }
+  if (!listmatch) {
+    if (!hostmatch)
+      strerr_die2x(100,FATAL,ERR_BADHOST);
+    if (!localmatch)
+      strerr_die2x(100,FATAL,ERR_BADLOCAL);
+  }
+       /* assemble correct line */
+  if (!flaglist) {
+    if (!stralloc_copyb(&addr,strnum,fmt_ulong(strnum,mm))) die_nomem();
+    if (!stralloc_cats(&addr," ")) die_nomem();
+    dh = 0L;
+    if (deltah <= 3L) dh = deltah;
+    else if (deltah <= 6L) dh = 6L;
+    else if (deltah <= 12L) dh = 12L;
+    else if (deltah <= 24L) dh = 24L;
+    else if (deltah <= 48L) {
+      if (dow[0] == '*') dow = "1,3,5";
+    } else if (deltah <= 72L) {
+      if (dow[0] == '*') dow = "1,4";
+    } else
+    if (dow[0] == '*') dow = "1";
+
+    if (!dh) {
+      if (!stralloc_cats(&addr,"*")) die_nomem();
+    } else {
+      if (!stralloc_catb(&addr,strnum,fmt_ulong(strnum,hh))) die_nomem();
+      for (t = hh + dh; t < hh + 24L; t+=dh) {
+        if (!stralloc_cats(&addr,",")) die_nomem();
+        if (!stralloc_catb(&addr,strnum,fmt_ulong(strnum,t % 24L))) die_nomem();
+      }
+    }
+    if (!stralloc_cats(&addr," * * ")) die_nomem();
+    if (!stralloc_cats(&addr,dow)) die_nomem();
+    if (!stralloc_cats(&addr," ")) die_nomem();
+    part0start = addr.len;             /* /var/qmail/bin/qmail-inject */
+    if (!stralloc_cats(&addr,auto_qmail)) die_nomem();
+    if (!stralloc_cats(&addr,qmail_inject)) die_nomem();
+    part0len = addr.len - part0start;
+    if (!stralloc_cats(&addr,local)) die_nomem();
+    if (!stralloc_cats(&addr,"-dig-")) die_nomem();
+    if (!stralloc_cats(&addr,code)) die_nomem();
+    if (!stralloc_cats(&addr,"@")) die_nomem();
+    if (!stralloc_cats(&addr,host)) die_nomem();
+               /* feed 'Return-Path: <user@host>' to qmail-inject */
+    if (!stralloc_cats(&addr,"%Return-path: <")) die_nomem();
+    if (!stralloc_cats(&addr,user.s)) die_nomem();
+    if (!stralloc_cats(&addr,"@")) die_nomem();
+    if (!stralloc_cat(&addr,&rp)) die_nomem();
+    if (!stralloc_cats(&addr,">\n")) die_nomem();
+  }
+  if (!stralloc_0(&addr)) die_nomem();
+
+  if (!flaglist) {
+       /* now to rewrite crontab we need to lock */
+    fdlock = open_append("crontabl");
+    if (fdlock == -1)
+      strerr_die4sys(111,FATAL,ERR_OPEN,dir.s,"/crontabl: ");
+    if (lock_ex(fdlock) == -1) {
+      close(fdlock);
+    strerr_die4sys(111,FATAL,ERR_OBTAIN,dir.s,"/crontabl: ");
+    }
+  } /* if !flaglist */
+  if ((fdin = open_read("crontab")) == -1) {
+    if (errno != error_noent)
+      strerr_die4sys(111,FATAL,ERR_READ,dir.s,"/crontab: ");
+  } else
+    substdio_fdbuf(&ssin,read,fdin,inbuf,sizeof(inbuf));
+  if (flaglist)
+    substdio_fdbuf(&ssout,write,1,outbuf,sizeof(outbuf));
+  else {
+    if ((fdout = open_trunc("crontabn")) == -1)
+      strerr_die4sys(111,FATAL,ERR_WRITE,dir.s,"/crontabn: ");
+    substdio_fdbuf(&ssout,write,fdout,outbuf,sizeof(outbuf));
+  }
+  line.len = 0;
+
+  if (fdin != -1) {
+    for (;;) {
+      if (!flaglist && line.len) {
+        line.s[line.len-1] = '\n';
+        if (substdio_put(&ssout,line.s,line.len) == -1)
+          strerr_die4sys(111,FATAL,ERR_WRITE,dir.s,"/crontabn: ");
+      }
+      if (getln(&ssin,&line,&match,'\n') == -1)
+        strerr_die4sys(111,FATAL,ERR_READ,dir.s,"/crontab: ");
+      if (!match)
+        break;
+      flagours = 0;                    /* assume entry is not ours */
+      foundlocal = 0;
+      line.s[line.len - 1] = '\0';     /* match so at least 1 char */
+      pos = 0;
+      while (line.s[pos] == ' ' && line.s[pos] == '\t') ++pos;
+      if (line.s[pos] == '#')
+        continue;                      /* cron comment */
+      pos = str_chr(line.s,'/');
+      if (!str_start(line.s+pos,auto_qmail)) continue;
+      pos += str_len(auto_qmail);
+      if (!str_start(line.s+pos,qmail_inject)) continue;
+      pos += str_len(qmail_inject);
+      poslocal = pos;
+      pos = byte_rchr(line.s,line.len,'<');    /* should be Return-Path: < */
+      if (pos == line.len)
+        continue;                      /* not ezmlm-cron line */
+      pos++;
+     len = str_chr(line.s+pos,'@');
+      if (len == user.len - 1 && !str_diffn(line.s+pos,user.s,len)) {
+        flagours = 1;
+        ++nolists;             /* belongs to this user */
+      }
+      if (!local) {
+        foundlocal = 1;
+      } else {
+        pos = poslocal + str_chr(line.s+poslocal,'@');
+        if (pos + lenhost +1 >= line.len) continue;
+        if (case_diffb(line.s+pos+1,lenhost,host)) continue;
+        if (line.s[pos+lenhost+1] != '%') continue;
+                               /* check local */
+        if (poslocal + lenlocal + 5 >= line.len) continue;
+        if (!str_start(line.s+poslocal,local)) continue;
+        pos2 = poslocal+lenlocal;
+        if (!str_start(line.s+pos2,"-dig-")) continue;
+        foundlocal = 1;
+      }
+      if (foundlocal) {
+        foundmatch = 1;
+        if (flaglist && (local || flagours)) {
+          if (substdio_put(&ssout,line.s,line.len) == -1)
+            strerr_die3sys(111,FATAL,ERR_WRITE,"stdout: ");
+          if (substdio_put(&ssout,"\n",1) == -1)
+            strerr_die3sys(111,FATAL,ERR_WRITE,"stdout: ");
+        }
+        line.len = 0;          /* same - kill line */
+        if (flagours)
+          --nolists;
+      }
+    }
+    close(fdin);
+  }
+  if (flaglist) {
+    if (substdio_flush(&ssout) == -1)
+      strerr_die3sys(111,FATAL,ERR_FLUSH,"stdout: ");
+    if (foundmatch)            /* means we had a match */
+      _exit(0);
+    else
+      strerr_die2x(100,FATAL,ERR_NO_MATCH);
+  }
+       /* only -d and regular use left */
+
+  if (nolists >= maxlists && !flagdelete)
+    strerr_die2x(100,FATAL,ERR_LISTNO);
+  if (!flagdelete)
+    if (substdio_put(&ssout,addr.s,addr.len-1) == -1)
+      strerr_die4sys(111,FATAL,ERR_WRITE,dir.s,"/crontabn: ");
+  if (flagdelete && !foundlocal)
+    strerr_die2x(111,FATAL,ERR_NO_MATCH);
+  if (substdio_flush(&ssout) == -1)
+    strerr_die4sys(111,FATAL,ERR_FLUSH,dir.s,"/crontabn: ");
+  if (fsync(fdout) == -1)
+    strerr_die4sys(111,FATAL,ERR_SYNC,dir.s,"/crontabn++: ");
+  if (close(fdout) == -1)
+    strerr_die4sys(111,FATAL,ERR_CLOSE,dir.s,"/crontabn: ");
+  if (rename("crontabn","crontab") == -1)
+    strerr_die4sys(111,FATAL,ERR_MOVE,dir.s,"/crontabn: ");
+  sendargs[0] = "sh";
+  sendargs[1] = "-c";
+
+  if (!stralloc_copys(&line,auto_cron)) die_nomem();
+  if (!stralloc_cats(&line,"/crontab '")) die_nomem();
+  if (!stralloc_cats(&line,dir.s)) die_nomem();
+  if (!stralloc_cats(&line,"/crontab'")) die_nomem();
+  if (!stralloc_0(&line)) die_nomem();
+  sendargs[2] = line.s;
+  sendargs[3] = 0;
+  switch(child = fork()) {
+      case -1:
+        strerr_die2sys(111,FATAL,ERR_FORK);
+      case 0:
+        if (setreuid(euid,euid) == -1)
+          strerr_die2sys(100,FATAL,ERR_SETUID);
+        execvp(*sendargs,sendargs);
+        if (errno == error_txtbsy || errno == error_nomem ||
+            errno == error_io)
+          strerr_die4sys(111,FATAL,ERR_EXECUTE,sendargs[2],": ");
+        else
+          strerr_die4sys(100,FATAL,ERR_EXECUTE,sendargs[2],": ");
+  }
+         /* parent */
+  wait_pid(&wstat,child);
+  if (wait_crashed(wstat))
+    strerr_die2x(111,FATAL,ERR_CHILD_CRASHED);
+  switch(wait_exitcode(wstat)) {
+      case 0:
+        _exit(0);
+      default:
+        strerr_die2x(111,FATAL,ERR_CRONTAB);
+  }
+}