Mercurial > sm-archive
view src/sm-archive.cpp @ 13:75e1a9bcbc2e
gpl3, add removal option for original recipients
author | carl |
---|---|
date | Sat, 25 Aug 2007 11:14:49 -0700 |
parents | f9e8bbf33a2a |
children | 8ebecad6530f |
line wrap: on
line source
/* Copyright (c) 2007 Carl Byington - 510 Software Group, released under the GPL version 3 or any later version at your choice available at http://www.gnu.org/licenses/gpl-3.0.txt Based on a sample milter Copyright (c) 2000-2003 Sendmail, Inc. and its suppliers. -p port The port through which the MTA will connect to this milter. -t sec The timeout value. -c Check the config, and print a copy to stdout. Don't start the milter or do anything with the socket. -d increase debug level */ // from sendmail sample #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <sysexits.h> #include <unistd.h> // needed for thread #include <pthread.h> // needed for std c++ collections #include <set> #include <map> #include <list> // misc stuff needed here #include <ctype.h> #include <syslog.h> #include <pwd.h> #include <sys/wait.h> /* header for waitpid() and various macros */ #include <signal.h> /* header for signal functions */ #include "includes.h" #ifndef HAVE_DAEMON #include "daemon.h" #include "daemon.c" #endif static const char* smarchive_version="$Id$"; extern "C" { #include <libmilter/mfapi.h> sfsistat mlfi_connect(SMFICTX *ctx, char *hostname, _SOCK_ADDR *hostaddr); sfsistat mlfi_envfrom(SMFICTX *ctx, char **argv); sfsistat mlfi_envrcpt(SMFICTX *ctx, char **argv); sfsistat mlfi_eom(SMFICTX *ctx); sfsistat mlfi_abort(SMFICTX *ctx); sfsistat mlfi_close(SMFICTX *ctx); void sig_chld(int signo); } int debug_syslog = 0; bool syslog_opened = false; bool use_syslog = true; // false to printf bool loader_run = true; // used to stop the config loader thread CONFIG *config = NULL; // protected by the config_mutex int generation = 0; // protected by the config_mutex const int maxlen = 1000; // used for snprintf buffers pthread_mutex_t config_mutex; pthread_mutex_t syslog_mutex; mlfiPriv::mlfiPriv() { pthread_mutex_lock(&config_mutex); pc = config; pc->reference_count++; pthread_mutex_unlock(&config_mutex); mailaddr = NULL; queueid = NULL; } mlfiPriv::~mlfiPriv() { pthread_mutex_lock(&config_mutex); pc->reference_count--; pthread_mutex_unlock(&config_mutex); reset(true); } void mlfiPriv::reset(bool final) { targets.clear(); for (string_set::iterator i=removal.begin(); i!=removal.end(); i++) { char *remove = (*i); free(remove); } removal.clear(); if (mailaddr) free(mailaddr); if (queueid) free(queueid); if (!final) { mailaddr = NULL; queueid = NULL; } } #define MLFIPRIV ((struct mlfiPriv *) smfi_getpriv(ctx)) //////////////////////////////////////////////// // syslog a message // void my_syslog(mlfiPriv *priv, char *text) { char buf[maxlen]; if (priv) { snprintf(buf, sizeof(buf), "%s: %s", priv->queueid, text); text = buf; } if (use_syslog) { pthread_mutex_lock(&syslog_mutex); if (!syslog_opened) { openlog("sm-archive", LOG_PID, LOG_MAIL); syslog_opened = true; } syslog(LOG_NOTICE, "%s", text); pthread_mutex_unlock(&syslog_mutex); } else { printf("%s \n", text); } } void my_syslog(char *text) { my_syslog(NULL, text); } //////////////////////////////////////////////// // this email address is passed in from sendmail, and will // always be enclosed in <>. It may have mixed case, just // as the mail client sent it. We dup the string and convert // the duplicate to lower case. // char *to_lower_string(char *email); char *to_lower_string(char *email) { int n = strlen(email)-2; if (n < 1) return strdup(email); char *key = strdup(email+1); key[n] = '\0'; for (int i=0; i<n; i++) key[i] = tolower(key[i]); return key; } //////////////////////////////////////////////// // start of sendmail milter interfaces // sfsistat mlfi_connect(SMFICTX *ctx, char *hostname, _SOCK_ADDR *hostaddr) { // allocate some private memory mlfiPriv *priv = new mlfiPriv; // save the private data smfi_setpriv(ctx, (void*)priv); // continue processing return SMFIS_CONTINUE; } sfsistat mlfi_envfrom(SMFICTX *ctx, char **from) { mlfiPriv &priv = *MLFIPRIV; priv.mailaddr = to_lower_string(from[0]); return SMFIS_CONTINUE; } void add_target(mlfiPriv &priv, char *target); void add_target(mlfiPriv &priv, char *target) { if (target) { string_set::iterator i = priv.targets.find(target); if (i == priv.targets.end()) priv.targets.insert(target); } } void add_remove(mlfiPriv &priv, char *remove); void add_remove(mlfiPriv &priv, char *remove) { if (remove) { string_set::iterator i = priv.removal.find(remove); if (i == priv.removal.end()) priv.removal.insert(remove); } } sfsistat mlfi_envrcpt(SMFICTX *ctx, char **rcpt) { mlfiPriv &priv = *MLFIPRIV; CONFIG &dc = *priv.pc; if (!priv.queueid) priv.queueid = strdup(smfi_getsymval(ctx, "i")); char *rcptaddr = to_lower_string(rcpt[0]); if (debug_syslog > 1) { char msg[maxlen]; snprintf(msg, sizeof(msg), "from <%s> to <%s>", priv.mailaddr, rcptaddr); my_syslog(&priv, msg); } char *target = dc.find_to(rcptaddr); add_target(priv, target); bool remove = dc.find_remove(rcptaddr); if (remove) add_remove(priv, strdup(rcptaddr)); free(rcptaddr); return SMFIS_CONTINUE; } sfsistat mlfi_eom(SMFICTX *ctx) { mlfiPriv &priv = *MLFIPRIV; CONFIG &dc = *priv.pc; char *target = dc.find_from(priv.mailaddr); add_target(priv, target); for (string_set::iterator i=priv.targets.begin(); i!=priv.targets.end(); i++) { target = (*i); smfi_addrcpt(ctx, target); if (debug_syslog > 1) { char msg[maxlen]; snprintf(msg, sizeof(msg), "adding recipient <%s>", target); my_syslog(&priv, msg); } } for (string_set::iterator i=priv.removal.begin(); i!=priv.removal.end(); i++) { char *remove = (*i); smfi_delrcpt(ctx, remove); if (debug_syslog > 1) { char msg[maxlen]; snprintf(msg, sizeof(msg), "removing recipient <%s>", remove); my_syslog(&priv, msg); } } // reset for a new message on the same connection mlfi_abort(ctx); return SMFIS_CONTINUE; } sfsistat mlfi_abort(SMFICTX *ctx) { mlfiPriv &priv = *MLFIPRIV; priv.reset(); return SMFIS_CONTINUE; } sfsistat mlfi_close(SMFICTX *ctx) { mlfiPriv *priv = MLFIPRIV; if (!priv) return SMFIS_CONTINUE; delete priv; smfi_setpriv(ctx, NULL); return SMFIS_CONTINUE; } struct smfiDesc smfilter = { "SM-ARCHIVE", // filter name SMFI_VERSION, // version code -- do not change SMFIF_ADDRCPT | \ SMFIF_DELRCPT, // flags mlfi_connect, // connection info filter NULL, // SMTP HELO command filter mlfi_envfrom, // envelope sender filter mlfi_envrcpt, // envelope recipient filter NULL, // header filter NULL, // end of header NULL, // body block filter mlfi_eom, // end of message mlfi_abort, // message aborted mlfi_close, // connection cleanup }; //////////////////////////////////////////////// // reload the config // CONFIG* new_conf(); CONFIG* new_conf() { CONFIG *newc = new CONFIG; pthread_mutex_lock(&config_mutex); newc->generation = generation++; pthread_mutex_unlock(&config_mutex); if (debug_syslog) { char buf[maxlen]; snprintf(buf, sizeof(buf), "loading configuration generation %d", newc->generation); my_syslog(buf); } if (load_conf(*newc, "sm-archive.conf")) { newc->load_time = time(NULL); return newc; } delete newc; return NULL; } //////////////////////////////////////////////// // thread to watch the old config files for changes // and reload when needed. we also cleanup old // configs whose reference count has gone to zero. // extern "C" {void* config_loader(void *arg);} void* config_loader(void *arg) { while (loader_run) { sleep(180); // look for modifications every 3 minutes if (!loader_run) break; CONFIG &dc = *config; time_t then = dc.load_time; struct stat st; bool reload = false; for (string_set::iterator i=dc.config_files.begin(); i!=dc.config_files.end(); i++) { char *fn = *i; if (stat(fn, &st)) reload = true; // file disappeared else if (st.st_mtime > then) reload = true; // file modified if (reload) break; } if (reload) { CONFIG *newc = new_conf(); if (newc) { // replace the global config pointer pthread_mutex_lock(&config_mutex); CONFIG *old = config; bool last = old && (!old->reference_count); config = newc; pthread_mutex_unlock(&config_mutex); if (last) delete old; // there were no references to this config } else { // failed to load new config my_syslog("failed to load new configuration"); system("echo 'failed to load new sm-archive configuration from /etc/sm-archive' | mail -s 'error in /etc/sm-archive configuration' root"); // update the load time on the current config to prevent complaining every 3 minutes dc.load_time = time(NULL); } } } return NULL; } void usage(char *prog); void usage(char *prog) { fprintf(stderr, "Usage: %s [-d [level]] [-c] -p sm-sock-addr [-t timeout]\n", prog); fprintf(stderr, "where sm-sock-addr is for the connection to sendmail\n"); fprintf(stderr, " and should be one of\n"); fprintf(stderr, " inet:port@ip-address\n"); fprintf(stderr, " local:local-domain-socket-file-name\n"); fprintf(stderr, "-c will load and dump the config to stdout\n"); fprintf(stderr, "-d will set the syslog message level, currently 0 to 3\n"); } void setup_socket(char *sock); void setup_socket(char *sock) { unlink(sock); // sockaddr_un addr; // memset(&addr, '\0', sizeof addr); // addr.sun_family = AF_UNIX; // strncpy(addr.sun_path, sock, sizeof(addr.sun_path)-1); // int s = socket(AF_UNIX, SOCK_STREAM, 0); // bind(s, (sockaddr*)&addr, sizeof(addr)); // close(s); } /* * The signal handler function -- only gets called when a SIGCHLD * is received, ie when a child terminates */ void sig_chld(int signo) { int status; /* Wait for any child without blocking */ while (waitpid(-1, &status, WNOHANG) > 0) { // ignore child exit status, we only do this to cleanup zombies } } int main(int argc, char**argv) { token_init(); bool check = false; bool setconn = false; int c; const char *args = "p:t:d:ch"; extern char *optarg; // Process command line options while ((c = getopt(argc, argv, args)) != -1) { switch (c) { case 'p': if (optarg == NULL || *optarg == '\0') { fprintf(stderr, "Illegal sendmail socket: %s\n", optarg); exit(EX_USAGE); } if (smfi_setconn(optarg) == MI_FAILURE) { fprintf(stderr, "smfi_setconn failed\n"); exit(EX_SOFTWARE); } if (strncasecmp(optarg, "unix:", 5) == 0) setup_socket(optarg + 5); else if (strncasecmp(optarg, "local:", 6) == 0) setup_socket(optarg + 6); setconn = true; break; case 't': if (optarg == NULL || *optarg == '\0') { fprintf(stderr, "Illegal timeout: %s\n", optarg); exit(EX_USAGE); } if (smfi_settimeout(atoi(optarg)) == MI_FAILURE) { fprintf(stderr, "smfi_settimeout failed\n"); exit(EX_SOFTWARE); } break; case 'c': check = true; break; case 'd': if (optarg == NULL || *optarg == '\0') debug_syslog = 1; else debug_syslog = atoi(optarg); break; case 'h': default: usage(argv[0]); exit(EX_USAGE); } } if (check) { use_syslog = false; debug_syslog = 10; CONFIG *conf = new_conf(); if (conf) { conf->dump(); delete conf; clear_strings(); // for valgrind checking return 0; } else { return 1; // config failed to load } } if (!setconn) { fprintf(stderr, "%s: Missing required -p argument\n", argv[0]); usage(argv[0]); exit(EX_USAGE); } if (smfi_register(smfilter) == MI_FAILURE) { fprintf(stderr, "smfi_register failed\n"); exit(EX_UNAVAILABLE); } // switch to background mode if (daemon(1,0) < 0) { fprintf(stderr, "daemon() call failed\n"); exit(EX_UNAVAILABLE); } // write the pid const char *pidpath = "/var/run/sm-archive.pid"; unlink(pidpath); FILE *f = fopen(pidpath, "w"); if (f) { #ifdef linux // from a comment in the DCC source code: // Linux threads are broken. Signals given the // original process are delivered to only the // thread that happens to have that PID. The // sendmail libmilter thread that needs to hear // SIGINT and other signals does not, and that breaks // scripts that need to stop milters. // However, signaling the process group works. fprintf(f, "-%d\n", (u_int)getpgrp()); #else fprintf(f, "%d\n", (u_int)getpid()); #endif fclose(f); } // initialize the thread sync objects pthread_mutex_init(&config_mutex, 0); pthread_mutex_init(&syslog_mutex, 0); // drop root privs struct passwd *pw = getpwnam("sm-archive"); if (pw) { if (setgid(pw->pw_gid) == -1) { my_syslog("failed to switch to group sm-archive"); } if (setuid(pw->pw_uid) == -1) { my_syslog("failed to switch to user sm-archive"); } } // load the initial config config = new_conf(); if (!config) { my_syslog("failed to load initial configuration, quitting"); exit(1); } // only create threads after the fork() in daemon pthread_t tid; if (pthread_create(&tid, 0, config_loader, 0)) my_syslog("failed to create config loader thread"); if (pthread_detach(tid)) my_syslog("failed to detach config loader thread"); time_t starting = time(NULL); int rc = smfi_main(); if ((rc != MI_SUCCESS) && (time(NULL) > starting+5*60)) { my_syslog("trying to restart after smfi_main()"); loader_run = false; // eventually the config loader thread will terminate execvp(argv[0], argv); } exit((rc == MI_SUCCESS) ? 0 : EX_UNAVAILABLE); }