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.
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.
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
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:
Alias dynamic delivery is documented by Courier. While the rcptfilter compiled executable runs for any delivery, the file:
|| /etc/courier/aliasdir/rcptfilter.shmight 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.
This example code is in the public domain.