RCPTFILTER
An alternative example of using Courier's whitelisting APIs

[localmailfilter] [Example code] [License]

Courier-MTA is a modular mail system that features various kinds of mail filtering. This page is an example of an alternative usage of the localmailfilter subsystem, using C and shell scripts.

Localmailfilter

Local filters constitute a way of filtering mail according to recipients. Unlike global filters, local filters only do incoming mail; that is, messages destined to the local server. Normally, maildrop is used for this task.

Maildrop is best known for being used as a delivery agent; that is, in delivery mode. Let's quickly recall that there is a sysconfdir/maildrop 1-line configuration file that contains the path of the delivery agent, as an alternative to the DEFAULTDELIVERY setting. When called in delivery mode, maildrop reads the files sysconfdir/maildroprc and $HOME/.mailfilter. Local mail filtering happens before a message is accepted, while the SMTP dialog is underway. There is a different 1-line configuration file, sysconfdir/maildropfilter, that contains the path of the filtering executable. As documented in localmailfilter, it can point to the same maildrop binary. The example described here, instead, points to a tiny executable that runs a shell script.

Alternative localmailfilter

The interesting point of running a shell script is that one can run exactly the same script for delivery. For example, one can handle aliases dynamically having aliasdir/.courier-default consisting of the single line:

|| rcptfilter.sh

Download the example source

You can download the C code or read it below. To install it, compile the code (please use -DNDEBUG for production, as mentioned in the top comment) and configure sysconfdir/maildropfilter with the relevant path. Mind permissions.

001: /*
002: * rcptfilter.c - written by vesely in milan on 8 aug 2006
003: *
004: * do BLOCKn and run rcptfilter.sh
005: * gcc -W -O -DNDEBUG -o /usr/local/sbin/rcptfilter rcptfilter.c
006: * gcc -W -Wall -Wno-parentheses -O0 -g -o rcptfilter rcptfilter.c
007: */
008: 
009: #define SCRIPTFILE "rcptfilter.sh"
010: #define NOBLOCKFILE "NOBLOCK"
011: #define QUOTE(x) #x
012: #define QUOTE_VAL(x) QUOTE(x)
013: 
014: // must be MAX_BL <= 10 for '0', '1', ..., '9'
015: #define MAX_BL 4
016: #define MAX_BL_STRING QUOTE_VAL(MAX_BL)
017: #define BLOCKn_STRING "BLOCKn"
018: 
019: static char const usage[] =
020: "usage:\n"
021: "\trcptfilter -h\n"
022: "\trcptfilter -D uid/gid -M rcptfilter[-ext] ...\n"
023: "The first format is only useful to print this note.\n"
024: "\n"
025: "The second format is used by Courier's local output module if the\n"
026: "maildropfilter configuration file contains the path of this executable.\n"
027: "In this case, rcptfilter changes directory to HOME and does two things:\n"
028: "First, it looks for variables " BLOCKn_STRING " (0 <= n < " MAX_BL_STRING "),"
029:   " and, if any is found\n"
030: "and the file \"" NOBLOCKFILE "\" does not exist, it rejects the message using the\n"
031: "value of the first " BLOCKn_STRING " variable.\n"
032: "Don't forget to set \"aliasfilteracct\" to block spam to aliases if using that.\n"
033: "\n"
034: "Second, if not blocked, it looks for file \"" SCRIPTFILE "\" and runs it.\n"
035: "Input and output are set to null, whilst stderr is logged via syslog in MAIL.\n"
036: "It then returns an exit code of 0 if the script exits rc < 50, 1 otherwise.\n"
037: "The script will have argument $1 set to \"RCPT\" and the other arguments\n"
038: "set to whatever was passed to rcptfilter as ellipsis (...). The variables\n"
039: "LOCAL and HOST will be set from ext. The rest of the environment remains.";
040: 
041: #include <stdio.h>
042: #include <string.h>
043: #include <stdlib.h>
044: #include <sys/types.h>
045: #include <sys/stat.h>
046: #include <sys/wait.h>
047: #include <fcntl.h>
048: #include <unistd.h>
049: #include <signal.h>
050: #include <syslog.h>
051: #include <ctype.h>
052: #include <errno.h>
053: #include <assert.h>
054: 
055: static volatile int
056:    signal_child = 0,
057:    signal_timed_out = 0,
058:    signal_break = 0;
059: 
060: static void sig_catcher(int sig)
061: {
062: #if !defined(NDEBUG)
063:    char buf[80];
064:    unsigned s = snprintf(buf, sizeof buf,
065:       "rcptfilter[%d]: received signal %d\n",
066:       (int)getpid(), sig);
067:    if (s >= sizeof buf)
068:    {
069:       buf[sizeof buf - 1] = '\n';
070:       s = sizeof buf;
071:    }
072:    write(2, buf, s);
073: #endif
074:    switch(sig)
075:    {
076:       case SIGALRM:
077:          signal_timed_out = 1;
078:          break;
079: 
080:       case SIGHUP:
081:       case SIGPIPE:
082:       case SIGINT:
083:       case SIGQUIT:
084:       case SIGTERM:
085:          signal_break = 1;
086:          break;
087: 
088:       case SIGCHLD:
089:          signal_child = 1;
090:          break;
091: 
092:       default:
093:          break;
094:    }
095: }
096: 
097: #if 0
098: static void reset_signal(void)
099: {
100:    struct sigaction act;
101:    memset(&act, 0, sizeof act);
102:    sigemptyset(&act.sa_mask);
103:    act.sa_handler = SIG_DFL;
104: 
105:    sigaction(SIGALRM, &act, NULL);
106:    sigaction(SIGPIPE, &act, NULL);
107:    sigaction(SIGINT, &act, NULL);
108:    sigaction(SIGTERM, &act, NULL);
109:    sigaction(SIGHUP, &act, NULL);
110:    sigaction(SIGCHLD, &act, NULL);
111: }
112: #endif
113: 
114: static void set_signal(void)
115: {
116:    struct sigaction act;
117:    memset(&act, 0, sizeof act);
118:    sigemptyset(&act.sa_mask);
119:       
120:    act.sa_handler = sig_catcher;
121:    sigaction(SIGALRM, &act, NULL);
122:    sigaction(SIGPIPE, &act, NULL);
123:    sigaction(SIGINT, &act, NULL);
124:    sigaction(SIGTERM, &act, NULL);
125:    sigaction(SIGHUP, &act, NULL);
126:    sigaction(SIGCHLD, &act, NULL);
127: }
128: 
129: static const char *whitelisted_ip[] = {
130:    "68.166.206.83",
131:    NULL
132: };
133: 
134: static int bl_block(char *home)
135: /*
136: * return 0 if not blocking, after a variable BLOCKn (n = 0, 1, ..., MAX_BL)
137: * print 550 Rejected msg if blocking
138: */
139: {
140:    static char const block[] = BLOCKn_STRING;
141:    static int const blndx = sizeof block - 2;
142: 
143:    int i, lstndx = 0;
144:    char blocklst[MAX_BL + 1], name[sizeof block], *blockmsg = NULL;
145:    
146:    if (getenv("RELAYCLIENT") == NULL)
147:    {
148:       strcpy(name, block);
149:       for (i = 0; i < MAX_BL; ++i)
150:       {
151:          char *e;
152:          int const n = i + '0';
153:          
154:          name[blndx] = n;
155:          e = getenv(name);
156:          if (e && *e)
157:          {
158:             blocklst[lstndx++] = n;
159:             if (blockmsg == NULL)
160:                blockmsg = e;
161:          }
162:       }
163:       
164:       if (blockmsg)
165:       {
166:          struct stat buf;
167:          char *h = strchr(home, '/');
168: 
169:          while (h)
170:          {
171:             char *const h1 = strchr(++h, '/');
172:             if (h1 == NULL)
173:             {
174:                h = NULL;
175:                break;
176:             }
177:    
178:             home = h;
179:             h = h1;
180:          }
181:          
182:          if (h == NULL)
183:             h = home;
184: 
185:          blocklst[lstndx] = 0;
186: 
187:          char const *whip = NULL;
188:          char *ip = getenv("TCPREMOTEIP");
189:          if (ip == NULL)
190:          {
191:             ip = "(missing)";
192:             blockmsg = NULL;
193:          }
194:          else
195:             for (int i = 0; (whip = whitelisted_ip[i]) != NULL; ++i)
196:                if (strcmp(ip, whip) == 0)
197:                {
198:                   blockmsg = NULL;
199:                   break;
200:                }
201: 
202:          if (stat(NOBLOCKFILE, &buf) == 0)
203:             blockmsg = NULL;
204:          else if (errno != ENOENT)
205:          {
206:             blockmsg = NULL;
207:             syslog(LOG_CRIT, "Cannot stat " NOBLOCKFILE ": %s\n",
208:                strerror(errno));
209:          }
210: 
211:          syslog(LOG_DEBUG, "%sblocking %sip %s: BLOCK=\"%s\" for %s\n",
212:             blockmsg? "": "not ", whip? "whitelisted ": "",
213:             ip, blocklst, h);
214: 
215:          if (blockmsg)
216:          {
217:             fputs(blockmsg, stdout);
218:             return 1;
219:          }
220:       }
221:    }
222:    
223:    return 0;
224: }
225: 
226: static void addtoenv(char const *name, char const *value)
227: {
228:    char *freeonexit = (char*)malloc(strlen(name) + strlen(value) + 1);
229:    if (freeonexit)
230:       putenv(strcat(strcpy(freeonexit, name), value));
231: }
232: 
233: static int run_script(char const *ext, char *argv[], char const *home)
234: {
235:    int rtc = 0;
236:    char *local = strdup(ext);
237:    if (local)
238:    {
239:       int err[2];
240:       char *host = strchr(local, '@');
241:       
242:       if (host)
243:          *host++ = 0;
244:       else
245:          host = "";
246:       addtoenv("HOST=", host);
247:       addtoenv("LOCAL=", local);
248:       free(local);
249:       
250:       if (pipe(err) < 0)
251:          syslog(LOG_CRIT, "Cannot open pipe: %s\n", strerror(errno));
252:       else
253:       {
254:          pid_t const pid = fork();
255:          if (pid < 0)
256:             syslog(LOG_CRIT, "Cannot fork: %s\n", strerror(errno));
257:          else if (pid)
258:          {
259:             char buf[2048], *next = &buf[0];
260:             char *const first = &buf[0], *const last = &buf[sizeof buf - 2];
261:             
262:             close(err[1]);
263:             alarm(30);
264:             last[1] = 0; // terminator on forced newline
265:             while (signal_timed_out == 0 &&
266:                signal_break == 0 &&
267:                signal_child == 0)
268:             {
269:                int rd = read(err[0], next, last - next);
270: #if !defined NDEBUG
271:    printf("rd=%2d, next=%2ld\n", rd, next - first);
272: #endif
273:                if (rd > 0)
274:                {
275:                   char *p = first, *br;
276:                   next += rd;
277:                
278:                   *next = next == last? '\n': 0; // force newline if full
279:                
280:                   while ((br = strchr(p, '\n')) != NULL)
281:                   {
282:                      int level = LOG_CRIT;
283:                      *br = 0;
284:                
285:                      /*
286:                      * e.g., "##This is a warning\n"
287:                      */
288:                      while (*p == '#' && level < LOG_DEBUG)
289:                         ++p, ++level;
290:                   
291:                      if (*p) syslog(level, "script: %s\n", p);
292:                      p = br + (br < last);     // +1 if not forced newline
293:                      assert(first <= p && p <= next);
294:                   }
295:                
296:                   memmove(first, p, next - p);
297:                   next -= p - first;
298:                   assert(first <= next && next < last);
299:                }
300:                else if (rd == 0 || errno != EINTR && errno != EAGAIN)
301:                {
302:                   if (rd)
303:                      syslog(LOG_CRIT, "Pipe broken: %s\n", strerror(errno));
304:                   break;
305:                }
306:             }
307:             alarm(0);
308:             if (signal_timed_out || signal_break)
309:             {
310:                kill(pid, SIGTERM);
311:             }
312:             close(err[0]);
313:             
314:             for (;;)
315:             {
316:                int status;
317:                pid_t wpid = wait(&status);
318:                if (wpid < 0 && errno != EAGAIN && errno != EINTR)
319:                {
320:                   syslog(LOG_CRIT,
321:                      "Cannot wait %s/" SCRIPTFILE "[%u]: %s\n",
322:                      home, (unsigned)wpid, strerror(errno));
323:                   break;
324:                }
325:                else if (wpid == pid)
326:                {
327:                   if (WIFEXITED(status))
328:                   {
329:                      int level, s_rtc = WEXITSTATUS(status);
330:                      switch (s_rtc)
331:                      {
332:                         case 0: case 64: case 99: level = LOG_DEBUG; break;
333:                         default: level = LOG_CRIT; break;
334:                      }
335:                      
336:                      /*
337:                      * we never give 99 for examining content: just 0 or 1
338:                      */
339:                      rtc = s_rtc > 50;
340:                      syslog(level,
341:                         "%s/" SCRIPTFILE " with %s exited %d, rtc=%d\n",
342:                         home, ext, s_rtc, rtc);
343:                   }
344:                   else if (WIFSIGNALED(status))
345:                   {
346:                      syslog(LOG_CRIT,
347:                         "%s/" SCRIPTFILE " terminated with signal %d, rtc=%d\n",
348:                         home, WTERMSIG(status), rtc);
349:                   }
350:                   else continue; // stopped?
351:                   
352:                   break;
353:                }
354:             }
355:          }
356:          else // child process
357:          {
358:             close(0);
359:             open("/dev/null", O_RDONLY);
360:             close(1);
361:             open("/dev/null", O_WRONLY);
362:             close(2);
363:             dup(err[1]);
364:             close(err[0]);
365:             close(err[1]);
366:             closelog();
367:             execv(SCRIPTFILE, argv);
368:             syslog(LOG_MAIL|LOG_CRIT, "rcptfilter: cannot execv: %s\n",
369:                strerror(errno));
370:             exit(0);
371:          }
372:       }
373:       
374:    }
375:    return rtc;
376: }
377: 
378: int main(int argc, char *argv[])
379: {
380:    int rtc = 0;
381:    int i, uid = 0, gid = 0, err = 0;
382:    char *ext = NULL, *home = getenv("HOME");
383:    static char const argerror[] = "invoked with wrong argument: ";
384: 
385:    char *xargv[32];
386:    size_t xargc = 0;
387:    
388:    openlog("rcptfilter", LOG_PID, LOG_MAIL);
389:    xargv[xargc++] = SCRIPTFILE;
390:    xargv[xargc++] = "RCPT";
391:    
392:    for (i = 1; i < argc; ++i)
393:    {
394:       char *a = argv[i];
395:       int pass_it = 1;
396:       
397:       if (*a == '-')
398:       {
399:          pass_it = 0;
400:          switch (*++a)
401:          {
402:             case 'D':
403:                if (i + 1 >= argc)
404:                {
405:                   syslog(LOG_CRIT, "%s-D requires a value\n", argerror);
406:                   ++err;
407:                }
408:                else
409:                {
410:                   char *t = NULL;
411:                   a = argv[++i];
412:                   uid = strtoul(a, &t, 10);
413:                   if (t && *t == '/')
414:                   {
415:                      gid = strtoul(t + 1, &t, 10);
416:                      if (t && *t) t = NULL;
417:                   }
418:                   else t = NULL;
419:                   if (t == NULL)
420:                   {
421:                      syslog(LOG_CRIT, "%suidgid is %s\n", argerror, a);
422:                      ++err;
423:                   }
424:                }
425:                break;
426:             
427:             case 'M':
428:                if (i + 1 >= argc)
429:                {
430:                   syslog(LOG_CRIT, "%s-M requires a value\n", argerror);
431:                   ++err;
432:                }
433:                else if (strncmp(a = argv[++i],
434:                   "rcptfilter" , sizeof "rcptfilter" - 1) != 0)
435:                {
436:                   syslog(LOG_CRIT, "%s-M with value %s\n", argerror, a);
437:                   ++err;
438:                }
439:                else
440:                {
441:                   ext = strchr(a, '-');
442:                   if (ext)
443:                      ++ext;
444:                   else
445:                      ext = "";
446:                }
447:                break;
448:                
449:             case 'h':
450:                puts(usage);
451:                return 0;
452:             
453:             default:
454:                pass_it = 1;
455:                break;
456:          }
457:       }
458:       
459:       if (pass_it && xargc + 1 < sizeof xargv/ sizeof xargv[0])
460:       {
461:          xargv[xargc++] = a;
462:       }
463:    }
464:    
465:    xargv[xargc] = NULL;
466: 
467:    if (geteuid() == 0 && (uid || gid))
468:    {
469:       if (gid) setgid(gid);
470:       setuid(uid);
471:    }
472: 
473:    set_signal();
474:    if (home && ext)
475:    {
476:       struct stat buf;
477:       uid_t const me = geteuid();
478:       gid_t const myg = getegid();
479:       
480:       if (chdir(home))
481:       {
482:          syslog(LOG_CRIT, "Cannot chdir to %s: %s\n",
483:             home, strerror(errno));
484:       }
485:       else if (bl_block(home))
486:       {
487:          rtc = 1;
488:       }
489:       else if (stat(SCRIPTFILE, &buf) != 0)
490:       {
491:          if (errno != ENOENT)
492:             syslog(LOG_CRIT, "Cannot stat %s/" SCRIPTFILE ": %s\n",
493:                home, strerror(errno));
494:       }
495:       else if (!S_ISREG(buf.st_mode) || !(
496:          ((S_IXUSR & buf.st_mode) && (me == 0 || me == buf.st_uid)) ||
497:          ((S_IXGRP & buf.st_mode) && (me == 0 || myg == buf.st_gid)) ||
498:          (S_IXOTH & buf.st_mode)))
499:       {
500:          syslog(LOG_INFO, "%s/" SCRIPTFILE " not executable by %d/%d\n",
501:             home, (int)me, (int)myg);
502:       }
503:       else
504:       {
505:          rtc = run_script(ext, xargv, home);
506:       }
507:    }
508:    else
509:    {
510:       syslog(LOG_CRIT, "%smissing %s%s%s\n", argerror,
511:          home ? "" : "HOME env variable",
512:          home || ext ? "" : " and ",
513:          ext ? "" : "-M argument");
514:    }
515:       
516:    return rtc;
517: }
518: 

License

This example code is in the public domain.