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.
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
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:
This example code is in the public domain.