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

Here is the file that I posted on the courier-users list on Friday 27 October 2007. 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: * 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: 
011: static char const usage[] =
012: "usage:\n"
013: "\trcptfilter -h\n"
014: "\trcptfilter -D uid/gid -M rcptfilter[-ext] ...\n"
015: "The first format is only useful to print this note.\n"
016: "The second format is used by Courier's local output module if the\n"
017: "maildropfilter configuration file contains the path of this executable.\n"
018: "In this case, rcptfilter changes directory to HOME and looks for file\n"
019: "\"" SCRIPTFILE "\" and runs it (with null input and output, logged error)\n"
020: "and then returns an exit code of 0 if the script exits rc < 50, 1 otherwise.\n"
021: "The script will have argument $1 set to \"RCPT\" and the other arguments\n"
022: "set to whatever was passed to rcptfilter as ellipsis (...). The variables\n"
023: "LOCAL and HOST will be set from ext. The rest of the environment remains.";
024: 
025: #include <stdio.h>
026: #include <string.h>
027: #include <stdlib.h>
028: #include <sys/types.h>
029: #include <sys/stat.h>
030: #include <sys/wait.h>
031: #include <fcntl.h>
032: #include <unistd.h>
033: #include <signal.h>
034: #include <syslog.h>
035: #include <ctype.h>
036: #include <errno.h>
037: #include <assert.h>
038: 
039: static volatile int
040:    signal_child = 0,
041:    signal_timed_out = 0,
042:    signal_break = 0;
043: 
044: static void sig_catcher(int sig)
045: {
046: #if !defined(NDEBUG)
047:    char buf[80];
048:    unsigned s = snprintf(buf, sizeof buf,
049:       "rcptfilter[%d]: received signal %d\n",
050:       (int)getpid(), sig);
051:    if (s >= sizeof buf)
052:    {
053:       buf[sizeof buf - 1] = '\n';
054:       s = sizeof buf;
055:    }
056:    write(2, buf, s);
057: #endif
058:    switch(sig)
059:    {
060:       case SIGALRM:
061:          signal_timed_out = 1;
062:          break;
063: 
064:       case SIGHUP:
065:       case SIGPIPE:
066:       case SIGINT:
067:       case SIGQUIT:
068:       case SIGTERM:
069:          signal_break = 1;
070:          break;
071: 
072:       case SIGCHLD:
073:          signal_child = 1;
074:          break;
075: 
076:       default:
077:          break;
078:    }
079: }
080: 
081: #if 0
082: static void reset_signal(void)
083: {
084:    struct sigaction act;
085:    memset(&act, 0, sizeof act);
086:    sigemptyset(&act.sa_mask);
087:    act.sa_handler = SIG_DFL;
088: 
089:    sigaction(SIGALRM, &act, NULL);
090:    sigaction(SIGPIPE, &act, NULL);
091:    sigaction(SIGINT, &act, NULL);
092:    sigaction(SIGTERM, &act, NULL);
093:    sigaction(SIGHUP, &act, NULL);
094:    sigaction(SIGCHLD, &act, NULL);
095: }
096: #endif
097: 
098: static void set_signal(void)
099: {
100:    struct sigaction act;
101:    memset(&act, 0, sizeof act);
102:    sigemptyset(&act.sa_mask);
103:       
104:    act.sa_handler = sig_catcher;
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: 
113: static void addtoenv(char const *name, char const *value)
114: {
115:    char *freeonexit = (char*)malloc(strlen(name) + strlen(value) + 1);
116:    if (freeonexit)
117:       putenv(strcat(strcpy(freeonexit, name), value));
118: }
119: 
120: static int run_script(char const *ext, char *argv[], char const *home)
121: {
122:    int rtc = 0;
123:    char *local = strdup(ext);
124:    if (local)
125:    {
126:       int err[2];
127:       char *host = strchr(local, '@');
128:       
129:       if (host)
130:          *host++ = 0;
131:       else
132:          host = "";
133:       addtoenv("HOST=", host);
134:       addtoenv("LOCAL=", local);
135:       free(local);
136:       
137:       if (pipe(err) < 0)
138:          syslog(LOG_CRIT, "Cannot open pipe: %s\n", strerror(errno));
139:       else
140:       {
141:          pid_t const pid = fork();
142:          if (pid < 0)
143:             syslog(LOG_CRIT, "Cannot fork: %s\n", strerror(errno));
144:          else if (pid)
145:          {
146:             char buf[2048], *next = &buf[0];
147:             char *const first = &buf[0], *const last = &buf[sizeof buf - 2];
148:             
149:             close(err[1]);
150:             alarm(30);
151:             last[1] = 0; // terminator on forced newline
152:             while (signal_timed_out == 0 &&
153:                signal_break == 0 &&
154:                signal_child == 0)
155:             {
156:                int rd = read(err[0], next, last - next);
157: #if !defined NDEBUG
158:    printf("rd=%2d, next=%2d\n", rd, next - first);
159: #endif
160:                if (rd > 0)
161:                {
162:                   char *p = first, *br;
163:                   next += rd;
164:                
165:                   *next = next == last? '\n': 0; // force newline if full
166:                
167:                   while ((br = strchr(p, '\n')) != NULL)
168:                   {
169:                      int level = LOG_CRIT;
170:                      *br = 0;
171:                
172:                      /*
173:                      * e.g., "##This is a warning\n"
174:                      */
175:                      while (*p == '#' && level < LOG_DEBUG)
176:                         ++p, ++level;
177:                   
178:                      if (*p) syslog(level, "script: %s\n", p);
179:                      p = br + (br < last);     // +1 if not forced newline
180:                      assert(first <= p && p <= next);
181:                   }
182:                
183:                   memmove(first, p, next - p);
184:                   next -= p - first;
185:                   assert(first <= next && next < last);
186:                }
187:                else if (rd == 0 || errno != EINTR && errno != EAGAIN)
188:                {
189:                   if (rd)
190:                      syslog(LOG_CRIT, "Pipe broken: %s\n", strerror(errno));
191:                   break;
192:                }
193:             }
194:             alarm(0);
195:             if (signal_timed_out || signal_break)
196:             {
197:                kill(pid, SIGTERM);
198:             }
199:             close(err[0]);
200:             
201:             for (;;)
202:             {
203:                int status;
204:                pid_t wpid = wait(&status);
205:                if (wpid < 0 && errno != EAGAIN && errno != EINTR)
206:                {
207:                   syslog(LOG_CRIT,
208:                      "Cannot wait %s/" SCRIPTFILE "[%u]: %s\n",
209:                      home, (unsigned)wpid, strerror(errno));
210:                   break;
211:                }
212:                else if (wpid == pid)
213:                {
214:                   if (WIFEXITED(status))
215:                   {
216:                      int level, s_rtc = WEXITSTATUS(status);
217:                      switch (s_rtc)
218:                      {
219:                         case 0: case 64: case 99: level = LOG_DEBUG; break;
220:                         default: level = LOG_CRIT; break;
221:                      }
222:                      rtc = s_rtc > 50;
223:                      syslog(level,
224:                         "%s/" SCRIPTFILE " with %s exited %d, rtc=%d\n",
225:                         home, ext, s_rtc, rtc);
226:                   }
227:                   else if (WIFSIGNALED(status))
228:                   {
229:                      syslog(LOG_CRIT,
230:                         "%s/" SCRIPTFILE " terminated with signal %d, rtc=%d\n",
231:                         home, WTERMSIG(status), rtc);
232:                   }
233:                   else continue; // stopped?
234:                   
235:                   break;
236:                }
237:             }
238:          }
239:          else // child process
240:          {
241:             close(0);
242:             open("/dev/null", O_RDONLY);
243:             close(1);
244:             open("/dev/null", O_WRONLY);
245:             close(2);
246:             dup(err[1]);
247:             close(err[0]);
248:             close(err[1]);
249:             closelog();
250:             execv(SCRIPTFILE, argv);
251:             syslog(LOG_MAIL|LOG_CRIT, "rcptfilter: cannot execv: %s\n",
252:                strerror(errno));
253:             exit(0);
254:          }
255:       }
256:       
257:    }
258:    return rtc;
259: }
260: 
261: int main(int argc, char *argv[])
262: {
263:    int rtc = 0;
264:    int i, uid = 0, gid = 0, err = 0;
265:    char *ext = NULL, *home = getenv("HOME");
266:    static char const argerror[] = "invoked with wrong argument: ";
267: 
268:    char *xargv[32];
269:    size_t xargc = 0;
270:    
271:    openlog("rcptfilter", LOG_PID, LOG_MAIL);
272:    xargv[xargc++] = SCRIPTFILE;
273:    xargv[xargc++] = "RCPT";
274:    
275:    for (i = 1; i < argc; ++i)
276:    {
277:       char *a = argv[i];
278:       int pass_it = 1;
279:       
280:       if (*a == '-')
281:       {
282:          pass_it = 0;
283:          switch (*++a)
284:          {
285:             case 'D':
286:                if (i + 1 >= argc)
287:                {
288:                   syslog(LOG_CRIT, "%s-D requires a value\n", argerror);
289:                   ++err;
290:                }
291:                else
292:                {
293:                   char *t = NULL;
294:                   a = argv[++i];
295:                   uid = strtoul(a, &t, 10);
296:                   if (t && *t == '/')
297:                   {
298:                      gid = strtoul(t + 1, &t, 10);
299:                      if (t && *t) t = NULL;
300:                   }
301:                   else t = NULL;
302:                   if (t == NULL)
303:                   {
304:                      syslog(LOG_CRIT, "%suidgid is %s\n", argerror, a);
305:                      ++err;
306:                   }
307:                }
308:                break;
309:             
310:             case 'M':
311:                if (i + 1 >= argc)
312:                {
313:                   syslog(LOG_CRIT, "%s-M requires a value\n", argerror);
314:                   ++err;
315:                }
316:                else if (strncmp(a = argv[++i],
317:                   "rcptfilter" , sizeof "rcptfilter" - 1) != 0)
318:                {
319:                   syslog(LOG_CRIT, "%s-M with value %s\n", argerror, a);
320:                   ++err;
321:                }
322:                else
323:                {
324:                   ext = strchr(a, '-');
325:                   if (ext)
326:                      ++ext;
327:                   else
328:                      ext = "";
329:                }
330:                break;
331:                
332:             case 'h':
333:                puts(usage);
334:                return 0;
335:             
336:             default:
337:                pass_it = 1;
338:                break;
339:          }
340:       }
341:       
342:       if (pass_it && xargc + 1 < sizeof xargv/ sizeof xargv[0])
343:       {
344:          xargv[xargc++] = a;
345:       }
346:    }
347:    
348:    xargv[xargc] = NULL;
349: 
350:    if (geteuid() == 0 && (uid || gid))
351:    {
352:       if (gid) setgid(gid);
353:       setuid(uid);
354:    }
355: 
356:    set_signal();
357:    if (home && ext)
358:    {
359:       struct stat buf;
360:       uid_t const me = geteuid();
361:       gid_t const myg = getegid();
362:       
363:       if (chdir(home))
364:       {
365:          syslog(LOG_CRIT, "Cannot chdir to %s: %s\n",
366:             home, strerror(errno));
367:       }
368:       else if (stat(SCRIPTFILE, &buf) != 0)
369:       {
370:          if (errno != ENOENT)
371:             syslog(LOG_CRIT, "Cannot stat %s/" SCRIPTFILE ": %s\n",
372:                home, strerror(errno));
373:       }
374:       else if (!S_ISREG(buf.st_mode) || !(
375:          ((S_IXUSR & buf.st_mode) && (me == 0 || me == buf.st_uid)) ||
376:          ((S_IXGRP & buf.st_mode) && (me == 0 || myg == buf.st_gid)) ||
377:          (S_IXOTH & buf.st_mode)))
378:       {
379:          syslog(LOG_INFO, "%s/" SCRIPTFILE " not executable by %d/%d\n",
380:             home, (int)me, (int)myg);
381:       }
382:       else
383:       {
384:          rtc = run_script(ext, xargv, home);
385:       }
386:    }
387:    else
388:    {
389:       syslog(LOG_CRIT, "%smissing %s%s%s\n", argerror,
390:          home ? "" : "HOME env variable",
391:          home || ext ? "" : " and ",
392:          ext ? "" : "-M argument");
393:    }
394:       
395:    return rtc;
396: }

License

This example code is in the public domain.