From f7548d6d28c313cf80e6f3ef89aed16a19815df1 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 11:51:24 +0200 Subject: Adding upstream version 1:2.3.19.1+dfsg1. Signed-off-by: Daniel Baumann --- src/doveadm/dsync/dsync-mailbox-import.c | 2997 ++++++++++++++++++++++++++++++ 1 file changed, 2997 insertions(+) create mode 100644 src/doveadm/dsync/dsync-mailbox-import.c (limited to 'src/doveadm/dsync/dsync-mailbox-import.c') diff --git a/src/doveadm/dsync/dsync-mailbox-import.c b/src/doveadm/dsync/dsync-mailbox-import.c new file mode 100644 index 0000000..8a8fbe4 --- /dev/null +++ b/src/doveadm/dsync/dsync-mailbox-import.c @@ -0,0 +1,2997 @@ +/* Copyright (c) 2013-2018 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "array.h" +#include "hash.h" +#include "str.h" +#include "hex-binary.h" +#include "istream.h" +#include "seq-range-array.h" +#include "imap-util.h" +#include "mail-storage-private.h" +#include "mail-search-build.h" +#include "dsync-transaction-log-scan.h" +#include "dsync-mail.h" +#include "dsync-mailbox.h" +#include "dsync-mailbox-import.h" + +struct importer_mail { + const char *guid; + uint32_t uid; +}; + +struct importer_new_mail { + /* linked list of mails for this GUID */ + struct importer_new_mail *next; + /* if non-NULL, this mail exists in both local and remote. this link + points to the other side. */ + struct importer_new_mail *link; + + const char *guid; + struct dsync_mail_change *change; + + /* the final UID for the message */ + uint32_t final_uid; + /* the original local UID, or 0 if exists only remotely */ + uint32_t local_uid; + /* the original remote UID, or 0 if exists only remotely */ + uint32_t remote_uid; + /* UID for the mail in the virtual \All mailbox */ + uint32_t virtual_all_uid; + + bool uid_in_local:1; + bool uid_is_usable:1; + bool skip:1; + bool expunged:1; + bool copy_failed:1; + bool saved:1; +}; + +/* for quickly testing that two-way sync doesn't actually do any unexpected + modifications. */ +#define IMPORTER_DEBUG_CHANGE(importer) /*i_assert(!importer->master_brain)*/ + +HASH_TABLE_DEFINE_TYPE(guid_new_mail, const char *, struct importer_new_mail *); +HASH_TABLE_DEFINE_TYPE(uid_new_mail, void *, struct importer_new_mail *); + +struct dsync_mailbox_importer { + pool_t pool; + struct mailbox *box; + uint32_t last_common_uid; + uint64_t last_common_modseq, last_common_pvt_modseq; + uint32_t remote_uid_next; + uint32_t remote_first_recent_uid; + uint64_t remote_highest_modseq, remote_highest_pvt_modseq; + time_t sync_since_timestamp; + time_t sync_until_timestamp; + uoff_t sync_max_size; + enum mailbox_transaction_flags transaction_flags; + unsigned int hdr_hash_version; + unsigned int commit_msgs_interval; + + const char *const *hashed_headers; + + enum mail_flags sync_flag; + const char *sync_keyword; + bool sync_flag_dontwant; + + struct mailbox_transaction_context *trans, *ext_trans; + struct mail_search_context *search_ctx; + struct mail *mail, *ext_mail; + + struct mailbox *virtual_all_box; + struct mailbox_transaction_context *virtual_trans; + struct mail *virtual_mail; + + struct mail *cur_mail; + const char *cur_guid; + const char *cur_hdr_hash; + + /* UID => struct dsync_mail_change */ + HASH_TABLE_TYPE(dsync_uid_mail_change) local_changes; + HASH_TABLE_TYPE(dsync_attr_change) local_attr_changes; + + ARRAY_TYPE(seq_range) maybe_expunge_uids; + ARRAY(struct dsync_mail_change *) maybe_saves; + + /* GUID => struct importer_new_mail */ + HASH_TABLE_TYPE(guid_new_mail) import_guids; + /* UID => struct importer_new_mail */ + HASH_TABLE_TYPE(uid_new_mail) import_uids; + + ARRAY(struct importer_new_mail *) newmails; + ARRAY_TYPE(uint32_t) wanted_uids; + ARRAY_TYPE(uint32_t) saved_uids; + uint32_t highest_wanted_uid; + + ARRAY(struct dsync_mail_request) mail_requests; + unsigned int mail_request_idx; + + uint32_t prev_uid, next_local_seq, local_uid_next; + uint64_t local_initial_highestmodseq, local_initial_highestpvtmodseq; + unsigned int import_pos, import_count; + unsigned int first_unsaved_idx, saves_since_commit; + + enum mail_error mail_error; + + bool failed:1; + bool require_full_resync:1; + bool debug:1; + bool stateful_import:1; + bool last_common_uid_found:1; + bool cur_uid_has_change:1; + bool cur_mail_skip:1; + bool local_expunged_guids_set:1; + bool new_uids_assigned:1; + bool want_mail_requests:1; + bool master_brain:1; + bool revert_local_changes:1; + bool mails_have_guids:1; + bool mails_use_guid128:1; + bool delete_mailbox:1; + bool empty_hdr_workaround:1; +}; + +static const char *dsync_mail_change_type_names[] = { + "save", "expunge", "flag-change" +}; + +static bool dsync_mailbox_save_newmails(struct dsync_mailbox_importer *importer, + const struct dsync_mail *mail, + struct importer_new_mail *all_newmails, + bool remote_mail); +static int dsync_mailbox_import_commit(struct dsync_mailbox_importer *importer, + bool final); + +static void ATTR_FORMAT(2, 3) +imp_debug(struct dsync_mailbox_importer *importer, const char *fmt, ...) +{ + va_list args; + + if (importer->debug) T_BEGIN { + va_start(args, fmt); + i_debug("brain %c: Import %s: %s", + importer->master_brain ? 'M' : 'S', + mailbox_get_vname(importer->box), + t_strdup_vprintf(fmt, args)); + va_end(args); + } T_END; +} + +static void +dsync_import_unexpected_state(struct dsync_mailbox_importer *importer, + const char *error) +{ + if (!importer->stateful_import) { + i_error("Mailbox %s: %s", mailbox_get_vname(importer->box), + error); + } else { + i_warning("Mailbox %s doesn't match previous state: %s " + "(dsync must be run again without the state)", + mailbox_get_vname(importer->box), error); + } + importer->require_full_resync = TRUE; +} + +static void +dsync_mailbox_import_search_init(struct dsync_mailbox_importer *importer) +{ + struct mail_search_args *search_args; + struct mail_search_arg *sarg; + + search_args = mail_search_build_init(); + sarg = mail_search_build_add(search_args, SEARCH_UIDSET); + p_array_init(&sarg->value.seqset, search_args->pool, 128); + seq_range_array_add_range(&sarg->value.seqset, + importer->last_common_uid+1, (uint32_t)-1); + + importer->search_ctx = + mailbox_search_init(importer->trans, search_args, NULL, + 0, NULL); + mail_search_args_unref(&search_args); + + if (mailbox_search_next(importer->search_ctx, &importer->cur_mail)) + importer->next_local_seq = importer->cur_mail->seq; + /* this flag causes cur_guid to be looked up later */ + importer->cur_mail_skip = TRUE; +} + +static void +dsync_mailbox_import_transaction_begin(struct dsync_mailbox_importer *importer) +{ + const enum mailbox_transaction_flags ext_trans_flags = + importer->transaction_flags | + MAILBOX_TRANSACTION_FLAG_EXTERNAL | + MAILBOX_TRANSACTION_FLAG_ASSIGN_UIDS; + + importer->trans = mailbox_transaction_begin(importer->box, + importer->transaction_flags, + "dsync import"); + importer->ext_trans = mailbox_transaction_begin(importer->box, + ext_trans_flags, + "dsync ext import"); + importer->mail = mail_alloc(importer->trans, 0, NULL); + importer->ext_mail = mail_alloc(importer->ext_trans, 0, NULL); +} + +struct dsync_mailbox_importer * +dsync_mailbox_import_init(struct mailbox *box, + struct mailbox *virtual_all_box, + struct dsync_transaction_log_scan *log_scan, + uint32_t last_common_uid, + uint64_t last_common_modseq, + uint64_t last_common_pvt_modseq, + uint32_t remote_uid_next, + uint32_t remote_first_recent_uid, + uint64_t remote_highest_modseq, + uint64_t remote_highest_pvt_modseq, + time_t sync_since_timestamp, + time_t sync_until_timestamp, + uoff_t sync_max_size, + const char *sync_flag, + unsigned int commit_msgs_interval, + enum dsync_mailbox_import_flags flags, + unsigned int hdr_hash_version, + const char *const *hashed_headers) +{ + struct dsync_mailbox_importer *importer; + struct mailbox_status status; + pool_t pool; + + pool = pool_alloconly_create(MEMPOOL_GROWING"dsync mailbox importer", + 10240); + importer = p_new(pool, struct dsync_mailbox_importer, 1); + importer->pool = pool; + importer->box = box; + importer->virtual_all_box = virtual_all_box; + importer->last_common_uid = last_common_uid; + importer->last_common_modseq = last_common_modseq; + importer->last_common_pvt_modseq = last_common_pvt_modseq; + importer->last_common_uid_found = + last_common_uid != 0 || last_common_modseq != 0; + importer->remote_uid_next = remote_uid_next; + importer->remote_first_recent_uid = remote_first_recent_uid; + importer->remote_highest_modseq = remote_highest_modseq; + importer->remote_highest_pvt_modseq = remote_highest_pvt_modseq; + importer->sync_since_timestamp = sync_since_timestamp; + importer->sync_until_timestamp = sync_until_timestamp; + importer->sync_max_size = sync_max_size; + importer->stateful_import = importer->last_common_uid_found; + importer->hashed_headers = hashed_headers; + + if (sync_flag != NULL) { + if (sync_flag[0] == '-') { + importer->sync_flag_dontwant = TRUE; + sync_flag++; + } + if (sync_flag[0] == '\\') + importer->sync_flag = imap_parse_system_flag(sync_flag); + else + importer->sync_keyword = p_strdup(pool, sync_flag); + } + importer->commit_msgs_interval = commit_msgs_interval; + importer->transaction_flags = MAILBOX_TRANSACTION_FLAG_SYNC; + if ((flags & DSYNC_MAILBOX_IMPORT_FLAG_NO_NOTIFY) != 0) + importer->transaction_flags |= MAILBOX_TRANSACTION_FLAG_NO_NOTIFY; + + hash_table_create(&importer->import_guids, pool, 0, str_hash, strcmp); + hash_table_create_direct(&importer->import_uids, pool, 0); + i_array_init(&importer->maybe_expunge_uids, 16); + i_array_init(&importer->maybe_saves, 128); + i_array_init(&importer->newmails, 128); + i_array_init(&importer->wanted_uids, 128); + i_array_init(&importer->saved_uids, 128); + + dsync_mailbox_import_transaction_begin(importer); + + if ((flags & DSYNC_MAILBOX_IMPORT_FLAG_WANT_MAIL_REQUESTS) != 0) { + i_array_init(&importer->mail_requests, 128); + importer->want_mail_requests = TRUE; + } + importer->master_brain = + (flags & DSYNC_MAILBOX_IMPORT_FLAG_MASTER_BRAIN) != 0; + importer->revert_local_changes = + (flags & DSYNC_MAILBOX_IMPORT_FLAG_REVERT_LOCAL_CHANGES) != 0; + importer->debug = (flags & DSYNC_MAILBOX_IMPORT_FLAG_DEBUG) != 0; + importer->mails_have_guids = + (flags & DSYNC_MAILBOX_IMPORT_FLAG_MAILS_HAVE_GUIDS) != 0; + importer->mails_use_guid128 = + (flags & DSYNC_MAILBOX_IMPORT_FLAG_MAILS_USE_GUID128) != 0; + importer->hdr_hash_version = hdr_hash_version; + importer->empty_hdr_workaround = + (flags & DSYNC_MAILBOX_IMPORT_FLAG_EMPTY_HDR_WORKAROUND) != 0; + + mailbox_get_open_status(importer->box, STATUS_UIDNEXT | + STATUS_HIGHESTMODSEQ | STATUS_HIGHESTPVTMODSEQ, + &status); + if (status.nonpermanent_modseqs) + status.highest_modseq = 0; + importer->local_uid_next = status.uidnext; + importer->local_initial_highestmodseq = status.highest_modseq; + importer->local_initial_highestpvtmodseq = status.highest_pvt_modseq; + dsync_mailbox_import_search_init(importer); + + if (!importer->stateful_import) + ; + else if (importer->local_uid_next <= last_common_uid) { + dsync_import_unexpected_state(importer, t_strdup_printf( + "local UIDNEXT %u <= last common UID %u", + importer->local_uid_next, last_common_uid)); + } else if (importer->local_initial_highestmodseq < last_common_modseq) { + dsync_import_unexpected_state(importer, t_strdup_printf( + "local HIGHESTMODSEQ %"PRIu64" < last common HIGHESTMODSEQ %"PRIu64, + importer->local_initial_highestmodseq, + last_common_modseq)); + } else if (importer->local_initial_highestpvtmodseq < last_common_pvt_modseq) { + dsync_import_unexpected_state(importer, t_strdup_printf( + "local HIGHESTMODSEQ %"PRIu64" < last common HIGHESTMODSEQ %"PRIu64, + importer->local_initial_highestpvtmodseq, + last_common_pvt_modseq)); + } + + importer->local_changes = dsync_transaction_log_scan_get_hash(log_scan); + importer->local_attr_changes = dsync_transaction_log_scan_get_attr_hash(log_scan); + return importer; +} + +static int +dsync_mailbox_import_lookup_attr(struct dsync_mailbox_importer *importer, + enum mail_attribute_type type, const char *key, + struct dsync_mailbox_attribute **attr_r) +{ + struct dsync_mailbox_attribute lookup_attr, *attr; + const struct dsync_mailbox_attribute *attr_change; + struct mail_attribute_value value; + + *attr_r = NULL; + + if (mailbox_attribute_get_stream(importer->box, type, key, &value) < 0) { + i_error("Mailbox %s: Failed to get attribute %s: %s", + mailbox_get_vname(importer->box), key, + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + importer->failed = TRUE; + return -1; + } + + lookup_attr.type = type; + lookup_attr.key = key; + + attr_change = hash_table_lookup(importer->local_attr_changes, + &lookup_attr); + if (attr_change == NULL && + value.value == NULL && value.value_stream == NULL) { + /* we have no knowledge of this attribute */ + return 0; + } + attr = t_new(struct dsync_mailbox_attribute, 1); + attr->type = type; + attr->key = key; + attr->value = value.value; + attr->value_stream = value.value_stream; + attr->last_change = value.last_change; + if (attr_change != NULL) { + attr->deleted = attr_change->deleted && + !DSYNC_ATTR_HAS_VALUE(attr); + attr->modseq = attr_change->modseq; + } + *attr_r = attr; + return 0; +} + +static int +dsync_istreams_cmp(struct istream *input1, struct istream *input2, int *cmp_r) +{ + const unsigned char *data1, *data2; + size_t size1, size2, size; + + *cmp_r = -1; /* quiet gcc */ + + for (;;) { + (void)i_stream_read_more(input1, &data1, &size1); + (void)i_stream_read_more(input2, &data2, &size2); + + if (size1 == 0 || size2 == 0) + break; + size = I_MIN(size1, size2); + *cmp_r = memcmp(data1, data2, size); + if (*cmp_r != 0) + return 0; + i_stream_skip(input1, size); + i_stream_skip(input2, size); + } + if (input1->stream_errno != 0) { + i_error("read(%s) failed: %s", i_stream_get_name(input1), + i_stream_get_error(input1)); + return -1; + } + if (input2->stream_errno != 0) { + i_error("read(%s) failed: %s", i_stream_get_name(input2), + i_stream_get_error(input2)); + return -1; + } + if (size1 == 0 && size2 == 0) + *cmp_r = 0; + else + *cmp_r = size1 == 0 ? -1 : 1; + return 0; +} + +static int +dsync_attributes_cmp_values(const struct dsync_mailbox_attribute *attr1, + const struct dsync_mailbox_attribute *attr2, + int *cmp_r) +{ + struct istream *input1, *input2; + int ret; + + i_assert(attr1->value_stream != NULL || attr1->value != NULL); + i_assert(attr2->value_stream != NULL || attr2->value != NULL); + + if (attr1->value != NULL && attr2->value != NULL) { + *cmp_r = strcmp(attr1->value, attr2->value); + return 0; + } + /* at least one of them is a stream. make both of them streams. */ + input1 = attr1->value_stream != NULL ? attr1->value_stream : + i_stream_create_from_data(attr1->value, strlen(attr1->value)); + input2 = attr2->value_stream != NULL ? attr2->value_stream : + i_stream_create_from_data(attr2->value, strlen(attr2->value)); + i_stream_seek(input1, 0); + i_stream_seek(input2, 0); + ret = dsync_istreams_cmp(input1, input2, cmp_r); + if (attr1->value_stream == NULL) + i_stream_unref(&input1); + if (attr2->value_stream == NULL) + i_stream_unref(&input2); + return ret; +} + +static int +dsync_attributes_cmp(const struct dsync_mailbox_attribute *attr, + const struct dsync_mailbox_attribute *local_attr, + int *cmp_r) +{ + if (DSYNC_ATTR_HAS_VALUE(attr) && + !DSYNC_ATTR_HAS_VALUE(local_attr)) { + /* remote has a value and local doesn't -> use it */ + *cmp_r = 1; + return 0; + } else if (!DSYNC_ATTR_HAS_VALUE(attr) && + DSYNC_ATTR_HAS_VALUE(local_attr)) { + /* remote doesn't have a value, bt local does -> skip */ + *cmp_r = -1; + return 0; + } + + return dsync_attributes_cmp_values(attr, local_attr, cmp_r); +} + +static int +dsync_mailbox_import_attribute_real(struct dsync_mailbox_importer *importer, + const struct dsync_mailbox_attribute *attr, + const struct dsync_mailbox_attribute *local_attr, + const char **result_r) +{ + struct mail_attribute_value value; + int cmp; + bool ignore = FALSE; + + i_assert(DSYNC_ATTR_HAS_VALUE(attr) || attr->deleted); + + if (attr->deleted && + (local_attr == NULL || !DSYNC_ATTR_HAS_VALUE(local_attr))) { + /* attribute doesn't exist on either side -> ignore */ + *result_r = "Nonexistent in both sides"; + return 0; + } + if (local_attr == NULL) { + /* we haven't seen this locally -> use whatever remote has */ + *result_r = "Nonexistent locally"; + } else if (local_attr->modseq <= importer->last_common_modseq && + attr->modseq > importer->last_common_modseq && + importer->last_common_modseq > 0) { + /* we're doing incremental syncing, and we can see that the + attribute was changed remotely, but not locally -> use it */ + *result_r = "Changed remotely"; + } else if (local_attr->modseq > importer->last_common_modseq && + attr->modseq <= importer->last_common_modseq && + importer->last_common_modseq > 0) { + /* we're doing incremental syncing, and we can see that the + attribute was changed locally, but not remotely -> ignore */ + *result_r = "Changed locally"; + ignore = TRUE; + } else if (attr->last_change > local_attr->last_change) { + /* remote has a newer timestamp -> use it */ + *result_r = "Remote has newer timestamp"; + } else if (attr->last_change < local_attr->last_change) { + /* remote has an older timestamp -> ignore */ + *result_r = "Local has newer timestamp"; + ignore = TRUE; + } else { + /* the timestamps are the same. now we're down to guessing + the right answer, unless the values are actually equal, + so check that first. next try to use modseqs, but if even + they are the same, fallback to just picking one based on the + value. */ + if (dsync_attributes_cmp(attr, local_attr, &cmp) < 0) { + importer->mail_error = MAIL_ERROR_TEMP; + importer->failed = TRUE; + return -1; + } + if (cmp == 0) { + /* identical scripts */ + *result_r = "Unchanged value"; + return 0; + } + + if (attr->modseq > local_attr->modseq) { + /* remote has a higher modseq -> use it */ + *result_r = "Remote has newer modseq"; + } else if (attr->modseq < local_attr->modseq) { + /* remote has an older modseq -> ignore */ + *result_r = "Local has newer modseq"; + ignore = TRUE; + } else if (cmp < 0) { + ignore = TRUE; + *result_r = "Value changed, but unknown which is newer - picking local"; + } else { + *result_r = "Value changed, but unknown which is newer - picking remote"; + } + } + if (ignore) + return 0; + + i_zero(&value); + value.value = attr->value; + value.value_stream = attr->value_stream; + value.last_change = attr->last_change; + if (mailbox_attribute_set(importer->trans, attr->type, + attr->key, &value) < 0) { + i_error("Mailbox %s: Failed to set attribute %s: %s", + mailbox_get_vname(importer->box), attr->key, + mailbox_get_last_internal_error(importer->box, NULL)); + /* the attributes aren't vital, don't fail everything just + because of them. */ + } + return 0; +} + +int dsync_mailbox_import_attribute(struct dsync_mailbox_importer *importer, + const struct dsync_mailbox_attribute *attr) +{ + struct dsync_mailbox_attribute *local_attr; + const char *result = ""; + int ret; + + if (dsync_mailbox_import_lookup_attr(importer, attr->type, + attr->key, &local_attr) < 0) + ret = -1; + else { + ret = dsync_mailbox_import_attribute_real(importer, attr, + local_attr, &result); + if (local_attr != NULL && local_attr->value_stream != NULL) + i_stream_unref(&local_attr->value_stream); + } + imp_debug(importer, "Import attribute %s: %s", attr->key, + ret < 0 ? "failed" : result); + return ret; +} + +static void dsync_mail_error(struct dsync_mailbox_importer *importer, + struct mail *mail, const char *field) +{ + const char *errstr; + enum mail_error error; + + errstr = mailbox_get_last_internal_error(importer->box, &error); + if (error == MAIL_ERROR_EXPUNGED) + return; + + i_error("Mailbox %s: Can't lookup %s for UID=%u: %s", + mailbox_get_vname(mail->box), field, mail->uid, errstr); + importer->mail_error = error; + importer->failed = TRUE; +} + +static bool +dsync_mail_change_guid_equals(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change, + const char *guid, const char **cmp_guid_r) +{ + guid_128_t guid_128, change_guid_128; + + if (change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) { + if (guid_128_from_string(change->guid, change_guid_128) < 0) + i_unreached(); + } else if (importer->mails_use_guid128) { + mail_generate_guid_128_hash(change->guid, change_guid_128); + } else { + if (cmp_guid_r != NULL) + *cmp_guid_r = change->guid; + return strcmp(change->guid, guid) == 0; + } + + mail_generate_guid_128_hash(guid, guid_128); + if (memcmp(change_guid_128, guid_128, GUID_128_SIZE) != 0) { + if (cmp_guid_r != NULL) { + *cmp_guid_r = t_strdup_printf("%s(guid128, orig=%s)", + binary_to_hex(change_guid_128, sizeof(change_guid_128)), + change->guid); + } + return FALSE; + } + return TRUE; +} + +static int +importer_try_next_mail(struct dsync_mailbox_importer *importer, + uint32_t wanted_uid) +{ + struct mail_private *pmail; + const char *hdr_hash; + + if (importer->cur_mail == NULL) { + /* end of search */ + return -1; + } + while (importer->cur_mail->seq < importer->next_local_seq || + importer->cur_mail->uid < wanted_uid) { + if (!importer->cur_uid_has_change && + !importer->last_common_uid_found) { + /* this message exists locally, but remote didn't send + expunge-change for it. if the message's + uid <= last-common-uid, it should be deleted */ + seq_range_array_add(&importer->maybe_expunge_uids, + importer->cur_mail->uid); + } + + importer->cur_mail_skip = FALSE; + if (!mailbox_search_next(importer->search_ctx, + &importer->cur_mail)) { + importer->cur_mail = NULL; + importer->cur_guid = NULL; + importer->cur_hdr_hash = NULL; + return -1; + } + importer->cur_uid_has_change = FALSE; + } + importer->cur_uid_has_change = importer->cur_mail->uid == wanted_uid; + if (importer->mails_have_guids) { + if (mail_get_special(importer->cur_mail, MAIL_FETCH_GUID, + &importer->cur_guid) < 0) { + dsync_mail_error(importer, importer->cur_mail, "GUID"); + return 0; + } + } else { + if (dsync_mail_get_hdr_hash(importer->cur_mail, + importer->hdr_hash_version, + importer->hashed_headers, + &hdr_hash) < 0) { + dsync_mail_error(importer, importer->cur_mail, + "header hash"); + return 0; + } + pmail = (struct mail_private *)importer->cur_mail; + importer->cur_hdr_hash = p_strdup(pmail->pool, hdr_hash); + importer->cur_guid = ""; + } + /* make sure next_local_seq gets updated in case we came here + because of min_uid */ + importer->next_local_seq = importer->cur_mail->seq; + return 1; +} + +static bool +importer_next_mail(struct dsync_mailbox_importer *importer, uint32_t wanted_uid) +{ + int ret; + + for (;;) { + T_BEGIN { + ret = importer_try_next_mail(importer, wanted_uid); + } T_END; + if (ret != 0 || importer->failed) + break; + importer->next_local_seq = importer->cur_mail->seq + 1; + } + return ret > 0; +} + +static int +importer_mail_cmp(const struct importer_mail *m1, + const struct importer_mail *m2) +{ + int ret; + + if (m1->guid == NULL) + return 1; + if (m2->guid == NULL) + return -1; + + ret = strcmp(m1->guid, m2->guid); + if (ret != 0) + return ret; + + if (m1->uid < m2->uid) + return -1; + if (m1->uid > m2->uid) + return 1; + return 0; +} + +static void newmail_link(struct dsync_mailbox_importer *importer, + struct importer_new_mail *newmail, uint32_t remote_uid) +{ + struct importer_new_mail *first_mail, **last, *mail, *link = NULL; + + if (*newmail->guid != '\0') { + first_mail = hash_table_lookup(importer->import_guids, + newmail->guid); + if (first_mail == NULL) { + /* first mail for this GUID */ + hash_table_insert(importer->import_guids, + newmail->guid, newmail); + return; + } + } else { + if (remote_uid == 0) { + /* mail exists locally. we don't want to request + it, and we'll assume it has no duplicate + instances. */ + return; + } + first_mail = hash_table_lookup(importer->import_uids, + POINTER_CAST(remote_uid)); + if (first_mail == NULL) { + /* first mail for this UID */ + hash_table_insert(importer->import_uids, + POINTER_CAST(remote_uid), newmail); + return; + } + } + /* 1) add the newmail to the end of the linked list + 2) find our link + + FIXME: this loop is slow if the same GUID has a ton of instances. + Could it be improved in some way? */ + last = &first_mail->next; + for (mail = first_mail; mail != NULL; mail = mail->next) { + if (mail->final_uid == newmail->final_uid) + mail->uid_is_usable = TRUE; + if (link == NULL && mail->link == NULL && + mail->uid_in_local != newmail->uid_in_local) + link = mail; + last = &mail->next; + } + *last = newmail; + if (link != NULL && newmail->link == NULL) { + link->link = newmail; + newmail->link = link; + } +} + +static void +dsync_mailbox_revert_existing_uid(struct dsync_mailbox_importer *importer, + uint32_t uid, const char *reason) +{ + i_assert(importer->revert_local_changes); + + /* UID either already exists or UIDNEXT is too high. we can't set the + wanted UID, so we'll need to delete the whole mailbox and resync */ + i_warning("Deleting mailbox '%s': UID=%u already exists locally for a different mail: %s", + mailbox_get_vname(importer->box), uid, reason); + importer->delete_mailbox = TRUE; + importer->mail_error = MAIL_ERROR_TEMP; + importer->failed = TRUE; +} + +static bool dsync_mailbox_try_save_cur(struct dsync_mailbox_importer *importer, + struct dsync_mail_change *save_change) +{ + struct importer_mail m1, m2; + struct importer_new_mail *newmail; + int diff; + bool remote_saved; + + i_zero(&m1); + if (importer->cur_mail != NULL) { + m1.guid = importer->mails_have_guids ? + importer->cur_guid : importer->cur_hdr_hash; + m1.uid = importer->cur_mail->uid; + } + i_zero(&m2); + if (save_change != NULL) { + m2.guid = importer->mails_have_guids ? + save_change->guid : save_change->hdr_hash; + m2.uid = save_change->uid; + i_assert(save_change->type != DSYNC_MAIL_CHANGE_TYPE_EXPUNGE); + } + + if (importer->empty_hdr_workaround && !importer->mails_have_guids && + importer->cur_mail != NULL && save_change != NULL && + (dsync_mail_hdr_hash_is_empty(m1.guid) || + dsync_mail_hdr_hash_is_empty(m2.guid))) { + /* one of the headers is empty. assume it's broken and that + the header matches what we have currently. */ + diff = 0; + } else { + diff = importer_mail_cmp(&m1, &m2); + } + if (diff < 0) { + /* add a record for local mail */ + i_assert(importer->cur_mail != NULL); + if (importer->revert_local_changes) { + if (save_change == NULL && + importer->cur_mail->uid >= importer->remote_uid_next) { + dsync_mailbox_revert_existing_uid(importer, importer->cur_mail->uid, + t_strdup_printf("higher than remote's UIDs (remote UIDNEXT=%u)", importer->remote_uid_next)); + return TRUE; + } + mail_expunge(importer->cur_mail); + importer->cur_mail_skip = TRUE; + importer->next_local_seq++; + return FALSE; + } + newmail = p_new(importer->pool, struct importer_new_mail, 1); + newmail->guid = p_strdup(importer->pool, importer->cur_guid); + newmail->final_uid = importer->cur_mail->uid; + newmail->local_uid = importer->cur_mail->uid; + newmail->uid_in_local = TRUE; + newmail->uid_is_usable = + newmail->final_uid >= importer->remote_uid_next; + remote_saved = FALSE; + } else if (diff > 0) { + i_assert(save_change != NULL); + newmail = p_new(importer->pool, struct importer_new_mail, 1); + newmail->guid = save_change->guid; + newmail->final_uid = save_change->uid; + newmail->remote_uid = save_change->uid; + newmail->uid_in_local = FALSE; + newmail->uid_is_usable = + newmail->final_uid >= importer->local_uid_next; + if (!newmail->uid_is_usable && importer->revert_local_changes) { + dsync_mailbox_revert_existing_uid(importer, newmail->final_uid, + t_strdup_printf("UID >= local UIDNEXT=%u", importer->local_uid_next)); + return TRUE; + } + remote_saved = TRUE; + } else { + /* identical */ + i_assert(importer->cur_mail != NULL); + i_assert(save_change != NULL); + newmail = p_new(importer->pool, struct importer_new_mail, 1); + newmail->guid = save_change->guid; + newmail->final_uid = importer->cur_mail->uid; + newmail->local_uid = importer->cur_mail->uid; + newmail->remote_uid = save_change->uid; + newmail->uid_in_local = TRUE; + newmail->uid_is_usable = TRUE; + newmail->link = newmail; + remote_saved = TRUE; + } + + if (newmail->uid_in_local) { + importer->cur_mail_skip = TRUE; + importer->next_local_seq++; + } + /* NOTE: assumes save_change is allocated from importer pool */ + newmail->change = save_change; + + array_push_back(&importer->newmails, &newmail); + if (newmail->uid_in_local) + newmail_link(importer, newmail, 0); + else { + i_assert(save_change != NULL); + newmail_link(importer, newmail, save_change->uid); + } + return remote_saved; +} + +static bool ATTR_NULL(2) +dsync_mailbox_try_save(struct dsync_mailbox_importer *importer, + struct dsync_mail_change *save_change) +{ + if (importer->cur_mail_skip) { + if (!importer_next_mail(importer, 0) && save_change == NULL) + return FALSE; + } + return dsync_mailbox_try_save_cur(importer, save_change); +} + +static void dsync_mailbox_save(struct dsync_mailbox_importer *importer, + struct dsync_mail_change *save_change) +{ + while (!dsync_mailbox_try_save(importer, save_change)) ; +} + +static bool +dsync_import_set_mail(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change) +{ + const char *guid, *cmp_guid; + + if (!mail_set_uid(importer->mail, change->uid)) + return FALSE; + if (change->guid == NULL) { + /* GUID is unknown */ + return TRUE; + } + if (*change->guid == '\0') { + /* backend doesn't support GUIDs. if hdr_hash is set, we could + verify it, but since this message really is supposed to + match, it's probably too much trouble. */ + return TRUE; + } + + /* verify that GUID matches, just in case */ + if (mail_get_special(importer->mail, MAIL_FETCH_GUID, &guid) < 0) { + dsync_mail_error(importer, importer->mail, "GUID"); + return FALSE; + } + if (!dsync_mail_change_guid_equals(importer, change, guid, &cmp_guid)) { + dsync_import_unexpected_state(importer, t_strdup_printf( + "Unexpected GUID mismatch for UID=%u: %s != %s", + change->uid, guid, cmp_guid)); + return FALSE; + } + return TRUE; +} + +static bool dsync_check_cur_guid(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change) +{ + const char *cmp_guid; + + if (change->guid == NULL || change->guid[0] == '\0' || + importer->cur_guid[0] == '\0') + return TRUE; + if (!dsync_mail_change_guid_equals(importer, change, + importer->cur_guid, &cmp_guid)) { + dsync_import_unexpected_state(importer, t_strdup_printf( + "Unexpected GUID mismatch (2) for UID=%u: %s != %s", + change->uid, importer->cur_guid, cmp_guid)); + return FALSE; + } + return TRUE; +} + +static void +merge_flags(uint32_t local_final, uint32_t local_add, uint32_t local_remove, + uint32_t remote_final, uint32_t remote_add, uint32_t remote_remove, + uint32_t pvt_mask, bool prefer_remote, bool prefer_pvt_remote, + uint32_t *change_add_r, uint32_t *change_remove_r, + bool *remote_changed, bool *remote_pvt_changed) +{ + uint32_t combined_add, combined_remove, conflict_flags; + uint32_t local_wanted, remote_wanted, conflict_pvt_flags; + + /* resolve conflicts */ + conflict_flags = local_add & remote_remove; + if (conflict_flags != 0) { + conflict_pvt_flags = conflict_flags & pvt_mask; + conflict_flags &= ~pvt_mask; + if (prefer_remote) + local_add &= ~conflict_flags; + else + remote_remove &= ~conflict_flags; + if (prefer_pvt_remote) + local_add &= ~conflict_pvt_flags; + else + remote_remove &= ~conflict_pvt_flags; + } + conflict_flags = local_remove & remote_add; + if (conflict_flags != 0) { + conflict_pvt_flags = conflict_flags & pvt_mask; + conflict_flags &= ~pvt_mask; + if (prefer_remote) + local_remove &= ~conflict_flags; + else + remote_add &= ~conflict_flags; + if (prefer_pvt_remote) + local_remove &= ~conflict_pvt_flags; + else + remote_add &= ~conflict_pvt_flags; + } + + combined_add = local_add|remote_add; + combined_remove = local_remove|remote_remove; + i_assert((combined_add & combined_remove) == 0); + + /* don't change flags that are currently identical in both sides */ + conflict_flags = local_final ^ remote_final; + combined_add &= conflict_flags; + combined_remove &= conflict_flags; + + /* see if there are conflicting final flags */ + local_wanted = (local_final|combined_add) & ~combined_remove; + remote_wanted = (remote_final|combined_add) & ~combined_remove; + + conflict_flags = local_wanted ^ remote_wanted; + if (conflict_flags != 0) { + if (prefer_remote && prefer_pvt_remote) + local_wanted = remote_wanted; + else if (prefer_remote && !prefer_pvt_remote) { + local_wanted = (local_wanted & pvt_mask) | + (remote_wanted & ~pvt_mask); + } else if (!prefer_remote && prefer_pvt_remote) { + local_wanted = (local_wanted & ~pvt_mask) | + (remote_wanted & pvt_mask); + } + } + + *change_add_r = local_wanted & ~local_final; + *change_remove_r = local_final & ~local_wanted; + if ((local_wanted & ~pvt_mask) != (remote_final & ~pvt_mask)) + *remote_changed = TRUE; + if ((local_wanted & pvt_mask) != (remote_final & pvt_mask)) + *remote_pvt_changed = TRUE; +} + +static bool +keyword_find(ARRAY_TYPE(const_string) *keywords, const char *name, + unsigned int *idx_r) +{ + const char *const *names; + unsigned int i, count; + + names = array_get(keywords, &count); + for (i = 0; i < count; i++) { + if (strcmp(names[i], name) == 0) { + *idx_r = i; + return TRUE; + } + } + return FALSE; +} + +static void keywords_append(ARRAY_TYPE(const_string) *dest, + const ARRAY_TYPE(const_string) *keywords, + uint32_t bits, unsigned int start_idx) +{ + const char *name; + unsigned int i; + + for (i = 0; i < 32; i++) { + if ((bits & (1U << i)) == 0) + continue; + + name = array_idx_elem(keywords, start_idx+i); + array_push_back(dest, &name); + } +} + +static void +merge_keywords(struct mail *mail, const ARRAY_TYPE(const_string) *local_changes, + const ARRAY_TYPE(const_string) *remote_changes, + bool prefer_remote, + bool *remote_changed, bool *remote_pvt_changed) +{ + /* local_changes and remote_changes are assumed to have no + duplicates names */ + uint32_t *local_add, *local_remove, *local_final; + uint32_t *remote_add, *remote_remove, *remote_final; + uint32_t *change_add, *change_remove; + ARRAY_TYPE(const_string) all_keywords, add_keywords, remove_keywords; + const char *const *changes, *name, *const *local_keywords; + struct mail_keywords *kw; + unsigned int i, count, name_idx, array_size; + + local_keywords = mail_get_keywords(mail); + + /* we'll assign a common index for each keyword name and place + the changes to separate bit arrays. */ + if (array_is_created(remote_changes)) + changes = array_get(remote_changes, &count); + else { + changes = NULL; + count = 0; + } + + array_size = str_array_length(local_keywords) + count; + if (array_is_created(local_changes)) + array_size += array_count(local_changes); + if (array_size == 0) { + /* this message has no keywords */ + return; + } + t_array_init(&all_keywords, array_size); + t_array_init(&add_keywords, array_size); + t_array_init(&remove_keywords, array_size); + + /* @UNSAFE: create large enough arrays to fit all keyword indexes. */ + array_size = (array_size+31)/32; + local_add = t_new(uint32_t, array_size); + local_remove = t_new(uint32_t, array_size); + local_final = t_new(uint32_t, array_size); + remote_add = t_new(uint32_t, array_size); + remote_remove = t_new(uint32_t, array_size); + remote_final = t_new(uint32_t, array_size); + change_add = t_new(uint32_t, array_size); + change_remove = t_new(uint32_t, array_size); + + /* get remote changes */ + for (i = 0; i < count; i++) { + name = changes[i]+1; + name_idx = array_count(&all_keywords); + array_push_back(&all_keywords, &name); + + switch (changes[i][0]) { + case KEYWORD_CHANGE_ADD: + remote_add[name_idx/32] |= 1U << (name_idx%32); + break; + case KEYWORD_CHANGE_REMOVE: + remote_remove[name_idx/32] |= 1U << (name_idx%32); + break; + case KEYWORD_CHANGE_FINAL: + remote_final[name_idx/32] |= 1U << (name_idx%32); + break; + case KEYWORD_CHANGE_ADD_AND_FINAL: + remote_add[name_idx/32] |= 1U << (name_idx%32); + remote_final[name_idx/32] |= 1U << (name_idx%32); + break; + } + } + + /* get local changes. use existing indexes for names when they exist. */ + if (array_is_created(local_changes)) + changes = array_get(local_changes, &count); + else { + changes = NULL; + count = 0; + } + for (i = 0; i < count; i++) { + name = changes[i]+1; + if (!keyword_find(&all_keywords, name, &name_idx)) { + name_idx = array_count(&all_keywords); + array_push_back(&all_keywords, &name); + } + + switch (changes[i][0]) { + case KEYWORD_CHANGE_ADD: + case KEYWORD_CHANGE_ADD_AND_FINAL: + local_add[name_idx/32] |= 1U << (name_idx%32); + break; + case KEYWORD_CHANGE_REMOVE: + local_remove[name_idx/32] |= 1U << (name_idx%32); + break; + case KEYWORD_CHANGE_FINAL: + break; + } + } + for (i = 0; local_keywords[i] != NULL; i++) { + name = local_keywords[i]; + if (!keyword_find(&all_keywords, name, &name_idx)) { + name_idx = array_count(&all_keywords); + array_push_back(&all_keywords, &name); + } + local_final[name_idx/32] |= 1U << (name_idx%32); + } + i_assert(array_count(&all_keywords) <= array_size*32); + array_size = (array_count(&all_keywords)+31) / 32; + + /* merge keywords */ + for (i = 0; i < array_size; i++) { + merge_flags(local_final[i], local_add[i], local_remove[i], + remote_final[i], remote_add[i], remote_remove[i], + 0, prefer_remote, prefer_remote, + &change_add[i], &change_remove[i], + remote_changed, remote_pvt_changed); + if (change_add[i] != 0) { + keywords_append(&add_keywords, &all_keywords, + change_add[i], i*32); + } + if (change_remove[i] != 0) { + keywords_append(&remove_keywords, &all_keywords, + change_remove[i], i*32); + } + } + + /* apply changes */ + if (array_count(&add_keywords) > 0) { + array_append_zero(&add_keywords); + kw = mailbox_keywords_create_valid(mail->box, + array_front(&add_keywords)); + mail_update_keywords(mail, MODIFY_ADD, kw); + mailbox_keywords_unref(&kw); + } + if (array_count(&remove_keywords) > 0) { + array_append_zero(&remove_keywords); + kw = mailbox_keywords_create_valid(mail->box, + array_front(&remove_keywords)); + mail_update_keywords(mail, MODIFY_REMOVE, kw); + mailbox_keywords_unref(&kw); + } +} + +static void +dsync_mailbox_import_replace_flags(struct mail *mail, + const struct dsync_mail_change *change) +{ + ARRAY_TYPE(const_string) keywords; + struct mail_keywords *kw; + const char *const *changes, *name; + unsigned int i, count; + + if (array_is_created(&change->keyword_changes)) + changes = array_get(&change->keyword_changes, &count); + else { + changes = NULL; + count = 0; + } + t_array_init(&keywords, count+1); + for (i = 0; i < count; i++) { + switch (changes[i][0]) { + case KEYWORD_CHANGE_ADD: + case KEYWORD_CHANGE_FINAL: + case KEYWORD_CHANGE_ADD_AND_FINAL: + name = changes[i]+1; + array_push_back(&keywords, &name); + break; + case KEYWORD_CHANGE_REMOVE: + break; + } + } + array_append_zero(&keywords); + + kw = mailbox_keywords_create_valid(mail->box, array_front(&keywords)); + mail_update_keywords(mail, MODIFY_REPLACE, kw); + mailbox_keywords_unref(&kw); + + mail_update_flags(mail, MODIFY_REPLACE, + change->add_flags | change->final_flags); + if (mail_get_modseq(mail) < change->modseq) + mail_update_modseq(mail, change->modseq); + if (mail_get_pvt_modseq(mail) < change->pvt_modseq) + mail_update_pvt_modseq(mail, change->pvt_modseq); +} + +static void +dsync_mailbox_import_flag_change(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change) +{ + const struct dsync_mail_change *local_change; + enum mail_flags local_add, local_remove; + uint32_t change_add, change_remove; + uint64_t new_modseq; + ARRAY_TYPE(const_string) local_keyword_changes = ARRAY_INIT; + struct mail *mail; + bool prefer_remote, prefer_pvt_remote; + bool remote_changed = FALSE, remote_pvt_changed = FALSE; + + i_assert((change->add_flags & change->remove_flags) == 0); + + if (importer->cur_mail != NULL && + importer->cur_mail->uid == change->uid) { + if (!dsync_check_cur_guid(importer, change)) + return; + mail = importer->cur_mail; + } else { + if (!dsync_import_set_mail(importer, change)) + return; + mail = importer->mail; + } + + if (importer->revert_local_changes) { + /* dsync backup: just make the local look like remote. */ + dsync_mailbox_import_replace_flags(mail, change); + return; + } + + local_change = hash_table_lookup(importer->local_changes, + POINTER_CAST(change->uid)); + if (local_change == NULL) { + local_add = local_remove = 0; + } else { + local_add = local_change->add_flags; + local_remove = local_change->remove_flags; + local_keyword_changes = local_change->keyword_changes; + } + + if (mail_get_modseq(mail) < change->modseq) + prefer_remote = TRUE; + else if (mail_get_modseq(mail) > change->modseq) + prefer_remote = FALSE; + else { + /* identical modseq, we'll just have to pick one. + Note that both brains need to pick the same one, otherwise + they become unsynced. */ + prefer_remote = !importer->master_brain; + } + if (mail_get_pvt_modseq(mail) < change->pvt_modseq) + prefer_pvt_remote = TRUE; + else if (mail_get_pvt_modseq(mail) > change->pvt_modseq) + prefer_pvt_remote = FALSE; + else + prefer_pvt_remote = !importer->master_brain; + + /* merge flags */ + merge_flags(mail_get_flags(mail), local_add, local_remove, + change->final_flags, change->add_flags, change->remove_flags, + mailbox_get_private_flags_mask(mail->box), + prefer_remote, prefer_pvt_remote, + &change_add, &change_remove, + &remote_changed, &remote_pvt_changed); + + if (change_add != 0) + mail_update_flags(mail, MODIFY_ADD, change_add); + if (change_remove != 0) + mail_update_flags(mail, MODIFY_REMOVE, change_remove); + + /* merge keywords */ + merge_keywords(mail, &local_keyword_changes, &change->keyword_changes, + prefer_remote, &remote_changed, &remote_pvt_changed); + + /* update modseqs. try to anticipate when we have to increase modseq + to get it closer to what remote has (although we can't guess it + exactly correctly) */ + new_modseq = change->modseq; + if (remote_changed && new_modseq <= importer->remote_highest_modseq) + new_modseq = importer->remote_highest_modseq+1; + if (mail_get_modseq(mail) < new_modseq) + mail_update_modseq(mail, new_modseq); + + new_modseq = change->pvt_modseq; + if (remote_pvt_changed && new_modseq <= importer->remote_highest_pvt_modseq) + new_modseq = importer->remote_highest_pvt_modseq+1; + if (mail_get_pvt_modseq(mail) < new_modseq) + mail_update_pvt_modseq(mail, new_modseq); +} + +static bool +dsync_mail_change_have_keyword(const struct dsync_mail_change *change, + const char *keyword) +{ + const char *str; + + if (!array_is_created(&change->keyword_changes)) + return FALSE; + + array_foreach_elem(&change->keyword_changes, str) { + switch (str[0]) { + case KEYWORD_CHANGE_FINAL: + case KEYWORD_CHANGE_ADD_AND_FINAL: + if (strcasecmp(str+1, keyword) == 0) + return TRUE; + break; + default: + break; + } + } + return FALSE; +} + +static bool +dsync_mailbox_import_want_change(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change, + const char **result_r) +{ + if (importer->sync_since_timestamp > 0) { + i_assert(change->received_timestamp > 0); + if (change->received_timestamp < importer->sync_since_timestamp) { + /* mail has too old timestamp - skip it */ + *result_r = "Ignoring missing local mail with too old timestamp"; + return FALSE; + } + } + if (importer->sync_until_timestamp > 0) { + i_assert(change->received_timestamp > 0); + if (change->received_timestamp > importer->sync_until_timestamp) { + /* mail has too new timestamp - skip it */ + *result_r = "Ignoring missing local mail with too new timestamp"; + return FALSE; + } + } + if (importer->sync_max_size > 0) { + i_assert(change->virtual_size != UOFF_T_MAX); + if (change->virtual_size > importer->sync_max_size) { + /* mail is too large - skip it */ + *result_r = "Ignoring missing local mail with too large size"; + return FALSE; + } + } + if (importer->sync_flag != 0) { + bool have_flag = (change->final_flags & importer->sync_flag) != 0; + + if (have_flag && importer->sync_flag_dontwant) { + *result_r = "Ignoring missing local mail that doesn't have wanted flags"; + return FALSE; + } + if (!have_flag && !importer->sync_flag_dontwant) { + *result_r = "Ignoring missing local mail that has unwanted flags"; + return FALSE; + } + } + if (importer->sync_keyword != NULL) { + bool have_kw = dsync_mail_change_have_keyword(change, importer->sync_keyword); + + if (have_kw && importer->sync_flag_dontwant) { + *result_r = "Ignoring missing local mail that doesn't have wanted keywords"; + return FALSE; + } + if (!have_kw && !importer->sync_flag_dontwant) { + *result_r = "Ignoring missing local mail that has unwanted keywords"; + return FALSE; + } + } + return TRUE; +} + +static void +dsync_mailbox_import_save(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change) +{ + struct dsync_mail_change *save; + const char *result; + + i_assert(change->guid != NULL); + + if (change->uid == importer->last_common_uid) { + /* we've already verified that the GUID matches. + apply flag changes if there are any. */ + i_assert(!importer->last_common_uid_found); + dsync_mailbox_import_flag_change(importer, change); + return; + } + if (!dsync_mailbox_import_want_change(importer, change, &result)) + return; + + save = p_new(importer->pool, struct dsync_mail_change, 1); + dsync_mail_change_dup(importer->pool, change, save); + + if (importer->last_common_uid_found) { + /* this is a new mail. its UID may or may not conflict with + an existing local mail, we'll figure it out later. */ + i_assert(change->uid > importer->last_common_uid); + dsync_mailbox_save(importer, save); + } else { + /* the local mail is expunged. we'll decide later if we want + to save this mail locally or expunge it form remote. */ + i_assert(change->uid > importer->last_common_uid); + i_assert(importer->cur_mail == NULL || + change->uid < importer->cur_mail->uid); + array_push_back(&importer->maybe_saves, &save); + } +} + +static void +dsync_mailbox_import_expunge(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change) +{ + + if (importer->last_common_uid_found) { + /* expunge the message, unless its GUID unexpectedly doesn't + match */ + i_assert(change->uid <= importer->last_common_uid); + if (dsync_import_set_mail(importer, change)) + mail_expunge(importer->mail); + } else if (importer->cur_mail == NULL || + change->uid < importer->cur_mail->uid) { + /* already expunged locally, we can ignore this. + uid=last_common_uid if we managed to verify from + transaction log that the GUIDs match */ + i_assert(change->uid >= importer->last_common_uid); + } else if (change->uid == importer->last_common_uid) { + /* already verified that the GUID matches */ + i_assert(importer->cur_mail->uid == change->uid); + if (dsync_check_cur_guid(importer, change)) + mail_expunge(importer->cur_mail); + } else { + /* we don't know yet if we should expunge this + message or not. queue it until we do. */ + i_assert(change->uid > importer->last_common_uid); + seq_range_array_add(&importer->maybe_expunge_uids, change->uid); + } +} + +static void +dsync_mailbox_rewind_search(struct dsync_mailbox_importer *importer) +{ + /* If there are local mails after last_common_uid which we skipped + while trying to match the next message, we need to now go back */ + if (importer->cur_mail != NULL && + importer->cur_mail->uid <= importer->last_common_uid+1) + return; + + importer->cur_mail = NULL; + importer->cur_guid = NULL; + importer->cur_hdr_hash = NULL; + importer->next_local_seq = 0; + + (void)mailbox_search_deinit(&importer->search_ctx); + dsync_mailbox_import_search_init(importer); +} + +static void +dsync_mailbox_common_uid_found(struct dsync_mailbox_importer *importer) +{ + struct dsync_mail_change *const *saves; + struct seq_range_iter iter; + unsigned int n, i, count; + uint32_t uid; + + if (importer->debug) T_BEGIN { + string_t *expunges = t_str_new(64); + + imap_write_seq_range(expunges, &importer->maybe_expunge_uids); + imp_debug(importer, "Last common UID=%u. Delayed expunges=%s", + importer->last_common_uid, str_c(expunges)); + } T_END; + + importer->last_common_uid_found = TRUE; + dsync_mailbox_rewind_search(importer); + + /* expunge the messages whose expunge-decision we delayed previously */ + seq_range_array_iter_init(&iter, &importer->maybe_expunge_uids); n = 0; + while (seq_range_array_iter_nth(&iter, n++, &uid)) { + if (uid > importer->last_common_uid) { + /* we expunge messages only up to last_common_uid, + ignore the rest */ + break; + } + + if (mail_set_uid(importer->mail, uid)) + mail_expunge(importer->mail); + } + + /* handle pending saves */ + saves = array_get(&importer->maybe_saves, &count); + for (i = 0; i < count; i++) { + if (saves[i]->uid > importer->last_common_uid) { + imp_debug(importer, "Delayed save UID=%u: Save", + saves[i]->uid); + dsync_mailbox_save(importer, saves[i]); + } else { + imp_debug(importer, "Delayed save UID=%u: Ignore", + saves[i]->uid); + } + } +} + +static int +dsync_mailbox_import_match_msg(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change, + const char **result_r) +{ + const char *hdr_hash, *cmp_guid; + + if (*change->guid != '\0' && *importer->cur_guid != '\0') { + /* we have GUIDs, verify them */ + if (dsync_mail_change_guid_equals(importer, change, + importer->cur_guid, &cmp_guid)) { + *result_r = "GUIDs match"; + return 1; + } else { + *result_r = t_strdup_printf("GUIDs don't match (%s vs %s)", + importer->cur_guid, cmp_guid); + return 0; + } + } + + /* verify hdr_hash if it exists */ + if (change->hdr_hash == NULL) { + i_assert(*importer->cur_guid == '\0'); + if (change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) { + /* the message was already expunged, so we don't know + its header. return "unknown". */ + *result_r = "Unknown match for expunge"; + return -1; + } + i_error("Mailbox %s: GUIDs not supported, " + "sync with header hashes instead", + mailbox_get_vname(importer->box)); + importer->mail_error = MAIL_ERROR_TEMP; + importer->failed = TRUE; + *result_r = "Error, invalid parameters"; + return -1; + } + + if (dsync_mail_get_hdr_hash(importer->cur_mail, + importer->hdr_hash_version, + importer->hashed_headers, &hdr_hash) < 0) { + dsync_mail_error(importer, importer->cur_mail, "hdr-stream"); + *result_r = "Error fetching header stream"; + return -1; + } + if (importer->empty_hdr_workaround && + (dsync_mail_hdr_hash_is_empty(change->hdr_hash) || + dsync_mail_hdr_hash_is_empty(hdr_hash))) { + *result_r = "Empty headers found with workaround enabled - assuming a match"; + return 1; + } else if (strcmp(change->hdr_hash, hdr_hash) == 0) { + *result_r = "Headers hashes match"; + return 1; + } else { + *result_r = t_strdup_printf("Headers hashes don't match (%s vs %s)", + change->hdr_hash, hdr_hash); + return 0; + } +} + +static bool +dsync_mailbox_find_common_expunged_uid(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change, + const char **result_r) +{ + const struct dsync_mail_change *local_change; + + if (*change->guid == '\0') { + /* remote doesn't support GUIDs, can't verify expunge */ + *result_r = "GUIDs not supported, can't verify expunge"; + return FALSE; + } + + /* local message is expunged. see if we can find its GUID from + transaction log and check if the GUIDs match. The GUID in + log is a 128bit GUID, so we may need to convert the remote's + GUID string to 128bit GUID first. */ + local_change = hash_table_lookup(importer->local_changes, + POINTER_CAST(change->uid)); + if (local_change == NULL || local_change->guid == NULL) { + *result_r = "Expunged local mail's GUID not found"; + return FALSE; + } + + i_assert(local_change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE); + if (dsync_mail_change_guid_equals(importer, local_change, + change->guid, NULL)) { + importer->last_common_uid = change->uid; + *result_r = "Expunged local mail's GUID matches remote"; + } else if (change->type != DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) { + dsync_mailbox_common_uid_found(importer); + *result_r = "Expunged local mail's GUID doesn't match remote GUID"; + } else { + /* GUID mismatch for two expunged mails. dsync can't update + GUIDs for already expunged messages, so we can't immediately + determine that the rest of the messages are a mismatch. so + for now we'll just skip over this pair. */ + *result_r = "Expunged mails' GUIDs don't match - delaying decision"; + /* NOTE: the return value here doesn't matter, because the only + caller that checks for it never reaches this code path */ + } + return TRUE; +} + +static void +dsync_mailbox_revert_missing(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change) +{ + i_assert(importer->revert_local_changes); + + /* mail exists on remote, but not locally. we'll need to + insert this mail back, which means deleting the whole + mailbox and resyncing. */ + i_warning("Deleting mailbox '%s': UID=%u GUID=%s is missing locally", + mailbox_get_vname(importer->box), + change->uid, change->guid); + importer->delete_mailbox = TRUE; + importer->mail_error = MAIL_ERROR_TEMP; + importer->failed = TRUE; +} + +static void +dsync_mailbox_find_common_uid(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change, + const char **result_r) +{ + int ret; + + i_assert(change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE || + ((change->received_timestamp > 0 || + (importer->sync_since_timestamp == 0 && + importer->sync_until_timestamp == 0)) && + (change->virtual_size != UOFF_T_MAX || importer->sync_max_size == 0))); + + /* try to find the matching local mail */ + if (!importer_next_mail(importer, change->uid)) { + /* no more local mails. we can still try to match + expunged mails though. */ + if (change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) { + /* mail doesn't exist remotely either, don't bother + looking it up locally. */ + *result_r = "Expunged mail not found locally"; + return; + } + i_assert(change->guid != NULL); + if (!dsync_mailbox_import_want_change(importer, change, result_r)) + ; + else if (importer->local_uid_next <= change->uid) { + dsync_mailbox_common_uid_found(importer); + *result_r = "Mail's UID is above local UIDNEXT"; + } else if (importer->revert_local_changes) { + dsync_mailbox_revert_missing(importer, change); + *result_r = "Reverting local change by deleting mailbox"; + } else if (!dsync_mailbox_find_common_expunged_uid(importer, change, result_r)) { + /* it's unknown if this mail existed locally and was + expunged. since we don't want to lose any mails, + assume that we need to preserve the mail. use the + last message with a matching GUID as the last common + UID. */ + dsync_mailbox_common_uid_found(importer); + } + *result_r = t_strdup_printf("%s - No more local mails found", *result_r); + return; + } + + if (change->guid == NULL) { + /* we can't know if this UID matches */ + i_assert(change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE); + *result_r = "Expunged mail has no GUID, can't verify it"; + return; + } + if (importer->cur_mail->uid == change->uid) { + /* we have a matching local UID. check GUID to see if it's + really the same mail or not */ + if ((ret = dsync_mailbox_import_match_msg(importer, change, result_r)) < 0) { + /* unknown */ + return; + } + if (ret > 0) { + importer->last_common_uid = change->uid; + } else if (!importer->revert_local_changes) { + /* mismatch - found the first non-common UID */ + dsync_mailbox_common_uid_found(importer); + } else { + /* mismatch and we want to revert local changes - + need to delete the mailbox. */ + dsync_mailbox_revert_existing_uid(importer, change->uid, *result_r); + } + return; + } + /* mail exists remotely, but doesn't exist locally. */ + if (!dsync_mailbox_import_want_change(importer, change, result_r)) + return; + if (importer->revert_local_changes && + change->type != DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) { + dsync_mailbox_revert_missing(importer, change); + *result_r = "Reverting local change by deleting mailbox"; + } else { + (void)dsync_mailbox_find_common_expunged_uid(importer, change, result_r); + } + *result_r = t_strdup_printf("%s (next local mail UID=%u)", + *result_r, importer->cur_mail == NULL ? 0 : importer->cur_mail->uid); +} + +int dsync_mailbox_import_change(struct dsync_mailbox_importer *importer, + const struct dsync_mail_change *change) +{ + const char *result; + + i_assert(!importer->new_uids_assigned); + i_assert(importer->prev_uid < change->uid); + + importer->prev_uid = change->uid; + + if (importer->failed) + return -1; + if (importer->require_full_resync) + return 0; + + if (!importer->last_common_uid_found) { + result = NULL; + dsync_mailbox_find_common_uid(importer, change, &result); + i_assert(result != NULL); + } else { + result = "New mail"; + } + + imp_debug(importer, "Import change type=%s GUID=%s UID=%u hdr_hash=%s result=%s", + dsync_mail_change_type_names[change->type], + change->guid != NULL ? change->guid : "", change->uid, + change->hdr_hash != NULL ? change->hdr_hash : "", result); + + if (importer->failed) + return -1; + if (importer->require_full_resync) + return 0; + + if (importer->last_common_uid_found) { + /* a) uid <= last_common_uid for flag changes and expunges. + this happens only when last_common_uid was originally given + as parameter to importer. + + when we're finding the last_common_uid ourself, + uid>last_common_uid always in here, because + last_common_uid_found=TRUE only after we find the first + mismatch. + + b) uid > last_common_uid for i) new messages, ii) expunges + that were sent "just in case" */ + if (change->uid <= importer->last_common_uid) { + i_assert(change->type != DSYNC_MAIL_CHANGE_TYPE_SAVE); + } else if (change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE) { + /* ignore */ + return 0; + } else { + i_assert(change->type == DSYNC_MAIL_CHANGE_TYPE_SAVE); + } + } else { + /* a) uid < last_common_uid can never happen */ + i_assert(change->uid >= importer->last_common_uid); + /* b) uid = last_common_uid if we've verified that the + messages' GUIDs match so far. + + c) uid > last_common_uid: i) TYPE_EXPUNGE change has + GUID=NULL, so we couldn't verify yet if it matches our + local message, ii) local message is expunged and we couldn't + find its GUID */ + if (change->uid > importer->last_common_uid) { + i_assert(change->type == DSYNC_MAIL_CHANGE_TYPE_EXPUNGE || + importer->cur_mail == NULL || + change->uid < importer->cur_mail->uid); + } + } + + switch (change->type) { + case DSYNC_MAIL_CHANGE_TYPE_SAVE: + dsync_mailbox_import_save(importer, change); + break; + case DSYNC_MAIL_CHANGE_TYPE_EXPUNGE: + dsync_mailbox_import_expunge(importer, change); + break; + case DSYNC_MAIL_CHANGE_TYPE_FLAG_CHANGE: + i_assert(importer->last_common_uid_found); + dsync_mailbox_import_flag_change(importer, change); + break; + } + return importer->failed ? -1 : 0; +} + +static int +importer_new_mail_final_uid_cmp(struct importer_new_mail *const *newmail1, + struct importer_new_mail *const *newmail2) +{ + if ((*newmail1)->final_uid < (*newmail2)->final_uid) + return -1; + if ((*newmail1)->final_uid > (*newmail2)->final_uid) + return 1; + return 0; +} + +static void +dsync_mailbox_import_assign_new_uids(struct dsync_mailbox_importer *importer) +{ + struct importer_new_mail *newmail; + uint32_t common_uid_next, new_uid; + + common_uid_next = I_MAX(importer->local_uid_next, + importer->remote_uid_next); + array_foreach_elem(&importer->newmails, newmail) { + if (newmail->skip) { + /* already assigned */ + i_assert(newmail->final_uid != 0); + continue; + } + + /* figure out what UID to use for the mail */ + if (newmail->uid_is_usable) { + /* keep the UID */ + new_uid = newmail->final_uid; + } else if (newmail->link != NULL && + newmail->link->uid_is_usable) { + /* we can use the linked message's UID and expunge + this mail */ + new_uid = newmail->link->final_uid; + } else { + i_assert(!importer->revert_local_changes); + new_uid = common_uid_next++; + imp_debug(importer, "UID %u isn't usable, assigning new UID %u", + newmail->final_uid, new_uid); + } + + newmail->final_uid = new_uid; + if (newmail->link != NULL && newmail->link != newmail) { + /* skip processing the linked mail */ + newmail->link->skip = TRUE; + } + } + importer->last_common_uid = common_uid_next-1; + importer->new_uids_assigned = TRUE; + /* Sort the newmails by their final_uid. This is used for tracking + whether an intermediate commit is allowed. */ + array_sort(&importer->newmails, importer_new_mail_final_uid_cmp); +} + +static int +dsync_mailbox_import_local_uid(struct dsync_mailbox_importer *importer, + struct mail *mail, uint32_t uid, const char *guid, + struct dsync_mail *dmail_r) +{ + const char *error_field, *errstr; + enum mail_error error; + + if (!mail_set_uid(mail, uid)) + return 0; + + /* NOTE: Errors are logged, but they don't cause the entire import + to fail. */ + if (dsync_mail_fill(mail, TRUE, dmail_r, &error_field) < 0) { + errstr = mailbox_get_last_internal_error(mail->box, &error); + if (error == MAIL_ERROR_EXPUNGED) + return 0; + + i_error("Mailbox %s: Can't lookup %s for UID=%u: %s", + mailbox_get_vname(importer->box), + error_field, uid, errstr); + return -1; + } + if (*guid != '\0' && strcmp(guid, dmail_r->guid) != 0) { + dsync_import_unexpected_state(importer, t_strdup_printf( + "Unexpected GUID mismatch (3) for UID=%u: %s != %s", + uid, dmail_r->guid, guid)); + return -1; + } + return 1; +} + +static void +dsync_mailbox_import_saved_uid(struct dsync_mailbox_importer *importer, + uint32_t uid) +{ + i_assert(importer->search_ctx == NULL); + + if (importer->highest_wanted_uid < uid) + importer->highest_wanted_uid = uid; + array_push_back(&importer->wanted_uids, &uid); +} + +static void +dsync_mailbox_import_update_first_saved(struct dsync_mailbox_importer *importer) +{ + struct importer_new_mail *const *newmails; + unsigned int count; + + newmails = array_get(&importer->newmails, &count); + while (importer->first_unsaved_idx < count) { + if (!newmails[importer->first_unsaved_idx]->saved) + break; + importer->first_unsaved_idx++; + } +} + +static void +dsync_mailbox_import_saved_newmail(struct dsync_mailbox_importer *importer, + struct importer_new_mail *newmail) +{ + dsync_mailbox_import_saved_uid(importer, newmail->final_uid); + newmail->saved = TRUE; + + dsync_mailbox_import_update_first_saved(importer); + importer->saves_since_commit++; + /* we can commit only if all the upcoming mails will have UIDs that + are larger than we're committing. + + Note that if any existing UIDs have been changed, the new UID is + usually higher than anything that is being saved so we can't do + an intermediate commit. It's too much extra work to try to handle + that situation. So here this never happens, because then + array_count(wanted_uids) is always higher than first_unsaved_idx. */ + if (importer->saves_since_commit >= importer->commit_msgs_interval && + importer->first_unsaved_idx == array_count(&importer->wanted_uids)) { + if (dsync_mailbox_import_commit(importer, FALSE) < 0) + importer->failed = TRUE; + importer->saves_since_commit = 0; + } +} + +static bool +dsync_msg_change_uid(struct dsync_mailbox_importer *importer, + uint32_t old_uid, uint32_t new_uid) +{ + struct mail_save_context *save_ctx; + + IMPORTER_DEBUG_CHANGE(importer); + + if (!mail_set_uid(importer->mail, old_uid)) + return FALSE; + + save_ctx = mailbox_save_alloc(importer->ext_trans); + mailbox_save_copy_flags(save_ctx, importer->mail); + mailbox_save_set_uid(save_ctx, new_uid); + if (mailbox_move(&save_ctx, importer->mail) < 0) + return FALSE; + dsync_mailbox_import_saved_uid(importer, new_uid); + return TRUE; +} + +static bool +dsync_mailbox_import_change_uid(struct dsync_mailbox_importer *importer, + ARRAY_TYPE(seq_range) *unwanted_uids, + uint32_t wanted_uid) +{ + const struct seq_range *range; + unsigned int count, n; + struct seq_range_iter iter; + uint32_t uid; + + /* optimize by first trying to use the latest UID */ + range = array_get(unwanted_uids, &count); + if (count == 0) + return FALSE; + if (dsync_msg_change_uid(importer, range[count-1].seq2, wanted_uid)) { + seq_range_array_remove(unwanted_uids, range[count-1].seq2); + return TRUE; + } + if (mailbox_get_last_mail_error(importer->box) == MAIL_ERROR_EXPUNGED) + seq_range_array_remove(unwanted_uids, range[count-1].seq2); + + /* now try to use any of them by iterating through them. (would be + easier&faster to just iterate backwards, but probably too much + trouble to add such API) */ + n = 0; seq_range_array_iter_init(&iter, unwanted_uids); + while (seq_range_array_iter_nth(&iter, n++, &uid)) { + if (dsync_msg_change_uid(importer, uid, wanted_uid)) { + seq_range_array_remove(unwanted_uids, uid); + return TRUE; + } + if (mailbox_get_last_mail_error(importer->box) == MAIL_ERROR_EXPUNGED) + seq_range_array_remove(unwanted_uids, uid); + } + return FALSE; +} + +static bool +dsync_mailbox_import_try_local(struct dsync_mailbox_importer *importer, + struct importer_new_mail *all_newmails, + ARRAY_TYPE(seq_range) *local_uids, + ARRAY_TYPE(seq_range) *wanted_uids) +{ + ARRAY_TYPE(seq_range) assigned_uids, unwanted_uids; + struct seq_range_iter local_iter, wanted_iter; + unsigned int local_n, wanted_n; + uint32_t local_uid, wanted_uid; + struct importer_new_mail *mail; + struct dsync_mail dmail; + + if (array_count(local_uids) == 0) + return FALSE; + + local_n = wanted_n = 0; + seq_range_array_iter_init(&local_iter, local_uids); + seq_range_array_iter_init(&wanted_iter, wanted_uids); + + /* wanted_uids contains UIDs that need to exist at the end. those that + don't already exist in local_uids have a higher UID than any + existing local UID */ + t_array_init(&assigned_uids, array_count(wanted_uids)); + t_array_init(&unwanted_uids, 8); + while (seq_range_array_iter_nth(&local_iter, local_n++, &local_uid)) { + if (seq_range_array_iter_nth(&wanted_iter, wanted_n, + &wanted_uid)) { + if (local_uid == wanted_uid) { + /* we have exactly the UID we want. keep it. */ + seq_range_array_add(&assigned_uids, wanted_uid); + wanted_n++; + continue; + } + i_assert(local_uid < wanted_uid); + } + /* we no longer want this local UID. */ + seq_range_array_add(&unwanted_uids, local_uid); + } + + /* reuse as many existing messages as possible by changing their UIDs */ + while (seq_range_array_iter_nth(&wanted_iter, wanted_n, &wanted_uid)) { + if (!dsync_mailbox_import_change_uid(importer, &unwanted_uids, + wanted_uid)) + break; + seq_range_array_add(&assigned_uids, wanted_uid); + wanted_n++; + } + + /* expunge all unwanted messages */ + local_n = 0; seq_range_array_iter_init(&local_iter, &unwanted_uids); + while (seq_range_array_iter_nth(&local_iter, local_n++, &local_uid)) { + IMPORTER_DEBUG_CHANGE(importer); + if (mail_set_uid(importer->mail, local_uid)) + mail_expunge(importer->mail); + } + + /* mark mails whose UIDs we got to be skipped over later */ + for (mail = all_newmails; mail != NULL; mail = mail->next) { + if (!mail->skip && + seq_range_exists(&assigned_uids, mail->final_uid)) + mail->skip = TRUE; + } + + if (!seq_range_array_iter_nth(&wanted_iter, wanted_n, &wanted_uid)) { + /* we've assigned all wanted UIDs */ + return TRUE; + } + + /* try to find one existing message that we can use to copy to the + other instances */ + local_n = 0; seq_range_array_iter_init(&local_iter, local_uids); + while (seq_range_array_iter_nth(&local_iter, local_n++, &local_uid)) { + if (dsync_mailbox_import_local_uid(importer, importer->mail, + local_uid, all_newmails->guid, + &dmail) > 0) { + if (dsync_mailbox_save_newmails(importer, &dmail, + all_newmails, FALSE)) + return TRUE; + } + } + return FALSE; +} + +static bool +dsync_mailbox_import_try_virtual_all(struct dsync_mailbox_importer *importer, + struct importer_new_mail *all_newmails) +{ + struct dsync_mail dmail; + + if (all_newmails->virtual_all_uid == 0) + return FALSE; + + if (dsync_mailbox_import_local_uid(importer, importer->virtual_mail, + all_newmails->virtual_all_uid, + all_newmails->guid, &dmail) > 0) { + if (dsync_mailbox_save_newmails(importer, &dmail, + all_newmails, FALSE)) + return TRUE; + } + return FALSE; +} + +static bool +dsync_mailbox_import_handle_mail(struct dsync_mailbox_importer *importer, + struct importer_new_mail *all_newmails) +{ + ARRAY_TYPE(seq_range) local_uids, wanted_uids; + struct dsync_mail_request *request; + struct importer_new_mail *mail; + const char *request_guid = NULL; + uint32_t request_uid = 0; + + i_assert(all_newmails != NULL); + + /* get the list of the current local UIDs and the wanted UIDs. + find the first remote instance that we can request in case there are + no local instances */ + t_array_init(&local_uids, 8); + t_array_init(&wanted_uids, 8); + for (mail = all_newmails; mail != NULL; mail = mail->next) { + if (mail->uid_in_local) + seq_range_array_add(&local_uids, mail->local_uid); + else if (request_guid == NULL) { + if (*mail->guid != '\0') + request_guid = mail->guid; + request_uid = mail->remote_uid; + i_assert(request_uid != 0); + } + if (!mail->skip) + seq_range_array_add(&wanted_uids, mail->final_uid); + } + i_assert(array_count(&wanted_uids) > 0); + + if (!dsync_mailbox_import_try_local(importer, all_newmails, + &local_uids, &wanted_uids) && + !dsync_mailbox_import_try_virtual_all(importer, all_newmails)) { + /* no local instance. request from remote */ + IMPORTER_DEBUG_CHANGE(importer); + if (importer->want_mail_requests) { + request = array_append_space(&importer->mail_requests); + request->guid = request_guid; + request->uid = request_uid; + } + return FALSE; + } + /* successfully handled all the mails locally */ + importer->import_pos++; + return TRUE; +} + +static void +dsync_mailbox_import_find_virtual_uids(struct dsync_mailbox_importer *importer) +{ + struct mail_search_context *search_ctx; + struct mail_search_args *search_args; + struct importer_new_mail *newmail; + struct mail *mail; + const char *guid; + + if (mailbox_sync(importer->virtual_all_box, 0) < 0) { + i_error("Couldn't sync \\All mailbox '%s': %s", + mailbox_get_vname(importer->virtual_all_box), + mailbox_get_last_internal_error(importer->virtual_all_box, NULL)); + return; + } + + search_args = mail_search_build_init(); + mail_search_build_add_all(search_args); + + importer->virtual_trans = + mailbox_transaction_begin(importer->virtual_all_box, + importer->transaction_flags, + __func__); + search_ctx = mailbox_search_init(importer->virtual_trans, search_args, + NULL, MAIL_FETCH_GUID, NULL); + mail_search_args_unref(&search_args); + + while (mailbox_search_next(search_ctx, &mail)) { + if (mail_get_special(mail, MAIL_FETCH_GUID, &guid) < 0) { + /* ignore errors */ + continue; + } + newmail = hash_table_lookup(importer->import_guids, guid); + if (newmail != NULL && newmail->virtual_all_uid == 0) + newmail->virtual_all_uid = mail->uid; + } + if (mailbox_search_deinit(&search_ctx) < 0) { + i_error("Couldn't search \\All mailbox '%s': %s", + mailbox_get_vname(importer->virtual_all_box), + mailbox_get_last_internal_error(importer->virtual_all_box, NULL)); + } + + importer->virtual_mail = mail_alloc(importer->virtual_trans, 0, NULL); +} + +static void +dsync_mailbox_import_handle_local_mails(struct dsync_mailbox_importer *importer) +{ + struct hash_iterate_context *iter; + const char *key; + void *key2; + struct importer_new_mail *mail; + + if (importer->virtual_all_box != NULL && + hash_table_count(importer->import_guids) > 0) { + /* find UIDs in \All mailbox for all wanted GUIDs. */ + dsync_mailbox_import_find_virtual_uids(importer); + } + + iter = hash_table_iterate_init(importer->import_guids); + while (hash_table_iterate(iter, importer->import_guids, &key, &mail)) { + T_BEGIN { + if (dsync_mailbox_import_handle_mail(importer, mail)) + hash_table_remove(importer->import_guids, key); + } T_END; + } + hash_table_iterate_deinit(&iter); + + iter = hash_table_iterate_init(importer->import_uids); + while (hash_table_iterate(iter, importer->import_uids, &key2, &mail)) { + T_BEGIN { + if (dsync_mailbox_import_handle_mail(importer, mail)) + hash_table_remove(importer->import_uids, key2); + } T_END; + } + hash_table_iterate_deinit(&iter); + if (!importer->mails_have_guids) { + array_foreach_elem(&importer->newmails, mail) { + if (mail->uid_in_local) + (void)dsync_mailbox_import_handle_mail(importer, mail); + } + } +} + +int dsync_mailbox_import_changes_finish(struct dsync_mailbox_importer *importer) +{ + i_assert(!importer->new_uids_assigned); + + if (!importer->last_common_uid_found) { + /* handle pending expunges and flag updates */ + dsync_mailbox_common_uid_found(importer); + } + /* skip common local mails */ + (void)importer_next_mail(importer, importer->last_common_uid+1); + /* if there are any local mails left, add them to newmails list */ + while (importer->cur_mail != NULL && !importer->failed) + (void)dsync_mailbox_try_save(importer, NULL); + + if (importer->search_ctx != NULL) { + if (mailbox_search_deinit(&importer->search_ctx) < 0) { + i_error("Mailbox %s: Search failed: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + importer->failed = TRUE; + } + } + importer->import_count = hash_table_count(importer->import_guids) + + hash_table_count(importer->import_uids); + + dsync_mailbox_import_assign_new_uids(importer); + /* save mails from local sources where possible, + request the rest from remote */ + if (!importer->failed) + dsync_mailbox_import_handle_local_mails(importer); + return importer->failed ? -1 : 0; +} + +const struct dsync_mail_request * +dsync_mailbox_import_next_request(struct dsync_mailbox_importer *importer) +{ + const struct dsync_mail_request *requests; + unsigned int count; + + requests = array_get(&importer->mail_requests, &count); + if (importer->mail_request_idx == count) + return NULL; + return &requests[importer->mail_request_idx++]; +} + +static const char *const * +dsync_mailbox_get_final_keywords(const struct dsync_mail_change *change) +{ + ARRAY_TYPE(const_string) keywords; + const char *const *changes; + unsigned int i, count; + + if (!array_is_created(&change->keyword_changes)) + return NULL; + + changes = array_get(&change->keyword_changes, &count); + t_array_init(&keywords, count); + for (i = 0; i < count; i++) { + if (changes[i][0] == KEYWORD_CHANGE_ADD || + changes[i][0] == KEYWORD_CHANGE_FINAL || + changes[i][0] == KEYWORD_CHANGE_ADD_AND_FINAL) { + const char *name = changes[i]+1; + + array_push_back(&keywords, &name); + } + } + if (array_count(&keywords) == 0) + return NULL; + + array_append_zero(&keywords); + return array_front(&keywords); +} + +static void +dsync_mailbox_save_set_metadata(struct dsync_mailbox_importer *importer, + struct mail_save_context *save_ctx, + const struct dsync_mail_change *change) +{ + const char *const *keyword_names; + struct mail_keywords *keywords; + + keyword_names = dsync_mailbox_get_final_keywords(change); + keywords = keyword_names == NULL ? NULL : + mailbox_keywords_create_valid(importer->box, + keyword_names); + mailbox_save_set_flags(save_ctx, change->final_flags, keywords); + if (keywords != NULL) + mailbox_keywords_unref(&keywords); + + if (change->modseq > 1) { + (void)mailbox_enable(importer->box, MAILBOX_FEATURE_CONDSTORE); + mailbox_save_set_min_modseq(save_ctx, change->modseq); + } + /* FIXME: if there already are private flags, they get lost because + saving can't handle updating private index. they get added on the + next sync though. if this is fixed here, set min_pvt_modseq also. */ +} + +static int +dsync_msg_try_copy(struct dsync_mailbox_importer *importer, + struct mail_save_context **save_ctx_p, + struct importer_new_mail **all_newmails_forcopy) +{ + struct importer_new_mail *inst; + + for (inst = *all_newmails_forcopy; inst != NULL; inst = inst->next) { + if (inst->uid_in_local && !inst->copy_failed && + mail_set_uid(importer->mail, inst->local_uid)) { + if (mailbox_copy(save_ctx_p, importer->mail) < 0) { + enum mail_error error; + const char *errstr; + + errstr = mailbox_get_last_internal_error(importer->box, &error); + if (error != MAIL_ERROR_EXPUNGED) { + i_warning("Failed to copy mail from UID=%u: " + "%s - falling back to other means", + inst->local_uid, errstr); + } + inst->copy_failed = TRUE; + return -1; + } + *all_newmails_forcopy = inst; + return 1; + } + } + *all_newmails_forcopy = NULL; + return 0; +} + +static void +dsync_mailbox_save_set_nonminimal(struct mail_save_context *save_ctx, + const struct dsync_mail *mail) +{ + if (mail->pop3_uidl != NULL && *mail->pop3_uidl != '\0') + mailbox_save_set_pop3_uidl(save_ctx, mail->pop3_uidl); + if (mail->pop3_order > 0) + mailbox_save_set_pop3_order(save_ctx, mail->pop3_order); + mailbox_save_set_received_date(save_ctx, mail->received_date, 0); +} + +static struct mail_save_context * +dsync_mailbox_save_init(struct dsync_mailbox_importer *importer, + const struct dsync_mail *mail, + struct importer_new_mail *newmail) +{ + struct mail_save_context *save_ctx; + + save_ctx = mailbox_save_alloc(importer->ext_trans); + mailbox_save_set_uid(save_ctx, newmail->final_uid); + if (*mail->guid != '\0') + mailbox_save_set_guid(save_ctx, mail->guid); + if (mail->saved_date != 0) + mailbox_save_set_save_date(save_ctx, mail->saved_date); + dsync_mailbox_save_set_metadata(importer, save_ctx, newmail->change); + + if (!mail->minimal_fields) + dsync_mailbox_save_set_nonminimal(save_ctx, mail); + return save_ctx; +} + +static bool +dsync_mailbox_save_body(struct dsync_mailbox_importer *importer, + const struct dsync_mail *mail, + struct importer_new_mail *newmail, + struct importer_new_mail **all_newmails_forcopy, + bool remote_mail) +{ + struct mail_save_context *save_ctx; + struct istream *input; + ssize_t ret; + bool save_failed = FALSE; + + /* try to save the mail by copying an existing mail */ + save_ctx = dsync_mailbox_save_init(importer, mail, newmail); + if ((ret = dsync_msg_try_copy(importer, &save_ctx, all_newmails_forcopy)) < 0) { + if (save_ctx == NULL) + save_ctx = dsync_mailbox_save_init(importer, mail, newmail); + } + if (ret <= 0 && mail->input_mail != NULL) { + /* copy using the source mail */ + i_assert(mail->input_mail->uid == mail->input_mail_uid); + if (mailbox_copy(&save_ctx, mail->input_mail) == 0) + ret = 1; + else { + enum mail_error error; + const char *errstr; + + errstr = mailbox_get_last_internal_error(importer->box, &error); + if (error != MAIL_ERROR_EXPUNGED) { + i_warning("Failed to copy source UID=%u mail: " + "%s - falling back to regular saving", + mail->input_mail->uid, errstr); + } + ret = -1; + save_ctx = dsync_mailbox_save_init(importer, mail, newmail); + } + + } + if (ret > 0) { + i_assert(save_ctx == NULL); + dsync_mailbox_import_saved_newmail(importer, newmail); + return TRUE; + } + /* fallback to saving from remote stream */ + if (!remote_mail) { + /* the mail isn't remote yet. we were just trying to copy a + local mail to avoid downloading the remote mail. */ + mailbox_save_cancel(&save_ctx); + return FALSE; + } + if (mail->minimal_fields) { + struct dsync_mail mail2; + const char *error_field; + + i_assert(mail->input_mail != NULL); + + if (dsync_mail_fill_nonminimal(mail->input_mail, &mail2, + &error_field) < 0) { + i_error("Mailbox %s: Failed to read mail %s uid=%u: %s", + mailbox_get_vname(importer->box), + error_field, mail->uid, + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + importer->failed = TRUE; + mailbox_save_cancel(&save_ctx); + return TRUE; + } + dsync_mailbox_save_set_nonminimal(save_ctx, &mail2); + input = mail2.input; + } else { + input = mail->input; + } + + if (input == NULL) { + /* it was just expunged in remote, skip it */ + mailbox_save_cancel(&save_ctx); + return TRUE; + } + + i_stream_seek(input, 0); + if (mailbox_save_begin(&save_ctx, input) < 0) { + i_error("Mailbox %s: Saving failed: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + importer->failed = TRUE; + return TRUE; + } + while ((ret = i_stream_read(input)) > 0 || ret == -2) { + if (mailbox_save_continue(save_ctx) < 0) { + save_failed = TRUE; + ret = -1; + break; + } + } + i_assert(ret == -1); + + if (input->stream_errno != 0) { + i_error("Mailbox %s: read(msg input) failed: %s", + mailbox_get_vname(importer->box), + i_stream_get_error(input)); + mailbox_save_cancel(&save_ctx); + importer->mail_error = MAIL_ERROR_TEMP; + importer->failed = TRUE; + } else if (save_failed) { + i_error("Mailbox %s: Saving failed: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + mailbox_save_cancel(&save_ctx); + importer->failed = TRUE; + } else { + i_assert(input->eof); + if (mailbox_save_finish(&save_ctx) < 0) { + i_error("Mailbox %s: Saving failed: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + importer->failed = TRUE; + } else { + dsync_mailbox_import_saved_newmail(importer, newmail); + } + } + return TRUE; +} + +static bool dsync_mailbox_save_newmails(struct dsync_mailbox_importer *importer, + const struct dsync_mail *mail, + struct importer_new_mail *all_newmails, + bool remote_mail) +{ + struct importer_new_mail *newmail, *all_newmails_forcopy; + bool ret = TRUE; + + /* if all_newmails list is large, avoid scanning through the + uninteresting ones for each newmail */ + all_newmails_forcopy = all_newmails; + + /* save all instances of the message */ + for (newmail = all_newmails; newmail != NULL && ret; newmail = newmail->next) { + if (!newmail->skip) T_BEGIN { + if (!dsync_mailbox_save_body(importer, mail, newmail, + &all_newmails_forcopy, + remote_mail)) + ret = FALSE; + } T_END; + } + return ret; +} + +int dsync_mailbox_import_mail(struct dsync_mailbox_importer *importer, + const struct dsync_mail *mail) +{ + struct importer_new_mail *all_newmails; + + i_assert(mail->input == NULL || mail->input->seekable); + i_assert(importer->new_uids_assigned); + + if (importer->failed) + return -1; + if (importer->require_full_resync) + return 0; + + imp_debug(importer, "Import mail body for GUID=%s UID=%u", + mail->guid, mail->uid); + + all_newmails = *mail->guid != '\0' ? + hash_table_lookup(importer->import_guids, mail->guid) : + hash_table_lookup(importer->import_uids, POINTER_CAST(mail->uid)); + if (all_newmails == NULL) { + if (importer->want_mail_requests) { + i_error("Mailbox %s: Remote sent unwanted message body for " + "GUID=%s UID=%u", + mailbox_get_vname(importer->box), + mail->guid, mail->uid); + } else { + imp_debug(importer, "Skip unwanted mail body for " + "GUID=%s UID=%u", mail->guid, mail->uid); + } + return 0; + } + if (*mail->guid != '\0') + hash_table_remove(importer->import_guids, mail->guid); + else { + hash_table_remove(importer->import_uids, + POINTER_CAST(mail->uid)); + } + importer->import_pos++; + if (!dsync_mailbox_save_newmails(importer, mail, all_newmails, TRUE)) + i_unreached(); + return importer->failed ? -1 : 0; +} + +static int +reassign_uids_in_seq_range(struct dsync_mailbox_importer *importer, + const ARRAY_TYPE(seq_range) *unwanted_uids) +{ + struct mailbox *box = importer->box; + const enum mailbox_transaction_flags trans_flags = + importer->transaction_flags | + MAILBOX_TRANSACTION_FLAG_EXTERNAL | + MAILBOX_TRANSACTION_FLAG_ASSIGN_UIDS; + struct mailbox_transaction_context *trans; + struct mail_search_args *search_args; + struct mail_search_arg *arg; + struct mail_search_context *search_ctx; + struct mail_save_context *save_ctx; + struct mail *mail; + unsigned int renumber_count = 0; + int ret = 1; + + if (array_count(unwanted_uids) == 0) + return 1; + + if (importer->debug) T_BEGIN { + string_t *str = t_str_new(256); + imap_write_seq_range(str, unwanted_uids); + imp_debug(importer, "Reassign UIDs: %s", str_c(str)); + } T_END; + + search_args = mail_search_build_init(); + arg = mail_search_build_add(search_args, SEARCH_UIDSET); + p_array_init(&arg->value.seqset, search_args->pool, + array_count(unwanted_uids)); + array_append_array(&arg->value.seqset, unwanted_uids); + + trans = mailbox_transaction_begin(box, trans_flags, __func__); + search_ctx = mailbox_search_init(trans, search_args, NULL, 0, NULL); + mail_search_args_unref(&search_args); + + while (mailbox_search_next(search_ctx, &mail)) { + save_ctx = mailbox_save_alloc(trans); + mailbox_save_copy_flags(save_ctx, mail); + if (mailbox_move(&save_ctx, mail) < 0) { + i_error("Mailbox %s: Couldn't move mail within mailbox: %s", + mailbox_get_vname(box), + mailbox_get_last_internal_error(box, &importer->mail_error)); + ret = -1; + } else if (ret > 0) { + ret = 0; + } + renumber_count++; + } + if (mailbox_search_deinit(&search_ctx) < 0) { + i_error("Mailbox %s: mail search failed: %s", + mailbox_get_vname(box), + mailbox_get_last_internal_error(box, &importer->mail_error)); + ret = -1; + } + + if (mailbox_transaction_commit(&trans) < 0) { + i_error("Mailbox %s: UID reassign commit failed: %s", + mailbox_get_vname(box), + mailbox_get_last_internal_error(box, &importer->mail_error)); + ret = -1; + } + if (ret == 0) { + imp_debug(importer, "Mailbox %s: Change during sync: " + "Renumbered %u of %u unwanted UIDs", + mailbox_get_vname(box), + renumber_count, array_count(unwanted_uids)); + } + return ret; +} + +static int +reassign_unwanted_uids(struct dsync_mailbox_importer *importer, + const char **changes_during_sync_r) +{ + ARRAY_TYPE(seq_range) unwanted_uids; + const uint32_t *wanted_uids, *saved_uids; + uint32_t highest_seen_uid; + unsigned int i, wanted_count, saved_count; + int ret = 0; + + wanted_uids = array_get(&importer->wanted_uids, &wanted_count); + saved_uids = array_get(&importer->saved_uids, &saved_count); + i_assert(wanted_count == saved_count); + if (wanted_count == 0) + return 0; + /* wanted_uids contains the UIDs we tried to save mails with. + if nothing changed during dsync, we should have the expected UIDs + (saved_uids) and all is well. + + if any new messages got inserted during dsync, we'll need to fix up + the UIDs and let the next dsync fix up the other side. for example: + + remote uids = 5,7,9 = wanted_uids + remote uidnext = 12 + locally added new uid=5 -> + saved_uids = 10,7,9 + + we'll now need to reassign UIDs 5 and 10. to be fully future-proof + we'll reassign all UIDs between [original local uidnext .. highest + UID we think we know] that aren't in saved_uids. */ + + /* create uidset for the list of UIDs we don't want to exist */ + t_array_init(&unwanted_uids, 8); + highest_seen_uid = I_MAX(importer->remote_uid_next-1, + importer->highest_wanted_uid); + i_assert(importer->local_uid_next <= highest_seen_uid); + seq_range_array_add_range(&unwanted_uids, + importer->local_uid_next, highest_seen_uid); + for (i = 0; i < wanted_count; i++) { + i_assert(i < wanted_count); + if (saved_uids[i] == wanted_uids[i]) + seq_range_array_remove(&unwanted_uids, saved_uids[i]); + } + + ret = reassign_uids_in_seq_range(importer, &unwanted_uids); + if (ret == 0) { + *changes_during_sync_r = t_strdup_printf( + "%u UIDs changed due to UID conflicts", + seq_range_count(&unwanted_uids)); + /* conflicting changes during sync, revert our last-common-uid + back to a safe value. */ + importer->last_common_uid = importer->local_uid_next - 1; + } + return ret < 0 ? -1 : 0; +} + +static int +dsync_mailbox_import_commit(struct dsync_mailbox_importer *importer, bool final) +{ + struct mail_transaction_commit_changes changes; + struct seq_range_iter iter; + uint32_t uid; + unsigned int n; + int ret = importer->failed ? -1 : 0; + + mail_free(&importer->mail); + mail_free(&importer->ext_mail); + + /* commit saves */ + if (mailbox_transaction_commit_get_changes(&importer->ext_trans, + &changes) < 0) { + i_error("Mailbox %s: Save commit failed: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, &importer->mail_error)); + /* removed wanted_uids that weren't actually saved */ + array_delete(&importer->wanted_uids, + array_count(&importer->saved_uids), + array_count(&importer->wanted_uids) - + array_count(&importer->saved_uids)); + mailbox_transaction_rollback(&importer->trans); + ret = -1; + } else { + /* remember the UIDs that were successfully saved */ + if (importer->debug) T_BEGIN { + string_t *str = t_str_new(256); + imap_write_seq_range(str, &changes.saved_uids); + imp_debug(importer, "Saved UIDs: %s", str_c(str)); + } T_END; + seq_range_array_iter_init(&iter, &changes.saved_uids); n = 0; + while (seq_range_array_iter_nth(&iter, n++, &uid)) + array_push_back(&importer->saved_uids, &uid); + pool_unref(&changes.pool); + + /* commit flag changes and expunges */ + if (mailbox_transaction_commit(&importer->trans) < 0) { + i_error("Mailbox %s: Commit failed: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + ret = -1; + } + } + + if (!final) + dsync_mailbox_import_transaction_begin(importer); + return ret; +} + +static int dsync_mailbox_import_finish(struct dsync_mailbox_importer *importer, + const char **changes_during_sync_r) +{ + struct mailbox_update update; + int ret; + + ret = dsync_mailbox_import_commit(importer, TRUE); + + if (ret == 0) { + /* update mailbox metadata if we successfully saved + everything. */ + i_zero(&update); + update.min_next_uid = importer->remote_uid_next; + update.min_first_recent_uid = + I_MIN(importer->last_common_uid+1, + importer->remote_first_recent_uid); + update.min_highest_modseq = importer->remote_highest_modseq; + update.min_highest_pvt_modseq = importer->remote_highest_pvt_modseq; + + imp_debug(importer, "Finish update: min_next_uid=%u " + "min_first_recent_uid=%u min_highest_modseq=%"PRIu64" " + "min_highest_pvt_modseq=%"PRIu64, + update.min_next_uid, update.min_first_recent_uid, + update.min_highest_modseq, + update.min_highest_pvt_modseq); + + if (mailbox_update(importer->box, &update) < 0) { + i_error("Mailbox %s: Update failed: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + ret = -1; + } + } + + /* sync mailbox to finish flag changes and expunges. */ + if (mailbox_sync(importer->box, 0) < 0) { + i_error("Mailbox %s: Sync failed: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + ret = -1; + } + if (ret == 0) { + /* give new UIDs to messages that got saved with unwanted UIDs. + do it only if the whole transaction succeeded. */ + if (reassign_unwanted_uids(importer, changes_during_sync_r) < 0) + ret = -1; + } + return ret; +} + +static void +dsync_mailbox_import_check_missing_guid_imports(struct dsync_mailbox_importer *importer) +{ + struct hash_iterate_context *iter; + const char *key; + struct importer_new_mail *mail; + + iter = hash_table_iterate_init(importer->import_guids); + while (hash_table_iterate(iter, importer->import_guids, &key, &mail)) { + for (; mail != NULL; mail = mail->next) { + if (mail->skip) + continue; + + i_error("Mailbox %s: Remote didn't send mail GUID=%s (UID=%u)", + mailbox_get_vname(importer->box), + mail->guid, mail->remote_uid); + importer->mail_error = MAIL_ERROR_TEMP; + importer->failed = TRUE; + } + } + hash_table_iterate_deinit(&iter); +} + +static void +dsync_mailbox_import_check_missing_uid_imports(struct dsync_mailbox_importer *importer) +{ + struct hash_iterate_context *iter; + void *key; + struct importer_new_mail *mail; + + iter = hash_table_iterate_init(importer->import_uids); + while (hash_table_iterate(iter, importer->import_uids, &key, &mail)) { + for (; mail != NULL; mail = mail->next) { + if (mail->skip) + continue; + + i_error("Mailbox %s: Remote didn't send mail UID=%u", + mailbox_get_vname(importer->box), + mail->remote_uid); + importer->mail_error = MAIL_ERROR_TEMP; + importer->failed = TRUE; + } + } + hash_table_iterate_deinit(&iter); +} + +int dsync_mailbox_import_deinit(struct dsync_mailbox_importer **_importer, + bool success, + uint32_t *last_common_uid_r, + uint64_t *last_common_modseq_r, + uint64_t *last_common_pvt_modseq_r, + uint32_t *last_messages_count_r, + const char **changes_during_sync_r, + bool *require_full_resync_r, + enum mail_error *error_r) +{ + struct dsync_mailbox_importer *importer = *_importer; + struct mailbox_status status; + int ret; + + *_importer = NULL; + *changes_during_sync_r = NULL; + *require_full_resync_r = importer->require_full_resync; + + if ((!success || importer->require_full_resync) && !importer->failed) { + importer->mail_error = MAIL_ERROR_TEMP; + importer->failed = TRUE; + } + + if (!importer->new_uids_assigned && !importer->failed) + dsync_mailbox_import_assign_new_uids(importer); + + if (!importer->failed) { + dsync_mailbox_import_check_missing_guid_imports(importer); + dsync_mailbox_import_check_missing_uid_imports(importer); + } + + if (importer->search_ctx != NULL) { + if (mailbox_search_deinit(&importer->search_ctx) < 0) { + i_error("Mailbox %s: Search failed: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + importer->failed = TRUE; + } + } + if (dsync_mailbox_import_finish(importer, changes_during_sync_r) < 0) + importer->failed = TRUE; + + if (importer->virtual_mail != NULL) + mail_free(&importer->virtual_mail); + if (importer->virtual_trans != NULL) + (void)mailbox_transaction_commit(&importer->virtual_trans); + + hash_table_destroy(&importer->import_guids); + hash_table_destroy(&importer->import_uids); + array_free(&importer->maybe_expunge_uids); + array_free(&importer->maybe_saves); + array_free(&importer->wanted_uids); + array_free(&importer->saved_uids); + array_free(&importer->newmails); + if (array_is_created(&importer->mail_requests)) + array_free(&importer->mail_requests); + + *last_common_uid_r = importer->last_common_uid; + if (*changes_during_sync_r == NULL) { + *last_common_modseq_r = importer->remote_highest_modseq; + *last_common_pvt_modseq_r = importer->remote_highest_pvt_modseq; + } else { + /* local changes occurred during dsync. we exported changes up + to local_initial_highestmodseq, so all of the changes have + happened after it. we want the next run to see those changes, + so return it as the last common modseq */ + *last_common_modseq_r = importer->local_initial_highestmodseq; + *last_common_pvt_modseq_r = importer->local_initial_highestpvtmodseq; + } + if (importer->delete_mailbox) { + if (mailbox_delete(importer->box) < 0) { + i_error("Couldn't delete mailbox %s: %s", + mailbox_get_vname(importer->box), + mailbox_get_last_internal_error(importer->box, + &importer->mail_error)); + importer->failed = TRUE; + } + *last_messages_count_r = 0; + } else { + mailbox_get_open_status(importer->box, STATUS_MESSAGES, &status); + *last_messages_count_r = status.messages; + } + + i_assert(importer->failed == (importer->mail_error != 0)); + ret = importer->failed ? -1 : 0; + *error_r = importer->mail_error; + pool_unref(&importer->pool); + return ret; +} + +const char *dsync_mailbox_import_get_proctitle(struct dsync_mailbox_importer *importer) +{ + if (importer->search_ctx != NULL) + return ""; + return t_strdup_printf("%u/%u", importer->import_pos, + importer->import_count); +} -- cgit v1.2.3