RCPTFILTER
An alternative example of using Courier's whitelisting APIs

[localmailfilter] [Example code] [Dynamic aliasing] [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

An interesting point of running a shell script is that one can run exactly the same script for delivery by using Courier's dynamic delivery; that is, having a dot-courier file of a 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: // whitelist IPs that are wrongly listed, e.g. mailx.courier-mta.com
130: static const char *whitelisted_ip[] = {
131:    "96.56.228.18",
132:    NULL
133: };
134: 
135: static int bl_block(char *home)
136: /*
137: * return 0 if not blocking, after a variable BLOCKn (n = 0, 1, ..., MAX_BL)
138: * print 550 Rejected msg if blocking
139: */
140: {
141:    static char const block[] = BLOCKn_STRING;
142:    static int const blndx = sizeof block - 2;
143: 
144:    int i, lstndx = 0;
145:    char blocklst[MAX_BL + 1], name[sizeof block], *blockmsg = NULL;
146:    
147:    if (getenv("RELAYCLIENT") == NULL)
148:    {
149:       strcpy(name, block);
150:       for (i = 0; i < MAX_BL; ++i)
151:       {
152:          char *e;
153:          int const n = i + '0';
154:          
155:          name[blndx] = n;
156:          e = getenv(name);
157:          if (e && *e)
158:          {
159:             blocklst[lstndx++] = n;
160:             if (blockmsg == NULL)
161:                blockmsg = e;
162:          }
163:       }
164:       
165:       if (blockmsg)
166:       {
167:          struct stat buf;
168:          char *h = strchr(home, '/');
169: 
170:          while (h)
171:          {
172:             char *const h1 = strchr(++h, '/');
173:             if (h1 == NULL)
174:             {
175:                h = NULL;
176:                break;
177:             }
178:    
179:             home = h;
180:             h = h1;
181:          }
182:          
183:          if (h == NULL)
184:             h = home;
185: 
186:          blocklst[lstndx] = 0;
187: 
188:          char const *whip = NULL;
189:          char *ip = getenv("TCPREMOTEIP");
190:          if (ip == NULL)
191:          {
192:             ip = "(missing)";
193:             blockmsg = NULL;
194:          }
195:          else
196:             for (int i = 0; (whip = whitelisted_ip[i]) != NULL; ++i)
197:                if (strcmp(ip, whip) == 0)
198:                {
199:                   blockmsg = NULL;
200:                   break;
201:                }
202: 
203:          if (stat(NOBLOCKFILE, &buf) == 0)
204:             blockmsg = NULL;
205:          else if (errno != ENOENT)
206:          {
207:             blockmsg = NULL;
208:             syslog(LOG_CRIT, "Cannot stat " NOBLOCKFILE ": %s\n",
209:                strerror(errno));
210:          }
211: 
212:          syslog(LOG_DEBUG, "%sblocking %sip %s: BLOCK=\"%s\" for %s\n",
213:             blockmsg? "": "not ", whip? "whitelisted ": "",
214:             ip, blocklst, h);
215: 
216:          if (blockmsg)
217:          {
218:             fputs(blockmsg, stdout);
219:             return 1;
220:          }
221:       }
222:    }
223:    
224:    return 0;
225: }
226: 
227: static void addtoenv(char const *name, char const *value)
228: {
229:    char *freeonexit = (char*)malloc(strlen(name) + strlen(value) + 1);
230:    if (freeonexit)
231:       putenv(strcat(strcpy(freeonexit, name), value));
232: }
233: 
234: static int run_script(char const *ext, char *argv[], char const *home)
235: {
236:    int rtc = 0;
237:    char *local = strdup(ext);
238:    if (local)
239:    {
240:       int err[2];
241:       char *host = strchr(local, '@');
242:       
243:       if (host)
244:          *host++ = 0;
245:       else
246:          host = "";
247:       addtoenv("HOST=", host);
248:       addtoenv("LOCAL=", local);
249:       free(local);
250:       
251:       if (pipe(err) < 0)
252:          syslog(LOG_CRIT, "Cannot open pipe: %s\n", strerror(errno));
253:       else
254:       {
255:          pid_t const pid = fork();
256:          if (pid < 0)
257:             syslog(LOG_CRIT, "Cannot fork: %s\n", strerror(errno));
258:          else if (pid)
259:          {
260:             char buf[2048], *next = &buf[0];
261:             char *const first = &buf[0], *const last = &buf[sizeof buf - 2];
262:             
263:             close(err[1]);
264:             alarm(30);
265:             last[1] = 0; // terminator on forced newline
266:             while (signal_timed_out == 0 &&
267:                signal_break == 0 &&
268:                signal_child == 0)
269:             {
270:                int rd = read(err[0], next, last - next);
271: #if !defined NDEBUG
272:    printf("rd=%2d, next=%2ld\n", rd, next - first);
273: #endif
274:                if (rd > 0)
275:                {
276:                   char *p = first, *br;
277:                   next += rd;
278:                
279:                   *next = next == last? '\n': 0; // force newline if full
280:                
281:                   while ((br = strchr(p, '\n')) != NULL)
282:                   {
283:                      int level = LOG_CRIT;
284:                      *br = 0;
285:                
286:                      /*
287:                      * e.g., "##This is a warning\n"
288:                      */
289:                      while (*p == '#' && level < LOG_DEBUG)
290:                         ++p, ++level;
291:                   
292:                      if (*p) syslog(level, "script: %s\n", p);
293:                      p = br + (br < last);     // +1 if not forced newline
294:                      assert(first <= p && p <= next);
295:                   }
296:                
297:                   memmove(first, p, next - p);
298:                   next -= p - first;
299:                   assert(first <= next && next < last);
300:                }
301:                else if (rd == 0 || errno != EINTR && errno != EAGAIN)
302:                {
303:                   if (rd)
304:                      syslog(LOG_CRIT, "Pipe broken: %s\n", strerror(errno));
305:                   break;
306:                }
307:             }
308:             alarm(0);
309:             if (signal_timed_out || signal_break)
310:             {
311:                kill(pid, SIGTERM);
312:             }
313:             close(err[0]);
314:             
315:             for (;;)
316:             {
317:                int status;
318:                pid_t wpid = wait(&status);
319:                if (wpid < 0 && errno != EAGAIN && errno != EINTR)
320:                {
321:                   syslog(LOG_CRIT,
322:                      "Cannot wait %s/" SCRIPTFILE "[%u]: %s\n",
323:                      home, (unsigned)wpid, strerror(errno));
324:                   break;
325:                }
326:                else if (wpid == pid)
327:                {
328:                   if (WIFEXITED(status))
329:                   {
330:                      int level, s_rtc = WEXITSTATUS(status);
331:                      switch (s_rtc)
332:                      {
333:                         case 0: case 64: case 99: level = LOG_DEBUG; break;
334:                         default: level = LOG_CRIT; break;
335:                      }
336:                      
337:                      /*
338:                      * we never give 99 for examining content: just 0 or 1
339:                      */
340:                      rtc = s_rtc > 50;
341:                      syslog(level,
342:                         "%s/" SCRIPTFILE " with %s exited %d, rtc=%d\n",
343:                         home, ext, s_rtc, rtc);
344:                   }
345:                   else if (WIFSIGNALED(status))
346:                   {
347:                      syslog(LOG_CRIT,
348:                         "%s/" SCRIPTFILE " terminated with signal %d, rtc=%d\n",
349:                         home, WTERMSIG(status), rtc);
350:                   }
351:                   else continue; // stopped?
352:                   
353:                   break;
354:                }
355:             }
356:          }
357:          else // child process
358:          {
359:             close(0);
360:             open("/dev/null", O_RDONLY);
361:             close(1);
362:             open("/dev/null", O_WRONLY);
363:             close(2);
364:             dup(err[1]);
365:             close(err[0]);
366:             close(err[1]);
367:             closelog();
368:             execv(SCRIPTFILE, argv);
369:             syslog(LOG_MAIL|LOG_CRIT, "rcptfilter: cannot execv: %s\n",
370:                strerror(errno));
371:             exit(0);
372:          }
373:       }
374:       
375:    }
376:    return rtc;
377: }
378: 
379: int main(int argc, char *argv[])
380: {
381:    int rtc = 0;
382:    int i, uid = 0, gid = 0, err = 0;
383:    char *ext = NULL, *home = getenv("HOME");
384:    static char const argerror[] = "invoked with wrong argument: ";
385: 
386:    char *xargv[32];
387:    size_t xargc = 0;
388:    
389:    openlog("rcptfilter", LOG_PID, LOG_MAIL);
390:    xargv[xargc++] = SCRIPTFILE;
391:    xargv[xargc++] = "RCPT";
392:    
393:    for (i = 1; i < argc; ++i)
394:    {
395:       char *a = argv[i];
396:       int pass_it = 1;
397:       
398:       if (*a == '-')
399:       {
400:          pass_it = 0;
401:          switch (*++a)
402:          {
403:             case 'D':
404:                if (i + 1 >= argc)
405:                {
406:                   syslog(LOG_CRIT, "%s-D requires a value\n", argerror);
407:                   ++err;
408:                }
409:                else
410:                {
411:                   char *t = NULL;
412:                   a = argv[++i];
413:                   uid = strtoul(a, &t, 10);
414:                   if (t && *t == '/')
415:                   {
416:                      gid = strtoul(t + 1, &t, 10);
417:                      if (t && *t) t = NULL;
418:                   }
419:                   else t = NULL;
420:                   if (t == NULL)
421:                   {
422:                      syslog(LOG_CRIT, "%suidgid is %s\n", argerror, a);
423:                      ++err;
424:                   }
425:                }
426:                break;
427:             
428:             case 'M':
429:                if (i + 1 >= argc)
430:                {
431:                   syslog(LOG_CRIT, "%s-M requires a value\n", argerror);
432:                   ++err;
433:                }
434:                else if (strncmp(a = argv[++i],
435:                   "rcptfilter" , sizeof "rcptfilter" - 1) != 0)
436:                {
437:                   syslog(LOG_CRIT, "%s-M with value %s\n", argerror, a);
438:                   ++err;
439:                }
440:                else
441:                {
442:                   ext = strchr(a, '-');
443:                   if (ext)
444:                      ++ext;
445:                   else
446:                      ext = "";
447:                }
448:                break;
449:                
450:             case 'h':
451:                puts(usage);
452:                return 0;
453:             
454:             default:
455:                pass_it = 1;
456:                break;
457:          }
458:       }
459:       
460:       if (pass_it && xargc + 1 < sizeof xargv/ sizeof xargv[0])
461:       {
462:          xargv[xargc++] = a;
463:       }
464:    }
465:    
466:    xargv[xargc] = NULL;
467: 
468:    if (geteuid() == 0 && (uid || gid))
469:    {
470:       if (gid) setgid(gid);
471:       setuid(uid);
472:    }
473: 
474:    set_signal();
475:    if (home && ext)
476:    {
477:       struct stat buf;
478:       uid_t const me = geteuid();
479:       gid_t const myg = getegid();
480:       
481:       if (chdir(home))
482:       {
483:          syslog(LOG_CRIT, "Cannot chdir to %s: %s\n",
484:             home, strerror(errno));
485:       }
486:       else if (bl_block(home))
487:       {
488:          rtc = 1;
489:       }
490:       else if (stat(SCRIPTFILE, &buf) != 0)
491:       {
492:          if (errno != ENOENT)
493:             syslog(LOG_CRIT, "Cannot stat %s/" SCRIPTFILE ": %s\n",
494:                home, strerror(errno));
495:       }
496:       else if (!S_ISREG(buf.st_mode) || !(
497:          ((S_IXUSR & buf.st_mode) && (me == 0 || me == buf.st_uid)) ||
498:          ((S_IXGRP & buf.st_mode) && (me == 0 || myg == buf.st_gid)) ||
499:          (S_IXOTH & buf.st_mode)))
500:       {
501:          syslog(LOG_INFO, "%s/" SCRIPTFILE " not executable by %d/%d\n",
502:             home, (int)me, (int)myg);
503:       }
504:       else
505:       {
506:          rtc = run_script(ext, xargv, home);
507:       }
508:    }
509:    else
510:    {
511:       syslog(LOG_CRIT, "%smissing %s%s%s\n", argerror,
512:          home ? "" : "HOME env variable",
513:          home || ext ? "" : " and ",
514:          ext ? "" : "-M argument");
515:    }
516:       
517:    return rtc;
518: }
519: 

Catch-all template

Alias dynamic delivery is documented by Courier. While the rcptfilter compiled executable runs for any delivery, the file:

|| /etc/courier/aliasdir/rcptfilter.sh
might only exist in that directory. A possible template for it might be something like so:
01: #!/bin/bash
02: # this script is called both _online_ by rcptfilter with $1 = RCPT,
03: # and _offline_ for actual delivery (stdin has message body) in
04: # dynamic delivery mode (i.e. accept delivery commands on stdout)
05: #
06: # rtc: online                                     | offline
07: # 0    OK, run command if any or assume delivered | OK, accept
08: # 64   No, send back a DSN                        | No, reject
09: # 99   OK, assume delivered (or run command)      | No, reject
10: #
11: # Note: rcptfilter is configured in /etc/courier/maildropfilter.
12: # It returns 0 if we return < 50, or 1 otherwise (never 99).
13: # The DELI call comes after .courier-default.
14: 
15: RUNMODE="$1"
16: if [ -z "$RUNMODE" ]; then
17:     RUNMODE="DELI"
18: fi
19: 
20: LCLOCAL=`echo $LOCAL | tr '[A-Z]' '[a-z]'`
21: fgrep "${LCLOCAL}@${HOST}" notwanted > /dev/null 2>&1
22: if [ $? -eq 0 ]; then
23:     printf "%s -$RUNMODE Msg for %s at %s got rtc=99\n" \
24:         "$(date -R)" "$LOCAL" "$HOST" >> /var/log/mailalias.log
25:     exit 99
26: fi
27: 
28: case $LCLOCAL in
29:     postmaster)
30:         NEWADDR="me@mydomain.dom";;
31:     # ...
32:     *)
33:         NEWADDR="";;
34: esac
35: 
36: if [ "$NEWADDR" = "" ]; then
37:     case $HOST in
38:         hosted_domain_1)
39:             ;;
40:     # ...
41:     esac
42:     
43:     if [ "$NEWADDR" = "" ]; then
44:     #   bounce (either from courier or from rcptfilter)
45:         printf "%s -$RUNMODE Msg for %s at %s got rtc=64\n" \
46:             "$(date -R)" "$LOCAL" "$HOST" >> /var/log/mailalias.log
47:         exit 64
48:     fi
49: fi
50: 
51: printf "%s -$RUNMODE Msg for %s at %s redirected to %s\n" \
52:     "$(date -R)" "$LOCAL" "$HOST" "$NEWADDR" \
53:     >> /var/log/mailalias.log
54: printf "$NEWADDR\n"
55: exit 0
56: 

The LOCAL and HOST environment variables are set by the executable before invoking the script.

Instead of logging to a custom log file, it may be convenient to log to stderr, >&2, which is copied to the mail log.

License

This example code is in the public domain.