#! /bin/sh /usr/share/dpatch/dpatch-run ## 95-automurder-frontend.dpatch by Duncan Gibb ## ## All lines beginning with `## DP:' are a description of the patch. ## DP: Run a helper application on frontends which can decide whether to ## DP: pretend mailboxes exist on a backend - and if so on which - in order to ## DP: trigger autocreate @DPATCH@ diff -Nrub --exclude '*svn*' --exclude '*~' --exclude debian reference/imap/automurder.c editing/imap/automurder.c --- reference/imap/automurder.c 1970-01-01 01:00:00.000000000 +0100 +++ editing/imap/automurder.c 2009-05-10 10:01:09.000000000 +0100 @@ -0,0 +1,259 @@ +/* automurder.c + * + * Use an external helper to decide which backend should + * to assume a non-existant mailbox onto. + * + * Copyright 2008 Duncan Gibb, Sirius Corporation plc + */ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "automurder.h" +#include "imap_err.h" +#include "libconfig.h" +#include "mboxlist.h" +#include "xmalloc.h" + +#define BUFSIZE MAX_MAILBOX_NAME+2 + +void spawn_helper(void); +void shutdown_helper(void); +void fault(void); + +static int helper_pid = 0; +static int qfd, afd; +static int respawn_count; +static int fault_count; + +static char automurder_last_refused_mbox[BUFSIZE]; + +void automurder_init(void) +{ + respawn_count = 0; + automurder_last_refused_mbox[0] = '\0'; + + if(config_getswitch(IMAPOPT_AUTOMURDER_HELPER_PREFORK)) { + spawn_helper(); + } +} + +void automurder_shutdown(void) +{ + shutdown_helper(); +} + +void fault(void) +{ + if( ++fault_count > config_getint(IMAPOPT_AUTOMURDER_HELPER_FAULT_TOLLERANCE) ) { + syslog(LOG_ERR, "automurder helper fault tollerance reached. Respawning helper."); + shutdown_helper(); + spawn_helper(); + } +} + +void spawn_helper(void) +{ + int helper_stdin; + int helper_stdout; + int mypipe[2]; + + if(!config_getswitch(IMAPOPT_AUTOMURDER_FRONTEND)) + return; + + if(!config_getstring(IMAPOPT_AUTOMURDER_HELPER)) { + syslog(LOG_ERR, "automurder_frontend enabled without configuring helper application"); + return; + } + + if(respawn_count++ > config_getint(IMAPOPT_AUTOMURDER_HELPER_MAXRESPAWN)) { + return; + } else if(respawn_count == config_getint(IMAPOPT_AUTOMURDER_HELPER_MAXRESPAWN)) { + syslog(LOG_ERR, "automurder_helper_maxrespawn reached."); + return; + } + + if(pipe (mypipe)) { + syslog(LOG_ERR, "Could not create question pipe for automurder helper."); + return; + } + + helper_stdin = mypipe[0]; + qfd = mypipe[1]; + + if(pipe (mypipe)) { + syslog(LOG_ERR, "Could not create answer pipe for automurder helper."); + return; + } + + afd = mypipe[0]; + helper_stdout = mypipe[1]; + + fcntl( afd, F_SETFL, O_NONBLOCK ); + + fault_count = 0; + + helper_pid=fork(); + + if(helper_pid == -1) { + syslog(LOG_ERR, "Could not fork to create automurder helper."); + return; + } + + if(helper_pid == 0) { + const char *av = NULL; + const char *env = NULL; + + dup2( helper_stdin, 0 ); + dup2( helper_stdout, 1 ); + + execve( config_getstring(IMAPOPT_AUTOMURDER_HELPER), &av, &env ); + + /* Should not return */ + + syslog(LOG_ERR, "Failed to exec() automurder helper '%s'.", + config_getstring(IMAPOPT_AUTOMURDER_HELPER)); + + _exit(errno); + } +} + +void shutdown_helper(void) +{ + if(helper_pid) + kill(helper_pid, SIGKILL); + + if(afd) + close(afd); + + if(qfd) + close(qfd); + + helper_pid = 0; +} + +/* Callout to an administrator-defined helper program which establishes + * which backend a given mailbox ought to be created on. Only useful + * if the backend has autocreateonpost enabled. + * The command defined by automurder_helper should return a + * string of the form "OK servername" if the mailbox should be created + * or "BAD human-understandable reason" + * A non-OK response or silence for automurder_helper_timeout seconds + * will return IMAP_MAILBOX_NONEXISTENT. Otherwise 'server' will be + * populated. + */ + +int automurder_lookup(const char *name, char **server, char **aclp) +{ + static char servername[HOSTNAME_SIZE+1]; + char buf[BUFSIZE]; + fd_set fds; + struct timeval tv; + int r; + + if(aclp) *aclp = ""; + + if(!server) { + syslog(LOG_DEBUG, "automurder_lookup called for mailbox %s without a server", name); + return IMAP_INTERNAL; + } + + /* Some clients won't take "no" for an answer */ + if(!strcmp(name, automurder_last_refused_mbox)) { + return IMAP_MAILBOX_NONEXISTENT; + } + + if(!helper_pid) { + spawn_helper(); + } + + /* Throw away any pending input */ + errno = 0; + while( read( afd, buf, BUFSIZE ) && (!errno) ); + + if( errno != EAGAIN ) { + syslog(LOG_ERR, "Error emptying the automurder helper answer pipe." ); + fault(); + return IMAP_SERVER_UNAVAILABLE; + } + + if( snprintf( buf, BUFSIZE , "%s\n", name ) > BUFSIZE ) { + syslog(LOG_ERR, "Error composing automurder question - mailbox name '%s' too long.", name); + return IMAP_MAILBOX_BADNAME; + } + + if( write( qfd, buf, strlen(buf) ) == -1) { + syslog(LOG_ERR, "Error writing to automurder helper for mailbox '%s'.", name); + fault(); + return IMAP_SERVER_UNAVAILABLE; + } + + FD_ZERO( &fds ); + FD_SET( afd, &fds ); + tv.tv_sec = config_getint(IMAPOPT_AUTOMURDER_HELPER_TIMEOUT); + tv.tv_usec = 0; + + select( afd+1, &fds, NULL, NULL, &tv ); + if( !FD_ISSET( afd, &fds )) { + syslog(LOG_ERR, "Automurder lookup timed out for mailbox '%s'.", name); + fault(); + return IMAP_SERVER_UNAVAILABLE; + } + + buf[0] = '\0'; + if( (r = read( afd, buf, BUFSIZE )) == -1 ) { + syslog(LOG_ERR, "Error reading from automurder helper for mailbox '%s'.", name); + fault(); + return IMAP_SERVER_UNAVAILABLE; + } + buf[r] = '\0'; /* We're going to do string operations on a partial buffer */ + + if(!(strncmp( buf, "OK ", 3 ))) { + int i; + char c; + for( i = 0; i < HOSTNAME_SIZE && i < (r-3); i++ ) { + c = servername[i] = buf[i+3]; + if( c == ' ' || c == '\n' || c == '\r' || c == '\t' ) { + servername[i] = '\0'; + break; + } + } + + syslog(LOG_INFO, "Automurder helper approved creation of '%s' on backend '%s'.", + name, servername); + + *server = servername; + return 0; + } + + if(!(strncmp( buf, "BAD " , 4 ))) { + int i; + for( i = 4; i < BUFSIZE; i++ ) { + if( buf[i] == '\n' || buf[i] == '\r' || buf[i] == '\0' ) { + buf[i] = '\0'; + break; + } + } + syslog(LOG_INFO, "Automurder helper denied creation of '%s' because '%s'.", + name, buf+4); + strcpy(automurder_last_refused_mbox, name); + return IMAP_MAILBOX_NONEXISTENT; + } + + syslog(LOG_ERR, "Automurder helper protocol violation: '%s' returned '%s'.", + name, buf); + + /* We don't tollerate protocol violations. Just kill the helper. */ + shutdown_helper(); + spawn_helper(); + + return IMAP_SERVER_UNAVAILABLE; +} + diff -Nrub --exclude '*svn*' --exclude '*~' --exclude debian reference/imap/automurder.h editing/imap/automurder.h --- reference/imap/automurder.h 1970-01-01 01:00:00.000000000 +0100 +++ editing/imap/automurder.h 2009-05-09 13:04:58.000000000 +0100 @@ -0,0 +1,16 @@ +/* automurder.h + * + * Use an external helper to decide which backend should + * to assume a non-existant mailbox onto. + * + * Copyright 2008 Duncan Gibb, Sirius Corporation plc + */ + +#ifndef INCLUDED_AUTOMURDER_H +#define INCLUDED_AUTOMURDER_H + +void automurder_init(void); +void automurder_shutdown(void); +int automurder_lookup(const char *name, char **server, char **aclp); + +#endif /* INCLUDED_AUTOMURDER_H */ diff -Nrub --exclude '*svn*' --exclude '*~' --exclude debian reference/imap/imapd.c editing/imap/imapd.c --- reference/imap/imapd.c 2009-05-09 13:04:40.000000000 +0100 +++ editing/imap/imapd.c 2009-05-09 13:04:58.000000000 +0100 @@ -73,6 +73,7 @@ #include "annotate.h" #include "append.h" #include "auth.h" +#include "automurder.h" #include "backend.h" #include "charset.h" #include "exitcodes.h" @@ -476,6 +477,11 @@ r = mboxlist_detail(name, &mbtype, pathp, mpathp, &remote, &acl, tid); } + if (r == IMAP_MAILBOX_NONEXISTENT && config_getswitch(IMAPOPT_AUTOMURDER_FRONTEND)) { + r = automurder_lookup( name, &remote, &acl ); + mbtype = MBTYPE_REMOTE; + } + if(partp) *partp = remote; if(aclp) *aclp = acl; if(flags) *flags = mbtype; @@ -697,6 +703,8 @@ statuscache_open(NULL); } + automurder_init(); + /* Create a protgroup for input from the client and selected backend */ protin = protgroup_new(2); @@ -819,6 +827,7 @@ /* Called by service API to shut down the service */ void service_abort(int error) { + automurder_shutdown(); shut_down(error); } diff -Nrub --exclude '*svn*' --exclude '*~' --exclude debian reference/imap/lmtpd.c editing/imap/lmtpd.c --- reference/imap/lmtpd.c 2009-05-09 13:04:40.000000000 +0100 +++ editing/imap/lmtpd.c 2009-05-09 13:04:58.000000000 +0100 @@ -70,6 +70,7 @@ #include "append.h" #include "assert.h" #include "auth.h" +#include "automurder.h" #include "backend.h" #include "duplicate.h" #include "exitcodes.h" @@ -195,6 +196,7 @@ config_mupdate_server, error_message(r)); fatal("error connecting with MUPDATE server", EC_TEMPFAIL); } + automurder_init(); } else { dupelim = config_getswitch(IMAPOPT_DUPLICATESUPPRESSION); @@ -329,6 +331,7 @@ /* Called by service API to shut down the service */ void service_abort(int error) { + automurder_shutdown(); shut_down(error); } @@ -433,6 +436,9 @@ r = mupdate_find(mhandle, name, &mailboxdata); if (r == MUPDATE_MAILBOX_UNKNOWN) { + if(config_getswitch(IMAPOPT_AUTOMURDER_FRONTEND)) { + return automurder_lookup( name, server, aclp ); + } return IMAP_MAILBOX_NONEXISTENT; } else if (r) { /* xxx -- yuck: our error handling for now will be to exit; diff -Nrub --exclude '*svn*' --exclude '*~' --exclude debian reference/imap/Makefile.in editing/imap/Makefile.in --- reference/imap/Makefile.in 2009-05-09 13:04:40.000000000 +0100 +++ editing/imap/Makefile.in 2009-05-09 13:04:58.000000000 +0100 @@ -102,7 +102,7 @@ annotate.o search_engines.o squat.o squat_internal.o mbdump.o \ imapparse.o telemetry.o user.o notify.o idle.o quota_db.o \ sync_log.o autosieve.o $(SEEN) mboxkey.o backend.o tls.o tls_th-lock.o \ - message_guid.o statuscache_db.o + message_guid.o statuscache_db.o automurder.o IMAPDOBJS=pushstats.o imapd.o proxy.o imap_proxy.o index.o version.o @@ -187,21 +187,21 @@ $(CC) $(LDFLAGS) -o idled \ idled.o mutex_fake.o libimap.a $(SIEVE_LIBS) $(DEPLIBS) $(LIBS) -lmtpd: lmtpd.o proxy.o $(LMTPOBJS) $(SIEVE_OBJS) mutex_fake.o \ +lmtpd: lmtpd.o proxy.o automurder.o $(LMTPOBJS) $(SIEVE_OBJS) mutex_fake.o \ libimap.a $(SIEVE_LIBS) $(DEPLIBS) $(SERVICE) $(CC) $(LDFLAGS) -o lmtpd \ - $(SERVICE) lmtpd.o proxy.o $(LMTPOBJS) $(SIEVE_OBJS) \ + $(SERVICE) lmtpd.o proxy.o automurder.o $(LMTPOBJS) $(SIEVE_OBJS) \ mutex_fake.o libimap.a $(SIEVE_LIBS) $(DEPLIBS) $(LIBS) $(LIB_WRAP) -lmtpd.pure: lmtpd.o proxy.o $(LMTPOBJS) $(SIEVE_OBJS) \ +lmtpd.pure: lmtpd.o proxy.o automurder.o $(LMTPOBJS) $(SIEVE_OBJS) \ mutex_fake.o libimap.a $(SIEVE_LIBS) $(DEPLIBS) $(SERVICE) $(PURIFY) $(PUREOPT) $(CC) $(LDFLAGS) -o lmtpd.pure \ - $(SERVICE) lmtpd.o proxy.o $(LMTPOBJS) $(SIEVE_OBJS) \ + $(SERVICE) lmtpd.o proxy.o automurder.o $(LMTPOBJS) $(SIEVE_OBJS) \ mutex_fake.o libimap.a $(SIEVE_LIBS) $(DEPLIBS) $(LIBS) $(LIB_WRAP) -imapd: $(IMAPDOBJS) mutex_fake.o libimap.a $(SIEVE_LIBS) $(DEPLIBS) $(SERVICE) +imapd: $(IMAPDOBJS) mutex_fake.o libimap.a automurder.o $(SIEVE_LIBS) $(DEPLIBS) $(SERVICE) $(CC) $(LDFLAGS) -o imapd \ - $(SERVICE) $(IMAPDOBJS) mutex_fake.o \ + $(SERVICE) $(IMAPDOBJS) mutex_fake.o automurder.o \ libimap.a $(SIEVE_LIBS) $(DEPLIBS) $(LIBS) $(LIB_WRAP) imapd.pure: $(IMAPDOBJS) mutex_fake.o libimap.a $(SIEVE_LIBS) $(DEPLIBS) $(SERVICE) diff -Nrub --exclude '*svn*' --exclude '*~' --exclude debian reference/lib/imapoptions editing/lib/imapoptions --- reference/lib/imapoptions 2009-05-09 13:04:40.000000000 +0100 +++ editing/lib/imapoptions 2009-05-09 13:04:58.000000000 +0100 @@ -203,6 +203,39 @@ creating the mailbox INBOX. The user's quota is set to the value if it is positive, otherwise the user has unlimited quota. */ +{ "automurder_frontend", 0, SWITCH } +/* If non-zero, ask automurder_helper which backend to assume + non-existant mailboxes are located on. Only useful where backends + have autocreate enabled. */ + +{ "automurder_helper", NULL, STRING } +/* Helper application to use if automurder_frontend is enabled. + The command will receive newline-separated mailbox names on + stdin and should print "OK \n" to stdout if the + mailbox should be assumed to exist, where is the + name of the appropriate backend server. Otherwise it should + print "BAD human-understandable reason". + The helper will be killed and restarted if it prints anything else. + Only useful if the backend has autocreate enabled. + Note that this command is executed with the priviledges of the cyrus + user. */ + +{ "automurder_helper_fault_tollerance", 5, INT } +/* Number of lookup helper faults to tollerate before respawning it. */ + +{ "automurder_helper_maxrespawn", 5, INT } +/* Number of times to tollerate restarting a lookup helper before + abandoning it. */ + +{ "automurder_helper_prefork", 0, SWITCH } +/* If true, imapd and lmtpd will prefork automurder_helper when + they start. This causes a lot of process table clutter, so + only use it if the helper needs to do some complex setup. */ + +{ "automurder_helper_timeout", 5, INT } +/* Number of seconds to allow for automurder_lookup_command to respond + before assuming the possible mailbox should not exist. */ + { "berkeley_cachesize", 512, INT } /* Size (in kilobytes) of the shared memory buffer pool (cache) used by the berkeley environment. The minimum allowed value is 20. The