changeset 16025:c6082de4bf5b

dsync: Added support for syncing mailbox attributes.
author Timo Sirainen <tss@iki.fi>
date Thu, 14 Mar 2013 15:41:09 +0200
parents f3cd9e5cbe99
children 3ba8fa6d3cc2
files src/doveadm/dsync/Makefile.am src/doveadm/dsync/dsync-brain-mailbox.c src/doveadm/dsync/dsync-brain-mails.c src/doveadm/dsync/dsync-brain-private.h src/doveadm/dsync/dsync-ibc-pipe.c src/doveadm/dsync/dsync-ibc-private.h src/doveadm/dsync/dsync-ibc-stream.c src/doveadm/dsync/dsync-ibc.c src/doveadm/dsync/dsync-ibc.h src/doveadm/dsync/dsync-mailbox-export.c src/doveadm/dsync/dsync-mailbox-export.h src/doveadm/dsync/dsync-mailbox-import.c src/doveadm/dsync/dsync-mailbox-import.h src/doveadm/dsync/dsync-mailbox.c src/doveadm/dsync/dsync-mailbox.h src/doveadm/dsync/dsync-transaction-log-scan.c src/doveadm/dsync/dsync-transaction-log-scan.h
diffstat 17 files changed, 578 insertions(+), 20 deletions(-) [+]
line wrap: on
line diff
--- a/src/doveadm/dsync/Makefile.am	Thu Mar 14 15:32:14 2013 +0200
+++ b/src/doveadm/dsync/Makefile.am	Thu Mar 14 15:41:09 2013 +0200
@@ -21,6 +21,7 @@
 	dsync-brain-mails.c \
 	dsync-deserializer.c \
 	dsync-mail.c \
+	dsync-mailbox.c \
 	dsync-mailbox-import.c \
 	dsync-mailbox-export.c \
 	dsync-mailbox-state.c \
--- a/src/doveadm/dsync/dsync-brain-mailbox.c	Thu Mar 14 15:32:14 2013 +0200
+++ b/src/doveadm/dsync/dsync-brain-mailbox.c	Thu Mar 14 15:41:09 2013 +0200
@@ -84,7 +84,7 @@
 {
 	if (brain->backup_send) {
 		/* we have an exporter, but no importer. */
-		brain->box_send_state = DSYNC_BOX_STATE_CHANGES;
+		brain->box_send_state = DSYNC_BOX_STATE_ATTRIBUTES;
 		brain->box_recv_state = brain->mail_requests ?
 			DSYNC_BOX_STATE_MAIL_REQUESTS :
 			DSYNC_BOX_STATE_RECV_LAST_COMMON;
@@ -93,10 +93,10 @@
 		brain->box_send_state = brain->mail_requests ?
 			DSYNC_BOX_STATE_MAIL_REQUESTS :
 			DSYNC_BOX_STATE_DONE;
-		brain->box_recv_state = DSYNC_BOX_STATE_CHANGES;
+		brain->box_recv_state = DSYNC_BOX_STATE_ATTRIBUTES;
 	} else {
-		brain->box_send_state = DSYNC_BOX_STATE_CHANGES;
-		brain->box_recv_state = DSYNC_BOX_STATE_CHANGES;
+		brain->box_send_state = DSYNC_BOX_STATE_ATTRIBUTES;
+		brain->box_recv_state = DSYNC_BOX_STATE_ATTRIBUTES;
 	}
 }
 
--- a/src/doveadm/dsync/dsync-brain-mails.c	Thu Mar 14 15:32:14 2013 +0200
+++ b/src/doveadm/dsync/dsync-brain-mails.c	Thu Mar 14 15:41:09 2013 +0200
@@ -11,6 +11,7 @@
 const char *dsync_box_state_names[DSYNC_BOX_STATE_DONE+1] = {
 	"mailbox",
 	"changes",
+	"attributes",
 	"mail_requests",
 	"mails",
 	"recv_last_common",
@@ -59,6 +60,34 @@
 	return TRUE;
 }
 
+static bool dsync_brain_recv_mailbox_attribute(struct dsync_brain *brain)
+{
+	const struct dsync_mailbox_attribute *attr;
+	enum dsync_ibc_recv_ret ret;
+
+	if ((ret = dsync_ibc_recv_mailbox_attribute(brain->ibc, &attr)) == 0)
+		return FALSE;
+	if (ret == DSYNC_IBC_RECV_RET_FINISHED) {
+		brain->box_recv_state = DSYNC_BOX_STATE_CHANGES;
+		return TRUE;
+	}
+	if (dsync_mailbox_import_attribute(brain->box_importer, attr) < 0)
+		brain->failed = TRUE;
+	return TRUE;
+}
+
+static void dsync_brain_send_mailbox_attribute(struct dsync_brain *brain)
+{
+	const struct dsync_mailbox_attribute *attr;
+
+	while ((attr = dsync_mailbox_export_next_attr(brain->box_exporter)) != NULL) {
+		if (dsync_ibc_send_mailbox_attribute(brain->ibc, attr) == 0)
+			return;
+	}
+	dsync_ibc_send_end_of_list(brain->ibc);
+	brain->box_send_state = DSYNC_BOX_STATE_CHANGES;
+}
+
 static bool dsync_brain_recv_mail_change(struct dsync_brain *brain)
 {
 	const struct dsync_mail_change *change;
@@ -278,6 +307,9 @@
 	case DSYNC_BOX_STATE_MAILBOX:
 		changed = dsync_brain_master_sync_recv_mailbox(brain);
 		break;
+	case DSYNC_BOX_STATE_ATTRIBUTES:
+		changed = dsync_brain_recv_mailbox_attribute(brain);
+		break;
 	case DSYNC_BOX_STATE_CHANGES:
 		changed = dsync_brain_recv_mail_change(brain);
 		break;
@@ -299,6 +331,10 @@
 		case DSYNC_BOX_STATE_MAILBOX:
 			/* wait for mailbox to be received first */
 			break;
+		case DSYNC_BOX_STATE_ATTRIBUTES:
+			dsync_brain_send_mailbox_attribute(brain);
+			changed = TRUE;
+			break;
 		case DSYNC_BOX_STATE_CHANGES:
 			dsync_brain_send_mail_change(brain);
 			changed = TRUE;
--- a/src/doveadm/dsync/dsync-brain-private.h	Thu Mar 14 15:32:14 2013 +0200
+++ b/src/doveadm/dsync/dsync-brain-private.h	Thu Mar 14 15:41:09 2013 +0200
@@ -38,6 +38,7 @@
 enum dsync_box_state {
 	DSYNC_BOX_STATE_MAILBOX,
 	DSYNC_BOX_STATE_CHANGES,
+	DSYNC_BOX_STATE_ATTRIBUTES,
 	DSYNC_BOX_STATE_MAIL_REQUESTS,
 	DSYNC_BOX_STATE_MAILS,
 	DSYNC_BOX_STATE_RECV_LAST_COMMON,
--- a/src/doveadm/dsync/dsync-ibc-pipe.c	Thu Mar 14 15:32:14 2013 +0200
+++ b/src/doveadm/dsync/dsync-ibc-pipe.c	Thu Mar 14 15:41:09 2013 +0200
@@ -16,6 +16,7 @@
 	ITEM_MAILBOX_TREE_NODE,
 	ITEM_MAILBOX_DELETE,
 	ITEM_MAILBOX,
+	ITEM_MAILBOX_ATTRIBUTE,
 	ITEM_MAIL_CHANGE,
 	ITEM_MAIL_REQUEST,
 	ITEM_MAIL
@@ -31,6 +32,7 @@
 		struct dsync_mailbox_node node;
 		guid_128_t mailbox_guid;
 		struct dsync_mailbox dsync_box;
+		struct dsync_mailbox_attribute attr;
 		struct dsync_mail_change change;
 		struct dsync_mail_request request;
 		struct dsync_mail mail;
@@ -84,6 +86,7 @@
 	case ITEM_HANDSHAKE:
 	case ITEM_MAILBOX:
 	case ITEM_MAILBOX_TREE_NODE:
+	case ITEM_MAILBOX_ATTRIBUTE:
 	case ITEM_MAIL_CHANGE:
 	case ITEM_MAIL_REQUEST:
 	case ITEM_MAIL:
@@ -342,6 +345,35 @@
 }
 
 static void
+dsync_ibc_pipe_send_mailbox_attribute(struct dsync_ibc *ibc,
+				      const struct dsync_mailbox_attribute *attr)
+{
+	struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+	struct item *item;
+
+	item = dsync_ibc_pipe_push_item(pipe->remote, ITEM_MAILBOX_ATTRIBUTE);
+	dsync_mailbox_attribute_dup(item->pool, attr, &item->u.attr);
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_pipe_recv_mailbox_attribute(struct dsync_ibc *ibc,
+				      const struct dsync_mailbox_attribute **attr_r)
+{
+	struct dsync_ibc_pipe *pipe = (struct dsync_ibc_pipe *)ibc;
+	struct item *item;
+
+	if (dsync_ibc_pipe_try_pop_eol(pipe))
+		return DSYNC_IBC_RECV_RET_FINISHED;
+
+	item = dsync_ibc_pipe_pop_item(pipe, ITEM_MAILBOX_ATTRIBUTE);
+	if (item == NULL)
+		return DSYNC_IBC_RECV_RET_TRYAGAIN;
+
+	*attr_r = &item->u.attr;
+	return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void
 dsync_ibc_pipe_send_change(struct dsync_ibc *ibc,
 			   const struct dsync_mail_change *change)
 {
@@ -470,6 +502,8 @@
 	dsync_ibc_pipe_recv_mailbox_deletes,
 	dsync_ibc_pipe_send_mailbox,
 	dsync_ibc_pipe_recv_mailbox,
+	dsync_ibc_pipe_send_mailbox_attribute,
+	dsync_ibc_pipe_recv_mailbox_attribute,
 	dsync_ibc_pipe_send_change,
 	dsync_ibc_pipe_recv_change,
 	dsync_ibc_pipe_send_mail_request,
--- a/src/doveadm/dsync/dsync-ibc-private.h	Thu Mar 14 15:32:14 2013 +0200
+++ b/src/doveadm/dsync/dsync-ibc-private.h	Thu Mar 14 15:41:09 2013 +0200
@@ -43,6 +43,12 @@
 		(*recv_mailbox)(struct dsync_ibc *ibc,
 				const struct dsync_mailbox **dsync_box_r);
 
+	void (*send_mailbox_attribute)(struct dsync_ibc *ibc,
+				       const struct dsync_mailbox_attribute *attr);
+	enum dsync_ibc_recv_ret
+		(*recv_mailbox_attribute)(struct dsync_ibc *ibc,
+					  const struct dsync_mailbox_attribute **attr_r);
+
 	void (*send_change)(struct dsync_ibc *ibc,
 			    const struct dsync_mail_change *change);
 	enum dsync_ibc_recv_ret
--- a/src/doveadm/dsync/dsync-ibc-stream.c	Thu Mar 14 15:32:14 2013 +0200
+++ b/src/doveadm/dsync/dsync-ibc-stream.c	Thu Mar 14 15:41:09 2013 +0200
@@ -28,7 +28,10 @@
 #define DSYNC_IBC_STREAM_OUTBUF_THROTTLE_SIZE (1024*128)
 
 #define DSYNC_PROTOCOL_VERSION_MAJOR 3
-#define DSYNC_HANDSHAKE_VERSION "VERSION\tdsync\t3\t0\n"
+#define DSYNC_PROTOCOL_VERSION_MINOR 1
+#define DSYNC_HANDSHAKE_VERSION "VERSION\tdsync\t3\t1\n"
+
+#define DSYNC_PROTOCOL_MINOR_HAVE_ATTRIBUTES 1
 
 enum item_type {
 	ITEM_NONE,
@@ -40,6 +43,7 @@
 	ITEM_MAILBOX_DELETE,
 	ITEM_MAILBOX,
 
+	ITEM_MAILBOX_ATTRIBUTE,
 	ITEM_MAIL_CHANGE,
 	ITEM_MAIL_REQUEST,
 	ITEM_MAIL,
@@ -92,6 +96,11 @@
 		"first_recent_uid highest_modseq highest_pvt_modseq",
 	  .optional_keys = "mailbox_lost cache_fields have_guids"
 	},
+	{ .name = "mailbox_attribute",
+	  .chr = 'A',
+	  .required_keys = "type key",
+	  .optional_keys = "value deleted last_change modseq"
+	},
 	{ .name = "mail_change",
 	  .chr = 'C',
 	  .required_keys = "type uid",
@@ -126,6 +135,7 @@
 	struct io *io;
 	struct timeout *to;
 
+	unsigned int minor_version;
 	struct dsync_serializer *serializers[ITEM_END_OF_LIST];
 	struct dsync_deserializer *deserializers[ITEM_END_OF_LIST];
 
@@ -195,16 +205,9 @@
 					 &data, &size, 0)) > 0) {
 		add = '\0';
 		for (i = 0; i < size; i++) {
-			if (data[i] == '\n') {
-				if ((i == 0 && ibc->mail_output_last != '\r') ||
-				    (i > 0 && data[i-1] != '\r')) {
-					/* missing CR */
-					add = '\r';
-					break;
-				}
-			} else if (data[i] == '.' &&
-				   ((i == 0 && ibc->mail_output_last == '\n') ||
-				    (i > 0 && data[i-1] == '\n'))) {
+			if (data[i] == '.' &&
+			    ((i == 0 && ibc->mail_output_last == '\n') ||
+			     (i > 0 && data[i-1] == '\n'))) {
 				/* escape the dot */
 				add = '.';
 				break;
@@ -243,7 +246,8 @@
 		return -1;
 	}
 
-	/* finished sending the stream */
+	/* finished sending the stream. use "CRLF." instead of "LF." just in
+	   case we're sending binary data that ends with CR. */
 	o_stream_nsend_str(ibc->output, "\r\n.\r\n");
 	i_stream_unref(&ibc->mail_output);
 	return 1;
@@ -429,8 +433,9 @@
 		return TRUE;
 
 	if (!ibc->version_received) {
-		if (!version_string_verify(line, "dsync",
-					   DSYNC_PROTOCOL_VERSION_MAJOR)) {
+		if (!version_string_verify_full(line, "dsync",
+						DSYNC_PROTOCOL_VERSION_MAJOR,
+						&ibc->minor_version)) {
 			dsync_ibc_input_error(ibc, NULL,
 				"Remote dsync doesn't use compatible protocol");
 			return DSYNC_IBC_RECV_RET_TRYAGAIN;
@@ -1185,6 +1190,107 @@
 }
 
 static void
+dsync_ibc_stream_send_mailbox_attribute(struct dsync_ibc *_ibc,
+					const struct dsync_mailbox_attribute *attr)
+{
+	struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+	struct dsync_serializer_encoder *encoder;
+	string_t *str = t_str_new(128);
+	char type[2];
+
+	if (ibc->minor_version < DSYNC_PROTOCOL_MINOR_HAVE_ATTRIBUTES)
+		return;
+
+	str_append_c(str, items[ITEM_MAILBOX_ATTRIBUTE].chr);
+	encoder = dsync_serializer_encode_begin(ibc->serializers[ITEM_MAILBOX_ATTRIBUTE]);
+
+	type[0] = type[1] = '\0';
+	switch (attr->type) {
+	case MAIL_ATTRIBUTE_TYPE_PRIVATE:
+		type[0] = 'p';
+		break;
+	case MAIL_ATTRIBUTE_TYPE_SHARED:
+		type[0] = 's';
+		break;
+	}
+	i_assert(type[0] != '\0');
+	dsync_serializer_encode_add(encoder, "type", type);
+	dsync_serializer_encode_add(encoder, "key", attr->key);
+	if (attr->value != NULL)
+		dsync_serializer_encode_add(encoder, "value", attr->value);
+
+	if (attr->deleted)
+		dsync_serializer_encode_add(encoder, "deleted", "");
+	if (attr->last_change != 0) {
+		dsync_serializer_encode_add(encoder, "last_change",
+					    dec2str(attr->last_change));
+	}
+	if (attr->modseq != 0) {
+		dsync_serializer_encode_add(encoder, "modseq",
+					    dec2str(attr->modseq));
+	}
+
+	dsync_serializer_encode_finish(&encoder, str);
+	dsync_ibc_stream_send_string(ibc, str);
+}
+
+static enum dsync_ibc_recv_ret
+dsync_ibc_stream_recv_mailbox_attribute(struct dsync_ibc *_ibc,
+					const struct dsync_mailbox_attribute **attr_r)
+{
+	struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+	pool_t pool = ibc->ret_pool;
+	struct dsync_deserializer_decoder *decoder;
+	struct dsync_mailbox_attribute *attr;
+	const char *value;
+	enum dsync_ibc_recv_ret ret;
+
+	if (ibc->minor_version < DSYNC_PROTOCOL_MINOR_HAVE_ATTRIBUTES)
+		return DSYNC_IBC_RECV_RET_FINISHED;
+
+	p_clear(pool);
+	attr = p_new(pool, struct dsync_mailbox_attribute, 1);
+
+	ret = dsync_ibc_stream_input_next(ibc, ITEM_MAIL_CHANGE, &decoder);
+	if (ret != DSYNC_IBC_RECV_RET_OK)
+		return ret;
+
+	value = dsync_deserializer_decode_get(decoder, "type");
+	switch (*value) {
+	case 'p':
+		attr->type = MAIL_ATTRIBUTE_TYPE_PRIVATE;
+		break;
+	case 's':
+		attr->type = MAIL_ATTRIBUTE_TYPE_SHARED;
+		break;
+	default:
+		dsync_ibc_input_error(ibc, decoder, "Invalid type: %s", value);
+		return DSYNC_IBC_RECV_RET_TRYAGAIN;
+	}
+
+	value = dsync_deserializer_decode_get(decoder, "key");
+	attr->key = p_strdup(pool, value);
+
+	if (dsync_deserializer_decode_try(decoder, "value", &value))
+		attr->value = p_strdup(pool, value);
+	if (dsync_deserializer_decode_try(decoder, "deleted", &value))
+		attr->deleted = TRUE;
+	if (dsync_deserializer_decode_try(decoder, "last_change", &value) &&
+	    str_to_time(value, &attr->last_change) < 0) {
+		dsync_ibc_input_error(ibc, decoder, "Invalid last_change");
+		return DSYNC_IBC_RECV_RET_TRYAGAIN;
+	}
+	if (dsync_deserializer_decode_try(decoder, "modseq", &value) &&
+	    str_to_uint64(value, &attr->modseq) < 0) {
+		dsync_ibc_input_error(ibc, decoder, "Invalid modseq");
+		return DSYNC_IBC_RECV_RET_TRYAGAIN;
+	}
+
+	*attr_r = attr;
+	return DSYNC_IBC_RECV_RET_OK;
+}
+
+static void
 dsync_ibc_stream_send_change(struct dsync_ibc *_ibc,
 			     const struct dsync_mail_change *change)
 {
@@ -1580,6 +1686,8 @@
 	dsync_ibc_stream_recv_mailbox_deletes,
 	dsync_ibc_stream_send_mailbox,
 	dsync_ibc_stream_recv_mailbox,
+	dsync_ibc_stream_send_mailbox_attribute,
+	dsync_ibc_stream_recv_mailbox_attribute,
 	dsync_ibc_stream_send_change,
 	dsync_ibc_stream_recv_change,
 	dsync_ibc_stream_send_mail_request,
--- a/src/doveadm/dsync/dsync-ibc.c	Thu Mar 14 15:32:14 2013 +0200
+++ b/src/doveadm/dsync/dsync-ibc.c	Thu Mar 14 15:41:09 2013 +0200
@@ -123,6 +123,23 @@
 	return ibc->v.recv_mailbox(ibc, dsync_box_r);
 }
 
+enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT
+dsync_ibc_send_mailbox_attribute(struct dsync_ibc *ibc,
+				 const struct dsync_mailbox_attribute *attr)
+{
+	T_BEGIN {
+		ibc->v.send_mailbox_attribute(ibc, attr);
+	} T_END;
+	return dsync_ibc_send_ret(ibc);
+}
+
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_mailbox_attribute(struct dsync_ibc *ibc,
+				 const struct dsync_mailbox_attribute **attr_r)
+{
+	return ibc->v.recv_mailbox_attribute(ibc, attr_r);
+}
+
 enum dsync_ibc_send_ret
 dsync_ibc_send_change(struct dsync_ibc *ibc,
 		      const struct dsync_mail_change *change)
--- a/src/doveadm/dsync/dsync-ibc.h	Thu Mar 14 15:32:14 2013 +0200
+++ b/src/doveadm/dsync/dsync-ibc.h	Thu Mar 14 15:41:09 2013 +0200
@@ -11,6 +11,7 @@
 struct dsync_mailbox_state;
 struct dsync_mailbox_node;
 struct dsync_mailbox_delete;
+struct dsync_mailbox_attribute;
 struct dsync_mail;
 struct dsync_mail_change;
 struct dsync_mail_request;
@@ -96,6 +97,13 @@
 		       const struct dsync_mailbox **dsync_box_r);
 
 enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT
+dsync_ibc_send_mailbox_attribute(struct dsync_ibc *ibc,
+				 const struct dsync_mailbox_attribute *attr);
+enum dsync_ibc_recv_ret
+dsync_ibc_recv_mailbox_attribute(struct dsync_ibc *ibc,
+				 const struct dsync_mailbox_attribute **attr_r);
+
+enum dsync_ibc_send_ret ATTR_NOWARN_UNUSED_RESULT
 dsync_ibc_send_change(struct dsync_ibc *ibc,
 		      const struct dsync_mail_change *change);
 enum dsync_ibc_recv_ret
--- a/src/doveadm/dsync/dsync-mailbox-export.c	Thu Mar 14 15:32:14 2013 +0200
+++ b/src/doveadm/dsync/dsync-mailbox-export.c	Thu Mar 14 15:41:09 2013 +0200
@@ -8,6 +8,7 @@
 #include "mail-search-build.h"
 #include "dsync-transaction-log-scan.h"
 #include "dsync-mail.h"
+#include "dsync-mailbox.h"
 #include "dsync-mailbox-export.h"
 
 struct dsync_mail_guid_instances {
@@ -41,6 +42,9 @@
 	unsigned int change_idx;
 	uint32_t highest_changed_uid;
 
+	ARRAY(struct dsync_mailbox_attribute) attr_changes;
+	unsigned int attr_change_idx;
+
 	struct dsync_mail_change change;
 	struct dsync_mail dsync_mail;
 
@@ -396,6 +400,101 @@
 }
 
 static void
+dsync_mailbox_export_attr_changes(struct dsync_mailbox_exporter *exporter,
+				  enum mail_attribute_type type)
+{
+	HASH_TABLE_TYPE(dsync_attr_change) attr_changes;
+	struct mailbox_attribute_iter *iter;
+	struct dsync_mailbox_attribute lookup_attr, *attr;
+	struct dsync_mailbox_attribute *attr_change;
+	const char *key;
+	struct mail_attribute_value value;
+	bool export_all_attrs;
+
+	export_all_attrs = exporter->return_all_mails ||
+		exporter->last_common_uid == 0;
+	attr_changes = dsync_transaction_log_scan_get_attr_hash(exporter->log_scan);
+	lookup_attr.type = type;
+
+	iter = mailbox_attribute_iter_init(exporter->box, type, "");
+	while ((key = mailbox_attribute_iter_next(iter)) != NULL) {
+		lookup_attr.key = key;
+		attr_change = hash_table_lookup(attr_changes, &lookup_attr);
+		if (attr_change == NULL && !export_all_attrs)
+			continue;
+
+		if (mailbox_attribute_get(exporter->trans, type, key, &value) < 0) {
+			exporter->error = p_strdup_printf(exporter->pool,
+				"Mailbox attribute %s lookup failed: %s", key,
+				mailbox_get_last_error(exporter->box, NULL));
+			break;
+		}
+		if ((value.flags & MAIL_ATTRIBUTE_VALUE_FLAG_READONLY) != 0) {
+			/* readonly attributes can't be changed,
+			   no point in exporting them */
+			continue;
+		}
+
+		attr = array_append_space(&exporter->attr_changes);
+		attr->type = type;
+		attr->value = p_strdup(exporter->pool, value.value);
+		attr->last_change = value.last_change;
+		if (attr_change != NULL) {
+			i_assert(!attr_change->exported);
+			attr_change->exported = TRUE;
+			attr->key = attr_change->key;
+			attr->deleted = attr_change->deleted &&
+				attr->value == NULL;
+			attr->modseq = attr_change->modseq;
+		} else {
+			attr->key = p_strdup(exporter->pool, key);
+		}
+	}
+	if (mailbox_attribute_iter_deinit(&iter) < 0) {
+		exporter->error = p_strdup_printf(exporter->pool,
+			"Mailbox attribute iteration failed: %s",
+			mailbox_get_last_error(exporter->box, NULL));
+	}
+}
+
+static void
+dsync_mailbox_export_nonexistent_attrs(struct dsync_mailbox_exporter *exporter)
+{
+	HASH_TABLE_TYPE(dsync_attr_change) attr_changes;
+	struct hash_iterate_context *iter;
+	struct dsync_mailbox_attribute *attr;
+	struct mail_attribute_value value;
+
+	attr_changes = dsync_transaction_log_scan_get_attr_hash(exporter->log_scan);
+
+	iter = hash_table_iterate_init(attr_changes);
+	while (hash_table_iterate(iter, attr_changes, &attr, &attr)) {
+		if (attr->exported || !attr->deleted)
+			continue;
+
+		/* lookup the value mainly to get its last_change value. */
+		if (mailbox_attribute_get(exporter->trans, attr->type,
+					  attr->key, &value) < 0) {
+			exporter->error = p_strdup_printf(exporter->pool,
+				"Mailbox attribute %s lookup failed: %s", attr->key,
+				mailbox_get_last_error(exporter->box, NULL));
+			break;
+		}
+		if ((value.flags & MAIL_ATTRIBUTE_VALUE_FLAG_READONLY) != 0)
+			continue;
+
+		attr->last_change = value.last_change;
+		if (value.value != NULL) {
+			attr->value = p_strdup(exporter->pool, value.value);
+			attr->deleted = FALSE;
+		}
+		attr->exported = TRUE;
+		array_append(&exporter->attr_changes, attr, 1);
+	}
+	hash_table_iterate_deinit(&iter);
+}
+
+static void
 dsync_mailbox_export_log_scan(struct dsync_mailbox_exporter *exporter,
 			      struct dsync_transaction_log_scan *log_scan)
 {
@@ -456,9 +555,29 @@
 	dsync_mailbox_export_search(exporter);
 	/* get the changes sorted by UID */
 	dsync_mailbox_export_sort_changes(exporter);
+
+	p_array_init(&exporter->attr_changes, pool, 16);
+	dsync_mailbox_export_attr_changes(exporter, MAIL_ATTRIBUTE_TYPE_PRIVATE);
+	dsync_mailbox_export_attr_changes(exporter, MAIL_ATTRIBUTE_TYPE_SHARED);
+	dsync_mailbox_export_nonexistent_attrs(exporter);
 	return exporter;
 }
 
+const struct dsync_mailbox_attribute *
+dsync_mailbox_export_next_attr(struct dsync_mailbox_exporter *exporter)
+{
+	const struct dsync_mailbox_attribute *changes;
+	unsigned int count;
+
+	if (exporter->error != NULL)
+		return NULL;
+
+	changes = array_get(&exporter->attr_changes, &count);
+	if (exporter->attr_change_idx == count)
+		return NULL;
+	return &changes[exporter->attr_change_idx++];
+}
+
 const struct dsync_mail_change *
 dsync_mailbox_export_next(struct dsync_mailbox_exporter *exporter)
 {
@@ -471,7 +590,6 @@
 	changes = array_get(&exporter->sorted_changes, &count);
 	if (exporter->change_idx == count)
 		return NULL;
-
 	return changes[exporter->change_idx++];
 }
 
--- a/src/doveadm/dsync/dsync-mailbox-export.h	Thu Mar 14 15:32:14 2013 +0200
+++ b/src/doveadm/dsync/dsync-mailbox-export.h	Thu Mar 14 15:41:09 2013 +0200
@@ -11,6 +11,8 @@
 			  struct dsync_transaction_log_scan *log_scan,
 			  uint32_t last_common_uid,
 			  enum dsync_mailbox_exporter_flags flags);
+const struct dsync_mailbox_attribute *
+dsync_mailbox_export_next_attr(struct dsync_mailbox_exporter *exporter);
 const struct dsync_mail_change *
 dsync_mailbox_export_next(struct dsync_mailbox_exporter *exporter);
 
@@ -18,7 +20,6 @@
 				    const struct dsync_mail_request *request);
 const struct dsync_mail *
 dsync_mailbox_export_next_mail(struct dsync_mailbox_exporter *exporter);
-
 int dsync_mailbox_export_deinit(struct dsync_mailbox_exporter **exporter,
 				const char **error_r);
 
--- a/src/doveadm/dsync/dsync-mailbox-import.c	Thu Mar 14 15:32:14 2013 +0200
+++ b/src/doveadm/dsync/dsync-mailbox-import.c	Thu Mar 14 15:41:09 2013 +0200
@@ -10,6 +10,7 @@
 #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 {
@@ -66,6 +67,7 @@
 
 	/* 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;
@@ -196,9 +198,122 @@
 	dsync_mailbox_import_search_init(importer);
 
 	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,
+				 const 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(importer->trans, type, key, &value) < 0) {
+		i_error("Mailbox %s: Failed to get attribute %s: %s",
+			mailbox_get_vname(importer->box), key,
+			mailbox_get_last_error(importer->box, NULL));
+		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) {
+		/* 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->last_change = value.last_change;
+	if (attr_change != NULL) {
+		attr->deleted = attr_change->deleted && attr->value == NULL;
+		attr->modseq = attr_change->modseq;
+	}
+	*attr_r = attr;
+	return 0;
+}
+
+int dsync_mailbox_import_attribute(struct dsync_mailbox_importer *importer,
+				   const struct dsync_mailbox_attribute *attr)
+{
+	const struct dsync_mailbox_attribute *local_attr;
+	int ret;
+
+	i_assert(attr->value != NULL || attr->deleted);
+
+	if (dsync_mailbox_import_lookup_attr(importer, attr->type,
+					     attr->key, &local_attr) < 0)
+		return -1;
+	if (local_attr == NULL) {
+		/* we haven't seen this locally -> use whatever remote has */
+	} else if (null_strcmp(attr->value, local_attr->value) == 0) {
+		/* the values are identical anyway -> we can skip them.
+		   FIXME: but should timestamp/modseq still be updated?.. */
+		return 0;
+	} 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 */
+	} 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 */
+		return 0;
+	} else if (attr->last_change > local_attr->last_change) {
+		/* remote has a newer timestamp -> use it */
+	} else if (attr->last_change < local_attr->last_change) {
+		/* remote has an older timestamp -> ignore */
+		return 0;
+	} else {
+		/* the timestamps are the same. now we're down to guessing
+		   the right answer. first try to use modseqs, but if even they
+		   are the same, fallback to just picking one based on the
+		   value. */
+		if (attr->modseq > local_attr->modseq) {
+			/* remote has a higher modseq -> use it */
+		} else if (attr->modseq < local_attr->modseq) {
+			/* remote has an older modseq -> ignore */
+			return 0;
+		} else if (attr->value != NULL && local_attr->value == NULL) {
+			/* remote has a value and local doesn't -> use it */
+		} else if (attr->value == NULL && local_attr->value != NULL) {
+			/* remote doesn't have a value, bt local does -> skip */
+			return 0;
+		} else {
+			/* now we have absolutely no reasonable guesses left.
+			   just pick one of them that are used. */
+			if (strcmp(attr->value, local_attr->value) < 0)
+				return 0;
+		}
+	}
+	if (attr->value != NULL) {
+		ret = mailbox_attribute_set(importer->trans, attr->type,
+					    attr->key, attr->value);
+	} else {
+		ret = mailbox_attribute_unset(importer->trans, attr->type,
+					      attr->key);
+	}
+	if (ret < 0) {
+		i_error("Mailbox %s: Failed to set attribute %s: %s",
+			mailbox_get_vname(importer->box), attr->key,
+			mailbox_get_last_error(importer->box, NULL));
+		importer->failed = TRUE;
+	}
+	return ret;
+}
+
 static void dsync_mail_error(struct dsync_mailbox_importer *importer,
 			     struct mail *mail, const char *field)
 {
--- a/src/doveadm/dsync/dsync-mailbox-import.h	Thu Mar 14 15:32:14 2013 +0200
+++ b/src/doveadm/dsync/dsync-mailbox-import.h	Thu Mar 14 15:41:09 2013 +0200
@@ -10,6 +10,7 @@
 };
 
 struct mailbox;
+struct dsync_mailbox_attribute;
 struct dsync_mail;
 struct dsync_mail_change;
 struct dsync_transaction_log_scan;
@@ -25,6 +26,8 @@
 			  uint64_t remote_highest_modseq,
 			  uint64_t remote_highest_pvt_modseq,
 			  enum dsync_mailbox_import_flags flags);
+int dsync_mailbox_import_attribute(struct dsync_mailbox_importer *importer,
+				   const struct dsync_mailbox_attribute *attr);
 int dsync_mailbox_import_change(struct dsync_mailbox_importer *importer,
 				const struct dsync_mail_change *change);
 void dsync_mailbox_import_changes_finish(struct dsync_mailbox_importer *importer);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/doveadm/dsync/dsync-mailbox.c	Thu Mar 14 15:41:09 2013 +0200
@@ -0,0 +1,17 @@
+/* Copyright (c) 2013 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "dsync-mailbox.h"
+
+void dsync_mailbox_attribute_dup(pool_t pool,
+				 const struct dsync_mailbox_attribute *src,
+				 struct dsync_mailbox_attribute *dest_r)
+{
+	dest_r->type = src->type;
+	dest_r->key = p_strdup(pool, src->key);
+	dest_r->value = p_strdup(pool, src->value);
+
+	dest_r->deleted = src->deleted;
+	dest_r->last_change = src->last_change;
+	dest_r->modseq = src->modseq;
+}
--- a/src/doveadm/dsync/dsync-mailbox.h	Thu Mar 14 15:32:14 2013 +0200
+++ b/src/doveadm/dsync/dsync-mailbox.h	Thu Mar 14 15:41:09 2013 +0200
@@ -15,4 +15,20 @@
 	ARRAY_TYPE(mailbox_cache_field) cache_fields;
 };
 
+struct dsync_mailbox_attribute {
+	enum mail_attribute_type type;
+	const char *key;
+	const char *value; /* NULL = not looked up yet / deleted */
+
+	time_t last_change; /* 0 = unknown */
+	uint64_t modseq; /* 0 = unknown */
+
+	bool deleted; /* attribute is known to have been deleted */
+	bool exported; /* internally used by exporting */
+};
+
+void dsync_mailbox_attribute_dup(pool_t pool,
+				 const struct dsync_mailbox_attribute *src,
+				 struct dsync_mailbox_attribute *dest_r);
+
 #endif
--- a/src/doveadm/dsync/dsync-transaction-log-scan.c	Thu Mar 14 15:32:14 2013 +0200
+++ b/src/doveadm/dsync/dsync-transaction-log-scan.c	Thu Mar 14 15:41:09 2013 +0200
@@ -5,11 +5,13 @@
 #include "mail-index-modseq.h"
 #include "mail-storage-private.h"
 #include "dsync-mail.h"
+#include "dsync-mailbox.h"
 #include "dsync-transaction-log-scan.h"
 
 struct dsync_transaction_log_scan {
 	pool_t pool;
 	HASH_TABLE_TYPE(dsync_uid_mail_change) changes;
+	HASH_TABLE_TYPE(dsync_attr_change) attr_changes;
 	struct mail_index_view *view;
 	uint32_t highest_wanted_uid;
 
@@ -303,6 +305,44 @@
 	}
 }
 
+static void
+log_add_attribute_update_key(struct dsync_transaction_log_scan *ctx,
+			     const char *attr_change, uint64_t modseq)
+{
+	struct dsync_mailbox_attribute lookup_attr, *attr;
+
+	i_assert(strlen(attr_change) > 2); /* checked by lib-index */
+
+	lookup_attr.type = attr_change[1] == 'p' ?
+		MAIL_ATTRIBUTE_TYPE_PRIVATE : MAIL_ATTRIBUTE_TYPE_SHARED;
+	lookup_attr.key = attr_change+2;
+
+	attr = hash_table_lookup(ctx->attr_changes, &lookup_attr);
+	if (attr == NULL) {
+		attr = p_new(ctx->pool, struct dsync_mailbox_attribute, 1);
+		attr->type = lookup_attr.type;
+		attr->key = p_strdup(ctx->pool, lookup_attr.key);
+		hash_table_insert(ctx->attr_changes, attr, attr);
+	}
+	attr->deleted = attr_change[0] == '-';
+	attr->modseq = modseq;
+}
+
+static void
+log_add_attribute_update(struct dsync_transaction_log_scan *ctx,
+			 const void *data,
+			 const struct mail_transaction_header *hdr,
+			 uint64_t modseq)
+{
+	const char *attr_changes = data;
+	unsigned int i;
+
+	for (i = 0; i < hdr->size && attr_changes[i] != '\0'; ) {
+		log_add_attribute_update_key(ctx, attr_changes+i, modseq);
+		i += strlen(attr_changes+i) + 1;
+	}
+}
+
 static int
 dsync_log_set(struct dsync_transaction_log_scan *ctx,
 	      struct mail_index_view *view, bool pvt_scan,
@@ -342,6 +382,7 @@
 	const void *data;
 	uint32_t file_seq, max_seq;
 	uoff_t file_offset, max_offset;
+	uint64_t cur_modseq;
 
 	log_view = mail_transaction_log_view_open(view->index->log);
 	if (dsync_log_set(ctx, view, pvt_scan, log_view, modseq) < 0) {
@@ -393,6 +434,10 @@
 		case MAIL_TRANSACTION_MODSEQ_UPDATE:
 			log_add_modseq_update(ctx, data, hdr, pvt_scan);
 			break;
+		case MAIL_TRANSACTION_ATTRIBUTE_UPDATE:
+			cur_modseq = mail_transaction_log_view_get_prev_modseq(log_view);
+			log_add_attribute_update(ctx, data, hdr, cur_modseq);
+			break;
 		}
 	}
 
@@ -404,6 +449,23 @@
 	return 0;
 }
 
+static int
+dsync_mailbox_attribute_cmp(const struct dsync_mailbox_attribute *attr1,
+			    const struct dsync_mailbox_attribute *attr2)
+{
+	if (attr1->type < attr2->type)
+		return -1;
+	if (attr1->type > attr2->type)
+		return 1;
+	return strcmp(attr1->key, attr2->key);
+}
+
+static unsigned int
+dsync_mailbox_attribute_hash(const struct dsync_mailbox_attribute *attr)
+{
+	return str_hash(attr->key) ^ attr->type;
+}
+
 int dsync_transaction_log_scan_init(struct mail_index_view *view,
 				    struct mail_index_view *pvt_view,
 				    uint32_t highest_wanted_uid,
@@ -418,6 +480,9 @@
 	ctx = p_new(pool, struct dsync_transaction_log_scan, 1);
 	ctx->pool = pool;
 	hash_table_create_direct(&ctx->changes, pool, 0);
+	hash_table_create(&ctx->attr_changes, pool, 0,
+			  dsync_mailbox_attribute_hash,
+			  dsync_mailbox_attribute_cmp);
 	ctx->view = view;
 	ctx->highest_wanted_uid = highest_wanted_uid;
 
@@ -438,6 +503,12 @@
 	return scan->changes;
 }
 
+HASH_TABLE_TYPE(dsync_attr_change)
+dsync_transaction_log_scan_get_attr_hash(struct dsync_transaction_log_scan *scan)
+{
+	return scan->attr_changes;
+}
+
 bool
 dsync_transaction_log_scan_has_all_changes(struct dsync_transaction_log_scan *scan)
 {
@@ -491,5 +562,6 @@
 	*_scan = NULL;
 
 	hash_table_destroy(&scan->changes);
+	hash_table_destroy(&scan->attr_changes);
 	pool_unref(&scan->pool);
 }
--- a/src/doveadm/dsync/dsync-transaction-log-scan.h	Thu Mar 14 15:32:14 2013 +0200
+++ b/src/doveadm/dsync/dsync-transaction-log-scan.h	Thu Mar 14 15:41:09 2013 +0200
@@ -3,6 +3,9 @@
 
 HASH_TABLE_DEFINE_TYPE(dsync_uid_mail_change,
 		       void *, struct dsync_mail_change *);
+HASH_TABLE_DEFINE_TYPE(dsync_attr_change,
+		       struct dsync_mailbox_attribute *,
+		       struct dsync_mailbox_attribute *);
 
 struct mail_index_view;
 struct dsync_transaction_log_scan;
@@ -14,6 +17,8 @@
 				    struct dsync_transaction_log_scan **scan_r);
 HASH_TABLE_TYPE(dsync_uid_mail_change)
 dsync_transaction_log_scan_get_hash(struct dsync_transaction_log_scan *scan);
+HASH_TABLE_TYPE(dsync_attr_change)
+dsync_transaction_log_scan_get_attr_hash(struct dsync_transaction_log_scan *scan);
 /* Returns TRUE if the entire transaction log was scanned */
 bool dsync_transaction_log_scan_has_all_changes(struct dsync_transaction_log_scan *scan);
 /* If the given UID has been expunged after the initial log scan, create/update