/* fud.c -- long-lived finger information provider
 *
 * 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: fud.c,v 1.59 2010/06/28 12:04:16 brong Exp $
 */

#include <config.h>

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <syslog.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <errno.h>
#include <pwd.h>

#include "acl.h"
#include "assert.h"
#include "mboxlist.h"
#include "global.h"
#include "exitcodes.h"
#include "imap_err.h"
#include "mailbox.h"
#include "map.h"
#include "mboxname.h"
#include "proc.h"
#include "seen.h"
#include "xmalloc.h"
#include "xstrlcpy.h"
#include "xstrlcat.h"

#define REQ_OK		0
#define REQ_DENY	1
#define REQ_UNK		2

extern int optind;

/* current namespace */
static struct namespace fud_namespace;

/* config.c info.  note that technically we may need partition data, but
 * only if we're not on a frontend, so we won't flat-out require it here */
const int config_need_data = 0;

int handle_request(const char *who, const char *name, 
		   struct sockaddr_storage sfrom, socklen_t sfromsiz);

void send_reply(struct sockaddr_storage sfrom, socklen_t sfromsiz, int status,
		const char *user, const char *mbox,
		int numrecent, time_t lastread, time_t lastarrived);

int soc = 0; /* inetd (master) has handed us the port as stdin */

char who[16];

#define MAXLOGNAME 16		/* should find out for real */
#define MAXDOMNAME 20		/* should find out for real */

int begin_handling(void)
{
        struct sockaddr_storage sfrom;
        socklen_t sfromsiz = sizeof(sfrom);
        int r;
        char    buf[MAXLOGNAME + MAXDOMNAME + MAX_MAILBOX_BUFFER];
        char    username[MAXLOGNAME + MAXDOMNAME + 1];
        char    mbox[MAX_MAILBOX_BUFFER];
        char    *q;
        int     off;
	int     maxuserlen = MAXLOGNAME;

	if (config_virtdomains) 
	    maxuserlen += MAXDOMNAME + 1; /* @ + DOM */

        while(1) {
            /* For safety */
            memset(buf, 0, sizeof(buf));
            memset(username, 0, sizeof(username));
            memset(mbox, 0, sizeof(mbox));

	    if (signals_poll() == SIGHUP) {
		/* caught a SIGHUP, return */
		return 0;
	    }
            r = recvfrom(soc, buf, 511, 0, 
			 (struct sockaddr *) &sfrom, &sfromsiz);
            if (r == -1) {
		return(errno);
	    }
            for(off = 0; buf[off] != '|' && off < maxuserlen; off++);
            if(off > 0 && off < maxuserlen) {
		strncpy(username,buf,off);
		username[off] = '\0';
            } else {
		continue;
            }

	    /* Copy what is past the | to the mailbox name */
            q = buf + off + 1;
            strlcpy(mbox, q, sizeof(mbox));

            handle_request(username,mbox,sfrom,sfromsiz);
        }

	/* never reached */
}

void shut_down(int code) __attribute__((noreturn));
void shut_down(int code)
{
    in_shutdown = 1;

    seen_done();
    mboxlist_close();
    mboxlist_done();
    closelog();
    cyrus_done();
    exit(code);
}


/*
 * run once when process is forked;
 * MUST NOT exit directly; must return with non-zero error code
 */
int service_init(int argc, char **argv, char **envp)
{
    if (geteuid() == 0) fatal("must run as the Cyrus user", EC_USAGE);

    setproctitle_init(argc, argv, envp);

    signals_set_shutdown(&shut_down);

    mboxlist_init(0);
    mboxlist_open(NULL);

    return 0;
}

/* Called by service API to shut down the service */
void service_abort(int error)
{
    shut_down(error);
}

int service_main(int argc __attribute__((unused)),
		 char **argv __attribute__((unused)),
		 char **envp __attribute__((unused)))
{
    int r = 0; 

    /* Set namespace */
    if ((r = mboxname_init_namespace(&fud_namespace, 1)) != 0) {
	syslog(LOG_ERR, "%s", error_message(r));
	fatal(error_message(r), EC_CONFIG);
    }

    r = begin_handling();

    shut_down(r);
    return 0;
}

static void cyrus_timeout(int signo __attribute__((unused)))
{
    return;
}

static int setsigalrm(int enable)

{
    struct sigaction action;
    
    sigemptyset(&action.sa_mask);
    
    action.sa_flags = 0;

    if(enable) {
	action.sa_handler = cyrus_timeout;
    } else {
	action.sa_handler = SIG_IGN;
    }
    
    if (sigaction(SIGALRM, &action, NULL) < 0) {
	syslog(LOG_ERR, "installing SIGALRM handler: sigaction: %m");
	return -1;
    }
    
    return 0;
}


/* Send a proxy request to the backend, send their reply to sfrom */
int do_proxy_request(const char *who, const char *name,
		     const char *backend_host,
		     struct sockaddr_storage sfrom, socklen_t sfromsiz) 
{
    char tmpbuf[1024];
    int rc;
    int csoc = -1;
    int error;
    struct sockaddr_storage cin, cout;
    struct addrinfo hints, *res0, *res;
    socklen_t cinsiz, coutsiz;
    static char *backend_port = NULL; /* fud port */

    /* Open a UDP socket to the Cyrus mail server */
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = PF_UNSPEC;
    hints.ai_socktype = SOCK_DGRAM;
    if (!backend_port) {
	backend_port = "fud";
	error = getaddrinfo(backend_host, backend_port, &hints, &res0);
	if (error == EAI_SERVICE) {
	    backend_port = "4201"; /* default fud port */
	    error = getaddrinfo(backend_host, backend_port, &hints, &res0);
	}
    } else
	error = getaddrinfo(backend_host, backend_port, &hints, &res0);
    if (error != 0) {
	send_reply(sfrom, sfromsiz, REQ_UNK, who, name, 0, 0, 0);
	rc = IMAP_SERVER_UNAVAILABLE;
	goto done;
    }
    
    /*
     * XXX: Since UDP is used, we cannot use an IPv6->IPv4 fallback
     * strategy, here.  So, when we can use same address family with
     * incoming packet, just try it.  When same address family is not
     * found in DNS, we try another one.
     */
    csoc = -1;
    for (res = res0; res; res = res->ai_next) {
	if (res->ai_family == sfrom.ss_family) {
	    csoc = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
	    break;
	}
    }
    if (csoc < 0) {
	for (res = res0; res; res = res->ai_next) {
	    if (res->ai_family != sfrom.ss_family &&
		(res->ai_family == AF_INET || res->ai_family == AF_INET6)) {
		csoc = socket(res->ai_family, res->ai_socktype,
			      res->ai_protocol);
		break;
	    }
	}
    }
    if (csoc < 0) {
	send_reply(sfrom, sfromsiz, REQ_UNK, who, name, 0, 0, 0);
	rc = IMAP_SERVER_UNAVAILABLE;
	freeaddrinfo(res0);	
	goto done;
    }

    memcpy(&cin, res->ai_addr, res->ai_addrlen);
    cinsiz = res->ai_addrlen;
    freeaddrinfo(res0);

    /* Write a Cyrus query into *tmpbuf */
    memset (tmpbuf, '\0', sizeof(tmpbuf));
    snprintf (tmpbuf, sizeof(tmpbuf), "%s|%s", who, name);

    /* Send the query and wait for a reply */
    sendto (csoc, tmpbuf, strlen (tmpbuf), 0,
	    (struct sockaddr *) &cin, cinsiz);
    memset (tmpbuf, '\0', strlen (tmpbuf));

    if (setsigalrm(1) < 0) {
	rc = IMAP_SERVER_UNAVAILABLE;
	send_reply(sfrom, sfromsiz, REQ_UNK, who, name, 0, 0, 0);
	goto done;
    }

    rc = 0;
    coutsiz = sizeof(cout);
    alarm (1);
    rc = recvfrom (csoc, tmpbuf, sizeof(tmpbuf), 0,
		   (struct sockaddr *) &cout, &coutsiz);
    alarm (0);

    setsigalrm(0);  /* Failure isn't really terrible here */

    if (rc < 1) {
	rc = IMAP_SERVER_UNAVAILABLE;
	send_reply(sfrom, sfromsiz, REQ_UNK, who, name, 0, 0, 0);
	goto done;
    }

    /* Send reply back */
    /* rc is size */
    sendto(soc, tmpbuf, rc, 0, (struct sockaddr *) &sfrom, sfromsiz);
    rc = 0;

 done:
    if(csoc != -1) close(csoc);
    return rc;
}

int handle_request(const char *who, const char *name,
		   struct sockaddr_storage sfrom, socklen_t sfromsiz)
{
    int r;
    struct mailbox *mailbox = NULL;
    time_t lastread;
    time_t lastarrived;
    unsigned recentuid;
    unsigned numrecent;
    char mboxname[MAX_MAILBOX_BUFFER];
    struct mboxlist_entry mbentry;
    struct auth_state *mystate;
    int internalseen;

    numrecent = 0;
    lastread = 0;
    lastarrived = 0;

    r = (*fud_namespace.mboxname_tointernal)(&fud_namespace,name,who,mboxname);
    if (r) return r; 

    r = mboxlist_lookup(mboxname, &mbentry, NULL);
    if(r || mbentry.mbtype & MBTYPE_RESERVE) {
	send_reply(sfrom, sfromsiz, REQ_UNK, who, name, 0, 0, 0);
	return r;
    }

    mystate = auth_newstate("anonymous");

    if(mbentry.mbtype & MBTYPE_REMOTE) {
	char *p = NULL;

	/* xxx hide that we are storing partitions */
	p = strchr(mbentry.partition, '!');
	if(p) *p = '\0';

	/* Check the ACL */
	if(cyrus_acl_myrights(mystate, mbentry.acl) & ACL_USER0) {
	    /* We want to proxy this one */
	    auth_freestate(mystate);
	    return do_proxy_request(who, name, mbentry.partition, sfrom, sfromsiz);
	} else {
	    /* Permission Denied */
	    auth_freestate(mystate);
	    send_reply(sfrom, sfromsiz, REQ_DENY, who, name, 0, 0, 0);
	    return 0;
	}
    }

    /*
     * Open/lock header 
     */
    r = mailbox_open_irl(mboxname, &mailbox);
    if (r) {
        send_reply(sfrom, sfromsiz, REQ_UNK, who, name, 0, 0, 0);
	return r; 
    }

    if (mboxname_isusermailbox(mboxname, 0)) {
	int myrights = cyrus_acl_myrights(mystate, mailbox->acl);
	if (!(myrights & ACL_USER0)) {
	    auth_freestate(mystate);
	    mailbox_close(&mailbox);
	    send_reply(sfrom, sfromsiz, REQ_DENY, who, name, 0, 0, 0);
	    return 0;
	}
    }
    auth_freestate(mystate);
   
    internalseen = mailbox_internal_seen(mailbox, who);
    if (internalseen) {
	lastread = mailbox->i.recenttime;
	recentuid = mailbox->i.recentuid;
    } else {
	struct seen *seendb = NULL;
	struct seendata sd;
	r = seen_open(who, 0, &seendb);
	if (!r) r = seen_read(seendb, mailbox->uniqueid, &sd);
	seen_close(&seendb);

	if (r) {
	    /* Fake Data -- couldn't open seen database */
	    lastread = 0;
	    recentuid = 0;
	}
	else {
	    lastread = sd.lastread;
	    recentuid = sd.lastuid;
	    seen_freedata(&sd);
	}
    }

    lastarrived = mailbox->i.last_appenddate;
    {
	struct index_record record;
	uint32_t recno;
	for (recno = 1; recno <= mailbox->i.num_records; recno++) {
	    if (mailbox_read_index_record(mailbox, recno, &record))
		continue;
	    if (record.system_flags & FLAG_EXPUNGED)
		continue;
	    if (record.uid > recentuid) numrecent++;
	}
    }

    mailbox_close(&mailbox);
    
    send_reply(sfrom, sfromsiz, REQ_OK, who, name, numrecent,
	       lastread, lastarrived);
    
    return(0);
}

void
send_reply(struct sockaddr_storage sfrom, socklen_t sfromsiz,
	   int status, const char *user, const char *mbox,
	   int numrecent, time_t lastread, time_t lastarrived)
{
    char buf[MAX_MAILBOX_PATH + 16 + 9];
    int siz;

    switch(status) {
        case REQ_DENY:
            sendto(soc,"PERMDENY",9,0,(struct sockaddr *) &sfrom, sfromsiz);       
            break;
        case REQ_OK:
            siz = snprintf(buf, sizeof(buf), "%s|%s|%d|%d|%d",
			   user,mbox,numrecent,(int) lastread,
			   (int)lastarrived);
            sendto(soc,buf,siz,0,(struct sockaddr *) &sfrom, sfromsiz);       
            break;
        case REQ_UNK:
            sendto(soc,"UNKNOWN",8,0,(struct sockaddr *) &sfrom, sfromsiz);
            break;
    } 
}

void fatal(const char* s, int code)
{
    static int recurse_code = 0;
    if (recurse_code) {
        /* We were called recursively. Just give up */
	exit(code);
    }
    recurse_code = code;
    syslog(LOG_ERR, "Fatal error: %s", s);

    shut_down(code);
}

void printstring(const char *s __attribute__((unused)))
{
    /* needed to link against annotate.o */
    fatal("printstring() executed, but its not used for FUD!",
	  EC_SOFTWARE);
}
