chiark / gitweb /
Debianization and various other fixes.
[ezmlm] / ezmlm-cron.c
1 #include <sys/types.h>
2 #include <pwd.h>
3 #include "strerr.h"
4 #include "stralloc.h"
5 #include "sgetopt.h"
6 #include "substdio.h"
7 #include "error.h"
8 #include "str.h"
9 #include "fmt.h"
10 #include "fork.h"
11 #include "wait.h"
12 #include "readwrite.h"
13 #include "auto_qmail.h"
14 #include "auto_cron.h"
15 #include "errtxt.h"
16 #include "idx.h"
17
18 #define FATAL "ezmlm-cron: fatal: "
19
20 void die_usage()
21 {
22  strerr_die2x(100,FATAL,
23   "usage: ezmlm-cron [-cCdDlLvV] [-w dow] [-t hh:mm] [-i hrs] listadr code");
24 }
25
26 void die_dow()
27 {
28   strerr_die2x(100,FATAL,ERR_DOW);
29 }
30
31 void die_nomem() { strerr_die2x(111,FATAL,ERR_NOMEM); }
32
33 unsigned long deltah = 24L;     /* default interval 24h */
34 unsigned long hh = 4L;          /* default time 04:12 */
35 unsigned long mm = 12L;
36 char *dow = "*";                /* day of week */
37 char *qmail_inject = "/usr/sbin/qmail-inject ";
38 char strnum[FMT_ULONG];
39 unsigned long uid,euid;
40
41 stralloc line = {0};
42 stralloc rp = {0};
43 stralloc addr = {0};
44 stralloc user = {0};
45 stralloc euser = {0};
46 stralloc dir = {0};
47 stralloc listaddr = {0};
48
49 struct passwd *ppasswd;
50
51 int opt,match;
52 int hostmatch;
53 int localmatch;
54 unsigned long dh,t;
55 int founduser = 0;
56 int listmatch = 0;
57 int flagconfig = 0;
58 int flagdelete = 0;
59 int flaglist = 0;
60 int flagdigit = 0;
61 int flagours;
62 int foundlocal;
63 int foundmatch = 0;
64 int nolists = 0;
65 int maxlists;
66 unsigned int pos,pos2,poslocal,len;
67 unsigned int lenhost,lenlocal;
68 unsigned int part0start,part0len;
69 int fdlock,fdin,fdout;
70
71 char *local = (char *) 0;       /* list = local@host */
72 char *host = (char *) 0;
73 char *code = (char *) 0;        /* digest code */
74 char *cp;
75
76 void die_syntax()
77 {
78   if (!stralloc_0(&line)) die_nomem();
79   strerr_die5x(100,FATAL,TXT_EZCRONRC," ",ERR_SYNTAX,line.s);
80 }
81
82 void die_argument()
83 {
84   strerr_die2x(100,FATAL,ERR_NOT_CLEAN);
85 }
86
87 int isclean(addr,flagaddr)
88         /* assures that addr has only letters, digits, "-_" */
89         /* also checks allows single '@' if flagaddr = 1 */
90         /* returns 1 if clean, 0 otherwise */
91   char *addr;
92   int flagaddr;         /* 1 for addresses with '@', 0 for other args */
93 {
94   unsigned int pos;
95   register char ch;
96   register char *cp;
97   if (flagaddr) {               /* shoud have one '@' */
98     pos = str_chr(addr,'@');
99     if (!pos || !addr[pos])
100       return 0;                 /* at least 1 char for local */
101     if (!addr[pos+1])
102       return 0;                 /* host must be at least 1 char */
103     pos++;
104     case_lowerb(addr+pos,str_len(addr)-pos);
105   } else
106     pos = 0;
107   pos +=  str_chr(addr + pos,'@');
108   if (addr[pos])                /* but no more */
109     return 0;
110   cp = addr;
111   while ((ch = *(cp++)))
112     if (!(ch >= 'a' && ch <= 'z') &&
113         !(ch >= 'A' && ch <= 'Z') &&
114         !(ch >= '0' && ch <= '9') &&
115         ch != '.' && ch != '-' && ch != '_' && ch != '@')
116       return 0;
117   return 1;
118 }
119
120 char inbuf[512];
121 substdio ssin;
122
123 char outbuf[512];
124 substdio ssout;
125
126 void main(argc,argv)
127 int argc;
128 char **argv;
129
130 {
131   int child;
132   char *sendargs[4];
133   int wstat;
134
135   (void) umask(077);
136   sig_pipeignore();
137
138   while ((opt = getopt(argc,argv,"cCdDi:lLt:w:vV")) != opteof)
139     switch (opt) {
140       case 'c': flagconfig = 1; break;
141       case 'C': flagconfig = 0; break;
142       case 'd': flagdelete = 1; break;
143       case 'D': flagdelete = 0; break;
144       case 'i': scan_ulong(optarg,&deltah); break;
145       case 'l': flaglist = 1; break;
146       case 'L': flaglist = 0; break;
147       case 't':
148                 pos = scan_ulong(optarg,&hh);
149                 if (!optarg[pos++] == ':') die_usage();
150                 pos = scan_ulong(optarg + pos,&mm);
151                 break;
152       case 'w':
153                 dow = optarg;
154                 cp = optarg - 1;
155                 while (*(++cp)) {
156                   if (*cp >= '0' && *cp <= '7') {
157                     if (flagdigit) die_dow();
158                     flagdigit = 1;
159                   } else if (*cp == ',') {
160                     if (!flagdigit) die_dow();
161                     flagdigit = 0;
162                   } else
163                     die_dow();
164                 }
165                 break;
166       case 'v':
167       case 'V': strerr_die2x(100,"ezmlm-cron version: ",EZIDX_VERSION);
168       default:
169                 die_usage();
170     }
171   if (flaglist + flagdelete + flagconfig > 1)
172     strerr_die2x(100,FATAL,ERR_EXCLUSIVE);
173   uid = getuid();
174   if (uid && !(euid = geteuid()))
175     strerr_die2x(100,FATAL,ERR_SUID);
176   if (!(ppasswd = getpwuid(uid)))
177     strerr_die2x(100,FATAL,ERR_UID);
178   if (!stralloc_copys(&user,ppasswd->pw_name)) die_nomem();
179   if (!stralloc_0(&user)) die_nomem();
180   if (!(ppasswd = getpwuid(euid)))
181     strerr_die2x(100,FATAL,ERR_EUID);
182   if (!stralloc_copys(&dir.s,ppasswd->pw_dir)) die_nomem();
183   if (!stralloc_0(&dir)) die_nomem();
184   if (!stralloc_copys(&euser,ppasswd->pw_name)) die_nomem();
185   if (!stralloc_0(&euser)) die_nomem();
186
187   if (chdir(dir.s) == -1)
188     strerr_die4sys(111,FATAL,ERR_SWITCH,dir.s,": ");
189
190   local = argv[optind++];       /* list address, optional for -c & -l */
191   if (!local) {
192     if (!flagconfig && !flaglist)
193       die_usage();
194     lenlocal = 0;
195     lenhost = 0;
196   } else {
197     if (!stralloc_copys(&listaddr,local)) die_nomem();
198     if (!isclean(local,1))
199       die_argument();
200     pos = str_chr(local,'@');
201     lenlocal = pos;
202     local[pos] = '\0';
203     host = local + pos + 1;
204     lenhost = str_len(host);
205     code = argv[optind];
206     if (!code) {                /* ignored for -l, -c, and -d */
207       if (flagdelete || flaglist || flagconfig)
208                                 /* get away with not putting code for delete */
209         code = "a";     /* a hack - so what! */
210       else
211         die_usage();
212     } else
213       if (!isclean(code,0))
214         die_argument();
215   }
216   if ((fdin = open_read(TXT_EZCRONRC)) == -1)
217     strerr_die6sys(111,FATAL,ERR_OPEN,dir.s,"/",TXT_EZCRONRC,": ");
218         /* first line is special */
219   substdio_fdbuf(&ssin,read,fdin,inbuf,sizeof(inbuf));
220   if (getln(&ssin,&line,&match,'\n') == -1)
221     strerr_die6sys(111,FATAL,ERR_READ,dir.s,"/",TXT_EZCRONRC,": ");
222
223   if (!match)
224     strerr_die6sys(111,FATAL,ERR_READ,dir.s,"/",TXT_EZCRONRC,": ");
225         /* (since we have match line.len has to be >= 1) */
226   line.s[line.len - 1] = '\0';
227   if (!isclean(line.s,0))        /* host for bounces */
228     strerr_die4x(100,ERR_CFHOST,dir.s,"/",TXT_EZCRONRC);
229   if (!stralloc_copys(&rp,line.s)) die_nomem();
230
231   match = 1;
232   for(;;) {
233     if (!match) break;          /* to allow last line without '\n' */
234     if (getln(&ssin,&line,&match,'\n') == -1)
235     strerr_die6sys(111,FATAL,ERR_READ,dir.s,"/",TXT_EZCRONRC,": ");
236     if (!line.len)
237       break;
238     line.s[line.len-1] = '\0';
239     if (!case_startb(line.s,line.len,user.s))
240       continue;
241     pos = user.len - 1;
242     if (pos >= line.len || line.s[pos] != ':')
243       continue;
244     founduser = 1;               /* got user line */
245     break;
246   }
247   close(fdin);
248   if (!founduser)
249     strerr_die2x(100,FATAL,ERR_BADUSER);
250   
251   if (flagconfig) {
252     line.s[line.len-1] = '\n';  /* not very elegant ;-) */
253     substdio_fdbuf(&ssout,write,1,outbuf,sizeof(outbuf));
254     if (substdio_put(&ssout,line.s,line.len) == -1)
255       strerr_die3sys(111,FATAL,ERR_WRITE,"stdout: ");
256     if (substdio_flush(&ssout) == -1)
257       strerr_die3sys(111,FATAL,ERR_WRITE,"stdout: ");
258     _exit(0);
259   }
260   ++pos;                                /* points to first ':' */
261   len = str_chr(line.s+pos,':');        /* second ':' */
262     if (!line.s[pos + len])
263       die_syntax();
264   if (!local) {                         /* only -d and std left */
265     localmatch = 1;
266     hostmatch = 1;
267   } else {
268     hostmatch = 0;
269     if (len <= str_len(local))
270       if (!str_diffn(line.s+pos,local,len))
271         localmatch = 1;
272   }
273   pos += len + 1;
274   len = str_chr(line.s + pos,':');      /* third */
275   if (!line.s[pos + len])
276     die_syntax();
277   if (local) {                          /* check host */
278     if (len == 0)                       /* empty host => any host */
279       hostmatch = 1;
280     else
281       if (len == str_len(host))
282         if (!case_diffb(line.s+pos,len,host))
283           hostmatch = 1;
284   }
285   pos += len + 1;
286   pos += scan_ulong(line.s+pos,&maxlists);
287   if (line.s[pos]) {                    /* check additional lists */
288     if (line.s[pos] != ':')
289       die_syntax();
290     if (line.s[pos+1+str_chr(line.s+pos+1,':')])
291       die_syntax();     /* reminder lists are not separated by ':'  */
292                         /* otherwise a ':' or arg miscount will die */
293                         /* silently */
294     if (local) {
295       while (++pos < line.len) {
296         len = str_chr(line.s + pos,'@');
297         if (len == lenlocal && !str_diffn(line.s + pos,local,len)) {
298           pos += len;
299           if (!line.s[pos]) break;
300           pos++;
301           len = str_chr(line.s+pos,',');
302             if (len == lenhost && !case_diffb(line.s+pos,len,host)) {
303               listmatch = 1;
304               break;
305             }
306         }
307         pos += len;
308       }
309     }
310   }
311   if (!listmatch) {
312     if (!hostmatch)
313       strerr_die2x(100,FATAL,ERR_BADHOST);
314     if (!localmatch)
315       strerr_die2x(100,FATAL,ERR_BADLOCAL);
316   }
317         /* assemble correct line */
318   if (!flaglist) {
319     if (!stralloc_copyb(&addr,strnum,fmt_ulong(strnum,mm))) die_nomem();
320     if (!stralloc_cats(&addr," ")) die_nomem();
321     dh = 0L;
322     if (deltah <= 3L) dh = deltah;
323     else if (deltah <= 6L) dh = 6L;
324     else if (deltah <= 12L) dh = 12L;
325     else if (deltah <= 24L) dh = 24L;
326     else if (deltah <= 48L) {
327       if (dow[0] == '*') dow = "1,3,5";
328     } else if (deltah <= 72L) {
329       if (dow[0] == '*') dow = "1,4";
330     } else
331     if (dow[0] == '*') dow = "1";
332
333     if (!dh) {
334       if (!stralloc_cats(&addr,"*")) die_nomem();
335     } else {
336       if (!stralloc_catb(&addr,strnum,fmt_ulong(strnum,hh))) die_nomem();
337       for (t = hh + dh; t < hh + 24L; t+=dh) {
338         if (!stralloc_cats(&addr,",")) die_nomem();
339         if (!stralloc_catb(&addr,strnum,fmt_ulong(strnum,t % 24L))) die_nomem();
340       }
341     }
342     if (!stralloc_cats(&addr," * * ")) die_nomem();
343     if (!stralloc_cats(&addr,dow)) die_nomem();
344     if (!stralloc_cats(&addr," ")) die_nomem();
345     part0start = addr.len;              /* /var/qmail/bin/qmail-inject */
346     if (!stralloc_cats(&addr,qmail_inject)) die_nomem();
347     part0len = addr.len - part0start;
348     if (!stralloc_cats(&addr,local)) die_nomem();
349     if (!stralloc_cats(&addr,"-dig-")) die_nomem();
350     if (!stralloc_cats(&addr,code)) die_nomem();
351     if (!stralloc_cats(&addr,"@")) die_nomem();
352     if (!stralloc_cats(&addr,host)) die_nomem();
353                 /* feed 'Return-Path: <user@host>' to qmail-inject */
354     if (!stralloc_cats(&addr,"%Return-path: <")) die_nomem();
355     if (!stralloc_cats(&addr,user.s)) die_nomem();
356     if (!stralloc_cats(&addr,"@")) die_nomem();
357     if (!stralloc_cat(&addr,&rp)) die_nomem();
358     if (!stralloc_cats(&addr,">\n")) die_nomem();
359   }
360   if (!stralloc_0(&addr)) die_nomem();
361
362   if (!flaglist) {
363         /* now to rewrite crontab we need to lock */
364     fdlock = open_append("crontabl");
365     if (fdlock == -1)
366       strerr_die4sys(111,FATAL,ERR_OPEN,dir.s,"/crontabl: ");
367     if (lock_ex(fdlock) == -1) {
368       close(fdlock);
369     strerr_die4sys(111,FATAL,ERR_OBTAIN,dir.s,"/crontabl: ");
370     }
371   } /* if !flaglist */
372   if ((fdin = open_read("crontab")) == -1) {
373     if (errno != error_noent)
374       strerr_die4sys(111,FATAL,ERR_READ,dir.s,"/crontab: ");
375   } else
376     substdio_fdbuf(&ssin,read,fdin,inbuf,sizeof(inbuf));
377   if (flaglist)
378     substdio_fdbuf(&ssout,write,1,outbuf,sizeof(outbuf));
379   else {
380     if ((fdout = open_trunc("crontabn")) == -1)
381       strerr_die4sys(111,FATAL,ERR_WRITE,dir.s,"/crontabn: ");
382     substdio_fdbuf(&ssout,write,fdout,outbuf,sizeof(outbuf));
383   }
384   line.len = 0;
385
386   if (fdin != -1) {
387     for (;;) {
388       if (!flaglist && line.len) {
389         line.s[line.len-1] = '\n';
390         if (substdio_put(&ssout,line.s,line.len) == -1)
391           strerr_die4sys(111,FATAL,ERR_WRITE,dir.s,"/crontabn: ");
392       }
393       if (getln(&ssin,&line,&match,'\n') == -1)
394         strerr_die4sys(111,FATAL,ERR_READ,dir.s,"/crontab: ");
395       if (!match)
396         break;
397       flagours = 0;                     /* assume entry is not ours */
398       foundlocal = 0;
399       line.s[line.len - 1] = '\0';      /* match so at least 1 char */
400       pos = 0;
401       while (line.s[pos] == ' ' && line.s[pos] == '\t') ++pos;
402       if (line.s[pos] == '#')
403         continue;                       /* cron comment */
404       pos = str_chr(line.s,'/');
405       if (!str_start(line.s+pos,qmail_inject)) continue;
406       pos += str_len(qmail_inject);
407       poslocal = pos;
408       pos = byte_rchr(line.s,line.len,'<');     /* should be Return-Path: < */
409       if (pos == line.len)
410         continue;                       /* not ezmlm-cron line */
411       pos++;
412      len = str_chr(line.s+pos,'@');
413       if (len == user.len - 1 && !str_diffn(line.s+pos,user.s,len)) {
414         flagours = 1;
415         ++nolists;              /* belongs to this user */
416       }
417       if (!local) {
418         foundlocal = 1;
419       } else {
420         pos = poslocal + str_chr(line.s+poslocal,'@');
421         if (pos + lenhost +1 >= line.len) continue;
422         if (case_diffb(line.s+pos+1,lenhost,host)) continue;
423         if (line.s[pos+lenhost+1] != '%') continue;
424                                 /* check local */
425         if (poslocal + lenlocal + 5 >= line.len) continue;
426         if (!str_start(line.s+poslocal,local)) continue;
427         pos2 = poslocal+lenlocal;
428         if (!str_start(line.s+pos2,"-dig-")) continue;
429         foundlocal = 1;
430       }
431       if (foundlocal) {
432         foundmatch = 1;
433         if (flaglist && (local || flagours)) {
434           if (substdio_put(&ssout,line.s,line.len) == -1)
435             strerr_die3sys(111,FATAL,ERR_WRITE,"stdout: ");
436           if (substdio_put(&ssout,"\n",1) == -1)
437             strerr_die3sys(111,FATAL,ERR_WRITE,"stdout: ");
438         }
439         line.len = 0;           /* same - kill line */
440         if (flagours)
441           --nolists;
442       }
443     }
444     close(fdin);
445   }
446   if (flaglist) {
447     if (substdio_flush(&ssout) == -1)
448       strerr_die3sys(111,FATAL,ERR_FLUSH,"stdout: ");
449     if (foundmatch)             /* means we had a match */
450       _exit(0);
451     else
452       strerr_die2x(100,FATAL,ERR_NO_MATCH);
453   }
454         /* only -d and regular use left */
455
456   if (nolists >= maxlists && !flagdelete)
457     strerr_die2x(100,FATAL,ERR_LISTNO);
458   if (!flagdelete)
459     if (substdio_put(&ssout,addr.s,addr.len-1) == -1)
460       strerr_die4sys(111,FATAL,ERR_WRITE,dir.s,"/crontabn: ");
461   if (flagdelete && !foundlocal)
462     strerr_die2x(111,FATAL,ERR_NO_MATCH);
463   if (substdio_flush(&ssout) == -1)
464     strerr_die4sys(111,FATAL,ERR_FLUSH,dir.s,"/crontabn: ");
465   if (fsync(fdout) == -1)
466     strerr_die4sys(111,FATAL,ERR_SYNC,dir.s,"/crontabn++: ");
467   if (close(fdout) == -1)
468     strerr_die4sys(111,FATAL,ERR_CLOSE,dir.s,"/crontabn: ");
469   if (rename("crontabn","crontab") == -1)
470     strerr_die4sys(111,FATAL,ERR_MOVE,dir.s,"/crontabn: ");
471   sendargs[0] = "sh";
472   sendargs[1] = "-c";
473
474   if (!stralloc_copys(&line,auto_cron)) die_nomem();
475   if (!stralloc_cats(&line,"/crontab '")) die_nomem();
476   if (!stralloc_cats(&line,dir.s)) die_nomem();
477   if (!stralloc_cats(&line,"/crontab'")) die_nomem();
478   if (!stralloc_0(&line)) die_nomem();
479   sendargs[2] = line.s;
480   sendargs[3] = 0;
481   switch(child = fork()) {
482       case -1:
483         strerr_die2sys(111,FATAL,ERR_FORK);
484       case 0:
485         if (setreuid(euid,euid) == -1)
486           strerr_die2sys(100,FATAL,ERR_SETUID);
487         execvp(*sendargs,sendargs);
488         if (errno == error_txtbsy || errno == error_nomem ||
489             errno == error_io)
490           strerr_die4sys(111,FATAL,ERR_EXECUTE,sendargs[2],": ");
491         else
492           strerr_die4sys(100,FATAL,ERR_EXECUTE,sendargs[2],": ");
493   }
494          /* parent */
495   wait_pid(&wstat,child);
496   if (wait_crashed(wstat))
497     strerr_die2x(111,FATAL,ERR_CHILD_CRASHED);
498   switch(wait_exitcode(wstat)) {
499       case 0:
500         _exit(0);
501       default:
502         strerr_die2x(111,FATAL,ERR_CRONTAB);
503   }
504 }