/* Remotely purge old/too big articles
 *
 * 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.
 */

#include <config.h>

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#include <netinet/in.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/file.h>
#include <sysexits.h>
#include <syslog.h>

#include <pwd.h>

#include "prot.h"
#include "lib/times.h"

#include "imclient.h"
#include "util.h"
#include "xmalloc.h"

#include "readconfig.h"

#define SECS_IN_DAY (24*60*60)

#define NOTFINISHED 0
#define IMAP_OK 1
#define IMAP_NO 2
#define IMAP_BAD 3
#define IMAP_EOF 4

/* for statistical purposes */
typedef struct mbox_stats_s {

    int total;         /* total including those deleted */
    int total_bytes;
    int deleted;
    int deleted_bytes;

} mbox_stats_t;

typedef struct uid_list_s {

    unsigned long *list;
    int allocsize;
    int size;

} uid_list_t;

/* globals for callback functions */
static int days = -1;
int size = -1;

static int current_mbox_exists = 0;

static int verbose = 0;
static int noop = 0;
static char *username = NULL;
//char *authname = NULL;
static char *realm = NULL;

static struct imclient *imclient_conn;

static int cmd_done;
static char *cmd_resp = NULL;

static FILE *configstream;

static void spew(int level, const char *fmt, ...)
{
    va_list ap;
    char buf[1024];

    if (verbose < level) return;

    va_start(ap, fmt);
    vsnprintf(buf, sizeof buf, fmt, ap);
    va_end(ap);

    if (verbose) {
        printf("%s\n", buf);
    }
    syslog(LOG_DEBUG, "%s", buf);
}

/* libcyrus makes us define this */
EXPORTED void fatal(const char *s, int code)
{
    if (cmd_resp) {
        syslog(LOG_ERR, "fatal error: %s (%s)", s, cmd_resp);
        fprintf(stderr, "fatal error: %s (%s)\n", s, cmd_resp);
    } else {
        syslog(LOG_ERR, "fatal error: %s", s);
        fprintf(stderr, "fatal error: %s\n", s);
    }
    exit(code);
}

/***********************
 * Parse a mech list of the form: ... AUTH=foo AUTH=bar ...
 *
 * Return: string with mechs separated by spaces
 *
 ***********************/

typedef struct capabilities_s {

  char *mechs;

  /* 0 = false; 1 = true */
  int starttls;
  int logindisabled;

} capabilities_t;



static capabilities_t *parsecapabilitylist(char *str)
{
    char *tmp;
    int num=0;
    capabilities_t *ret=(capabilities_t *) xmalloc(sizeof(capabilities_t));
    ret->mechs = (char *)xmalloc(strlen(str)+1);
    ret->starttls=0;
    ret->logindisabled=0;

    /* check for stattls */
    if (strstr(str,"STARTTLS")!=NULL) {
        ret->starttls=1;
    }

    /* check for login being disabled */
    if (strstr(str,"LOGINDISABLED")!=NULL) {
        ret->logindisabled=1;
    }

    strcpy(ret->mechs,"");

    while ((tmp=strstr(str,"AUTH="))!=NULL) {
        char *end=tmp+5;
        tmp+=5;

        while(((*end)!=' ') && ((*end)!='\0'))
            end++;

        (*end)='\0';

        /* add entry to list */
        if (num>0)
            strcat(ret->mechs," ");
        strcat(ret->mechs, tmp);
        num++;

        /* reset the string */
        str=end+1;
    }

    return ret;
}

/*
 * IMAP command completion callback
 */
static void callback_capability(struct imclient *imclient,
                                void *rock,
                                struct imclient_reply *reply)

{
    (void)imclient;
    char *s;
    capabilities_t **caps = (capabilities_t **) rock;

    s = reply->text;

    *caps = parsecapabilitylist(s);
}

/*
 * IMAP command completion callback
 */
static void
callback_finish(struct imclient *imclient,
                void *rock,
                struct imclient_reply *reply)
{
    (void)imclient; (void)rock;
    if (!strcmp(reply->keyword, "OK")) {
        cmd_done = IMAP_OK;
    } else if (!strcmp(reply->keyword, "NO")) {
        cmd_resp = reply->text;
        cmd_done = IMAP_NO;
    }
    else if (!strcmp(reply->keyword, "BAD")) {
        cmd_resp = reply->text;
        cmd_done = IMAP_BAD;
    }
    else if (!strcmp(reply->keyword, "EOF")) {
        syslog(LOG_ERR, "connection closed prematurely");
        cmd_done = IMAP_EOF;
    }
    else {
        printf("Huh?\n");
        cmd_done = IMAP_BAD;
    }
}

/*
 * Callback to deal with untagged LIST/LSUB data
 */
extern void
callback_list(struct imclient *imclient,
              void *rock,
              struct imclient_reply *reply);

/*
void print_stats(mbox_stats_t *stats)
{
    syslog(LOG_INFO, "total messages considered %d deleted %d",
           stats->total, stats->deleted);
    printf("total messages    \t\t %d\n",stats->total);
    printf("deleted messages  \t\t %d\n",stats->deleted);
    printf("remaining messages\t\t %d\n\n",stats->total - stats->deleted);
}
*/

static void
callback_exists(struct imclient *imclient,
               void *rock,
               struct imclient_reply *reply)
{
    (void)imclient; (void)rock;
    current_mbox_exists = reply->msgno;
}

static void
callback_search(struct imclient *imclient,
               void *rock,
               struct imclient_reply *reply)
{
    (void)imclient;
    uid_list_t *uids = (uid_list_t *) rock;
    const char *s;
    uint32_t num;

    s = reply->text;

    while (Uisdigit(*s)) {
        if (parseuint32(s, &s, &num)) break;

        if (uids->size >= uids->allocsize)
        {
            if (uids->allocsize) uids->allocsize *= 2;
            else uids->allocsize = 250;

            uids->list = xrealloc(uids->list,
                                  sizeof(unsigned long) * uids->allocsize);
        }

        uids->list[uids->size] = num;
        uids->size++;

        if (*s == '\0') break;
        s++;
    }
}

static int send_delete(const char *mbox, const char *uidlist)
{
    imclient_send(imclient_conn, callback_finish, imclient_conn,
                  "UID STORE %a +FLAGS.SILENT (\\Deleted)", uidlist);
    cmd_done = NOTFINISHED;
    while (cmd_done == NOTFINISHED) {
        imclient_processoneevent(imclient_conn);
    }
    if (cmd_done == IMAP_OK) return 0;
    else if (cmd_done == IMAP_NO) {
        syslog(LOG_ERR, "%s can't mark messages deleted: %s",
               mbox, cmd_resp ? cmd_resp : "");
        return -1;
    }
    else fatal("marking message deleted", EX_TEMPFAIL);
}

static void mark_all_deleted(const char *mbox, uid_list_t *list, mbox_stats_t *stats)
{
    int i;
    char buf[1024];
    int pos;
    unsigned long run_start;
    int first_time;
    unsigned long *A = list->list;
    int r;

    if (list->size == 0) return;

    /* we send blocks of 500 or so characters */
    i = 0;

    pos = 0; first_time = 1;
    run_start = A[i++];
    r = 0;
    for (; i < list->size && r == 0; i++) {
        if (A[i] == A[i-1] + 1)
            continue; /* continue this run */
        if (first_time) {
            first_time = 0;
        } else {
            buf[pos++] = ',';
        }
        if (run_start != A[i-1]) {
            /* run contains more than one entry */
            pos += sprintf(buf + pos, "%lu:%lu", run_start, A[i-1]);
        } else {
            /* singleton */
            pos += sprintf(buf + pos, "%lu", A[i-1]);
        }
        if (pos > 500) {
            r = send_delete(mbox, buf);
            pos = 0; first_time = 1;
        }
        run_start = A[i];
    }

    if (!r) {
        /* handle the last entry */
        if (!first_time) {
            buf[pos++] = ',';
        }
        if (run_start != A[i-1]) {
            sprintf(buf + pos, "%lu:%lu", run_start, A[i-1]);
        } else {
            sprintf(buf + pos, "%lu", A[i-1]);
        }

        /* send out the last one */
        send_delete(mbox, buf);

        stats->deleted += list->size;
    }
}

/* we don't check what comes in on matchlen and category, should we? */
static int purge_me(char *name, time_t when)
{
    mbox_stats_t   stats;
    char search_string[200];
    static uid_list_t uidlist;
    struct tm *my_tm;

    if (when == 0) return 0;

    my_tm = gmtime(&when);

    snprintf(search_string,sizeof(search_string),
             "BEFORE %d-%s-%d",
             my_tm->tm_mday,
             monthname[my_tm->tm_mon],
             1900+my_tm->tm_year);

    if (noop) {
        printf("%s: %s\n", name, search_string);
        return 0;
    }

    memset(&stats, '\0', sizeof(mbox_stats_t));

    spew(2, "%s selecting", name);

    /* select mailbox */
    imclient_addcallback(imclient_conn,
                         "EXISTS", CALLBACK_NUMBERED, callback_exists,
                         (void *)0, (char *)0);
    imclient_send(imclient_conn, callback_finish, (void *)imclient_conn,
                  "%a %s", "SELECT", name);

    cmd_done = NOTFINISHED;

    while (cmd_done == NOTFINISHED) {
        imclient_processoneevent(imclient_conn);
    }

    spew(2, "%s selecting", name);

    if (cmd_done == IMAP_NO) {
        syslog(LOG_ERR, "unable to select %s: %s", name, cmd_resp);
        return 0;
    } else if (cmd_done != IMAP_OK) {
        fatal("selecting mailbox", EX_TEMPFAIL);
    }

    stats.total = current_mbox_exists;

    spew(2, "%s exists %d", name, current_mbox_exists);

    /* Only search if there are actually messages in the mailbox! */
    if(current_mbox_exists) {
        /* make out list of uids */
        uidlist.size = 0;               /* reset to 0 */

        spew(3, "%s searching for messages %s", name, search_string);

        imclient_addcallback(imclient_conn,
                             "SEARCH", 0, callback_search,
                             (void *)&uidlist, (char *)0);
        imclient_send(imclient_conn, callback_finish, (void *)imclient_conn,
                      "UID SEARCH %a", search_string);


        cmd_done = NOTFINISHED;
        while (cmd_done == NOTFINISHED) {
            imclient_processoneevent(imclient_conn);
        }
        if (cmd_done != IMAP_OK) {
            fatal("UID Search failed", EX_TEMPFAIL);
        }

        if (uidlist.size > 0) {
            mark_all_deleted(name, &uidlist, &stats);
        }
    }

 after_search:
    /* close mailbox */
    imclient_send(imclient_conn, callback_finish, (void *)imclient_conn,
                  "CLOSE");

    cmd_done = NOTFINISHED;
    while (cmd_done == NOTFINISHED) {
        imclient_processoneevent(imclient_conn);
    }

    if (cmd_done != IMAP_OK) {
        fatal("unable to CLOSE mailbox", EX_TEMPFAIL);
    }

    if(current_mbox_exists) {
        spew(1, "%s exists %d deleted %d",
             name, current_mbox_exists, uidlist.size);
    } else {
        spew(1, "%s exists %d (skipped)",
             name, current_mbox_exists, uidlist.size);
    }

    return 0;
}



static int purge_all(void)
{
    int num = 0;
    int ret = 0;

    while (ret == 0) {
        ret = ExpireExists(num);

        if (ret == 0)
            purge_me(GetExpireName(num), GetExpireTime(num));

        num++;
    }

    return 0;
}

static void do_list(char *matchstr)
{
    imclient_send(imclient_conn, callback_finish, (void *)imclient_conn,
                  "%a %s %s", "LIST", "*",
                  matchstr);

    cmd_done = NOTFINISHED;

    while (cmd_done == NOTFINISHED) {
        imclient_processoneevent(imclient_conn);
    }

    if (cmd_done!=IMAP_OK) fatal("unable to LIST mailboxes", EX_TEMPFAIL);
}

/*
 *  What we were given on the command line might just be a path or might not have an extension etc...
 */

static char *parseconfigpath(char *str)
{
    char *ret;

    /* if it ends with a '/' add expire.ctl */

    if (str[strlen(str)-1] == '/')
    {
        ret = (char *) xmalloc(strlen(str)+strlen("expire.ctl")+1);
        strcpy(ret,str);
        strcat(ret,"expire.ctl");

        return ret;
    }

    return str;
}

static void remote_purge(char *configpath, char **matches)
{
    char *name;

    imclient_addcallback(imclient_conn,
                         "LIST", 0, callback_list,
                         (void *)0, (char *)0);

    if (matches[0]==NULL) {
        syslog(LOG_WARNING, "matching all mailboxes for possible purge");
        spew(1, "matching all mailboxes");

        do_list("*");
    } else {
        while (matches[0]!=NULL) {
            spew(0, "matching %s", matches[0]);
            do_list(matches[0]);
            matches++;
        }
    }

    spew(1, "completed list");

    if (configpath!=NULL) {
        name = parseconfigpath(configpath);

        configstream = fopen(name,"r");

        if (configstream == NULL)
            fatal("unable to open config file", EX_CONFIG);

        EXPreadfile(configstream);
        /* ret val */
    } else {
        artificial_matchall(days);
    }

    purge_all();
}

/* didn't give correct parameters; let's exit */
static void usage(void)
{
  printf("Usage: remotepurge [options] hostname [[match1] ... ]\n");
  printf("  -p port  : port to use\n");
  printf("  -k #     : minimum protection layer required\n");
  printf("  -l #     : max protection layer (0=none; 1=integrity; etc)\n");
  printf("  -u user  : authorization name to use\n");
  printf("  -v       : verbose\n");
  printf("  -n       : don't actually purge\n");
  printf("  -m mech  : SASL mechanism to use (\"login\" for LOGIN)\n");
  printf("  -r realm : realm\n");

  printf("  -e expire.ctl : use expire.ctl file (specify full path)\n");

  printf("  -d days  : purge all message <days> old\n");

  exit(EX_USAGE);
}

int main(int argc, char **argv)
{
    char *mechanism=NULL;
    char servername[1024];
    char *expirectlfile = NULL;

    int maxssf = 128;
    int minssf = 0;
    int c;

    char *tls_keyfile="";(void)tls_keyfile;
    char *port = "imap";
    int dotls=0;
    int r;
    capabilities_t *capabilitylist;

    /* look at all the extra args */
    while ((c = getopt(argc, argv, "d:vne:k:l:p:u:a:m:t:")) != EOF)
        switch (c) {
        case 'd':
            days = atoi(optarg);
            break;
        case 'e':
            expirectlfile = optarg;
            break;
        case 'v':
            verbose++;
            break;
        case 'k':
            minssf=atoi(optarg);
            break;
        case 'l':
            maxssf=atoi(optarg);
            break;
        case 'p':
            port = optarg;
            break;
        case 'u':
            username = optarg;
            break;
        case 'm':
            mechanism=optarg;
            break;
        case 'r':
            realm=optarg;
            break;
        case 't':
            dotls=1;
            tls_keyfile=optarg;
            break;
        case 'n':
            noop = 1;
            break;
        case '?':
        default:
            usage();
            break;
        }

    (void)dotls;(void)mechanism;
    if (optind >= argc) usage();


    if ((days==-1) && (expirectlfile == NULL))
    {
        printf("Must specify expire.ctl file OR days old OR bytes large\n\n");
        usage();
    }

    /* next to last arg is server name */
    strncpy(servername, argv[optind], 1023);

    r = imclient_connect (&imclient_conn, servername, port, NULL);

    if (r!=0) {
        fatal("imclient_connect()", EX_TEMPFAIL);
    }

    spew(0, "connected");

    /* get capabilities */
    imclient_addcallback(imclient_conn, "CAPABILITY", 0,
                         callback_capability, (void *) &capabilitylist,
                         (char *) 0);

    imclient_send(imclient_conn, callback_finish, NULL,
                  "CAPABILITY");

    cmd_done = 0;

    while (cmd_done == 0) {
        imclient_processoneevent(imclient_conn);
    }

    r = imclient_authenticate(imclient_conn,
                              capabilitylist->mechs,
                              "imap",
                              username,
                              minssf,
                              maxssf);

    if (r!=0) {
        fatal("imclient_authenticate()\n", EX_CONFIG);
    }

    spew(0, "authenticated");

    readconfig_init();

    remote_purge(expirectlfile, argv+(optind+1));

    spew(0, "done");

    exit(0);
}
