]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
Added initial implementation of a director process (for NFS users).
authorTimo Sirainen <tss@iki.fi>
Wed, 19 May 2010 07:56:49 +0000 (09:56 +0200)
committerTimo Sirainen <tss@iki.fi>
Wed, 19 May 2010 07:56:49 +0000 (09:56 +0200)
There are still some unimplemented features and bugs. Also changing mail
server list doesn't yet make sure that other directors won't assign the
same user to a different server at the same time.

--HG--
branch : HEAD

30 files changed:
.hgignore
configure.in
doc/example-config/conf.d/10-director.conf [new file with mode: 0644]
src/Makefile.am
src/director/Makefile.am [new file with mode: 0644]
src/director/auth-connection.c [new file with mode: 0644]
src/director/auth-connection.h [new file with mode: 0644]
src/director/director-connection.c [new file with mode: 0644]
src/director/director-connection.h [new file with mode: 0644]
src/director/director-host.c [new file with mode: 0644]
src/director/director-host.h [new file with mode: 0644]
src/director/director-request.c [new file with mode: 0644]
src/director/director-request.h [new file with mode: 0644]
src/director/director-settings.c [new file with mode: 0644]
src/director/director-settings.h [new file with mode: 0644]
src/director/director.c [new file with mode: 0644]
src/director/director.h [new file with mode: 0644]
src/director/doveadm-connection.c [new file with mode: 0644]
src/director/doveadm-connection.h [new file with mode: 0644]
src/director/login-connection.c [new file with mode: 0644]
src/director/login-connection.h [new file with mode: 0644]
src/director/mail-host.c [new file with mode: 0644]
src/director/mail-host.h [new file with mode: 0644]
src/director/main.c [new file with mode: 0644]
src/director/user-directory.c [new file with mode: 0644]
src/director/user-directory.h [new file with mode: 0644]
src/doveadm/Makefile.am
src/doveadm/doveadm-director.c [new file with mode: 0644]
src/doveadm/doveadm.c
src/doveadm/doveadm.h

index 151c0bd4f25b3d22183f76b117b5f6530c43df78..f9ae67d6f2a83e28fa323ccddc184bc565b60bd7 100644 (file)
--- a/.hgignore
+++ b/.hgignore
@@ -61,6 +61,7 @@ src/config/config
 src/config/doveconf
 src/lda/dovecot-lda
 src/dict/dict
+src/director/director
 src/dns/dns-client
 src/doveadm/doveadm
 src/dsync/dsync
index b197924848fa1ab3762668e26f661ade9ac351fa..9e211740e0c85186a07cc3a262bc0f0bf9a208bc 100644 (file)
@@ -2659,6 +2659,7 @@ src/lda/Makefile
 src/log/Makefile
 src/lmtp/Makefile
 src/dict/Makefile
+src/director/Makefile
 src/dns/Makefile
 src/imap/Makefile
 src/imap-login/Makefile
diff --git a/doc/example-config/conf.d/10-director.conf b/doc/example-config/conf.d/10-director.conf
new file mode 100644 (file)
index 0000000..0361c85
--- /dev/null
@@ -0,0 +1,41 @@
+##
+## Director-specific settings.
+##
+
+# Director can be used by Dovecot proxy to keep a temporary user -> mail server
+# mapping. As long as user has simultaneous connections, the user is always
+# redirected to the same server. Each proxy server is running its own director
+# process, and the directors are communicating the state to each others.
+# Directors are mainly useful with NFS-like setups.
+
+# List of IPs or hostnames to all director servers, including ourself.
+# Ports can be specified as ip:port. The default port is the same as
+# what director service's inet_listener is using.
+director_servers = 
+
+# List of IPs or hostnames to all backend mail servers. Ranges are allowed
+# too, like 10.0.0.10-10.0.0.30.
+director_mail_servers = 
+
+# How long to redirect users to a specific server after it no longer has
+# any connections.
+director_user_expire = 15 min
+
+# To enable director service, uncomment the mode and assign a port.
+service director {
+  unix_listener login/director {
+    #mode = 0666
+  }
+  inet_listener {
+    #port = 
+  }
+}
+
+# Enable director for the wanted login services by telling them to
+# connect to director socket instead of the default login socket:
+service imap-login {
+  #executable = imap-login director
+}
+service pop3-login {
+  #executable = pop3-login director
+}
index 17eec0fd03034048c7aa1a2a24e073b2e298b219..43c2312ec93d7bf2f88aa13bc470321d47818ee7 100644 (file)
@@ -33,6 +33,7 @@ SUBDIRS = \
        lmtp \
        log \
        config \
+       director \
        util \
        doveadm \
        dsync \
diff --git a/src/director/Makefile.am b/src/director/Makefile.am
new file mode 100644 (file)
index 0000000..eee9f97
--- /dev/null
@@ -0,0 +1,38 @@
+pkglibexecdir = $(libexecdir)/dovecot
+
+pkglibexec_PROGRAMS = director
+
+AM_CPPFLAGS = \
+       -I$(top_srcdir)/src/lib \
+       -I$(top_srcdir)/src/lib-auth \
+       -I$(top_srcdir)/src/lib-settings \
+       -I$(top_srcdir)/src/lib-master \
+       -DPKG_RUNDIR=\""$(rundir)"\"
+
+director_LDADD = $(LIBDOVECOT)
+director_DEPENDENCIES = $(LIBDOVECOT_DEPS)
+
+director_SOURCES = \
+       main.c \
+       auth-connection.c \
+       director.c \
+       director-connection.c \
+       director-host.c \
+       director-request.c \
+       director-settings.c \
+       doveadm-connection.c \
+       login-connection.c \
+       mail-host.c \
+       user-directory.c
+
+noinst_HEADERS = \
+       auth-connection.h \
+       director.h \
+       director-connection.h \
+       director-host.h \
+       director-request.h \
+       director-settings.h \
+       doveadm-connection.h \
+       login-connection.h \
+       mail-host.h \
+       user-directory.h
diff --git a/src/director/auth-connection.c b/src/director/auth-connection.c
new file mode 100644 (file)
index 0000000..76349ba
--- /dev/null
@@ -0,0 +1,127 @@
+/* Copyright (c) 2010 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "istream.h"
+#include "ostream.h"
+#include "network.h"
+#include "llist.h"
+#include "safe-memset.h"
+#include "auth-client-interface.h"
+#include "auth-connection.h"
+
+#include <unistd.h>
+
+struct auth_connection {
+       struct auth_connection *prev, *next;
+
+       char *path;
+       int fd;
+       struct io *io;
+       struct istream *input;
+       struct ostream *output;
+
+       auth_input_callback *callback;
+       void *context;
+};
+
+static struct auth_connection *auth_connections;
+
+static void auth_connection_input(struct auth_connection *conn)
+{
+       char *line;
+
+       switch (i_stream_read(conn->input)) {
+       case 0:
+               return;
+       case -1:
+               /* disconnected */
+               auth_connection_deinit(&conn);
+               return;
+       case -2:
+               /* buffer full */
+               i_error("BUG: Auth server sent us more than %d bytes",
+                       (int)AUTH_CLIENT_MAX_LINE_LENGTH);
+               auth_connection_deinit(&conn);
+               return;
+       }
+
+       while ((line = i_stream_next_line(conn->input)) != NULL) {
+               T_BEGIN {
+                       conn->callback(line, conn->context);
+                       safe_memset(line, 0, strlen(line));
+               } T_END;
+       }
+}
+
+struct auth_connection *auth_connection_init(const char *path)
+{
+       struct auth_connection *conn;
+       conn = i_new(struct auth_connection, 1);
+       conn->fd = -1;
+       conn->path = i_strdup(path);
+       DLLIST_PREPEND(&auth_connections, conn);
+       return conn;
+}
+
+void auth_connection_set_callback(struct auth_connection *conn,
+                                 auth_input_callback *callback, void *context)
+{
+       conn->callback = callback;
+       conn->context = context;
+}
+
+int auth_connection_connect(struct auth_connection *conn)
+{
+       i_assert(conn->fd == -1);
+
+       conn->fd = net_connect_unix_with_retries(conn->path, 1000);
+       if (conn->fd == -1) {
+               i_error("connect(%s) failed: %m", conn->path);
+               return -1;
+       }
+
+       conn->input = i_stream_create_fd(conn->fd, AUTH_CLIENT_MAX_LINE_LENGTH,
+                                        FALSE);
+       conn->output = o_stream_create_fd(conn->fd, (size_t)-1, FALSE);
+       conn->io = io_add(conn->fd, IO_READ, auth_connection_input, conn);
+       return 0;
+}
+
+void auth_connection_deinit(struct auth_connection **_conn)
+{
+       struct auth_connection *conn = *_conn;
+
+       *_conn = NULL;
+
+       DLLIST_REMOVE(&auth_connections, conn);
+       if (conn->fd != -1) {
+               io_remove(&conn->io);
+               i_stream_unref(&conn->input);
+               o_stream_unref(&conn->output);
+
+               if (close(conn->fd) < 0)
+                       i_error("close(auth connection) failed: %m");
+               conn->callback(NULL, conn->context);
+       }
+       i_free(conn->path);
+       i_free(conn);
+}
+
+void auth_connection_send(struct auth_connection *conn,
+                         const void *data, size_t size)
+{
+       i_assert(conn->fd != -1);
+
+       (void)o_stream_send(conn->output, data, size);
+}
+
+void auth_connections_deinit(void)
+{
+       while (auth_connections != NULL) {
+               struct auth_connection *conn = auth_connections;
+
+               auth_connection_deinit(&conn);
+       }
+}
diff --git a/src/director/auth-connection.h b/src/director/auth-connection.h
new file mode 100644 (file)
index 0000000..6808fc9
--- /dev/null
@@ -0,0 +1,22 @@
+#ifndef AUTH_CONNECTION_H
+#define AUTH_CONNECTION_H
+
+/* Called for each input line. This is also called with line=NULL if
+   connection gets disonnected. */
+typedef void auth_input_callback(const char *line, void *context);
+
+struct auth_connection *auth_connection_init(const char *path);
+void auth_connection_deinit(struct auth_connection **conn);
+
+void auth_connection_set_callback(struct auth_connection *conn,
+                                 auth_input_callback *callback, void *context);
+
+/* Start connecting. Returns 0 if ok, -1 if connect failed. */
+int auth_connection_connect(struct auth_connection *conn);
+/* Send data to auth connection. */
+void auth_connection_send(struct auth_connection *conn,
+                         const void *data, size_t size);
+
+void auth_connections_deinit(void);
+
+#endif
diff --git a/src/director/director-connection.c b/src/director/director-connection.c
new file mode 100644 (file)
index 0000000..1e0f7b0
--- /dev/null
@@ -0,0 +1,735 @@
+/* Copyright (c) 2010 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "network.h"
+#include "istream.h"
+#include "ostream.h"
+#include "str.h"
+#include "mail-host.h"
+#include "director.h"
+#include "director-host.h"
+#include "director-request.h"
+#include "user-directory.h"
+#include "director-connection.h"
+
+#include <stdlib.h>
+#include <unistd.h>
+
+#define DIRECTOR_VERSION_NAME "director"
+#define DIRECTOR_VERSION_MAJOR 1
+#define DIRECTOR_VERSION_MINOR 0
+
+#define MAX_INBUF_SIZE 1024
+#define MAX_OUTBUF_SIZE (1024*1024*10)
+#define OUTBUF_FLUSH_THRESHOLD (1024*128)
+#define DIRECTOR_CONNECTION_PING_TIMEOUT_MSECS (2*1000)
+
+struct director_connection {
+       struct director *dir;
+       const char *name;
+
+       /* for incoming connections the director host isn't known until
+          ME-line is received */
+       struct director_host *host;
+
+       int fd;
+       struct io *io;
+       struct istream *input;
+       struct ostream *output;
+       struct timeout *to, *to_ping;
+
+       struct user_directory_iter *user_iter;
+
+       unsigned int in:1;
+       unsigned int connected:1;
+       unsigned int version_received:1;
+       unsigned int me_received:1;
+       unsigned int handshake_received:1;
+};
+
+static void director_connection_ping(struct director_connection *conn);
+
+static bool
+director_args_parse_ip_port(struct director_connection *conn,
+                           const char *const *args,
+                           struct ip_addr *ip_r, unsigned int *port_r)
+{
+       if (net_addr2ip(args[0], ip_r) < 0) {
+               i_error("director(%s): Command has invalid IP address: %s",
+                       conn->name, args[0]);
+               return FALSE;
+       }
+       if (str_to_uint(args[1], port_r) < 0) {
+               i_error("director(%s): Command has invalid port: %s",
+                       conn->name, args[1]);
+               return FALSE;
+       }
+       return TRUE;
+}
+
+static bool director_cmd_me(struct director_connection *conn,
+                           const char *const *args)
+{
+       struct director *dir = conn->dir;
+       struct director_host *host;
+       const char *connect_str;
+       struct ip_addr ip;
+       unsigned int port;
+
+       if (!director_args_parse_ip_port(conn, args, &ip, &port))
+               return FALSE;
+
+       if (!conn->in && (!net_ip_compare(&conn->host->ip, &ip) ||
+                         conn->host->port != port)) {
+               i_error("Remote director thinks it's someone else "
+                       "(connected to %s:%u, remote says it's %s:%u)",
+                       net_ip2addr(&conn->host->ip), conn->host->port,
+                       net_ip2addr(&ip), port);
+               return FALSE;
+       }
+       host = director_host_get(dir, &ip, port);
+       conn->me_received = TRUE;
+
+       if (!conn->in)
+               return TRUE;
+
+       conn->host = host;
+       connect_str = t_strdup_printf("CONNECT\t%s\t%u\n",
+                                     net_ip2addr(&host->ip), host->port);
+       /* make sure this is the correct incoming connection */
+       if (host->self) {
+               /* probably we're trying to find our own ip. it's no */
+               i_error("director(%s): Connection from self, dropping",
+                       host->name);
+               return FALSE;
+       } else if (dir->left == NULL) {
+               /* no conflicts yet */
+       } else if (dir->left->host == host) {
+               i_warning("director(%s): Dropping existing connection "
+                         "in favor of its new connection", host->name);
+               director_connection_deinit(&dir->left);
+       } else {
+               if (director_host_cmp_to_self(dir->left->host, host,
+                                             dir->self_host) > 0) {
+                       /* the old connection is the correct one.
+                          refer the client there. */
+                       director_connection_send(conn, t_strdup_printf(
+                               "CONNECT\t%s\t%u\n",
+                               net_ip2addr(&dir->left->host->ip),
+                               dir->left->host->port));
+                       /* also make sure that the connection is alive */
+                       director_connection_ping(dir->left);
+                       return FALSE;
+               }
+
+               /* this new connection is the correct one. disconnect the old
+                  one, but before that tell it to connect to the new one.
+                  that message might not reach it, so also send the same
+                  message to right side. */
+               director_connection_send(dir->left, connect_str);
+               (void)o_stream_flush(dir->left->output);
+               director_connection_deinit(&dir->left);
+       }
+       dir->left = conn;
+
+       /* tell the ring's right side to connect to this new director. */
+       if (dir->right != NULL) {
+               if (dir->left->host != dir->right->host)
+                       director_connection_send(dir->right, connect_str);
+               else {
+                       /* there are only two directors */
+               }
+       } else {
+               /* looks like we're the right side. */
+               (void)director_connect_host(dir, host);
+       }
+       return TRUE;
+}
+
+static bool
+director_user_refresh(struct director *dir, unsigned int username_hash,
+                     struct mail_host *host, time_t timestamp,
+                     struct user **user_r)
+{
+       struct user *user;
+       bool ret = FALSE;
+
+       user = user_directory_lookup(dir->users, username_hash);
+       if (user == NULL) {
+               *user_r = user_directory_add(dir->users, username_hash,
+                                            host, timestamp);
+               return TRUE;
+       }
+       if (timestamp == ioloop_time && user->timestamp != timestamp) {
+               user_directory_refresh(dir->users, user);
+               ret = TRUE;
+       }
+
+       if (user->host != host) {
+               i_error("User hash %u is being redirected to two hosts: "
+                       "%s and %s", username_hash,
+                       net_ip2addr(&user->host->ip),
+                       net_ip2addr(&host->ip));
+               user->host = host;
+               ret = TRUE;
+       }
+       *user_r = user;
+       return ret;
+}
+
+static bool
+director_handshake_cmd_user(struct director_connection *conn,
+                           const char *const *args)
+{
+       unsigned int username_hash, timestamp;
+       struct ip_addr ip;
+       struct mail_host *host;
+       struct user *user;
+
+       if (str_array_length(args) != 3 ||
+           str_to_uint(args[0], &username_hash) < 0 ||
+           net_addr2ip(args[1], &ip) < 0 ||
+           str_to_uint(args[2], &timestamp) < 0) {
+               i_error("director(%s): Invalid USER handshake args",
+                       conn->name);
+               return FALSE;
+       }
+
+       host = mail_host_lookup(&ip);
+       if (host == NULL) {
+               i_error("director(%s): USER used unknown host %s in handshake",
+                       conn->name, args[1]);
+               return FALSE;
+       }
+
+       director_user_refresh(conn->dir, username_hash, host, timestamp, &user);
+       return TRUE;
+}
+
+static bool director_cmd_director(struct director_connection *conn,
+                                 const char *const *args)
+{
+       struct director_host *host;
+       struct ip_addr ip;
+       unsigned int port;
+
+       if (!director_args_parse_ip_port(conn, args, &ip, &port))
+               return FALSE;
+
+       host = director_host_lookup(conn->dir, &ip, port);
+       if (host != NULL) {
+               /* already have this, skip */
+               return TRUE;
+       }
+
+       /* save the director and forward it */
+       director_host_add(conn->dir, &ip, port);
+       director_connection_send(conn->dir->right,
+               t_strdup_printf("DIRECTOR\t%s\t%u\n", net_ip2addr(&ip), port));
+       return TRUE;
+}
+
+static bool
+director_cmd_host(struct director_connection *conn, const char *const *args)
+{
+       struct mail_host *host;
+       struct ip_addr ip;
+       unsigned int vhost_count;
+       bool update;
+
+       if (str_array_length(args) != 2 ||
+           net_addr2ip(args[0], &ip) < 0 ||
+           str_to_uint(args[1], &vhost_count) < 0) {
+               i_error("director(%s): Invalid HOST args", conn->name);
+               return FALSE;
+       }
+
+       host = mail_host_lookup(&ip);
+       if (host == NULL) {
+               host = mail_host_add_ip(&ip);
+               update = TRUE;
+       } else {
+               update = host->vhost_count != vhost_count;
+       }
+
+       if (update) {
+               /* FIXME: 1) shouldn't be unconditional, 2) if we're not
+                  handshaking, we should do SYNC before making it visible */
+               host->vhost_count = vhost_count;
+               director_update_host(conn->dir, conn->host, host);
+       }
+       return TRUE;
+}
+
+static bool
+director_cmd_host_remove(struct director_connection *conn,
+                        const char *const *args)
+{
+       struct mail_host *host;
+       struct ip_addr ip;
+
+       if (str_array_length(args) != 1 ||
+           net_addr2ip(args[0], &ip) < 0) {
+               i_error("director(%s): Invalid HOST-REMOVE args", conn->name);
+               return FALSE;
+       }
+
+       host = mail_host_lookup(&ip);
+       if (host != NULL)
+               director_remove_host(conn->dir, conn->host, host);
+       return TRUE;
+}
+
+static void director_handshake_cmd_done(struct director_connection *conn)
+{
+       struct director *dir = conn->dir;
+
+       conn->handshake_received = TRUE;
+       if (conn->in) {
+               /* handshaked to left side. tell it we've received the
+                  whole handshake. */
+               director_connection_send(conn, "DONE\n");
+
+               /* tell the right director about the left one */
+               if (dir->right != NULL) {
+                       director_connection_send(dir->right,
+                               t_strdup_printf("DIRECTOR\t%s\t%u\n",
+                                               net_ip2addr(&conn->host->ip),
+                                               conn->host->port));
+               }
+       }
+
+       if (dir->left != NULL && dir->right != NULL) {
+               /* we're connected to both directors. see if the ring is
+                  finished by sending a SYNC. if we get it back, it's done. */
+               dir->sync_seq = ++dir->self_host->last_seq;
+               director_connection_send(dir->right,
+                       t_strdup_printf("SYNC\t%s\t%u\t%u\n",
+                                       net_ip2addr(&dir->self_ip),
+                                       dir->self_port, dir->sync_seq));
+       }
+}
+
+static bool
+director_connection_handle_handshake(struct director_connection *conn,
+                                    const char *cmd, const char *const *args)
+{
+       struct director_host *host;
+       struct ip_addr ip;
+       unsigned int port;
+
+       /* both incoming and outgoing connections get VERSION and ME */
+       if (strcmp(cmd, "VERSION") == 0 && str_array_length(args) >= 3) {
+               if (strcmp(args[0], DIRECTOR_VERSION_NAME) != 0) {
+                       i_error("director(%s): Wrong protocol in socket "
+                               "(%s vs %s)",
+                               conn->name, args[0], DIRECTOR_VERSION_NAME);
+                       return FALSE;
+               } else if (atoi(args[1]) != DIRECTOR_VERSION_MAJOR) {
+                       i_error("director(%s): Incompatible protocol version: "
+                               "%u vs %u", conn->name, atoi(args[1]),
+                               DIRECTOR_VERSION_MAJOR);
+                       return FALSE;
+               }
+               conn->version_received = TRUE;
+               return TRUE;
+       }
+       if (!conn->version_received) {
+               i_error("director(%s): Incompatible protocol", conn->name);
+               return FALSE;
+       }
+
+       if (strcmp(cmd, "ME") == 0 && !conn->me_received &&
+           str_array_length(args) == 2)
+               return director_cmd_me(conn, args);
+
+       /* only outgoing connections get a CONNECT reference */
+       if (!conn->in && strcmp(cmd, "CONNECT") == 0 &&
+           str_array_length(args) == 2) {
+               /* remote wants us to connect elsewhere */
+               if (!director_args_parse_ip_port(conn, args, &ip, &port))
+                       return FALSE;
+
+               conn->dir->right = NULL;
+               host = director_host_get(conn->dir, &ip, port);
+               (void)director_connect_host(conn->dir, host);
+               return FALSE;
+       }
+       /* only incoming connections get DIRECTOR and HOST lists */
+       if (conn->in && strcmp(cmd, "DIRECTOR") == 0 && conn->me_received)
+               return director_cmd_director(conn, args);
+       if (conn->in && strcmp(cmd, "HOST") == 0 && conn->me_received)
+               return director_cmd_host(conn, args);
+       /* only incoming connections get a USER list */
+       if (conn->in && strcmp(cmd, "USER") == 0 && conn->me_received)
+               return director_handshake_cmd_user(conn, args);
+       /* both get DONE */
+       if (strcmp(cmd, "DONE") == 0 && !conn->handshake_received) {
+               director_handshake_cmd_done(conn);
+               return TRUE;
+       }
+       i_error("director(%s): Unknown command (in this state): %s",
+               conn->name, cmd);
+       return FALSE;
+}
+
+static bool
+director_cmd_user(struct director_connection *conn, const char *const *args)
+{
+       unsigned int username_hash;
+       struct ip_addr ip;
+       struct mail_host *host;
+       struct user *user;
+
+       if (str_array_length(args) != 2 ||
+           str_to_uint(args[0], &username_hash) < 0 ||
+           net_addr2ip(args[1], &ip) < 0) {
+               i_error("director(%s): Invalid USER args", conn->name);
+               return FALSE;
+       }
+
+       host = mail_host_lookup(&ip);
+       if (host == NULL) {
+               /* we probably just removed this host. */
+               return TRUE;
+       }
+
+       if (director_user_refresh(conn->dir, username_hash,
+                                 host, ioloop_time, &user))
+               director_update_user(conn->dir, conn->host, user);
+       return TRUE;
+}
+
+static bool director_connection_sync(struct director_connection *conn,
+                                    const char *const *args, const char *line)
+{
+       struct director_host *host;
+       struct ip_addr ip;
+       unsigned int port, seq;
+
+       if (str_array_length(args) != 3 ||
+           director_args_parse_ip_port(conn, args, &ip, &port) < 0 ||
+           str_to_uint(args[2], &seq) < 0) {
+               i_error("director(%s): Invalid SYNC args", conn->name);
+               return FALSE;
+       }
+
+       /* find the originating director. if we don't see it, it was already
+          removed and we can ignore this sync. */
+       host = director_host_lookup(conn->dir, &ip, port);
+       if (host == NULL)
+               return TRUE;
+
+       if (host->self) {
+               if (conn->dir->sync_seq != seq) {
+                       /* stale SYNC event */
+                       return TRUE;
+               }
+               if (conn->dir->ring_handshaked)
+                       return TRUE;
+
+               /* the ring is handshaked */
+               conn->dir->ring_handshaked = TRUE;
+               director_set_state_changed(conn->dir);
+               return TRUE;
+       }
+
+       /* forward it to the connection on right */
+       if (conn->dir->right != NULL) {
+               director_connection_send(conn->dir->right,
+                                        t_strconcat(line, "\n", NULL));
+       }
+       return TRUE;
+}
+
+static bool
+director_connection_handle_line(struct director_connection *conn,
+                               const char *line)
+{
+       const char *cmd, *const *args;
+
+       args = t_strsplit(line, "\t");
+       cmd = args[0]; args++;
+       if (cmd == NULL) {
+               i_error("director(%s): Received empty line", conn->name);
+               return FALSE;
+       }
+       if (!conn->handshake_received)
+               return director_connection_handle_handshake(conn, cmd, args);
+
+       if (strcmp(cmd, "USER") == 0)
+               return director_cmd_user(conn, args);
+       if (strcmp(cmd, "HOST") == 0)
+               return director_cmd_host(conn, args);
+       if (strcmp(cmd, "HOST-REMOVE") == 0)
+               return director_cmd_host_remove(conn, args);
+       if (strcmp(cmd, "DIRECTOR") == 0)
+               return director_cmd_director(conn, args);
+       if (strcmp(cmd, "SYNC") == 0)
+               return director_connection_sync(conn, args, line);
+
+       if (strcmp(cmd, "PING") == 0) {
+               director_connection_send(conn, "PONG\n");
+               return TRUE;
+       }
+       if (strcmp(cmd, "PONG") == 0) {
+               if (conn->to_ping != NULL)
+                       timeout_remove(&conn->to_ping);
+               return TRUE;
+       }
+       i_error("director(%s): Unknown command (in this state): %s",
+               conn->name, cmd);
+       return FALSE;
+}
+
+static void director_connection_input(struct director_connection *conn)
+{
+       char *line;
+       bool ret;
+
+       if (conn->to_ping != NULL)
+               timeout_reset(conn->to_ping);
+       switch (i_stream_read(conn->input)) {
+       case 0:
+               return;
+       case -1:
+               /* disconnected */
+               director_connection_deinit(&conn);
+               return;
+       case -2:
+               /* buffer full */
+               i_error("BUG: Director sent us more than %d bytes",
+                       MAX_INBUF_SIZE);
+               director_connection_deinit(&conn);
+               return;
+       }
+
+       while ((line = i_stream_next_line(conn->input)) != NULL) {
+               T_BEGIN {
+                       ret = director_connection_handle_line(conn, line);
+               } T_END;
+
+               if (!ret) {
+                       director_connection_deinit(&conn);
+                       break;
+               }
+       }
+}
+
+static void director_connection_send_directors(struct director_connection *conn,
+                                              string_t *str)
+{
+       struct director_host *const *hostp;
+
+       array_foreach(&conn->dir->dir_hosts, hostp) {
+               str_printfa(str, "DIRECTOR\t%s\t%u\n",
+                           net_ip2addr(&(*hostp)->ip), (*hostp)->port);
+       }
+}
+
+static void director_connection_send_hosts(string_t *str)
+{
+       struct mail_host *const *hostp;
+
+       array_foreach(mail_hosts_get(), hostp) {
+               str_printfa(str, "HOST\t%s\t%u\n",
+                           net_ip2addr(&(*hostp)->ip), (*hostp)->vhost_count);
+       }
+}
+
+static int director_connection_send_users(struct director_connection *conn)
+{
+       struct user *user;
+       int ret;
+
+       o_stream_cork(conn->output);
+       while ((user = user_directory_iter_next(conn->user_iter)) != NULL) {
+               T_BEGIN {
+                       const char *line;
+
+                       line = t_strdup_printf("USER\t%u\t%s\t%u\n",
+                                              user->username_hash,
+                                              net_ip2addr(&user->host->ip),
+                                              user->timestamp);
+                       director_connection_send(conn, line);
+               } T_END;
+
+               if (o_stream_get_buffer_used_size(conn->output) >= OUTBUF_FLUSH_THRESHOLD) {
+                       if ((ret = o_stream_flush(conn->output)) <= 0) {
+                               /* continue later */
+                               return ret;
+                       }
+               }
+       }
+       user_directory_iter_deinit(&conn->user_iter);
+       director_connection_send(conn, "DONE\n");
+
+       i_assert(conn->io == NULL);
+       conn->io = io_add(conn->fd, IO_READ, director_connection_input, conn);
+
+       ret = o_stream_flush(conn->output);
+       o_stream_uncork(conn->output);
+       return ret;
+}
+
+static int director_connection_output(struct director_connection *conn)
+{
+       if (conn->user_iter != NULL)
+               return director_connection_send_users(conn);
+       else
+               return o_stream_flush(conn->output);
+}
+
+static struct director_connection *
+director_connection_init_common(struct director *dir, int fd)
+{
+       struct director_connection *conn;
+
+       conn = i_new(struct director_connection, 1);
+       conn->fd = fd;
+       conn->dir = dir;
+       conn->input = i_stream_create_fd(conn->fd, MAX_INBUF_SIZE, FALSE);
+       conn->output = o_stream_create_fd(conn->fd, MAX_OUTBUF_SIZE, FALSE);
+       o_stream_set_flush_callback(conn->output,
+                                   director_connection_output, conn);
+       return conn;
+}
+
+static void director_connection_send_handshake(struct director_connection *conn)
+{
+       director_connection_send(conn, t_strdup_printf(
+               "VERSION\t"DIRECTOR_VERSION_NAME"\t%u\t%u\n"
+               "ME\t%s\t%u\n",
+               DIRECTOR_VERSION_MAJOR, DIRECTOR_VERSION_MINOR,
+               net_ip2addr(&conn->dir->self_ip), conn->dir->self_port));
+}
+
+struct director_connection *
+director_connection_init_in(struct director *dir, int fd)
+{
+       struct director_connection *conn;
+
+       conn = director_connection_init_common(dir, fd);
+       conn->in = TRUE;
+       conn->connected = TRUE;
+       conn->name = "<incoming>";
+       conn->io = io_add(conn->fd, IO_READ, director_connection_input, conn);
+
+       director_connection_send_handshake(conn);
+       return conn;
+}
+
+static void director_connection_connected(struct director_connection *conn)
+{
+       string_t *str = t_str_new(1024);
+       int err;
+
+       if ((err = net_geterror(conn->fd)) != 0) {
+               i_error("director(%s): connect() failed: %s", conn->name,
+                       strerror(err));
+               director_connection_deinit(&conn);
+               return;
+       }
+       conn->connected = TRUE;
+
+       io_remove(&conn->io);
+
+       director_connection_send_handshake(conn);
+       director_connection_send_directors(conn, str);
+       director_connection_send_hosts(str);
+       director_connection_send(conn, str_c(str));
+
+       conn->user_iter = user_directory_iter_init(conn->dir->users);
+       (void)director_connection_send_users(conn);
+}
+
+struct director_connection *
+director_connection_init_out(struct director *dir, int fd,
+                            struct director_host *host)
+{
+       struct director_connection *conn;
+
+       conn = director_connection_init_common(dir, fd);
+       conn->name = host->name;
+       conn->host = host;
+       conn->io = io_add(conn->fd, IO_WRITE,
+                         director_connection_connected, conn);
+       return conn;
+}
+
+void director_connection_deinit(struct director_connection **_conn)
+{
+       struct director_connection *conn = *_conn;
+
+       *_conn = NULL;
+
+       if (conn->dir->left == conn)
+               conn->dir->left = NULL;
+       if (conn->dir->right == conn)
+               conn->dir->right = NULL;
+
+       if (conn->to != NULL)
+               timeout_remove(&conn->to);
+       if (conn->to_ping != NULL)
+               timeout_remove(&conn->to_ping);
+       if (conn->io != NULL)
+               io_remove(&conn->io);
+       i_stream_unref(&conn->input);
+       o_stream_unref(&conn->output);
+       if (close(conn->fd) < 0)
+               i_error("close(director connection) failed: %m");
+       i_free(conn);
+}
+
+static void director_connection_timeout(struct director_connection *conn)
+{
+       director_connection_deinit(&conn);
+}
+
+void director_connection_send(struct director_connection *conn,
+                             const char *data)
+{
+       unsigned int len = strlen(data);
+       off_t ret;
+
+       if (conn->output->closed || !conn->connected)
+               return;
+
+       ret = o_stream_send(conn->output, data, len);
+       if (ret != (off_t)len) {
+               if (ret < 0)
+                       i_error("director(%s): write() failed: %m", conn->name);
+               else {
+                       i_error("director(%s): Output buffer full, "
+                               "disconnecting", conn->name);
+               }
+               o_stream_close(conn->output);
+               conn->to = timeout_add(0, director_connection_timeout, conn);
+       }
+}
+
+void director_connection_send_except(struct director_connection *conn,
+                                    struct director_host *skip_host,
+                                    const char *data)
+{
+       if (conn->host != skip_host)
+               director_connection_send(conn, data);
+}
+
+static void director_connection_ping_timeout(struct director_connection *conn)
+{
+       i_error("director(%s): Ping timed out, disconnecting", conn->name);
+       director_connection_deinit(&conn);
+}
+
+static void director_connection_ping(struct director_connection *conn)
+{
+       if (conn->to_ping != NULL)
+               return;
+
+       conn->to_ping = timeout_add(DIRECTOR_CONNECTION_PING_TIMEOUT_MSECS,
+                                   director_connection_ping_timeout, conn);
+       director_connection_send(conn, "PING\n");
+}
diff --git a/src/director/director-connection.h b/src/director/director-connection.h
new file mode 100644 (file)
index 0000000..c56bbab
--- /dev/null
@@ -0,0 +1,20 @@
+#ifndef DIRECTOR_CONNECTION_H
+#define DIRECTOR_CONNECTION_H
+
+struct director_host;
+struct director;
+
+struct director_connection *
+director_connection_init_in(struct director *dir, int fd);
+struct director_connection *
+director_connection_init_out(struct director *dir, int fd,
+                            struct director_host *host);
+void director_connection_deinit(struct director_connection **conn);
+
+void director_connection_send(struct director_connection *conn,
+                             const char *data);
+void director_connection_send_except(struct director_connection *conn,
+                                    struct director_host *skip_host,
+                                    const char *data);
+
+#endif
diff --git a/src/director/director-host.c b/src/director/director-host.c
new file mode 100644 (file)
index 0000000..2c3ca9d
--- /dev/null
@@ -0,0 +1,138 @@
+/* Copyright (c) 2010 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "director.h"
+#include "director-host.h"
+
+static int director_host_cmp(const struct director_host *b1,
+                            const struct director_host *b2)
+{
+       int ret;
+
+       ret = net_ip_cmp(&b1->ip, &b2->ip);
+       if (ret != 0)
+               return ret;
+       return (int)b1->port - (int)b2->port;
+}
+
+static int director_host_cmp_p(struct director_host *const *host1,
+                              struct director_host *const *host2)
+{
+       return director_host_cmp(*host1, *host2);
+}
+
+struct director_host *
+director_host_add(struct director *dir,
+                 const struct ip_addr *ip, unsigned int port)
+{
+       struct director_host *host;
+
+       host = i_new(struct director_host, 1);
+       host->ip = *ip;
+       host->port = port;
+       host->name = i_strdup_printf("%s:%u", net_ip2addr(ip), port);
+
+       array_append(&dir->dir_hosts, &host, 1);
+
+       /* there are few enough directors that sorting after each
+          addition should be fine */
+       array_sort(&dir->dir_hosts, director_host_cmp_p);
+       return host;
+}
+
+void director_host_free(struct director_host *host)
+{
+       i_free(host->name);
+       i_free(host);
+}
+
+struct director_host *
+director_host_get(struct director *dir, const struct ip_addr *ip,
+                 unsigned int port)
+{
+       struct director_host *host;
+
+       host = director_host_lookup(dir, ip, port);
+       if (host == NULL)
+               host = director_host_add(dir, ip, port);
+       return host;
+}
+
+struct director_host *
+director_host_lookup(struct director *dir, const struct ip_addr *ip,
+                    unsigned int port)
+{
+       struct director_host *const *hostp;
+
+       array_foreach(&dir->dir_hosts, hostp) {
+               if (net_ip_compare(&(*hostp)->ip, ip) &&
+                   (*hostp)->port == port)
+                       return *hostp;
+       }
+       return NULL;
+}
+
+struct director_host *
+director_host_lookup_ip(struct director *dir, const struct ip_addr *ip)
+{
+       struct director_host *const *hostp;
+
+       array_foreach(&dir->dir_hosts, hostp) {
+               if (net_ip_compare(&(*hostp)->ip, ip))
+                       return *hostp;
+       }
+       return NULL;
+}
+
+int director_host_cmp_to_self(const struct director_host *b1,
+                             const struct director_host *b2,
+                             const struct director_host *self)
+{
+       if (director_host_cmp(b1, self) < 0)
+               return director_host_cmp(b1, b2);
+       else
+               return director_host_cmp(b2, b1);
+}
+
+static void director_host_add_string(struct director *dir, const char *host)
+{
+       struct ip_addr *ips;
+       unsigned int i, port, ips_count;
+       const char *p;
+
+       p = strrchr(host, ':');
+       if (p != NULL) {
+               if (str_to_uint(p + 1, &port) < 0 || port == 0 || port > 65535)
+                       i_fatal("Invalid director port in %s", host);
+               host = t_strdup_until(host, p);
+       } else {
+               port = dir->self_port;
+       }
+
+       if (net_gethostbyname(host, &ips, &ips_count) < 0)
+               i_fatal("Unknown director host: %s", host);
+
+       for (i = 0; i < ips_count; i++)
+               director_host_add(dir, &ips[i], port);
+}
+
+void director_host_add_from_string(struct director *dir, const char *hosts)
+{
+       T_BEGIN {
+               const char *const *tmp;
+
+               tmp = t_strsplit_spaces(hosts, " ");
+               for (; *tmp != NULL; tmp++)
+                       director_host_add_string(dir, *tmp);
+       } T_END;
+
+       if (array_count(&dir->dir_hosts) == 0) {
+               /* standalone director */
+               struct ip_addr ip;
+
+               net_addr2ip("127.0.0.1", &ip);
+               dir->self_host = director_host_add(dir, &ip, 0);
+               dir->self_host->self = TRUE;
+       }
+}
diff --git a/src/director/director-host.h b/src/director/director-host.h
new file mode 100644 (file)
index 0000000..77c3a4c
--- /dev/null
@@ -0,0 +1,48 @@
+#ifndef DIRECTOR_HOST_H
+#define DIRECTOR_HOST_H
+
+#include "network.h"
+
+struct director;
+
+struct director_host {
+       struct ip_addr ip;
+       unsigned int port;
+
+       /* name contains "ip:port" */
+       char *name;
+
+       /* each command between directors contains an increasing sequence.
+          if director A gets conflicting information about director B, it can
+          trust the one that has the highest sequence. */
+       unsigned int last_seq;
+
+       /* we are this director */
+       unsigned int self:1;
+};
+
+struct director_host *
+director_host_add(struct director *dir, const struct ip_addr *ip,
+                 unsigned int port);
+void director_host_free(struct director_host *host);
+
+struct director_host *
+director_host_get(struct director *dir, const struct ip_addr *ip,
+                 unsigned int port);
+struct director_host *
+director_host_lookup(struct director *dir, const struct ip_addr *ip,
+                    unsigned int port);
+struct director_host *
+director_host_lookup_ip(struct director *dir, const struct ip_addr *ip);
+
+/* Returns -1 if b1 is more on our left side than b2, 1 if b2 is,
+   0 if they equal. */
+int director_host_cmp_to_self(const struct director_host *b1,
+                             const struct director_host *b2,
+                             const struct director_host *self);
+
+/* Parse hosts list (e.g. "host1:port host2 host3:port") and them as
+   directors */
+void director_host_add_from_string(struct director *dir, const char *hosts);
+
+#endif
diff --git a/src/director/director-request.c b/src/director/director-request.c
new file mode 100644 (file)
index 0000000..e3c9282
--- /dev/null
@@ -0,0 +1,106 @@
+/* Copyright (c) 2010 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "mail-host.h"
+#include "user-directory.h"
+#include "director.h"
+#include "director-request.h"
+
+#define DIRECTOR_REQUEST_TIMEOUT_SECS 30
+
+struct director_request {
+       struct director *dir;
+
+       time_t create_time;
+       unsigned int username_hash;
+
+       director_request_callback *callback;
+       void *context;
+};
+
+static void director_request_timeout(struct director *dir)
+{
+       struct director_request **requestp, *request;
+
+       while (array_count(&dir->pending_requests) > 0) {
+               requestp = array_idx_modifiable(&dir->pending_requests, 0);
+               request = *requestp;
+
+               if (request->create_time +
+                   DIRECTOR_REQUEST_TIMEOUT_SECS > ioloop_time)
+                       break;
+
+               array_delete(&dir->pending_requests, 0, 1);
+               request->callback(NULL, request->context);
+               i_free(request);
+       }
+
+       if (array_count(&dir->pending_requests) == 0 && dir->to_request != NULL)
+               timeout_remove(&dir->to_request);
+}
+
+void director_request(struct director *dir, const char *username,
+                     director_request_callback *callback, void *context)
+{
+       struct director_request *request;
+       unsigned int username_hash = user_directory_get_username_hash(username);
+
+       request = i_new(struct director_request, 1);
+       request->dir = dir;
+       request->create_time = ioloop_time;
+       request->username_hash = username_hash;
+       request->callback = callback;
+       request->context = context;
+
+       if (director_request_continue(request))
+               return;
+
+       /* need to queue it */
+       if (dir->to_request == NULL) {
+               dir->to_request =
+                       timeout_add(DIRECTOR_REQUEST_TIMEOUT_SECS * 1000,
+                                   director_request_timeout, dir);
+       }
+       array_append(&dir->pending_requests, &request, 1);
+}
+
+bool director_request_continue(struct director_request *request)
+{
+       struct director *dir = request->dir;
+       struct mail_host *host;
+       struct user *user;
+
+       if (!dir->ring_handshaked) {
+               /* delay requests until ring handshaking is complete */
+               if (!dir->ring_handshake_warning_sent) {
+                       i_warning("Delaying connections until all "
+                                 "directors have connected");
+                       dir->ring_handshake_warning_sent = TRUE;
+               }
+               return FALSE;
+       }
+
+       user = user_directory_lookup(dir->users, request->username_hash);
+       if (user != NULL)
+               user_directory_refresh(dir->users, user);
+       else {
+               if (array_count(&dir->desynced_host_changes) != 0) {
+                       /* delay adding new users until ring is again synced */
+                       return FALSE;
+               }
+               host = mail_host_get_by_hash(request->username_hash);
+               if (host == NULL) {
+                       /* all hosts have been removed */
+                       return FALSE;
+               }
+               user = user_directory_add(dir->users, request->username_hash,
+                                         host, ioloop_time);
+       }
+
+       director_update_user(dir, dir->self_host, user);
+       request->callback(&user->host->ip, request->context);
+       i_free(request);
+       return TRUE;
+}
diff --git a/src/director/director-request.h b/src/director/director-request.h
new file mode 100644 (file)
index 0000000..6de5cae
--- /dev/null
@@ -0,0 +1,14 @@
+#ifndef DIRECTOR_REQUEST_H
+#define DIRECTOR_REQUEST_H
+
+struct director;
+struct director_request;
+
+typedef void
+director_request_callback(const struct ip_addr *ip, void *context);
+
+void director_request(struct director *dir, const char *username,
+                     director_request_callback *callback, void *context);
+bool director_request_continue(struct director_request *request);
+
+#endif
diff --git a/src/director/director-settings.c b/src/director/director-settings.c
new file mode 100644 (file)
index 0000000..37d3d5f
--- /dev/null
@@ -0,0 +1,81 @@
+/* Copyright (c) 2009-2010 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "buffer.h"
+#include "settings-parser.h"
+#include "service-settings.h"
+#include "director-settings.h"
+
+/* <settings checks> */
+static struct file_listener_settings director_unix_listeners_array[] = {
+       { "login/director", 0, "", "" },
+       { "director-admin", 0600, "", "" }
+};
+static struct file_listener_settings *director_unix_listeners[] = {
+       &director_unix_listeners_array[0],
+       &director_unix_listeners_array[1]
+};
+static buffer_t director_unix_listeners_buf = {
+       director_unix_listeners,
+       sizeof(director_unix_listeners), { 0, }
+};
+/* </settings checks> */
+
+struct service_settings director_service_settings = {
+       .name = "director",
+       .protocol = "",
+       .type = "",
+       .executable = "director",
+       .user = "$default_internal_user",
+       .group = "",
+       .privileged_group = "",
+       .extra_groups = "",
+       .chroot = "",
+
+       .drop_priv_before_exec = FALSE,
+
+       .process_min_avail = 0,
+       .process_limit = 1,
+       .client_limit = 0,
+       .service_count = 0,
+       .vsz_limit = -1U,
+
+       .unix_listeners = { { &director_unix_listeners_buf,
+                             sizeof(director_unix_listeners[0]) } },
+       .fifo_listeners = ARRAY_INIT,
+       .inet_listeners = ARRAY_INIT
+};
+#undef DEF
+#define DEF(type, name) \
+       { type, #name, offsetof(struct director_settings, name), NULL }
+
+static const struct setting_define director_setting_defines[] = {
+       DEF(SET_STR, base_dir),
+       DEF(SET_STR, master_user_separator),
+
+       DEF(SET_STR, director_servers),
+       DEF(SET_STR, director_mail_servers),
+       DEF(SET_TIME, director_user_expire),
+
+       SETTING_DEFINE_LIST_END
+};
+
+const struct director_settings director_default_settings = {
+       .base_dir = PKG_RUNDIR,
+       .master_user_separator = "",
+
+       .director_servers = "",
+       .director_mail_servers = "",
+       .director_user_expire = 60*15
+};
+
+const struct setting_parser_info director_setting_parser_info = {
+       .module_name = "director",
+       .defines = director_setting_defines,
+       .defaults = &director_default_settings,
+
+       .type_offset = (size_t)-1,
+       .struct_size = sizeof(struct director_settings),
+
+       .parent_offset = (size_t)-1
+};
diff --git a/src/director/director-settings.h b/src/director/director-settings.h
new file mode 100644 (file)
index 0000000..c46e7e8
--- /dev/null
@@ -0,0 +1,15 @@
+#ifndef DIRECTOR_SETTINGS_H
+#define DIRECTOR_SETTINGS_H
+
+struct director_settings {
+       const char *base_dir;
+       const char *master_user_separator;
+
+       const char *director_servers;
+       const char *director_mail_servers;
+       unsigned int director_user_expire;
+};
+
+extern const struct setting_parser_info director_setting_parser_info;
+
+#endif
diff --git a/src/director/director.c b/src/director/director.c
new file mode 100644 (file)
index 0000000..00dd700
--- /dev/null
@@ -0,0 +1,209 @@
+/* Copyright (c) 2010 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "str.h"
+#include "user-directory.h"
+#include "mail-host.h"
+#include "director-host.h"
+#include "director-connection.h"
+#include "director.h"
+
+static bool director_is_self_ip_set(struct director *dir)
+{
+       struct ip_addr ip;
+
+       net_get_ip_any4(&ip);
+       if (net_ip_compare(&dir->self_ip, &ip))
+               return FALSE;
+
+       net_get_ip_any6(&ip);
+       if (net_ip_compare(&dir->self_ip, &ip))
+               return FALSE;
+
+       return TRUE;
+}
+
+static void director_find_self_ip(struct director *dir)
+{
+       struct director_host *const *hosts;
+       unsigned int i, count;
+       int fd = -1;
+
+       hosts = array_get(&dir->dir_hosts, &count);
+       for (i = 0; i < count; i++) {
+               fd = net_connect_ip(&hosts[i]->ip, hosts[i]->port, NULL);
+               if (fd != -1)
+                       break;
+       }
+
+       if (fd == -1) {
+               i_fatal("Couldn't connect to any servers listed in "
+                       "director_servers (we should have been able to "
+                       "connect at least to ourself)");
+       }
+
+       if (net_getsockname(fd, &dir->self_ip, NULL) < 0)
+               i_fatal("getsockname() failed: %m");
+       net_disconnect(fd);
+}
+
+static void director_find_self(struct director *dir)
+{
+       if (dir->self_host != NULL)
+               return;
+
+       if (!director_is_self_ip_set(dir)) {
+               /* our IP isn't known yet. have to connect to some other
+                  server before we know it. */
+               director_find_self_ip(dir);
+       }
+
+       dir->self_host = director_host_lookup(dir, &dir->self_ip,
+                                             dir->self_port);
+       if (dir->self_host == NULL) {
+               i_fatal("director_servers doesn't list ourself (%s:%u)",
+                       net_ip2addr(&dir->self_ip), dir->self_port);
+       }
+       dir->self_host->self = TRUE;
+}
+
+static unsigned int director_find_self_idx(struct director *dir)
+{
+       struct director_host *const *hosts;
+       unsigned int i, count;
+
+       i_assert(dir->self_host != NULL);
+
+       hosts = array_get(&dir->dir_hosts, &count);
+       for (i = 0; i < count; i++) {
+               if (hosts[i] == dir->self_host)
+                       return i;
+       }
+       i_unreached();
+}
+
+int director_connect_host(struct director *dir, struct director_host *host)
+{
+       int fd;
+
+       i_assert(dir->right == NULL);
+
+       fd = net_connect_ip(&host->ip, host->port, NULL);
+       if (fd == -1) {
+               i_error("connect(%s) failed: %m", host->name);
+               return -1;
+       }
+
+       dir->right = director_connection_init_out(dir, fd, host);
+       return 1;
+}
+
+void director_connect(struct director *dir)
+{
+       struct director_host *const *hosts;
+       unsigned int i, count, self_idx;
+
+       director_find_self(dir);
+       self_idx = director_find_self_idx(dir);
+
+       /* try to connect to first working server on our right side.
+          the left side is supposed to connect to us. */
+       hosts = array_get(&dir->dir_hosts, &count);
+       for (i = 1; i < count; i++) {
+               unsigned int idx = (self_idx + i) % count;
+
+               if (director_connect_host(dir, hosts[idx]) > 0)
+                       break;
+       }
+       if (i == count) {
+               /* we're the only one */
+               dir->ring_handshaked = TRUE;
+               director_set_state_changed(dir);
+       }
+}
+
+void director_update_host(struct director *dir, struct director_host *src,
+                         struct mail_host *host)
+{
+       director_set_state_changed(dir);
+
+       director_update_send(dir, src, t_strdup_printf(
+               "HOST\t%s\t%u\n", net_ip2addr(&host->ip), host->vhost_count));
+}
+
+void director_remove_host(struct director *dir, struct director_host *src,
+                         struct mail_host *host)
+{
+       director_update_send(dir, src, t_strdup_printf(
+               "HOST-REMOVE\t%s\n", net_ip2addr(&host->ip)));
+       user_directory_remove_host(dir->users, host);
+       mail_host_remove(host);
+}
+
+void director_update_user(struct director *dir, struct director_host *src,
+                         struct user *user)
+{
+       director_update_send(dir, src, t_strdup_printf(
+               "USER\t%u\t%s\n", user->username_hash,
+               net_ip2addr(&user->host->ip)));
+}
+
+void director_set_state_changed(struct director *dir)
+{
+       dir->state_change_callback(dir);
+}
+
+void director_update_send(struct director *dir, struct director_host *src,
+                         const char *cmd)
+{
+       i_assert(src != NULL);
+
+       if (dir->left != NULL)
+               director_connection_send_except(dir->left, src, cmd);
+       if (dir->right != NULL && dir->right != dir->left)
+               director_connection_send_except(dir->right, src, cmd);
+}
+
+struct director *
+director_init(const struct director_settings *set,
+             const struct ip_addr *listen_ip, unsigned int listen_port,
+             director_state_change_callback_t *callback)
+{
+       struct director *dir;
+
+       dir = i_new(struct director, 1);
+       dir->set = set;
+       dir->self_port = listen_port;
+       dir->self_ip = *listen_ip;
+       dir->state_change_callback = callback;
+       i_array_init(&dir->dir_hosts, 16);
+       i_array_init(&dir->pending_requests, 16);
+       i_array_init(&dir->desynced_host_changes, 16);
+       dir->users = user_directory_init(set->director_user_expire);
+       return dir;
+}
+
+void director_deinit(struct director **_dir)
+{
+       struct director *dir = *_dir;
+       struct director_host *const *hostp;
+
+       *_dir = NULL;
+
+       if (dir->left != NULL)
+               director_connection_deinit(&dir->left);
+       if (dir->right != NULL)
+               director_connection_deinit(&dir->right);
+
+       user_directory_deinit(&dir->users);
+       if (dir->to_request != NULL)
+               timeout_remove(&dir->to_request);
+       array_foreach(&dir->dir_hosts, hostp)
+               director_host_free(*hostp);
+       array_free(&dir->desynced_host_changes);
+       array_free(&dir->pending_requests);
+       array_free(&dir->dir_hosts);
+       i_free(dir);
+}
diff --git a/src/director/director.h b/src/director/director.h
new file mode 100644 (file)
index 0000000..23b5494
--- /dev/null
@@ -0,0 +1,88 @@
+#ifndef DIRECTOR_H
+#define DIRECTOR_H
+
+#include "network.h"
+#include "director-settings.h"
+
+struct director;
+struct mail_host;
+struct user;
+
+typedef void director_state_change_callback_t(struct director *dir);
+
+struct director_host_change {
+       /* originating director for this change. keep ip/port here separately,
+          because by the time its sync comes, the director itself may have
+          already been removed. */
+       struct ip_addr ip;
+       unsigned int port;
+       /* highest change sequence from this director */
+       unsigned int seq;
+};
+
+struct director {
+       const struct director_settings *set;
+
+       /* IP and port of this director. self_host->ip/port must equal these. */
+       struct ip_addr self_ip;
+       unsigned int self_port;
+
+       struct director_host *self_host;
+       struct director_connection *left, *right;
+
+       /* temporary user -> host associations */
+       struct user_directory *users;
+
+       /* these requests are waiting for directors to be in synced */
+       ARRAY_DEFINE(pending_requests, struct director_request *);
+       struct timeout *to_request;
+
+       director_state_change_callback_t *state_change_callback;
+
+       /* director hosts are sorted by IP (and port) */
+       ARRAY_DEFINE(dir_hosts, struct director_host *);
+
+       /* this array contains host changes done by directors.
+          while it's non-empty, new user mappings can't be added, because
+          different directors may see different hosts. SYNC events remove
+          these changes. */
+       ARRAY_DEFINE(desynced_host_changes, struct director_host_change);
+
+       unsigned int sync_seq;
+
+       /* director ring handshaking is complete.
+          director can start serving clients. */
+       unsigned int ring_handshaked:1;
+       unsigned int ring_handshake_warning_sent:1;
+};
+
+/* Create a new director. If listen_ip specifies an actual IP, it's used with
+   listen_port for finding ourself from the director_servers setting.
+   listen_port is used regardless by director_host_add_from_string() for hosts
+   without specified port. */
+struct director *
+director_init(const struct director_settings *set,
+             const struct ip_addr *listen_ip, unsigned int listen_port,
+             director_state_change_callback_t *callback);
+void director_deinit(struct director **dir);
+
+/* Start connecting to other directors */
+void director_connect(struct director *dir);
+
+void director_set_state_changed(struct director *dir);
+
+void director_update_host(struct director *dir, struct director_host *src,
+                         struct mail_host *host);
+void director_remove_host(struct director *dir, struct director_host *src,
+                         struct mail_host *host);
+void director_update_user(struct director *dir, struct director_host *src,
+                         struct user *user);
+
+/* Send data to all directors using both left and right connections
+   (unless they're the same). */
+void director_update_send(struct director *dir, struct director_host *src,
+                         const char *data);
+
+int director_connect_host(struct director *dir, struct director_host *host);
+
+#endif
diff --git a/src/director/doveadm-connection.c b/src/director/doveadm-connection.c
new file mode 100644 (file)
index 0000000..05c6c56
--- /dev/null
@@ -0,0 +1,193 @@
+/* Copyright (c) 2010 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "network.h"
+#include "istream.h"
+#include "ostream.h"
+#include "array.h"
+#include "str.h"
+#include "llist.h"
+#include "user-directory.h"
+#include "mail-host.h"
+#include "director.h"
+#include "director-host.h"
+#include "director-request.h"
+#include "doveadm-connection.h"
+
+#include <unistd.h>
+
+#define DOVEADM_HANDSHAKE_EXPECTED "VERSION\tdirector-doveadm\t1\t"
+#define DOVEADM_HANDSHAKE DOVEADM_HANDSHAKE_EXPECTED"0\n"
+
+#define MAX_VALID_VHOST_COUNT 1000
+
+struct doveadm_connection {
+       struct doveadm_connection *prev, *next;
+
+       int fd;
+       struct io *io;
+       struct istream *input;
+       struct ostream *output;
+       struct director *dir;
+
+       unsigned int handshaked:1;
+};
+
+static struct doveadm_connection *doveadm_connections;
+
+static void doveadm_connection_deinit(struct doveadm_connection **_conn);
+
+static void doveadm_cmd_host_list(struct doveadm_connection *conn)
+{
+       struct mail_host *const *hostp;
+       string_t *str = t_str_new(1024);
+
+       array_foreach(mail_hosts_get(), hostp) {
+               str_printfa(str, "%s\t%u\t%u\n",
+                           net_ip2addr(&(*hostp)->ip), (*hostp)->vhost_count,
+                           (*hostp)->user_count);
+       }
+       str_append_c(str, '\n');
+       o_stream_send(conn->output, str_data(str), str_len(str));
+}
+
+static void doveadm_cmd_director_list(struct doveadm_connection *conn)
+{
+       struct director_host *const *hostp;
+       string_t *str = t_str_new(1024);
+
+       array_foreach(&conn->dir->dir_hosts, hostp) {
+               str_printfa(str, "%s\t%u\n",
+                           net_ip2addr(&(*hostp)->ip), (*hostp)->port);
+       }
+       str_append_c(str, '\n');
+       o_stream_send(conn->output, str_data(str), str_len(str));
+}
+
+static bool
+doveadm_cmd_host_set(struct doveadm_connection *conn, const char *line)
+{
+       const char *const *args;
+       struct mail_host *host;
+       struct ip_addr ip;
+       unsigned int vhost_count = -1U;
+
+       args = t_strsplit(line, "\t");
+       if (args[0] == NULL ||
+           net_addr2ip(args[0], &ip) < 0 ||
+           (args[1] != NULL && str_to_uint(args[1], &vhost_count) < 0)) {
+               i_error("doveadm sent invalid HOST-SET parameters");
+               return FALSE;
+       }
+       if (vhost_count > MAX_VALID_VHOST_COUNT && vhost_count != -1U) {
+               o_stream_send_str(conn->output, "vhost count too large\n");
+               return TRUE;
+       }
+       host = mail_host_lookup(&ip);
+       if (host == NULL)
+               host = mail_host_add_ip(&ip);
+       if (vhost_count != -1U)
+               mail_host_set_vhost_count(host, vhost_count);
+       director_update_host(conn->dir, conn->dir->self_host, host);
+
+       o_stream_send(conn->output, "OK\n", 3);
+       return TRUE;
+}
+
+static bool
+doveadm_cmd_host_remove(struct doveadm_connection *conn, const char *line)
+{
+       struct mail_host *host;
+       struct ip_addr ip;
+
+       if (net_addr2ip(line, &ip) < 0) {
+               i_error("doveadm sent invalid HOST-SET parameters");
+               return FALSE;
+       }
+       host = mail_host_lookup(&ip);
+       if (host == NULL)
+               o_stream_send_str(conn->output, "NOTFOUND\n");
+       else {
+               director_remove_host(conn->dir, conn->dir->self_host, host);
+               o_stream_send(conn->output, "OK\n", 3);
+       }
+       return TRUE;
+}
+
+static void doveadm_connection_input(struct doveadm_connection *conn)
+{
+       const char *line;
+       bool ret = TRUE;
+
+       if (!conn->handshaked) {
+               if ((line = i_stream_read_next_line(conn->input)) == NULL)
+                       return;
+
+               if (strncmp(line, DOVEADM_HANDSHAKE_EXPECTED,
+                           strlen(DOVEADM_HANDSHAKE_EXPECTED)) != 0) {
+                       i_error("doveadm not compatible with this server "
+                               "(mixed old and new binaries?)");
+                       doveadm_connection_deinit(&conn);
+                       return;
+               }
+               conn->handshaked = TRUE;
+       }
+
+       while ((line = i_stream_read_next_line(conn->input)) != NULL && ret) {
+               if (strcmp(line, "HOST-LIST") == 0)
+                       doveadm_cmd_host_list(conn);
+               else if (strcmp(line, "DIRECTOR-LIST") == 0)
+                       doveadm_cmd_director_list(conn);
+               else if (strncmp(line, "HOST-SET\t", 9) == 0)
+                       ret = doveadm_cmd_host_set(conn, line + 9);
+               else if (strncmp(line, "HOST-REMOVE\t", 12) == 0)
+                       ret = doveadm_cmd_host_remove(conn, line + 12);
+               else {
+                       i_error("doveadm sent unknown command: %s", line);
+                       ret = FALSE;
+               }
+       }
+       if (conn->input->eof || conn->input->stream_errno != 0 || !ret)
+               doveadm_connection_deinit(&conn);
+}
+
+struct doveadm_connection *
+doveadm_connection_init(struct director *dir, int fd)
+{
+       struct doveadm_connection *conn;
+
+       conn = i_new(struct doveadm_connection, 1);
+       conn->fd = fd;
+       conn->dir = dir;
+       conn->input = i_stream_create_fd(conn->fd, 1024, FALSE);
+       conn->output = o_stream_create_fd(conn->fd, (size_t)-1, FALSE);
+       conn->io = io_add(conn->fd, IO_READ, doveadm_connection_input, conn);
+       o_stream_send_str(conn->output, DOVEADM_HANDSHAKE);
+
+       DLLIST_PREPEND(&doveadm_connections, conn);
+       return conn;
+}
+
+static void doveadm_connection_deinit(struct doveadm_connection **_conn)
+{
+       struct doveadm_connection *conn = *_conn;
+
+       *_conn = NULL;
+
+       DLLIST_REMOVE(&doveadm_connections, conn);
+       io_remove(&conn->io);
+       o_stream_unref(&conn->output);
+       if (close(conn->fd) < 0)
+               i_error("close(doveadm connection) failed: %m");
+       i_free(conn);
+}
+
+void doveadm_connections_deinit(void)
+{
+       while (doveadm_connections != NULL) {
+               struct doveadm_connection *conn = doveadm_connections;
+
+               doveadm_connection_deinit(&conn);
+       }
+}
diff --git a/src/director/doveadm-connection.h b/src/director/doveadm-connection.h
new file mode 100644 (file)
index 0000000..bbfaedb
--- /dev/null
@@ -0,0 +1,10 @@
+#ifndef DOVEADM_CONNECTION_H
+#define DOVEADM_CONNECTION_H
+
+struct director;
+
+struct doveadm_connection *
+doveadm_connection_init(struct director *dir, int fd);
+void doveadm_connections_deinit(void);
+
+#endif
diff --git a/src/director/login-connection.c b/src/director/login-connection.c
new file mode 100644 (file)
index 0000000..4c3f1ad
--- /dev/null
@@ -0,0 +1,211 @@
+/* Copyright (c) 2010 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "network.h"
+#include "ostream.h"
+#include "llist.h"
+#include "director.h"
+#include "director-request.h"
+#include "auth-connection.h"
+#include "login-connection.h"
+
+#include <unistd.h>
+
+struct login_connection {
+       struct login_connection *prev, *next;
+
+       int refcount;
+
+       int fd;
+       struct io *io;
+       struct ostream *output;
+       struct auth_connection *auth;
+       struct director *dir;
+
+       unsigned int destroyed:1;
+};
+
+struct login_host_request {
+       struct login_connection *conn;
+       char *line;
+};
+
+static struct login_connection *login_connections;
+
+static void login_connection_unref(struct login_connection **_conn);
+
+static void login_connection_input(struct login_connection *conn)
+{
+       unsigned char buf[4096];
+       ssize_t ret;
+
+       ret = read(conn->fd, buf, sizeof(buf));
+       if (ret <= 0) {
+               if (ret < 0) {
+                       if (errno == EAGAIN)
+                               return;
+                       i_error("read(login connection) failed: %m");
+               }
+               login_connection_deinit(&conn);
+               return;
+       }
+       auth_connection_send(conn->auth, buf, ret);
+}
+
+static void
+login_connection_send_line(struct login_connection *conn, const char *line)
+{
+       struct const_iovec iov[2];
+
+       if (conn->destroyed)
+               return;
+
+       iov[0].iov_base = line;
+       iov[0].iov_len = strlen(line);
+       iov[1].iov_base = "\n";
+       iov[1].iov_len = 1;
+       (void)o_stream_sendv(conn->output, iov, N_ELEMENTS(iov));
+}
+
+static void login_host_callback(const struct ip_addr *ip, void *context)
+{
+       struct login_host_request *request = context;
+       const char *line;
+
+       T_BEGIN {
+               if (ip != NULL) {
+                       line = t_strconcat(request->line, "\thost=",
+                                          net_ip2addr(ip), NULL);
+               } else {
+                       i_assert(strncmp(request->line, "OK\t", 3) == 0);
+                       line = t_strconcat("FAIL\t",
+                                          t_strcut(request->line + 3, '\t'),
+                                          NULL);
+               }
+               login_connection_send_line(request->conn, line);
+       } T_END;
+
+       login_connection_unref(&request->conn);
+       i_free(request->line);
+       i_free(request);
+}
+
+static void auth_input_line(const char *line, void *context)
+{
+       struct login_connection *conn = context;
+       struct login_host_request *request;
+       const char *const *args, *username = NULL;
+       bool proxy = FALSE, host = FALSE;
+
+       if (line == NULL) {
+               /* auth connection died -> kill also this login connection */
+               login_connection_deinit(&conn);
+               return;
+       }
+       if (strncmp(line, "OK\t", 3) != 0) {
+               login_connection_send_line(conn, line);
+               return;
+       }
+
+       /* OK <id> [<parameters>] */
+       args = t_strsplit(line + 3, "\t");
+       if (*args != NULL) {
+               /* we should always get here, but in case we don't just
+                  forward as-is and let login process handle the error. */
+               args++;
+       }
+
+       for (; *args != NULL; args++) {
+               if (strncmp(*args, "proxy", 5) == 0 &&
+                   ((*args)[5] == '=' || (*args)[5] == '\0'))
+                       proxy = TRUE;
+               else if (strncmp(*args, "host=", 5) == 0)
+                       host = TRUE;
+               else if (strncmp(*args, "destuser=", 9) == 0)
+                       username = *args + 9;
+               else if (strncmp(*args, "user=", 5) == 0) {
+                       if (username == NULL)
+                               username = *args + 5;
+               }
+       }
+       if (*conn->dir->set->master_user_separator != '\0') {
+               /* with master user logins we still want to use only the
+                  login username */
+               username = t_strcut(username,
+                                   *conn->dir->set->master_user_separator);
+       }
+
+       if (!proxy || host || username == NULL) {
+               login_connection_send_line(conn, line);
+               return;
+       }
+
+       /* we need to add the host. the lookup might be asynchronous */
+       request = i_new(struct login_host_request, 1);
+       request->conn = conn;
+       request->line = i_strdup(line);
+
+       conn->refcount++;
+       director_request(conn->dir, username, login_host_callback, request);
+}
+
+struct login_connection *
+login_connection_init(struct director *dir, int fd,
+                     struct auth_connection *auth)
+{
+       struct login_connection *conn;
+
+       conn = i_new(struct login_connection, 1);
+       conn->refcount = 1;
+       conn->fd = fd;
+       conn->auth = auth;
+       conn->dir = dir;
+       conn->output = o_stream_create_fd(conn->fd, (size_t)-1, FALSE);
+       conn->io = io_add(conn->fd, IO_READ, login_connection_input, conn);
+
+       auth_connection_set_callback(conn->auth, auth_input_line, conn);
+       DLLIST_PREPEND(&login_connections, conn);
+       return conn;
+}
+
+void login_connection_deinit(struct login_connection **_conn)
+{
+       struct login_connection *conn = *_conn;
+
+       *_conn = NULL;
+
+       if (conn->destroyed)
+               return;
+       conn->destroyed = TRUE;
+
+       DLLIST_REMOVE(&login_connections, conn);
+       io_remove(&conn->io);
+       o_stream_unref(&conn->output);
+       if (close(conn->fd) < 0)
+               i_error("close(login connection) failed: %m");
+       conn->fd = -1;
+
+       auth_connection_deinit(&conn->auth);
+       login_connection_unref(&conn);
+}
+
+static void login_connection_unref(struct login_connection **_conn)
+{
+       struct login_connection *conn = *_conn;
+
+       *_conn = NULL;
+
+       i_assert(conn->refcount > 0);
+       if (--conn->refcount == 0)
+               i_free(conn);
+}
+
+void login_connections_deinit(void)
+{
+       while (login_connections != NULL) {
+               struct login_connection *conn = login_connections;
+
+               login_connection_deinit(&conn);
+       }
+}
diff --git a/src/director/login-connection.h b/src/director/login-connection.h
new file mode 100644 (file)
index 0000000..dbd9bef
--- /dev/null
@@ -0,0 +1,13 @@
+#ifndef LOGIN_CONNECTION_H
+#define LOGIN_CONNECTION_H
+
+struct director;
+
+struct login_connection *
+login_connection_init(struct director *dir, int fd,
+                     struct auth_connection *auth);
+void login_connection_deinit(struct login_connection **conn);
+
+void login_connections_deinit(void);
+
+#endif
diff --git a/src/director/mail-host.c b/src/director/mail-host.c
new file mode 100644 (file)
index 0000000..0cbe0f5
--- /dev/null
@@ -0,0 +1,182 @@
+/* Copyright (c) 2010 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "array.h"
+#include "mail-host.h"
+
+#define VHOST_MULTIPLIER 100
+
+static ARRAY_TYPE(mail_host) hosts;
+static ARRAY_DEFINE(vhosts, struct mail_host *);
+static bool hosts_unsorted;
+
+static int
+mail_host_cmp(struct mail_host *const *h1, struct mail_host *const *h2)
+{
+       return net_ip_cmp(&(*h1)->ip, &(*h2)->ip);
+}
+
+static void mail_hosts_sort(void)
+{
+       struct mail_host *const *hostp;
+       unsigned int i;
+
+       array_sort(&hosts, mail_host_cmp);
+
+       /* rebuild vhosts */
+       array_clear(&vhosts);
+       array_foreach(&hosts, hostp) {
+               for (i = 0; i < (*hostp)->vhost_count; i++)
+                       array_append(&vhosts, hostp, 1);
+       }
+       hosts_unsorted = FALSE;
+}
+
+struct mail_host *mail_host_add_ip(const struct ip_addr *ip)
+{
+       struct mail_host *host;
+
+       host = i_new(struct mail_host, 1);
+       host->vhost_count = VHOST_MULTIPLIER;
+       host->ip = *ip;
+       array_append(&hosts, &host, 1);
+
+       hosts_unsorted = TRUE;
+       return host;
+}
+
+static int mail_host_add(const char *host)
+{
+       struct ip_addr ip;
+
+       if (net_addr2ip(host, &ip) < 0) {
+               i_error("Invalid IP address: %s", host);
+               return -1;
+       }
+
+       mail_host_add_ip(&ip);
+       return 0;
+}
+
+static int mail_hosts_add_range(const char *host1, const char *host2)
+{
+       struct ip_addr ip1, ip2;
+
+       if (net_addr2ip(host1, &ip1) < 0) {
+               i_error("Invalid IP address: %s", host1);
+               return -1;
+       }
+       if (net_addr2ip(host2, &ip2) < 0) {
+               i_error("Invalid IP address: %s", host2);
+               return -1;
+       }
+
+       // FIXME
+
+       return 0;
+}
+
+int mail_hosts_parse_and_add(const char *hosts_list)
+{
+       int ret = 0;
+
+       T_BEGIN {
+               const char *const *tmp, *p;
+
+               tmp = t_strsplit_spaces(hosts_list, " ");
+               for (; *tmp != NULL; tmp++) {
+                       p = strchr(*tmp, '-');
+                       if (p == NULL) {
+                               if (mail_host_add(*tmp) < 0)
+                                       ret = -1;
+                       } else if (mail_hosts_add_range(t_strdup_until(*tmp, p),
+                                                       p + 1) < 0)
+                               ret = -1;
+               }
+       } T_END;
+
+       if (array_count(&hosts) == 0) {
+               if (ret < 0)
+                       i_error("No valid servers specified");
+               else
+                       i_error("Empty server list");
+               ret = -1;
+       }
+       return ret;
+}
+
+void mail_host_set_vhost_count(struct mail_host *host,
+                              unsigned int vhost_count)
+{
+       host->vhost_count = vhost_count;
+       mail_hosts_sort();
+}
+
+void mail_host_remove(struct mail_host *host)
+{
+       struct mail_host *const *h;
+       unsigned int i, count;
+
+       h = array_get(&hosts, &count);
+       for (i = 0; i < count; i++) {
+               if (h[i] == host) {
+                       array_delete(&hosts, i, 1);
+                       break;
+               }
+       }
+
+       i_free(host);
+       mail_hosts_sort();
+}
+
+struct mail_host *mail_host_lookup(const struct ip_addr *ip)
+{
+       struct mail_host *const *hostp;
+
+       if (hosts_unsorted)
+               mail_hosts_sort();
+
+       array_foreach(&hosts, hostp) {
+               if (net_ip_compare(&(*hostp)->ip, ip))
+                       return *hostp;
+       }
+       return NULL;
+}
+
+struct mail_host *mail_host_get_by_hash(unsigned int hash)
+{
+       struct mail_host *const *v;
+       unsigned int count;
+
+       if (hosts_unsorted)
+               mail_hosts_sort();
+
+       v = array_get(&vhosts, &count);
+       if (count == 0)
+               return NULL;
+
+       return v[hash % count];
+}
+
+const ARRAY_TYPE(mail_host) *mail_hosts_get(void)
+{
+       if (hosts_unsorted)
+               mail_hosts_sort();
+       return &hosts;
+}
+
+void mail_hosts_init(void)
+{
+       i_array_init(&hosts, 16);
+       i_array_init(&vhosts, 16*VHOST_MULTIPLIER);
+}
+
+void mail_hosts_deinit(void)
+{
+       struct mail_host **hostp;
+
+       array_foreach_modifiable(&hosts, hostp)
+               i_free(*hostp);
+       array_free(&hosts);
+       array_free(&vhosts);
+}
diff --git a/src/director/mail-host.h b/src/director/mail-host.h
new file mode 100644 (file)
index 0000000..1ad1013
--- /dev/null
@@ -0,0 +1,28 @@
+#ifndef MAIL_HOST_H
+#define MAIL_HOST_H
+
+#include "network.h"
+
+struct mail_host {
+       unsigned int user_count;
+       unsigned int vhost_count;
+
+       struct ip_addr ip;
+};
+ARRAY_DEFINE_TYPE(mail_host, struct mail_host *);
+
+struct mail_host *mail_host_add_ip(const struct ip_addr *ip);
+struct mail_host *mail_host_lookup(const struct ip_addr *ip);
+struct mail_host *mail_host_get_by_hash(unsigned int hash);
+
+int mail_hosts_parse_and_add(const char *hosts_list);
+void mail_host_set_vhost_count(struct mail_host *host,
+                              unsigned int vhost_count);
+void mail_host_remove(struct mail_host *host);
+
+const ARRAY_TYPE(mail_host) *mail_hosts_get(void);
+
+void mail_hosts_init(void);
+void mail_hosts_deinit(void);
+
+#endif
diff --git a/src/director/main.c b/src/director/main.c
new file mode 100644 (file)
index 0000000..d9de051
--- /dev/null
@@ -0,0 +1,178 @@
+/* Copyright (c) 2010 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "restrict-access.h"
+#include "master-interface.h"
+#include "master-service.h"
+#include "master-service-settings.h"
+#include "auth-connection.h"
+#include "doveadm-connection.h"
+#include "login-connection.h"
+#include "director.h"
+#include "director-host.h"
+#include "director-connection.h"
+#include "director-request.h"
+#include "mail-host.h"
+
+#include <unistd.h>
+
+#define AUTH_SOCKET_PATH "login/login"
+
+static struct director *director;
+static char *auth_socket_path;
+
+static int director_client_connected(int fd, const struct ip_addr *ip)
+{
+       if (director_host_lookup_ip(director, ip) == NULL) {
+               i_warning("Connection from %s: Server not listed in "
+                         "director_servers, dropping", net_ip2addr(ip));
+               return -1;
+       }
+
+       director_connection_init_in(director, fd);
+       return 0;
+}
+
+static void client_connected(const struct master_service_connection *conn)
+{
+       struct auth_connection *auth;
+       const char *path, *name;
+       struct ip_addr ip;
+       unsigned int port, len;
+
+       if (net_getpeername(conn->fd, &ip, &port) == 0 &&
+           (IPADDR_IS_V4(&ip) || IPADDR_IS_V6(&ip))) {
+               /* TCP/IP connection - this is another director */
+               if (director_client_connected(conn->fd, &ip) < 0)
+                       (void)close(conn->fd);
+               return;
+       }
+
+       if (net_getunixname(conn->listen_fd, &path) < 0)
+               i_fatal("getsockname(%d) failed: %m", conn->listen_fd);
+
+       name = strrchr(path, '/');
+       if (name == NULL)
+               name = path;
+       else
+               name++;
+
+       len = strlen(name);
+       if (len > 6 && strcmp(name + len - 6, "-admin") == 0) {
+               /* doveadm connection */
+               (void)doveadm_connection_init(director, conn->fd);
+       } else {
+               /* login connection */
+               auth = auth_connection_init(auth_socket_path);
+               if (auth_connection_connect(auth) == 0)
+                       login_connection_init(director, conn->fd, auth);
+               else {
+                       (void)close(conn->fd);
+                       auth_connection_deinit(&auth);
+               }
+       }
+}
+
+static unsigned int find_inet_listener_port(struct ip_addr *ip_r)
+{
+       unsigned int i, socket_count, port;
+
+       socket_count = master_service_get_socket_count(master_service);
+       for (i = 0; i < socket_count; i++) {
+               int fd = MASTER_LISTEN_FD_FIRST + i;
+
+               if (net_getsockname(fd, ip_r, &port) == 0 && port > 0)
+                       return port;
+       }
+       return 0;
+}
+
+static void director_state_changed(struct director *dir)
+{
+       struct director_request *const *requestp;
+       bool ret;
+
+       if (!dir->ring_handshaked ||
+           array_count(&dir->desynced_host_changes) != 0 ||
+           mail_host_get_by_hash(0) == NULL)
+               return;
+
+       /* if there are any pending client requests, finish them now */
+       array_foreach(&dir->pending_requests, requestp) {
+               ret = director_request_continue(*requestp);
+               i_assert(ret);
+       }
+       array_clear(&dir->pending_requests);
+
+       if (dir->to_request != NULL)
+               timeout_remove(&dir->to_request);
+}
+
+static void main_init(void)
+{
+       const struct director_settings *set;
+       struct ip_addr listen_ip;
+       unsigned int listen_port;
+
+       set = master_service_settings_get_others(master_service)[0];
+
+       auth_socket_path = i_strconcat(set->base_dir,
+                                      "/"AUTH_SOCKET_PATH, NULL);
+
+       mail_hosts_init();
+       if (mail_hosts_parse_and_add(set->director_mail_servers) < 0)
+               i_fatal("Invalid value for director_mail_servers setting");
+
+       listen_port = find_inet_listener_port(&listen_ip);
+       if (listen_port == 0 && *set->director_servers != '\0') {
+               i_fatal("No inet_listeners defined for director service "
+                       "(for standalone keep director_servers empty)");
+       }
+
+       director = director_init(set, &listen_ip, listen_port,
+                                director_state_changed);
+       director_host_add_from_string(director, set->director_servers);
+
+       director_connect(director);
+}
+
+static void main_deinit(void)
+{
+       director_deinit(&director);
+       doveadm_connections_deinit();
+       login_connections_deinit();
+       auth_connections_deinit();
+       mail_hosts_deinit();
+       i_free(auth_socket_path);
+}
+
+int main(int argc, char *argv[])
+{
+       const struct setting_parser_info *set_roots[] = {
+               &director_setting_parser_info,
+               NULL
+       };
+       const char *error;
+
+       master_service = master_service_init("director", 0, &argc, &argv, NULL);
+       if (master_getopt(master_service) > 0)
+               return FATAL_DEFAULT;
+       if (master_service_settings_read_simple(master_service, set_roots,
+                                               &error) < 0)
+               i_fatal("Error reading configuration: %s", error);
+
+       master_service_init_log(master_service, "director: ");
+
+       restrict_access_by_env(NULL, FALSE);
+       restrict_access_allow_coredumps(TRUE);
+       master_service_init_finish(master_service);
+
+       main_init();
+       master_service_run(master_service, client_connected);
+       main_deinit();
+
+       master_service_deinit(&master_service);
+        return 0;
+}
diff --git a/src/director/user-directory.c b/src/director/user-directory.c
new file mode 100644 (file)
index 0000000..7b4c39d
--- /dev/null
@@ -0,0 +1,191 @@
+/* Copyright (c) 2010 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "md5.h"
+#include "hash.h"
+#include "llist.h"
+#include "mail-host.h"
+#include "user-directory.h"
+
+#define MAX_CLOCK_DRIFT_SECS 2
+
+struct user_directory_iter {
+       struct user_directory *dir;
+       struct user *pos;
+};
+
+struct user_directory {
+       /* const char *username => struct user* */
+       struct hash_table *hash;
+       /* sorted by time */
+       struct user *head, *tail;
+
+       ARRAY_DEFINE(iters, struct user_directory_iter *);
+
+       unsigned int timeout_secs;
+};
+
+static void user_move_iters(struct user_directory *dir, struct user *user)
+{
+       struct user_directory_iter *const *iterp;
+
+       array_foreach(&dir->iters, iterp) {
+               if ((*iterp)->pos == user)
+                       (*iterp)->pos = user->next;
+       }
+}
+
+static void user_free(struct user_directory *dir, struct user *user)
+{
+       i_assert(user->host->user_count > 0);
+       user->host->user_count--;
+
+       user_move_iters(dir, user);
+
+       hash_table_remove(dir->hash, POINTER_CAST(user->username_hash));
+       DLLIST2_REMOVE(&dir->head, &dir->tail, user);
+       i_free(user);
+}
+
+static void user_directory_drop_expired(struct user_directory *dir)
+{
+       while (dir->head != NULL &&
+              ioloop_time > dir->head->timestamp + dir->timeout_secs)
+               user_free(dir, dir->head);
+}
+
+struct user *user_directory_lookup(struct user_directory *dir,
+                                  unsigned int username_hash)
+{
+       user_directory_drop_expired(dir);
+
+       return hash_table_lookup(dir->hash, POINTER_CAST(username_hash));
+}
+
+struct user *
+user_directory_add(struct user_directory *dir, unsigned int username_hash,
+                  struct mail_host *host, time_t timestamp)
+{
+       struct user *user;
+
+       user = i_new(struct user, 1);
+       user->username_hash = username_hash;
+       user->host = host;
+       user->host->user_count++;
+       user->timestamp = timestamp;
+       DLLIST2_APPEND(&dir->head, &dir->tail, user);
+
+       hash_table_insert(dir->hash, POINTER_CAST(user->username_hash), user);
+       return user;
+}
+
+void user_directory_refresh(struct user_directory *dir, struct user *user)
+{
+       user_move_iters(dir, user);
+
+       user->timestamp = ioloop_time;
+       DLLIST2_REMOVE(&dir->head, &dir->tail, user);
+       DLLIST2_APPEND(&dir->head, &dir->tail, user);
+}
+
+void user_directory_remove_host(struct user_directory *dir,
+                               struct mail_host *host)
+{
+       struct user *user, *next;
+
+       for (user = dir->head; user != NULL; user = next) {
+               next = user->next;
+
+               if (user->host == host)
+                       user_free(dir, user);
+       }
+}
+
+unsigned int user_directory_get_username_hash(const char *username)
+{
+       unsigned char md5[MD5_RESULTLEN];
+       unsigned int i, hash = 0;
+
+       md5_get_digest(username, strlen(username), md5);
+       for (i = 0; i < sizeof(hash); i++)
+               hash = (hash << CHAR_BIT) | md5[i];
+       return hash;
+}
+
+bool user_directory_user_has_connections(struct user_directory *dir,
+                                        struct user *user)
+{
+       return user->timestamp +
+               dir->timeout_secs - MAX_CLOCK_DRIFT_SECS >= ioloop_time;
+}
+
+struct user_directory *user_directory_init(unsigned int timeout_secs)
+{
+       struct user_directory *dir;
+
+       dir = i_new(struct user_directory, 1);
+       dir->timeout_secs = timeout_secs;
+       dir->hash = hash_table_create(default_pool, default_pool,
+                                     0, NULL, NULL);
+       i_array_init(&dir->iters, 8);
+       return dir;
+}
+
+void user_directory_deinit(struct user_directory **_dir)
+{
+       struct user_directory *dir = *_dir;
+
+       *_dir = NULL;
+
+       i_assert(array_count(&dir->iters) == 0);
+
+       while (dir->head != NULL)
+               user_free(dir, dir->head);
+       hash_table_destroy(&dir->hash);
+       array_free(&dir->iters);
+       i_free(dir);
+}
+
+struct user_directory_iter *
+user_directory_iter_init(struct user_directory *dir)
+{
+       struct user_directory_iter *iter;
+
+       iter = i_new(struct user_directory_iter, 1);
+       iter->dir = dir;
+       iter->pos = dir->head;
+       array_append(&dir->iters, &iter, 1);
+       return iter;
+}
+
+struct user *user_directory_iter_next(struct user_directory_iter *iter)
+{
+       struct user *user;
+
+       user = iter->pos;
+       if (user == NULL)
+               return FALSE;
+
+       iter->pos = user->next;
+       return user;
+}
+
+void user_directory_iter_deinit(struct user_directory_iter **_iter)
+{
+       struct user_directory_iter *iter = *_iter;
+       struct user_directory_iter *const *iters;
+       unsigned int i, count;
+
+       *_iter = NULL;
+
+       iters = array_get(&iter->dir->iters, &count);
+       for (i = 0; i < count; i++) {
+               if (iters[i] == iter) {
+                       array_delete(&iter->dir->iters, i, 1);
+                       break;
+               }
+       }
+       i_free(iter);
+}
diff --git a/src/director/user-directory.h b/src/director/user-directory.h
new file mode 100644 (file)
index 0000000..ad5df09
--- /dev/null
@@ -0,0 +1,47 @@
+#ifndef USER_DIRECTORY_H
+#define USER_DIRECTORY_H
+
+struct user {
+       /* sorted by time */
+       struct user *prev, *next;
+
+       /* first 32 bits of MD5(username). collisions are quite unlikely, but
+          even if they happen it doesn't matter - the users are just
+          redirected to same server */
+       unsigned int username_hash;
+       unsigned int timestamp;
+
+       struct mail_host *host;
+};
+
+/* Create a new directory. Users are dropped if their time gets older
+   than timeout_secs. */
+struct user_directory *user_directory_init(unsigned int timeout_secs);
+void user_directory_deinit(struct user_directory **dir);
+
+/* Look up username from directory. Returns NULL if not found. */
+struct user *user_directory_lookup(struct user_directory *dir,
+                                  unsigned int username_hash);
+/* Add a user to directory and return it. */
+struct user *
+user_directory_add(struct user_directory *dir, unsigned int username_hash,
+                  struct mail_host *host, time_t timestamp);
+/* Refresh user's timestamp */
+void user_directory_refresh(struct user_directory *dir, struct user *user);
+
+/* Remove all users that have pointers to given host */
+void user_directory_remove_host(struct user_directory *dir,
+                               struct mail_host *host);
+
+unsigned int user_directory_get_username_hash(const char *username);
+
+/* Returns TRUE if user still potentially has connections. */
+bool user_directory_user_has_connections(struct user_directory *dir,
+                                        struct user *user);
+
+struct user_directory_iter *
+user_directory_iter_init(struct user_directory *dir);
+struct user *user_directory_iter_next(struct user_directory_iter *iter);
+void user_directory_iter_deinit(struct user_directory_iter **iter);
+
+#endif
index 2387a293a12e7affc580b4cbceb72550000aa15b..025bedbf08914114fe4ce63737a7a909431dfa6a 100644 (file)
@@ -45,6 +45,7 @@ doveadm_DEPENDENCIES = \
 doveadm_SOURCES = \
        doveadm.c \
        doveadm-auth.c \
+       doveadm-director.c \
        doveadm-dump.c \
        doveadm-dump-index.c \
        doveadm-dump-log.c \
diff --git a/src/doveadm/doveadm-director.c b/src/doveadm/doveadm-director.c
new file mode 100644 (file)
index 0000000..2fde7a1
--- /dev/null
@@ -0,0 +1,208 @@
+/* Copyright (c) 2009-2010 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "network.h"
+#include "istream.h"
+#include "doveadm.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+struct director_context {
+       const char *socket_path;
+       struct istream *input;
+};
+
+extern struct doveadm_cmd doveadm_cmd_director[];
+
+static void
+director_send(struct director_context *ctx, const char *data)
+{
+       if (write(i_stream_get_fd(ctx->input), data, strlen(data)) < 0)
+               i_fatal("write(%s) failed: %m", ctx->socket_path);
+}
+
+static void director_connect(struct director_context *ctx)
+{
+#define DIRECTOR_HANDSHAKE_EXPECTED "VERSION\tdirector-doveadm\t1\t"
+#define DIRECTOR_HANDSHAKE DIRECTOR_HANDSHAKE_EXPECTED"0\n"
+       const char *line;
+       int fd;
+
+       fd = net_connect_unix(ctx->socket_path);
+       if (fd == -1)
+               i_fatal("net_connect_unix(%s) failed: %m", ctx->socket_path);
+       net_set_nonblock(fd, FALSE);
+
+       ctx->input = i_stream_create_fd(fd, (size_t)-1, TRUE);
+       director_send(ctx, DIRECTOR_HANDSHAKE);
+
+       line = i_stream_read_next_line(ctx->input);
+       if (line == NULL)
+               i_fatal("%s disconnected", ctx->socket_path);
+       if (strncmp(line, DIRECTOR_HANDSHAKE_EXPECTED,
+                   strlen(DIRECTOR_HANDSHAKE_EXPECTED)) != 0) {
+               i_fatal("%s not a compatible director-doveadm socket",
+                       ctx->socket_path);
+       }
+}
+
+static void director_disconnect(struct director_context *ctx)
+{
+       if (ctx->input->stream_errno != 0)
+               i_fatal("read(%s) failed: %m", ctx->socket_path);
+       i_stream_destroy(&ctx->input);
+}
+
+static struct director_context *
+cmd_director_init(int argc, char *argv[], unsigned int cmd_idx)
+{
+       struct director_context *ctx;
+       int c;
+
+       ctx = t_new(struct director_context, 1);
+       ctx->socket_path = PKG_RUNDIR"/director-admin";
+
+       while ((c = getopt(argc, argv, "a:")) > 0) {
+               switch (c) {
+               case 'a':
+                       ctx->socket_path = optarg;
+                       break;
+               default:
+                       help(&doveadm_cmd_director[cmd_idx]);
+               }
+       }
+       director_connect(ctx);
+       return ctx;
+}
+
+static void cmd_director_status(int argc, char *argv[])
+{
+       struct director_context *ctx;
+       const char *line, *const *args;
+
+       ctx = cmd_director_init(argc, argv, 0);
+       fprintf(stderr, "%-20s vhosts  users\n", "mail server ip");
+       director_send(ctx, "HOST-LIST\n");
+       while ((line = i_stream_read_next_line(ctx->input)) != NULL) {
+               if (*line == '\0')
+                       break;
+               T_BEGIN {
+                       args = t_strsplit(line, "\t");
+                       if (str_array_length(args) >= 3) {
+                               printf("%-20s %6s %6s\n",
+                                      args[0], args[1], args[2]);
+                       }
+               } T_END;
+       }
+       director_disconnect(ctx);
+}
+
+static void cmd_director_add(int argc, char *argv[])
+{
+       struct director_context *ctx;
+       struct ip_addr *ips;
+       unsigned int i, ips_count, vhost_count = -1U;
+       struct ip_addr ip;
+       const char *host, *cmd, *line;
+
+       ctx = cmd_director_init(argc, argv, 0);
+       host = argv[optind++];
+       if (host == NULL)
+               help(&doveadm_cmd_director[1]);
+       if (argv[optind] != NULL) {
+               if (str_to_uint(argv[optind++], &vhost_count) < 0)
+                       help(&doveadm_cmd_director[1]);
+       }
+       if (argv[optind] != NULL)
+               help(&doveadm_cmd_director[1]);
+
+       if (net_addr2ip(host, &ip) == 0) {
+               ips = &ip;
+               ips_count = 1;
+       } else {
+               if (net_gethostbyname(host, &ips, &ips_count) < 0)
+                       i_fatal("gethostname(%s) failed: %m", host);
+       }
+
+       for (i = 0; i < ips_count; i++) {
+               cmd = vhost_count == -1U ?
+                       t_strdup_printf("HOST-SET\t%s\n",
+                                       net_ip2addr(&ips[i])) :
+                       t_strdup_printf("HOST-SET\t%s\t%u\n",
+                                       net_ip2addr(&ips[i]), vhost_count);
+               director_send(ctx, cmd);
+       }
+       for (i = 0; i < ips_count; i++) {
+               line = i_stream_read_next_line(ctx->input);
+               if (line == NULL || strcmp(line, "OK") != 0) {
+                       fprintf(stderr, "%s: %s\n", net_ip2addr(&ips[i]),
+                               line == NULL ? "failed" : line);
+               } else if (doveadm_verbose) {
+                       printf("%s: OK\n", net_ip2addr(&ips[i]));
+               }
+       }
+       if (i != ips_count)
+               i_fatal("director add failed");
+       director_disconnect(ctx);
+}
+
+static void cmd_director_remove(int argc, char *argv[])
+{
+       struct director_context *ctx;
+       struct ip_addr *ips;
+       unsigned int i, ips_count;
+       struct ip_addr ip;
+       const char *host, *line;
+
+       ctx = cmd_director_init(argc, argv, 0);
+       host = argv[optind++];
+       if (host == NULL || argv[optind] != NULL)
+               help(&doveadm_cmd_director[2]);
+
+       if (net_addr2ip(host, &ip) == 0) {
+               ips = &ip;
+               ips_count = 1;
+       } else {
+               if (net_gethostbyname(host, &ips, &ips_count) < 0)
+                       i_fatal("gethostname(%s) failed: %m", host);
+       }
+
+       for (i = 0; i < ips_count; i++) {
+               director_send(ctx,
+                       t_strdup_printf("HOST-REMOVE\t%s\n", net_ip2addr(&ip)));
+       }
+       for (i = 0; i < ips_count; i++) {
+               line = i_stream_read_next_line(ctx->input);
+               if (line == NULL || strcmp(line, "OK") != 0) {
+                       fprintf(stderr, "%s: %s\n", net_ip2addr(&ips[i]),
+                               line == NULL ? "failed" :
+                               (strcmp(line, "NOTFOUND") == 0 ?
+                                "doesn't exist" : line));
+               } else if (doveadm_verbose) {
+                       printf("%s: removed\n", net_ip2addr(&ips[i]));
+               }
+       }
+       if (i != ips_count)
+               i_fatal("director remove failed");
+       director_disconnect(ctx);
+}
+
+struct doveadm_cmd doveadm_cmd_director[] = {
+       { cmd_director_status, "director status",
+         "[-a <director socket path>]", NULL },
+       { cmd_director_add, "director add",
+         "[-a <director socket path>] <host> [<vhost count>]", NULL },
+       { cmd_director_remove, "director remove",
+         "[-a <director socket path>] <host>", NULL }
+};
+
+
+void doveadm_register_director_commands(void)
+{
+       unsigned int i;
+
+       for (i = 0; i < N_ELEMENTS(doveadm_cmd_director); i++)
+               doveadm_register_cmd(&doveadm_cmd_director[i]);
+}
index aba9df99da575c965098b56d8be95538ca7b91f7..f069021454c4b26e103e3108a836efb5b5d3406e 100644 (file)
@@ -209,6 +209,7 @@ int main(int argc, char *argv[])
        i_array_init(&doveadm_cmds, 32);
        for (i = 0; i < N_ELEMENTS(doveadm_commands); i++)
                doveadm_register_cmd(doveadm_commands[i]);
+       doveadm_register_director_commands();
        doveadm_mail_init();
        doveadm_load_modules();
 
index ba844ea543f83964b5c3dace8985f6888b524e3e..4ec0e46e2ebd10ea8ab8ef984809b636bdf7011a 100644 (file)
@@ -29,5 +29,6 @@ void usage(void) ATTR_NORETURN;
 void help(const struct doveadm_cmd *cmd);
 
 const char *unixdate2str(time_t timestamp);
+void doveadm_register_director_commands(void);
 
 #endif