/* sievedir.c -- functions for managing scripts in a sievedir
 *
 * Copyright (c) 1994-2020 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>

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

#include <dirent.h>
#include <ctype.h>
#include <string.h>
#include <syslog.h>
#include <assert.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>

#include "assert.h"
#include "map.h"
#include "sievedir.h"
#include "util.h"
#include "xstrlcpy.h"

#ifdef USE_SIEVE
#include "sieve/bc_parse.h"
#include "sieve/sieve_interface.h"
#endif

EXPORTED int sievedir_foreach(const char *sievedir, unsigned flags,
                              int (*func)(const char *sievedir,
                                          const char *name, struct stat *sbuf,
                                          const char *target, void *rock),
                              void *rock)
{
    DIR *dp;
    struct dirent *dir;
    char path[PATH_MAX];
    int dir_len;
    int r = SIEVEDIR_OK;

    if ((dp = opendir(sievedir)) == NULL) {
        if (errno == ENOENT) return SIEVEDIR_OK;

        xsyslog(LOG_ERR, "IOERROR: can not open sieve directory",
                "path=<%s>", sievedir);
        return SIEVEDIR_IOERROR;
    }

    dir_len = snprintf(path, sizeof(path), "%s/", sievedir);

    while ((dir = readdir(dp)) != NULL) {
        const char *name = dir->d_name;
        char target[PATH_MAX] = "";
        struct stat sbuf;

        if (!strcmp(name, ".") || !strcmp(name, "..")) continue;

        strlcpy(path + dir_len, name, sizeof(path) - dir_len);

        if (lstat(path, &sbuf) < 0) continue;

        if (S_ISREG(sbuf.st_mode)) {
            if (flags) {
                const char *ext = strrchr(name, '.');

                if (!ext || strcmp(ext, SCRIPT_SUFFIX)) {
                    if (flags & SIEVEDIR_SCRIPTS_ONLY) {
                        /* ignore non-scripts */
                        continue;
                    }
                    if ((flags & SIEVEDIR_IGNORE_JUNK) &&
                        strcmpnull(ext, BYTECODE_SUFFIX)) {
                        /* ignore non-bytecode */
                        continue;
                    }
                }
            }
        }
        else if (flags & SIEVEDIR_SCRIPTS_ONLY) {
            /* ignore all other entries */
            continue;
        }
        else if (S_ISLNK(sbuf.st_mode)) {
            /* fetch link target */
            ssize_t tgt_len = readlink(path, target, sizeof(target) - 1);

            if (tgt_len > 0) target[tgt_len] = '\0';
        }
        else if (flags & SIEVEDIR_IGNORE_JUNK) {
            /* ignore all other entries */
            continue;
        }

        r = func(sievedir, name, &sbuf, target, rock);
        if (r != SIEVEDIR_OK) break;
    }

    closedir(dp);

    return r;
}

struct count_rock {
    int count;
    const char *myname;
};

static int count_cb(const char *sievedir __attribute__((unused)),
                    const char *name,
                    struct stat *sbuf __attribute__((unused)),
                    const char *link_target __attribute__((unused)),
                    void *rock)
{
    struct count_rock *crock = (struct count_rock *) rock;
    size_t name_len = strlen(name) - SCRIPT_SUFFIX_LEN;

    if (!crock->myname ||
        strlen(crock->myname) != name_len ||
        strncmp(crock->myname, name, name_len)) {
        /* and it's different from me */
        crock->count++;
    }

    return SIEVEDIR_OK;
}

/* counts the number of scripts user has that are DIFFERENT from name */
EXPORTED int sievedir_num_scripts(const char *sievedir, const char *name)
{
    struct count_rock crock = { 0, name };

    sievedir_foreach(sievedir, SIEVEDIR_SCRIPTS_ONLY, &count_cb, &crock);

    return crock.count;
}


EXPORTED struct buf *sievedir_get_script(const char *sievedir,
                                         const char *script)
{
    struct buf buf = BUF_INITIALIZER;

    buf_printf(&buf, "%s/%s", sievedir, script);

    int fd = open(buf_cstring(&buf), 0);
    buf_free(&buf);
    if (fd < 0) return NULL;

    buf_refresh_mmap(&buf, 1, fd, script, MAP_UNKNOWN_LEN, "sieve");

    close(fd);

    struct buf *ret = buf_new();

    buf_move(ret, &buf);

    return ret;
}
 
/* Everything but '/' and '\0' are valid. */
EXPORTED int sievedir_valid_name(const struct buf *name)
{
    size_t lup, len = buf_len(name);
    const char *ptr;

    /* must be at least one character long */
    if (len < 1) return 0;

    ptr = buf_base(name);

    for (lup = 0; lup < len; lup++) {
        if ((ptr[lup] == '/') || (ptr[lup] == '\0')) return 0;
    }

    return (lup < SIEVEDIR_MAX_NAME_LEN);
}

EXPORTED int sievedir_script_exists(const char *sievedir, const char *name)
{
    char path[PATH_MAX];
    struct stat sbuf;

    snprintf(path, sizeof(path), "%s/%s%s", sievedir, name, SCRIPT_SUFFIX);

    return ((stat(path, &sbuf) == 0) && S_ISREG(sbuf.st_mode));
}

EXPORTED const char *sievedir_get_active(const char *sievedir)
{
    static char target[PATH_MAX];
    char link[PATH_MAX];
    ssize_t tgt_len;

    snprintf(link, sizeof(link), "%s/%s", sievedir, DEFAULTBC_NAME);

    tgt_len = readlink(link, target, sizeof(target) - 1);

    if (tgt_len > BYTECODE_SUFFIX_LEN) {
        target[tgt_len - BYTECODE_SUFFIX_LEN] = '\0';
        return target;
    }
    else if (tgt_len == -1 && errno != ENOENT) {
        xsyslog(LOG_ERR, "IOERROR: failed to read active script link",
                "link=<%s>", link);
    }

    return NULL;
}

EXPORTED int sievedir_script_isactive(const char *sievedir, const char *name)
{
    if (!name) return 0;

    return (strcmpnull(name, sievedir_get_active(sievedir)) == 0);
}

EXPORTED int sievedir_activate_script(const char *sievedir, const char *name)
{
    char target[PATH_MAX];
    char active[PATH_MAX];
    char tmp[PATH_MAX+4];  /* +4 for ".NEW" */

    if (sievedir_script_isactive(sievedir, name)) {
        /* already active - nothing to do here */
        return SIEVEDIR_OK;
    }

    snprintf(target, sizeof(target), "%s%s", name, BYTECODE_SUFFIX);
    snprintf(active, sizeof(active), "%s/%s", sievedir, DEFAULTBC_NAME);
    snprintf(tmp, sizeof(tmp), "%s.NEW", active);

    /* N.B symlink() does NOT verify target for anything but string validity,
     * so activation of a nonexistent script will report success.
     */
    if (symlink(target, tmp) < 0) {
        xsyslog(LOG_ERR, "IOERROR: failed to create temp active script link",
                "target=<%s> link=<%s>", target, tmp);
        return SIEVEDIR_IOERROR;
    }

    if (rename(tmp, active) < 0) {
        xsyslog(LOG_ERR, "IOERROR: failed to rename active script link",
                "oldpath=<%s> newpath=<%s>", tmp, active);
        unlink(tmp);
        return SIEVEDIR_IOERROR;
    }

    return SIEVEDIR_OK;
}

EXPORTED int sievedir_deactivate_script(const char *sievedir)
{
    char active[PATH_MAX];

    snprintf(active, sizeof(active), "%s/defaultbc", sievedir);
    if (unlink(active) != 0 && errno != ENOENT) {
        xsyslog(LOG_ERR, "IOERROR: failed to delete active script link",
                "link=<%s>", active);
        return SIEVEDIR_IOERROR;
    }

    return SIEVEDIR_OK;
}

EXPORTED int sievedir_delete_script(const char *sievedir, const char *name)
{
    char path[PATH_MAX];
    int r;

    /* delete bytecode first, as its non-deterministic */
    snprintf(path, sizeof(path), "%s/%s%s", sievedir, name, BYTECODE_SUFFIX);
    r = unlink(path);
    if (r && errno != ENOENT) {
        xsyslog(LOG_ERR, "IOERROR: failed to delete bytecode file",
                "path=<%s>", path);
    }

    /* delete script file last, which determines result */
    snprintf(path, sizeof(path), "%s/%s%s", sievedir, name, SCRIPT_SUFFIX);
    r = unlink(path);
    if (r) {
        if (errno == ENOENT) return SIEVEDIR_NOTFOUND;

        xsyslog(LOG_ERR, "IOERROR: failed to delete script file",
                "path=<%s>", path);
        return SIEVEDIR_IOERROR;
    }

    return SIEVEDIR_OK;
}

EXPORTED int sievedir_rename_script(const char *sievedir,
                                    const char *oldname, const char *newname)
{
    /* rename script and bytecode; move active link */
    char oldpath[PATH_MAX], newpath[PATH_MAX];
    int r;

    snprintf(oldpath, sizeof(oldpath),
             "%s/%s%s", sievedir, oldname, SCRIPT_SUFFIX);
    snprintf(newpath, sizeof(oldpath),
             "%s/%s%s", sievedir, newname, SCRIPT_SUFFIX);
    r = rename(oldpath, newpath);
    if (r) {
        if (errno == ENOENT) return SIEVEDIR_NOTFOUND;

        xsyslog(LOG_ERR, "IOERROR: failed to rename script file",
                "oldpath=<%s> newpath=<%s>", oldpath, newpath);
        return SIEVEDIR_IOERROR;
    }

    snprintf(oldpath, sizeof(oldpath),
             "%s/%s%s", sievedir, oldname, BYTECODE_SUFFIX);
    snprintf(newpath, sizeof(newpath),
             "%s/%s%s", sievedir, newname, BYTECODE_SUFFIX);
    r = rename(oldpath, newpath);
    if (r) {
        xsyslog(LOG_ERR, "IOERROR: failed to rename bytecode file",
                "oldpath=<%s> newpath=<%s>", oldpath, newpath);
        return SIEVEDIR_IOERROR;
    }

    if (sievedir_script_isactive(sievedir, oldname)) {
        r = sievedir_activate_script(sievedir, newname);
    }

    return r;
}

#ifdef USE_SIEVE
EXPORTED int sievedir_put_script(const char *sievedir, const char *name,
                                 const char *content, char **errors)
{
    char new_path[PATH_MAX];
    FILE *f;

    /* parse the script */
    sieve_script_t *s = NULL;
    (void) sieve_script_parse_string(NULL, content, errors, &s);
    if (!s) return SIEVEDIR_INVALID;

    /* open a new file for the script */
    snprintf(new_path, sizeof(new_path),
             "%s/%s%s.NEW", sievedir, name, SCRIPT_SUFFIX);

    f = fopen(new_path, "w+");

    if (f == NULL) {
        xsyslog(LOG_ERR, "IOERROR: failed to open new script file",
                "newpath=<%s>", new_path);
        sieve_script_free(&s);
        return SIEVEDIR_IOERROR;
    }

    size_t i, content_len = strlen(content);
    int saw_cr = 0;

    /* copy data to file - replacing any lone CR or LF with the
     * CRLF pair so notify messages are SMTP compatible */
    for (i = 0; i < content_len; i++) {
        if (saw_cr) {
            if (content[i] != '\n') putc('\n', f);
        }
        else if (content[i] == '\n')
            putc('\r', f);

        putc(content[i], f);
        saw_cr = (content[i] == '\r');
    }
    if (saw_cr) putc('\n', f);

    fflush(f);
    fclose(f);

    /* generate the bytecode */
    bytecode_info_t *bc = NULL;
    if (sieve_generate_bytecode(&bc, s) == -1) {
        unlink(new_path);
        sieve_script_free(&s);
        return SIEVEDIR_FAIL;
    }

    /* open the new bytecode file */
    char new_bcpath[PATH_MAX];
    snprintf(new_bcpath, sizeof(new_bcpath),
             "%s/%s%s.NEW", sievedir, name, BYTECODE_SUFFIX);
    int fd = open(new_bcpath, O_CREAT | O_TRUNC | O_WRONLY, 0600);
    if (fd < 0) {
        xsyslog(LOG_ERR, "IOERROR: failed to open new bytecode file",
                "newpath=<%s>", new_bcpath);
        unlink(new_path);
        sieve_free_bytecode(&bc);
        sieve_script_free(&s);
        return SIEVEDIR_IOERROR;
    }

    /* emit the bytecode */
    if (sieve_emit_bytecode(fd, bc) == -1) {
        close(fd);
        unlink(new_path);
        unlink(new_bcpath);
        sieve_free_bytecode(&bc);
        sieve_script_free(&s);
        return SIEVEDIR_FAIL;
    }

    sieve_free_bytecode(&bc);
    sieve_script_free(&s);

    close(fd);

    /* rename */
    char path[PATH_MAX];
    snprintf(path, sizeof(path), "%s/%s%s", sievedir, name, SCRIPT_SUFFIX);
    int r = rename(new_path, path);
    if (r) {
        xsyslog(LOG_ERR, "IOERROR: failed to rename script file",
                "oldpath=<%s> newpath=<%s>", new_path, path);
    }
    else {
        snprintf(path, sizeof(path), "%s/%s%s", sievedir, name, BYTECODE_SUFFIX);
        r = rename(new_bcpath, path);
        if (r) {
            xsyslog(LOG_ERR, "IOERROR: failed to rename bytecode file",
                    "oldpath=<%s> newpath=<%s>", new_bcpath, path);
        }
    }

    return (r ? SIEVEDIR_IOERROR : SIEVEDIR_OK);
}
#endif /* USE_SIEVE */
