#include "config.h"
#include "cunit/unit.h"
#include "imap/imap_err.h"
#include "prot.h"
#include "retry.h"
#include "xmalloc.h"
#include "lib/libconfig.h"
#include "lib/libcyr_cfg.h"
#include "imap/spool.h"
#include "xunlink.h"

#define DBDIR       "test-dbdir"
#define DELIVERED   "Fri, 29 Oct 2010 13:07:07 +1100"
#define FIRST_RX    "Fri, 29 Oct 2010 13:05:01 +1100"
#define SECOND_RX   "Fri, 29 Oct 2010 13:03:03 +1100"
#define THIRD_RX    "Fri, 29 Oct 2010 13:01:01 +1100"
#define SENT        "Thu, 28 Oct 2010 18:37:26 +1100"
#define HFROM       "Fred Bloggs <fbloggs@fastmail.fm>"
#define HFROMFOLD   "Fred Bloggs\r\n <fbloggs@fastmail.fm>"
#define HFROM2      "Antoine Lavoisier <lavoisier@chemistry.fr>"
#define HTO         "Sarah Jane Smith <sjsmith@gmail.com>"
#define HDATE       SENT
#define HSUBJECT    "Simple testing email"
#define HMESSAGEID  "<fake1000@fastmail.fm>"
#define HRECEIVED1  "from mail.quux.com (mail.quux.com [10.0.0.1]) by mail.gmail.com (Software); " FIRST_RX
#define HRECEIVED2  "from mail.bar.com (mail.bar.com [10.0.0.1]) by mail.quux.com (Software); " SECOND_RX
#define HRECEIVED3  "from mail.fastmail.fm (mail.fastmail.fm [10.0.0.1]) by mail.bar.com (Software); " THIRD_RX

static int set_up(void)
{
    /* need basic configuration for parseheader */
    libcyrus_config_setstring(CYRUSOPT_CONFIG_DIR, DBDIR);
    config_read_string(
        "configdirectory: "DBDIR"/conf\n"
    );

    return 0;
}

static int tear_down(void)
{
    int r;

    config_reset();

    r = system("rm -rf " DBDIR);

    return r;
}

static void test_simple(void)
{
    hdrcache_t cache;
    const char **val;

    cache = spool_new_hdrcache();
    CU_ASSERT_PTR_NOT_NULL(cache);

    val = spool_getheader(cache, "Nonesuch");
    CU_ASSERT_PTR_NULL(val);
    val = spool_getheader(cache, "From");
    CU_ASSERT_PTR_NULL(val);
    val = spool_getheader(cache, "fRoM");
    CU_ASSERT_PTR_NULL(val);
    val = spool_getheader(cache, "from");
    CU_ASSERT_PTR_NULL(val);

    spool_cache_header(xstrdup("From"), xstrdup(HFROM), cache);
    val = spool_getheader(cache, "Nonesuch");
    CU_ASSERT_PTR_NULL(val);
    val = spool_getheader(cache, "From");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HFROM);
    CU_ASSERT_PTR_NULL(val[1]);
    val = spool_getheader(cache, "fRoM");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HFROM);
    CU_ASSERT_PTR_NULL(val[1]);
    val = spool_getheader(cache, "from");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HFROM);
    CU_ASSERT_PTR_NULL(val[1]);

    spool_cache_header(xstrdup("To"), xstrdup(HTO), cache);
    spool_cache_header(xstrdup("Date"), xstrdup(HDATE), cache);
    spool_cache_header(xstrdup("Subject"), xstrdup(HSUBJECT), cache);
    spool_cache_header(xstrdup("Message-ID"), xstrdup(HMESSAGEID), cache);
    spool_cache_header(xstrdup("Received"), xstrdup(HRECEIVED1), cache);
    spool_cache_header(xstrdup("Received"), xstrdup(HRECEIVED2), cache);
    spool_cache_header(xstrdup("Received"), xstrdup(HRECEIVED3), cache);

    val = spool_getheader(cache, "Nonesuch");
    CU_ASSERT_PTR_NULL(val);
    val = spool_getheader(cache, "From");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HFROM);
    CU_ASSERT_PTR_NULL(val[1]);
    val = spool_getheader(cache, "fRoM");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HFROM);
    CU_ASSERT_PTR_NULL(val[1]);
    val = spool_getheader(cache, "from");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HFROM);
    CU_ASSERT_PTR_NULL(val[1]);

    val = spool_getheader(cache, "To");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HTO);
    CU_ASSERT_PTR_NULL(val[1]);

    val = spool_getheader(cache, "Subject");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HSUBJECT);
    CU_ASSERT_PTR_NULL(val[1]);

    val = spool_getheader(cache, "message-id");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HMESSAGEID);
    CU_ASSERT_PTR_NULL(val[1]);

    val = spool_getheader(cache, "received");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HRECEIVED1);
    CU_ASSERT_STRING_EQUAL(val[1], HRECEIVED2);
    CU_ASSERT_STRING_EQUAL(val[2], HRECEIVED3);
    CU_ASSERT_PTR_NULL(val[3]);

    spool_free_hdrcache(cache);
}

static void test_fill(void)
{
    static const char MSG[] =
"From: " HFROM "\r\n"
"To: " HTO "\r\n"
"Date: " HDATE "\r\n"
"Subject: " HSUBJECT "\r\n"
"Message-ID: " HMESSAGEID "\r\n"
"Received: " HRECEIVED1 "\r\n"
"Received: " HRECEIVED2 "\r\n"
"Received: " HRECEIVED3 "\r\n"
"\r\n"
"Hello, World\r\n";

    hdrcache_t cache;
    const char **val;
    int fd;
    char tempfile[32];
    int r;
    struct protstream *pin;
    FILE *fout;

    /* Setup @pin to point to the start of a file open for (at least)
     * reading containing the message. */
    strcpy(tempfile, "/tmp/spooltestAXXXXXX");
    fd = mkstemp(tempfile);
    CU_ASSERT(fd >= 0);
    r = retry_write(fd, MSG, sizeof(MSG)-1);
    CU_ASSERT_EQUAL(r, sizeof(MSG)-1);
    lseek(fd, SEEK_SET, 0);
    pin = prot_new(fd, /*read*/0);
    CU_ASSERT_PTR_NOT_NULL(pin);

    /* Setup @fout to ignore data written to it */
    fout = fopen("/dev/null", "w");
    CU_ASSERT_PTR_NOT_NULL(fout);

    cache = spool_new_hdrcache();
    CU_ASSERT_PTR_NOT_NULL(cache);

    /* TODO: test non-NULL skipheaders */
    r = spool_fill_hdrcache(pin, fout, cache, NULL);
    CU_ASSERT_EQUAL(r, 0);

    val = spool_getheader(cache, "Nonesuch");
    CU_ASSERT_PTR_NULL(val);
    val = spool_getheader(cache, "From");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HFROM);
    CU_ASSERT_PTR_NULL(val[1]);
    val = spool_getheader(cache, "fRoM");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HFROM);
    CU_ASSERT_PTR_NULL(val[1]);
    val = spool_getheader(cache, "from");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HFROM);
    CU_ASSERT_PTR_NULL(val[1]);

    val = spool_getheader(cache, "To");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HTO);
    CU_ASSERT_PTR_NULL(val[1]);

    val = spool_getheader(cache, "Subject");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HSUBJECT);
    CU_ASSERT_PTR_NULL(val[1]);

    val = spool_getheader(cache, "message-id");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HMESSAGEID);
    CU_ASSERT_PTR_NULL(val[1]);

    val = spool_getheader(cache, "received");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HRECEIVED1);
    CU_ASSERT_STRING_EQUAL(val[1], HRECEIVED2);
    CU_ASSERT_STRING_EQUAL(val[2], HRECEIVED3);
    CU_ASSERT_PTR_NULL(val[3]);

    spool_free_hdrcache(cache);
    fclose(fout);
    prot_free(pin);
    xunlink(tempfile);
}

static void test_folded_headers(void)
{
    static const char MSG[] =
"From: " HFROMFOLD "\r\n"                   /* mid-value folding */
"Message-ID:\r\n " HMESSAGEID "\r\n"        /* leading whitespace is folded */
"\r\n"
"Hello, World\r\n";

    hdrcache_t cache;
    const char **val;
    int fd;
    char tempfile[32];
    int r;
    struct protstream *pin;
    FILE *fout;

    /* Setup @pin to point to the start of a file open for (at least)
     * reading containing the message. */
    strcpy(tempfile, "/tmp/spooltestAXXXXXX");
    fd = mkstemp(tempfile);
    CU_ASSERT(fd >= 0);
    r = retry_write(fd, MSG, sizeof(MSG)-1);
    CU_ASSERT_EQUAL(r, sizeof(MSG)-1);
    lseek(fd, SEEK_SET, 0);
    pin = prot_new(fd, /*read*/0);
    CU_ASSERT_PTR_NOT_NULL(pin);

    /* Setup @fout to ignore data written to it */
    fout = fopen("/dev/null", "w");
    CU_ASSERT_PTR_NOT_NULL(fout);

    cache = spool_new_hdrcache();
    CU_ASSERT_PTR_NOT_NULL(cache);

    /* TODO: test non-NULL skipheaders */
    r = spool_fill_hdrcache(pin, fout, cache, NULL);
    CU_ASSERT_EQUAL(r, 0);

    val = spool_getheader(cache, "Nonesuch");
    CU_ASSERT_PTR_NULL(val);
    val = spool_getheader(cache, "From");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HFROM);
    CU_ASSERT_PTR_NULL(val[1]);
    val = spool_getheader(cache, "fRoM");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HFROM);
    CU_ASSERT_PTR_NULL(val[1]);
    val = spool_getheader(cache, "from");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HFROM);
    CU_ASSERT_PTR_NULL(val[1]);

    val = spool_getheader(cache, "message-id");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HMESSAGEID);
    CU_ASSERT_PTR_NULL(val[1]);

    spool_free_hdrcache(cache);
    fclose(fout);
    prot_free(pin);
    xunlink(tempfile);
}

static void test_empty_headers(void)
{
    static const char MSG[] =
"From: " HFROM "\r\n"
"Message-ID: " HMESSAGEID "\r\n"
"Empty1:\r\n"                               /* not even a leading space */
"Empty2: \r\n"                              /* leading space */
"Empty3:\r\n \r\n"                          /* folded leading space */
"Empty4:\r"                                 /* same, with bare CR */
"Empty5: \r"
"Empty6:\r \r"
"Empty7:\n"                                 /* same, with bare LF */
"Empty8: \n"
"Empty9:\n \n"
"\r\n"
"Hello, World\r\n";

    hdrcache_t cache;
    const char **val;
    int fd;
    char tempfile[32];
    int r;
    struct protstream *pin;
    FILE *fout;

    /* Setup @pin to point to the start of a file open for (at least)
     * reading containing the message. */
    strcpy(tempfile, "/tmp/spooltestAXXXXXX");
    fd = mkstemp(tempfile);
    CU_ASSERT(fd >= 0);
    r = retry_write(fd, MSG, sizeof(MSG)-1);
    CU_ASSERT_EQUAL(r, sizeof(MSG)-1);
    lseek(fd, SEEK_SET, 0);
    pin = prot_new(fd, /*read*/0);
    CU_ASSERT_PTR_NOT_NULL(pin);

    /* Setup @fout to ignore data written to it */
    fout = fopen("/dev/null", "w");
    CU_ASSERT_PTR_NOT_NULL(fout);

    cache = spool_new_hdrcache();
    CU_ASSERT_PTR_NOT_NULL(cache);

    /* TODO: test non-NULL skipheaders */
    r = spool_fill_hdrcache(pin, fout, cache, NULL);
    CU_ASSERT_EQUAL(r, 0);

    val = spool_getheader(cache, "Nonesuch");
    CU_ASSERT_PTR_NULL(val);
    val = spool_getheader(cache, "From");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HFROM);
    CU_ASSERT_PTR_NULL(val[1]);
    val = spool_getheader(cache, "fRoM");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HFROM);
    CU_ASSERT_PTR_NULL(val[1]);
    val = spool_getheader(cache, "from");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HFROM);
    CU_ASSERT_PTR_NULL(val[1]);

    val = spool_getheader(cache, "message-id");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HMESSAGEID);
    CU_ASSERT_PTR_NULL(val[1]);

    val = spool_getheader(cache, "empty1");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], "");
    CU_ASSERT_PTR_NULL(val[1]);

    val = spool_getheader(cache, "empty2");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], "");
    CU_ASSERT_PTR_NULL(val[1]);

    val = spool_getheader(cache, "empty3");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], "");
    CU_ASSERT_PTR_NULL(val[1]);

    val = spool_getheader(cache, "empty4");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], "");
    CU_ASSERT_PTR_NULL(val[1]);

    val = spool_getheader(cache, "empty5");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], "");
    CU_ASSERT_PTR_NULL(val[1]);

    val = spool_getheader(cache, "empty6");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], "");
    CU_ASSERT_PTR_NULL(val[1]);

    val = spool_getheader(cache, "empty7");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], "");
    CU_ASSERT_PTR_NULL(val[1]);

    val = spool_getheader(cache, "empty8");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], "");
    CU_ASSERT_PTR_NULL(val[1]);

    val = spool_getheader(cache, "empty9");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], "");
    CU_ASSERT_PTR_NULL(val[1]);

    spool_free_hdrcache(cache);
    fclose(fout);
    prot_free(pin);
    xunlink(tempfile);
}

/* BZ3640: headers with NULL bytes shall be rejected. */
static void test_fill_null(void)
{
    static const char MSG[] =
"From: " HFROM "\r\n"
"To: " HTO "\r\n"
"Date: " HDATE "\r\n"
"Subject: " HSUBJECT "\r\n"
"Message-ID: " HMESSAGEID "\r\n"
"Received: " HRECEIVED1 "\r\n"
"Received:\0" HRECEIVED2 "\r\n"
"Received: " HRECEIVED3 "\r\n"
"\r\n"
"Hello, World\r\n";

    hdrcache_t cache;
    int fd;
    char tempfile[32];
    int r;
    struct protstream *pin;
    FILE *fout;

    /* Setup @pin to point to the start of a file open for (at least)
     * reading containing the message. */
    strcpy(tempfile, "/tmp/spooltestAXXXXXX");
    fd = mkstemp(tempfile);
    CU_ASSERT(fd >= 0);
    r = retry_write(fd, MSG, sizeof(MSG)-1);
    CU_ASSERT_EQUAL(r, sizeof(MSG)-1);
    lseek(fd, SEEK_SET, 0);
    pin = prot_new(fd, /*read*/0);
    CU_ASSERT_PTR_NOT_NULL(pin);

    /* Setup @fout to ignore data written to it */
    fout = fopen("/dev/null", "w");
    CU_ASSERT_PTR_NOT_NULL(fout);

    cache = spool_new_hdrcache();
    CU_ASSERT_PTR_NOT_NULL(cache);

    r = spool_fill_hdrcache(pin, fout, cache, NULL);
    CU_ASSERT_EQUAL(r, IMAP_MESSAGE_CONTAINSNULL);

    spool_free_hdrcache(cache);
    fclose(fout);
    prot_free(pin);
    xunlink(tempfile);
}

/* BZ3386: insert more unique headers than the internal limit of 4009
 * headers, and see what happens. */
static void test_bz3386(void)
{
    hdrcache_t cache;
#define N 5000
    int i;
    char name[32];
    char body[128];
    char body2[128];    /* use a different buffer Just In Case */
    const char **val;

    cache = spool_new_hdrcache();
    CU_ASSERT_PTR_NOT_NULL(cache);

    for (i = 0 ; i < N ; i++) {
        snprintf(name, sizeof(name), "X-Foo-%d-%c", i, 'A'+(i%26));
        snprintf(body, sizeof(body), "value %d %c", i, 'A'+(i%26));
        spool_cache_header(xstrdup(name), xstrdup(body), cache);
    }

    strcpy(body, "Old Buffer");

    for (i = 0 ; i < N ; i++) {
        snprintf(name, sizeof(name), "X-Foo-%d-%c", i, 'A'+(i%26));
        snprintf(body2, sizeof(body2), "value %d %c", i, 'A'+(i%26));
        val = spool_getheader(cache, name);
        CU_ASSERT_PTR_NOT_NULL(val);
        CU_ASSERT_STRING_EQUAL(val[0], body2);
        CU_ASSERT_PTR_NULL(val[1]);
    }

    spool_free_hdrcache(cache);
#undef N
}

static void test_replace(void)
{
#define FOOBAR1     "If music be the foo of love"
#define FOOBAR2     "Foo glorious foo"
#define FOOBAR3     "Foo is an important part of a balanced diet"
                    /* apologies to Fran Liebowitz */
#define FOOBAR4     "Foo and Drug Administration"
    hdrcache_t cache;
    const char **val;

    cache = spool_new_hdrcache();
    CU_ASSERT_PTR_NOT_NULL(cache);

    val = spool_getheader(cache, "From");
    CU_ASSERT_PTR_NULL(val);

    /* replace a single line */
    spool_cache_header(xstrdup("From"), xstrdup(HFROM), cache);
    val = spool_getheader(cache, "From");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HFROM);
    CU_ASSERT_PTR_NULL(val[1]);

    spool_replace_header(xstrdup("From"), xstrdup(HFROM2), cache);
    val = spool_getheader(cache, "From");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], HFROM2);
    CU_ASSERT_PTR_NULL(val[1]);

    /* replace multiple lines with one */
    spool_cache_header(xstrdup("X-FooBar"), xstrdup(FOOBAR1), cache);
    spool_cache_header(xstrdup("X-FooBar"), xstrdup(FOOBAR2), cache);
    spool_cache_header(xstrdup("X-FooBar"), xstrdup(FOOBAR3), cache);
    val = spool_getheader(cache, "X-FooBar");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], FOOBAR1);
    CU_ASSERT_STRING_EQUAL(val[1], FOOBAR2);
    CU_ASSERT_STRING_EQUAL(val[2], FOOBAR3);
    CU_ASSERT_PTR_NULL(val[3]);

    spool_replace_header(xstrdup("X-FooBar"), xstrdup(FOOBAR4), cache);
    val = spool_getheader(cache, "X-FooBar");
    CU_ASSERT_PTR_NOT_NULL(val);
    CU_ASSERT_STRING_EQUAL(val[0], FOOBAR4);
    CU_ASSERT_PTR_NULL(val[1]);

    spool_free_hdrcache(cache);
#undef FOOBAR1
#undef FOOBAR2
#undef FOOBAR3
#undef FOOBAR4
}
/* vim: set ft=c: */
