/* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "fs-api.h"
#include "master-service.h"
#include "mail-index-modseq.h"
#include "mail-search-build.h"
#include "mailbox-list-private.h"
#include "index-pop3-uidl.h"
#include "dbox-mail.h"
#include "dbox-save.h"
#include "sdbox-file.h"
#include "sdbox-sync.h"
#include "sdbox-settings.h"
#include "sdbox-storage.h"

extern struct mail_storage dbox_storage, sdbox_storage;
extern struct mailbox sdbox_mailbox;
extern struct dbox_storage_vfuncs sdbox_dbox_storage_vfuncs;

static struct event_category event_category_sdbox = {
	.name = "sdbox",
	.parent = &event_category_storage,
};

static struct mail_storage *sdbox_storage_alloc(void)
{
	struct sdbox_storage *storage;
	pool_t pool;

	pool = pool_alloconly_create("sdbox storage", 512+256);
	storage = p_new(pool, struct sdbox_storage, 1);
	storage->storage.v = sdbox_dbox_storage_vfuncs;
	storage->storage.storage = sdbox_storage;
	storage->storage.storage.pool = pool;
	return &storage->storage.storage;
}

static int sdbox_storage_create(struct mail_storage *_storage,
				struct mail_namespace *ns,
				const char **error_r)
{
	struct dbox_storage *storage = DBOX_STORAGE(_storage);
	enum fs_properties props;

	if (dbox_storage_create(_storage, ns, error_r) < 0)
		return -1;

	if (storage->attachment_fs != NULL) {
		props = fs_get_properties(storage->attachment_fs);
		if ((props & FS_PROPERTY_RENAME) == 0) {
			*error_r = "mail_ext_attachment: "
				"FS driver doesn't support renaming";
			return -1;
		}
	}
	return 0;
}

static const char *
sdbox_storage_find_root_dir(const struct mail_namespace *ns)
{
	struct event *event = ns->user->event;
	const char *home, *path;

	if (ns->owner != NULL &&
	    mail_user_get_home(ns->owner, &home) > 0) {
		path = t_strconcat(home, "/sdbox", NULL);
		if (i_faccessat2(AT_FDCWD, path, R_OK | W_OK | X_OK, AT_EACCESS) == 0) {
			e_debug(event,
				"sdbox autodetect: root exists (%s)", path);
			return path;
		}
		e_debug(event,
			"sdbox autodetect: access(%s, rwx): failed: %m", path);
	}
	return NULL;
}

static bool
sdbox_storage_autodetect(const struct mail_namespace *ns,
			 const struct mail_storage_settings *mail_set,
			 const char **root_path_r,
			 const char **inbox_path_r ATTR_UNUSED)
{
	struct event *event = ns->user->event;
	struct stat st;
	const char *path, *root_dir;

	if (mail_set->mail_path[0] != '\0')
		root_dir = mail_set->mail_path;
	else {
		root_dir = sdbox_storage_find_root_dir(ns);
		if (root_dir == NULL) {
			e_debug(event,
				"sdbox autodetect: couldn't find root dir");
			return FALSE;
		}
	}

	/* NOTE: this check works for mdbox as well. we'll rely on the
	   autodetect ordering to catch mdbox before we get here. */
	path = t_strconcat(root_dir, "/"DBOX_MAILBOX_DIR_NAME, NULL);
	if (stat(path, &st) < 0) {
		e_debug(event, "sdbox autodetect: stat(%s) failed: %m", path);
		return FALSE;
	}

	if (!S_ISDIR(st.st_mode)) {
		e_debug(event, "sdbox autodetect: %s not a directory", path);
		return FALSE;
	}

	*root_path_r = root_dir;
	return TRUE;
}

static struct mailbox *
sdbox_mailbox_alloc(struct mail_storage *storage, struct mailbox_list *list,
		    const char *vname, enum mailbox_flags flags)
{
	struct sdbox_mailbox *mbox;
	struct index_mailbox_context *ibox;
	pool_t pool;

	/* dbox can't work without index files */
	flags &= ENUM_NEGATE(MAILBOX_FLAG_NO_INDEX_FILES);

	pool = pool_alloconly_create("sdbox mailbox", 1024*3);
	mbox = p_new(pool, struct sdbox_mailbox, 1);
	mbox->box = sdbox_mailbox;
	mbox->box.pool = pool;
	mbox->box.storage = storage;
	mbox->box.list = list;
	mbox->box.mail_vfuncs = &sdbox_mail_vfuncs;

	index_storage_mailbox_alloc(&mbox->box, vname, flags, MAIL_INDEX_PREFIX);

	ibox = INDEX_STORAGE_CONTEXT(&mbox->box);
	ibox->index_flags |= MAIL_INDEX_OPEN_FLAG_KEEP_BACKUPS |
		MAIL_INDEX_OPEN_FLAG_NEVER_IN_MEMORY;

	mbox->storage = SDBOX_STORAGE(storage);
	return &mbox->box;
}

int sdbox_read_header(struct sdbox_mailbox *mbox,
		      struct sdbox_index_header *hdr, bool log_error,
		      bool *need_resize_r)
{
	struct mail_index_view *view;
	const void *data;
	size_t data_size;
	int ret = 0;

	i_assert(mbox->box.opened);

	view = mail_index_view_open(mbox->box.index);
	mail_index_get_header_ext(view, mbox->hdr_ext_id,
				  &data, &data_size);
	if (data_size < SDBOX_INDEX_HEADER_MIN_SIZE &&
	    (!mbox->box.creating || data_size != 0)) {
		if (log_error) {
			mailbox_set_critical(&mbox->box,
				"sdbox: Invalid dbox header size");
		}
		ret = -1;
	} else {
		i_zero(hdr);
		if (data_size > 0)
			memcpy(hdr, data, I_MIN(data_size, sizeof(*hdr)));
		if (guid_128_is_empty(hdr->mailbox_guid))
			ret = -1;
		else {
			/* data is valid. remember it in case mailbox
			   is being reset */
			mail_index_set_ext_init_data(mbox->box.index,
						     mbox->hdr_ext_id,
						     hdr, sizeof(*hdr));
		}
	}
	mail_index_view_close(&view);
	*need_resize_r = data_size < sizeof(*hdr);
	return ret;
}

static void sdbox_update_header(struct sdbox_mailbox *mbox,
				struct mail_index_transaction *trans,
				const struct mailbox_update *update)
{
	struct sdbox_index_header hdr, new_hdr;
	bool need_resize;

	if (sdbox_read_header(mbox, &hdr, TRUE, &need_resize) < 0) {
		i_zero(&hdr);
		need_resize = TRUE;
	}

	new_hdr = hdr;

	if (update != NULL && !guid_128_is_empty(update->mailbox_guid)) {
		memcpy(new_hdr.mailbox_guid, update->mailbox_guid,
		       sizeof(new_hdr.mailbox_guid));
	} else if (guid_128_is_empty(new_hdr.mailbox_guid)) {
		guid_128_generate(new_hdr.mailbox_guid);
	}

	if (need_resize) {
		mail_index_ext_resize_hdr(trans, mbox->hdr_ext_id,
					  sizeof(new_hdr));
	}
	if (memcmp(&hdr, &new_hdr, sizeof(hdr)) != 0) {
		mail_index_update_header_ext(trans, mbox->hdr_ext_id, 0,
					     &new_hdr, sizeof(new_hdr));
	}
	memcpy(mbox->mailbox_guid, new_hdr.mailbox_guid,
	       sizeof(mbox->mailbox_guid));
}

int sdbox_mailbox_create_indexes(struct mailbox *box,
				 const struct mailbox_update *update,
				 struct mail_index_transaction *trans)
{
	struct sdbox_mailbox *mbox = SDBOX_MAILBOX(box);
	struct mail_index_transaction *new_trans = NULL;
	const struct mail_index_header *hdr;
	uint32_t uid_validity, uid_next;

	if (trans == NULL) {
		new_trans = mail_index_transaction_begin(box->view, 0);
		trans = new_trans;
	}

	hdr = mail_index_get_header(box->view);
	if (update != NULL && update->uid_validity != 0)
		uid_validity = update->uid_validity;
	else if (hdr->uid_validity != 0)
		uid_validity = hdr->uid_validity;
	else {
		/* set uidvalidity */
		uid_validity = dbox_get_uidvalidity_next(box->list);
	}

	if (hdr->uid_validity != uid_validity) {
		mail_index_update_header(trans,
			offsetof(struct mail_index_header, uid_validity),
			&uid_validity, sizeof(uid_validity), TRUE);
	}
	if (update != NULL && hdr->next_uid < update->min_next_uid) {
		uid_next = update->min_next_uid;
		mail_index_update_header(trans,
			offsetof(struct mail_index_header, next_uid),
			&uid_next, sizeof(uid_next), TRUE);
	}
	if (update != NULL && update->min_first_recent_uid != 0 &&
	    hdr->first_recent_uid < update->min_first_recent_uid) {
		uint32_t first_recent_uid = update->min_first_recent_uid;

		mail_index_update_header(trans,
			offsetof(struct mail_index_header, first_recent_uid),
			&first_recent_uid, sizeof(first_recent_uid), FALSE);
	}
	if (update != NULL && update->min_highest_modseq != 0 &&
	    mail_index_modseq_get_highest(box->view) <
	    					update->min_highest_modseq) {
		mail_index_modseq_enable(box->index);
		mail_index_update_highest_modseq(trans,
						 update->min_highest_modseq);
	}

	if (box->inbox_user && box->creating) {
		/* initialize pop3-uidl header when creating mailbox
		   (not on mailbox_update()) */
		index_pop3_uidl_set_max_uid(box, trans, 0);
	}

	sdbox_update_header(mbox, trans, update);
	if (new_trans != NULL) {
		if (mail_index_transaction_commit(&new_trans) < 0) {
			mailbox_set_index_error(box);
			return -1;
		}
	}
	return 0;
}

static const char *
sdbox_get_attachment_path_suffix(struct dbox_file *_file)
{
	struct sdbox_file *file = (struct sdbox_file *)_file;

	return t_strdup_printf("-%s-%u",
			guid_128_to_string(file->mbox->mailbox_guid),
			file->uid);
}

void sdbox_set_mailbox_corrupted(struct mailbox *box, const char *reason)
{
	struct sdbox_mailbox *mbox = SDBOX_MAILBOX(box);
	struct sdbox_index_header hdr;
	bool need_resize;

	mailbox_set_critical(box, "%s", reason);
	if (sdbox_read_header(mbox, &hdr, TRUE, &need_resize) < 0 ||
	    hdr.rebuild_count == 0)
		mbox->corrupted_rebuild_count = 1;
	else
		mbox->corrupted_rebuild_count = hdr.rebuild_count;
}

static void sdbox_set_file_corrupted(struct dbox_file *_file,
				     const char *reason)
{
	mail_storage_set_critical(&_file->storage->storage, "%s", reason);
	(void)dbox_file_fix(_file, 0);
}

static int sdbox_mailbox_alloc_index(struct sdbox_mailbox *mbox)
{
	struct sdbox_index_header hdr;

	if (index_storage_mailbox_alloc_index(&mbox->box) < 0)
		return -1;

	mbox->hdr_ext_id =
		mail_index_ext_register(mbox->box.index, "dbox-hdr",
					sizeof(struct sdbox_index_header), 0, 0);
	/* set the initialization data in case the mailbox is created */
	i_zero(&hdr);
	guid_128_generate(hdr.mailbox_guid);
	mail_index_set_ext_init_data(mbox->box.index, mbox->hdr_ext_id,
				     &hdr, sizeof(hdr));
	return 0;
}

static int sdbox_mailbox_open(struct mailbox *box)
{
	struct sdbox_mailbox *mbox = SDBOX_MAILBOX(box);
	struct sdbox_index_header hdr;
	bool need_resize;
	int existence_ret;

	if ((existence_ret = dbox_mailbox_check_existence(box)) < 0)
		return -1;

	if (sdbox_mailbox_alloc_index(mbox) < 0)
		return -1;

	if (dbox_mailbox_open(box) < 0)
		return -1;

	if (box->creating) {
		/* wait for mailbox creation to initialize the index */
		return 0;
	}

	if (box->list->mail_set->mail_index_path[0] != '\0' &&
	    existence_ret == 0) {
		/* If there is an separate mail_index_path configured but no
		   index files found for this mailbox, do an index rebuild */
		if (sdbox_sync(mbox, SDBOX_SYNC_FLAG_FORCE_REBUILD) < 0)
			return -1;
	}

	/* get/generate mailbox guid */
	if (sdbox_read_header(mbox, &hdr, FALSE, &need_resize) < 0) {
		/* looks like the mailbox is corrupted */
		(void)sdbox_sync(mbox, SDBOX_SYNC_FLAG_FORCE);
		if (sdbox_read_header(mbox, &hdr, TRUE, &need_resize) < 0)
			i_zero(&hdr);
	}

	if (guid_128_is_empty(hdr.mailbox_guid)) {
		/* regenerate it */
		if (sdbox_mailbox_create_indexes(box, NULL, NULL) < 0 ||
		    sdbox_read_header(mbox, &hdr, TRUE, &need_resize) < 0)
			return -1;
	}
	memcpy(mbox->mailbox_guid, hdr.mailbox_guid,
	       sizeof(mbox->mailbox_guid));
	return 0;
}

static void sdbox_mailbox_close(struct mailbox *box)
{
	struct sdbox_mailbox *mbox = SDBOX_MAILBOX(box);

	if (mbox->corrupted_rebuild_count != 0)
		(void)sdbox_sync(mbox, 0);

	dbox_mailbox_close_cleanup(box);
	dbox_mailbox_close(box);
}

static int
sdbox_mailbox_create(struct mailbox *box,
		     const struct mailbox_update *update, bool directory)
{
	struct sdbox_mailbox *mbox = SDBOX_MAILBOX(box);
	struct sdbox_index_header hdr;
	bool need_resize;

	if (dbox_mailbox_create(box, update, directory) < 0)
		return -1;
	if (directory || !guid_128_is_empty(mbox->mailbox_guid))
		return 0;

	/* another process just created the mailbox. read the mailbox_guid. */
	if (sdbox_read_header(mbox, &hdr, FALSE, &need_resize) < 0) {
		mailbox_set_critical(box,
			"sdbox: Failed to read newly created dbox header");
		return -1;
	}
	memcpy(mbox->mailbox_guid, hdr.mailbox_guid,
	       sizeof(mbox->mailbox_guid));
	i_assert(!guid_128_is_empty(mbox->mailbox_guid));
	return 0;
}

static int
sdbox_mailbox_get_metadata(struct mailbox *box,
			   enum mailbox_metadata_items items,
			   struct mailbox_metadata *metadata_r)
{
	struct sdbox_mailbox *mbox = SDBOX_MAILBOX(box);

	if (index_mailbox_get_metadata(box, items, metadata_r) < 0)
		return -1;
	if ((items & MAILBOX_METADATA_GUID) != 0) {
		memcpy(metadata_r->guid, mbox->mailbox_guid,
		       sizeof(metadata_r->guid));
	}
	return 0;
}

static int
dbox_mailbox_update(struct mailbox *box, const struct mailbox_update *update)
{
	if (!box->opened) {
		if (mailbox_open(box) < 0)
			return -1;
	}
	if (sdbox_mailbox_create_indexes(box, update, NULL) < 0)
		return -1;
	return index_storage_mailbox_update_common(box, update);
}

struct mail_storage sdbox_storage = {
	.name = SDBOX_STORAGE_NAME,
	.class_flags = MAIL_STORAGE_CLASS_FLAG_FILE_PER_MSG |
		MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_GUIDS |
		MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_SAVE_GUIDS |
		MAIL_STORAGE_CLASS_FLAG_BINARY_DATA,
	.event_category = &event_category_sdbox,

	.v = {
		sdbox_storage_alloc,
		sdbox_storage_create,
		dbox_storage_destroy,
		NULL,
		sdbox_storage_autodetect,
		sdbox_mailbox_alloc,
		NULL,
		mail_storage_list_index_rebuild,
	}
};

struct mail_storage dbox_storage = {
	.name = "dbox", /* alias */
	.class_flags = MAIL_STORAGE_CLASS_FLAG_FILE_PER_MSG,
	.event_category = &event_category_sdbox,
	.set_info = &sdbox_setting_parser_info,

	.v = {
		sdbox_storage_alloc,
		sdbox_storage_create,
		dbox_storage_destroy,
		NULL,
		sdbox_storage_autodetect,
		sdbox_mailbox_alloc,
		NULL,
		mail_storage_list_index_rebuild,
	}
};

struct mailbox sdbox_mailbox = {
	.v = {
		index_storage_is_readonly,
		index_storage_mailbox_enable,
		index_storage_mailbox_exists,
		sdbox_mailbox_open,
		sdbox_mailbox_close,
		index_storage_mailbox_free,
		sdbox_mailbox_create,
		dbox_mailbox_update,
		index_storage_mailbox_delete,
		index_storage_mailbox_rename,
		index_storage_get_status,
		sdbox_mailbox_get_metadata,
		index_storage_set_subscribed,
		index_storage_attribute_set,
		index_storage_attribute_get,
		index_storage_attribute_iter_init,
		index_storage_attribute_iter_next,
		index_storage_attribute_iter_deinit,
		index_storage_list_index_has_changed,
		index_storage_list_index_update_sync,
		sdbox_storage_sync_init,
		index_mailbox_sync_next,
		index_mailbox_sync_deinit,
		NULL,
		dbox_notify_changes,
		index_transaction_begin,
		index_transaction_commit,
		index_transaction_rollback,
		NULL,
		dbox_mail_alloc,
		index_storage_search_init,
		index_storage_search_deinit,
		index_storage_search_next_nonblock,
		index_storage_search_next_update_seq,
		index_storage_search_next_match_mail,
		sdbox_save_alloc,
		sdbox_save_begin,
		dbox_save_continue,
		sdbox_save_finish,
		sdbox_save_cancel,
		sdbox_copy,
		sdbox_transaction_save_commit_pre,
		sdbox_transaction_save_commit_post,
		sdbox_transaction_save_rollback,
		index_storage_is_inconsistent
	}
};

struct dbox_storage_vfuncs sdbox_dbox_storage_vfuncs = {
	sdbox_file_free,
	sdbox_file_create_fd,
	sdbox_mail_file_set,
	sdbox_mail_open,
	sdbox_mailbox_create_indexes,
	sdbox_get_attachment_path_suffix,
	sdbox_set_mailbox_corrupted,
	sdbox_set_file_corrupted
};
