/* idled.c - daemon for handling IMAP IDLE notifications
*
* Copyright (c) 1994-2008 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
* $Id: idled.c,v 1.28 2010/01/06 17:01:32 murch Exp $
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <syslog.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <errno.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <signal.h>
#include <fcntl.h>
#include "idled.h"
#include "global.h"
#include "mboxlist.h"
#include "xmalloc.h"
#include "xstrlcpy.h"
#include "xstrlcat.h"
#include "hash.h"
#include "exitcodes.h"
/* global state */
const int config_need_data = 0;
extern int optind;
extern char *optarg;
static int verbose = 0;
static int debugmode = 0;
static time_t idle_timeout;
static int sigquit = 0;
struct ientry {
pid_t pid;
time_t itime;
struct ientry *next;
};
static struct hash_table itable;
static struct ientry *ifreelist;
static int itable_inc = 100;
void idle_done(char *mboxname, pid_t pid);
void fatal(const char *msg, int err)
{
if (debugmode) fprintf(stderr, "dying with %s %d\n",msg,err);
syslog(LOG_CRIT, "%s", msg);
syslog(LOG_NOTICE, "exiting");
cyrus_done();
exit(err);
}
static int mbox_count_cb(void *rockp,
const char *key __attribute__((unused)),
int keylen __attribute__((unused)),
const char *data __attribute__((unused)),
int datalen __attribute__((unused)))
{
int *ip = (int *) rockp;
(*ip)++;
return 0;
}
/* return a new 'ientry', either from the freelist or by malloc'ing it */
static struct ientry *get_ientry(void)
{
struct ientry *t;
if (!ifreelist) {
/* create child_table_inc more and add them to the freelist */
struct ientry *n;
int i;
n = xmalloc(itable_inc * sizeof(struct ientry));
ifreelist = n;
for (i = 0; i < itable_inc - 1; i++) {
n[i].next = n + (i + 1);
}
/* i == child_table_inc - 1, last item in block */
n[i].next = NULL;
}
t = ifreelist;
ifreelist = ifreelist->next;
return t;
}
/* remove pid from list of those idling on mboxname */
void idle_done(char *mboxname, pid_t pid)
{
struct ientry *t, *p = NULL;
t = (struct ientry *) hash_lookup(mboxname, &itable);
while (t && t->pid != pid) {
p = t;
t = t->next;
}
if (t) {
if (!p) {
/* first pid in the linked list */
p = t->next; /* remove node */
/* we just removed the data that the hash entry
was pointing to, so insert the new data */
hash_insert(mboxname, p, &itable);
}
else {
/* not the first pid in the linked list */
p->next = t->next; /* remove node */
}
t->next = ifreelist; /* add to freelist */
ifreelist = t;
}
}
void process_msg(idle_data_t *idledata)
{
struct ientry *t, *n;
switch (idledata->msg) {
case IDLE_INIT:
if (verbose || debugmode)
syslog(LOG_DEBUG, "imapd[%ld]: IDLE_INIT '%s'\n",
idledata->pid, idledata->mboxname);
/* add pid to list of those idling on mboxname */
t = (struct ientry *) hash_lookup(idledata->mboxname, &itable);
n = get_ientry();
n->pid = idledata->pid;
n->itime = time(NULL);
n->next = t;
hash_insert(idledata->mboxname, n, &itable);
break;
case IDLE_NOTIFY:
if (verbose || debugmode)
syslog(LOG_DEBUG, "IDLE_NOTIFY '%s'\n", idledata->mboxname);
/* send a message to all pids idling on mboxname */
t = (struct ientry *) hash_lookup(idledata->mboxname, &itable);
while (t) {
if ((t->itime + idle_timeout) < time(NULL)) {
/* This process has been idling for longer than the timeout
* period, so it probably died. Remove it from the list.
*/
if (verbose || debugmode)
syslog(LOG_DEBUG, " TIMEOUT %d\n", t->pid);
n = t;
t = t->next;
idle_done(idledata->mboxname, n->pid);
}
else { /* signal process to update */
if (verbose || debugmode)
syslog(LOG_DEBUG, " SIGUSR1 %d\n", t->pid);
kill(t->pid, SIGUSR1);
t = t->next;
}
}
break;
case IDLE_DONE:
if (verbose || debugmode)
syslog(LOG_DEBUG, "imapd[%ld]: IDLE_DONE '%s'\n",
idledata->pid, idledata->mboxname);
/* remove pid from list of those idling on mboxname */
idle_done(idledata->mboxname, idledata->pid);
break;
case IDLE_NOOP:
break;
default:
syslog(LOG_ERR, "unrecognized message: %lx", idledata->msg);
break;
}
}
void idle_alert(const char *key __attribute__((unused)),
void *data,
void *rock __attribute__((unused)))
{
struct ientry *t = (struct ientry *) data;
while (t) {
/* signal process to check ALERTs */
if (verbose || debugmode)
syslog(LOG_DEBUG, " SIGUSR2 %d\n", t->pid);
kill(t->pid, SIGUSR2);
t = t->next;
}
}
static void sighandler (int sig __attribute__((unused)))
{
sigquit = 1;
return;
}
int main(int argc, char **argv)
{
char shutdownfilename[1024];
char *p = NULL;
int opt;
int nmbox = 0;
int s, len;
struct sockaddr_un local;
idle_data_t idledata;
struct sockaddr_un from;
socklen_t fromlen;
mode_t oldumask;
fd_set read_set, rset;
int nfds;
struct timeval timeout;
pid_t pid;
int fd;
char *alt_config = NULL;
const char *idle_sock;
struct sigaction action;
p = getenv("CYRUS_VERBOSE");
if (p) verbose = atoi(p) + 1;
while ((opt = getopt(argc, argv, "C:d")) != EOF) {
switch (opt) {
case 'C': /* alt config file */
alt_config = optarg;
break;
case 'd': /* don't fork. debugging mode */
debugmode = 1;
break;
default:
fprintf(stderr, "invalid argument\n");
exit(EC_USAGE);
break;
}
}
/* fork unless we were given the -d option */
if (debugmode == 0) {
pid = fork();
if (pid == -1) {
perror("fork");
exit(1);
}
if (pid != 0) { /* parent */
exit(0);
}
}
/* child */
cyrus_init(alt_config, "idled", 0);
/* get name of shutdown file */
snprintf(shutdownfilename, sizeof(shutdownfilename), "%s/msg/shutdown",
config_dir);
/* Set inactivity timer (convert from minutes to seconds) */
idle_timeout = config_getint(IMAPOPT_TIMEOUT);
if (idle_timeout < 30) idle_timeout = 30;
idle_timeout *= 60;
/* count the number of mailboxes */
mboxlist_init(0);
mboxlist_open(NULL);
config_mboxlist_db->foreach(mbdb, "", 0, NULL, &mbox_count_cb,
&nmbox, NULL);
mboxlist_close();
mboxlist_done();
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
action.sa_handler = sighandler;
if (sigaction(SIGQUIT, &action, NULL) < 0) {
fatal("unable to install signal handler for %d: %m", SIGQUIT);
}
/* create idle table -- +1 to avoid a zero value */
construct_hash_table(&itable, nmbox + 1, 1);
ifreelist = NULL;
/* create socket we are going to use for listening */
if ((s = socket(AF_UNIX, SOCK_DGRAM, 0)) == -1) {
perror("socket");
cyrus_done();
exit(1);
}
/* bind it to a local file */
local.sun_family = AF_UNIX;
idle_sock = config_getstring(IMAPOPT_IDLESOCKET);
if (idle_sock) {
strlcpy(local.sun_path, idle_sock, sizeof(local.sun_path));
}
else {
strlcpy(local.sun_path, config_dir, sizeof(local.sun_path));
strlcat(local.sun_path, FNAME_IDLE_SOCK, sizeof(local.sun_path));
}
unlink(local.sun_path);
len = sizeof(local.sun_family) + strlen(local.sun_path) + 1;
oldumask = umask((mode_t) 0); /* for Linux */
if (bind(s, (struct sockaddr *)&local, len) == -1) {
perror("bind");
cyrus_done();
exit(1);
}
umask(oldumask); /* for Linux */
chmod(local.sun_path, 0777); /* for DUX */
/* get ready for select() */
FD_ZERO(&read_set);
FD_SET(s, &read_set);
nfds = s + 1;
for (;;) {
int n;
/* check for shutdown file */
if ((fd = open(shutdownfilename, O_RDONLY, 0)) != -1) {
/* signal all processes to shutdown */
if (verbose || debugmode)
syslog(LOG_DEBUG, "IDLE_ALERT\n");
hash_enumerate(&itable, idle_alert, NULL);
break;
}
if (sigquit) {
hash_enumerate(&itable, idle_alert, NULL);
break;
}
/* timeout for select is 1 second */
timeout.tv_sec = 1;
timeout.tv_usec = 0;
/* check for the next input */
rset = read_set;
n = select(nfds, &rset, NULL, NULL, &timeout);
if (n < 0 && errno == EAGAIN) continue;
if (n < 0 && errno == EINTR) continue;
if (n == -1) {
/* uh oh */
syslog(LOG_ERR, "select(): %m");
close(s);
fatal("select error",-1);
}
/* read on unix socket */
if (FD_ISSET(s, &rset)) {
fromlen = sizeof(from);
n = recvfrom(s, (void*) &idledata, sizeof(idle_data_t), 0,
(struct sockaddr *) &from, &fromlen);
if (n > 0) {
if (n <= IDLEDATA_BASE_SIZE ||
idledata.mboxname[n - 1 - IDLEDATA_BASE_SIZE] != '\0')
syslog(LOG_ERR, "Invalid message received, size=%d\n", n);
else
process_msg(&idledata);
}
} else {
/* log some sort of error */
}
}
cyrus_done();
exit(1);
}
void printstring(const char *s __attribute__((unused)))
{
/* needed to link against annotate.o */
fatal("printstring() executed, but its not used for POP3!",
EC_SOFTWARE);
}