different InterIMAP instance for each local namespace <-> remote
synchronization, for instance with the newly provided systemd
template unit file).
+ * Add a small test-suite (requires dovecot-imapd).
+ interimap: write which --target to use in --delete command
suggestions.
+ interimap: avoid caching hierarchy delimiters forever in the
install:
+test:
+ @for t in tests/*; do [ -d "$$t" ] || continue; ./tests/run "$$t" || exit 1; done
+
clean:
rm -f pullimap.1 interimap.1
-.PHONY: all install clean
+.PHONY: all install clean test
--- /dev/null
+namespace inbox {
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+namespace inbox {
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+# verify that database isn't created in --watch mode
+! interimap --watch=60
+xgrep -E "^DBI connect\(.*\) failed: unable to open database file at " <"$STDERR"
+
+# now create database
+interimap
+
+# start a background process
+interimap --watch=60 & pid=$!
+cleanup() {
+ # kill interimap process and its children
+ pkill -P "$pid" -TERM
+ kill -TERM "$pid"
+ wait
+}
+trap cleanup EXIT INT TERM
+
+sleep .05 # wait a short while so we have time to lock the database (ugly and racy...)
+# verify that subsequent runs fail as we can't acquire the exclusive lock
+! interimap
+
+# line 177 is `$DBH->do("PRAGMA locking_mode = EXCLUSIVE");`
+xgrep -Fx "DBD::SQLite::db do failed: database is locked at ./interimap line 177." <"$STDERR"
+
+# vim: set filetype=sh :
--- /dev/null
+../00-db-migration-0-to-1/before.sql
\ No newline at end of file
--- /dev/null
+namespace inbox {
+ separator = "\""
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+namespace inbox {
+ separator = ^
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+# import an existing non-migrated database
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" <"$TESTDIR/before.sql"
+! interimap
+
+# may happen if the server(s) software or its configuration changed
+xgrep -Fx 'ERROR: Local and remote hierachy delimiters differ (local "\"", remote "^"), refusing to update `mailboxes` table.' <"$STDERR"
+
+# vim: set filetype=sh :
--- /dev/null
+namespace inbox {
+ separator = .
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+namespace inbox {
+ separator = .
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+# create new schema and add INBOX
+interimap
+xgrep "^Creating new schema in database file " <"$STDERR"
+xgrep -Fx "database: Created mailbox INBOX" <"$STDERR"
+
+# empty table `mailboxes` and revert its schema to version 0
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" <<-EOF
+ PRAGMA foreign_keys = OFF;
+ PRAGMA user_version = 0;
+ DROP TABLE mailboxes;
+ CREATE TABLE mailboxes (
+ idx INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ mailbox TEXT NOT NULL CHECK (mailbox != '') UNIQUE,
+ subscribed BOOLEAN NOT NULL
+ );
+EOF
+
+# check that migration fails due to broken referential integrity
+! interimap
+xgrep -Fx "Upgrading database version from 0" <"$STDERR"
+xgrep -Fx "database: ERROR: Broken referential integrity! Refusing to commit changes." <"$STDERR"
+
+# vim: set filetype=sh :
--- /dev/null
+PRAGMA foreign_keys=OFF;
+BEGIN TRANSACTION;
+CREATE TABLE local (idx INTEGER NOT NULL PRIMARY KEY REFERENCES mailboxes(idx), UIDVALIDITY UNSIGNED INT NOT NULL CHECK (UIDVALIDITY > 0), UIDNEXT UNSIGNED INT NOT NULL, HIGHESTMODSEQ UNSIGNED BIGINT NOT NULL);
+CREATE TABLE remote (idx INTEGER NOT NULL PRIMARY KEY REFERENCES mailboxes(idx), UIDVALIDITY UNSIGNED INT NOT NULL CHECK (UIDVALIDITY > 0), UIDNEXT UNSIGNED INT NOT NULL, HIGHESTMODSEQ UNSIGNED BIGINT NOT NULL);
+CREATE TABLE mapping (idx INTEGER NOT NULL REFERENCES mailboxes(idx), lUID UNSIGNED INT NOT NULL CHECK (lUID > 0), rUID UNSIGNED INT NOT NULL CHECK (rUID > 0), PRIMARY KEY (idx,lUID), UNIQUE (idx,rUID));
+CREATE TABLE IF NOT EXISTS "mailboxes" (idx INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, mailbox BLOB COLLATE BINARY NOT NULL CHECK (mailbox != '') UNIQUE, subscribed BOOLEAN NOT NULL);
+INSERT INTO mailboxes VALUES(1,X'61006231006332',0);
+INSERT INTO mailboxes VALUES(2,X'61006231006331',0);
+INSERT INTO mailboxes VALUES(3,X'494e424f58',0);
+INSERT INTO mailboxes VALUES(4,X'6132',0);
+INSERT INTO mailboxes VALUES(5,X'610062320063',0);
+DELETE FROM sqlite_sequence;
+INSERT INTO sqlite_sequence VALUES('mailboxes',5);
+COMMIT;
--- /dev/null
+PRAGMA foreign_keys=OFF;
+BEGIN TRANSACTION;
+CREATE TABLE mailboxes (idx INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, mailbox TEXT NOT NULL CHECK (mailbox != '') UNIQUE, subscribed BOOLEAN NOT NULL);
+INSERT INTO mailboxes VALUES(1,'a.b1.c2',0);
+INSERT INTO mailboxes VALUES(2,'a.b1.c1',0);
+INSERT INTO mailboxes VALUES(3,'INBOX',0);
+INSERT INTO mailboxes VALUES(4,'a2',0);
+INSERT INTO mailboxes VALUES(5,'a.b2.c',0);
+CREATE TABLE local (idx INTEGER NOT NULL PRIMARY KEY REFERENCES mailboxes(idx), UIDVALIDITY UNSIGNED INT NOT NULL CHECK (UIDVALIDITY > 0), UIDNEXT UNSIGNED INT NOT NULL, HIGHESTMODSEQ UNSIGNED BIGINT NOT NULL);
+CREATE TABLE remote (idx INTEGER NOT NULL PRIMARY KEY REFERENCES mailboxes(idx), UIDVALIDITY UNSIGNED INT NOT NULL CHECK (UIDVALIDITY > 0), UIDNEXT UNSIGNED INT NOT NULL, HIGHESTMODSEQ UNSIGNED BIGINT NOT NULL);
+CREATE TABLE mapping (idx INTEGER NOT NULL REFERENCES mailboxes(idx), lUID UNSIGNED INT NOT NULL CHECK (lUID > 0), rUID UNSIGNED INT NOT NULL CHECK (rUID > 0), PRIMARY KEY (idx,lUID), UNIQUE (idx,rUID));
+DELETE FROM sqlite_sequence;
+INSERT INTO sqlite_sequence VALUES('mailboxes',5);
+COMMIT;
--- /dev/null
+namespace inbox {
+ separator = .
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+namespace inbox {
+ separator = .
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+# create some mailboxes
+doveadm -u "local" mailbox create "a.b1.c1" "a.b1.c2" "a.b2.c" "a2"
+doveadm -u "remote" mailbox create "a.b1.c1" "a.b1.c2" "a.b2.c" "a2"
+
+# import an existing non-migrated database
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" <"$TESTDIR/before.sql"
+
+# migrate
+interimap
+
+xgrep -Fx "Upgrading database version from 0" <"$STDERR"
+check_mailboxes_status "a.b1.c1" "a.b1.c2" "a.b2.c" "a2"
+
+# verify that the new schema is as expected
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" >"$TMPDIR/dump.sql" <<-EOF
+ DELETE FROM local;
+ DELETE FROM remote;
+ .dump
+EOF
+
+# XXX need 'user_version' PRAGMA in the dump for future migrations
+# http://sqlite.1065341.n5.nabble.com/dump-command-and-user-version-td101228.html
+diff -u --label="a/dump.sql" --label="b/dump.sql" \
+ "$TESTDIR/after.sql" "$TMPDIR/dump.sql"
+
+# vim: set filetype=sh :
--- /dev/null
+namespace inbox {
+ separator = .
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+namespace inbox {
+ separator = "\\"
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+doveadm -u "local" mailbox create "root.from" "root.from.child" "t.o"
+doveadm -u "remote" mailbox create "root\\from" "root\\from\\child" "t\\o"
+
+interimap
+check_mailbox_list
+
+# delete a mailbox on both servers but leave it in the database, then try to use it as target for --rename
+doveadm -u "local" mailbox delete "t.o"
+doveadm -u "remote" mailbox delete "t\\o"
+
+! interimap --rename "root.from" "t.o"
+xgrep -Fx 'database: ERROR: Mailbox t.o exists. Run `interimap --target=database --delete t.o` to delete.' <"$STDERR"
+
+# vim: set filetype=sh :
--- /dev/null
+namespace inbox {
+ separator = .
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+namespace inbox {
+ separator = "\\"
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+doveadm -u "local" mailbox create "root.from" "root.from.child" "t.o"
+doveadm -u "remote" mailbox create "root\\from" "root\\from\\child"
+
+interimap
+check_mailbox_list
+
+# delete a mailbox on the remote server, then try to use it as target for --rename
+doveadm -u "remote" mailbox delete "t\\o"
+
+! interimap --rename "root.from" "t.o"
+xgrep -Fx 'local: ERROR: Mailbox t.o exists. Run `interimap --target=local --delete t.o` to delete.' <"$STDERR"
+
+# vim: set filetype=sh :
--- /dev/null
+namespace inbox {
+ separator = .
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+namespace inbox {
+ separator = "\\"
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+doveadm -u "local" mailbox create "root.from" "root.from.child" "t.o"
+doveadm -u "remote" mailbox create "root\\from" "root\\from\\child" "t\\o"
+
+interimap
+check_mailbox_list
+
+# delete a mailbox on the local server, then try to use it as target for --rename
+doveadm -u "local" mailbox delete "t.o"
+
+! interimap --rename "root.from" "t.o"
+xgrep -Fx 'remote: ERROR: Mailbox t\o exists. Run `interimap --target=remote --delete t.o` to delete.' <"$STDERR"
+
+# vim: set filetype=sh :
--- /dev/null
+namespace inbox {
+ separator = .
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+namespace inbox {
+ separator = ^
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+doveadm -u "local" mailbox create "root.from" "root.from.child" "root.from.child2" "root.from.child.grandchild"
+doveadm -u "remote" mailbox create "root^sibbling" "root^sibbling^grandchild" "root2"
+
+for m in "root.from" "root.from.child" "root.from.child2" "root.from.child.grandchild" "INBOX"; do
+ sample_message | deliver -u "local" -- -m "$m"
+done
+for m in "root^sibbling" "root^sibbling^grandchild" "root2" "INBOX"; do
+ sample_message | deliver -u "remote" -- -m "$m"
+done
+
+interimap
+check_mailboxes_status "root.from" "root.from.child" "root.from.child2" "root.from.child.grandchild" \
+ "root.sibbling" "root.sibbling.grandchild" "root2" "INBOX"
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" >"$TMPDIR/mailboxes.csv" <<-EOF
+ .mode csv
+ SELECT idx, hex(mailbox)
+ FROM mailboxes
+ ORDER BY idx
+EOF
+
+# renaming a non-existent mailbox doesn't yield an error
+interimap --rename "nonexistent" "nonexistent2"
+check_mailbox_list
+
+# renaming to an existing name yields an error
+! interimap --rename "root2" "root"
+xgrep -E "^local: ERROR: Couldn't rename mailbox root2: NO \[ALREADYEXISTS\] .*" <"$STDERR"
+
+# rename 'root.from' to 'from.root', including inferiors
+interimap --rename "root.from" "from.root"
+xgrep -Fx 'local: Renamed mailbox root.from to from.root' <"$STDERR"
+xgrep -Fx 'remote: Renamed mailbox root^from to from^root' <"$STDERR"
+xgrep -Fx 'database: Renamed mailbox root.from to from.root' <"$STDERR"
+
+check_mailbox_list
+check_mailboxes_status "from.root" "from.root.child" "from.root.child2" "from.root.child.grandchild" \
+ "root.sibbling" "root.sibbling.grandchild" "root2" "INBOX"
+
+before="$(printf "%s\\0%s" "root" "from" | xxd -u -ps)"
+after="$(printf "%s\\0%s" "from" "root" | xxd -ps)"
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" >"$TMPDIR/mailboxes2.csv" <<-EOF
+ .mode csv
+ SELECT idx,
+ CASE
+ WHEN mailbox = x'$after' OR hex(mailbox) LIKE '${after}00%'
+ THEN '$before' || SUBSTR(hex(mailbox), $((${#after}+1)))
+ ELSE hex(mailbox)
+ END
+ FROM mailboxes
+ ORDER BY idx
+EOF
+diff -u --label="a/mailboxes.csv" --label="b/mailboxes.csv" \
+ "$TMPDIR/mailboxes.csv" "$TMPDIR/mailboxes2.csv"
+
+
+# Try to rename \NonExistent root and check that its children move
+interimap --rename "root" "newroot"
+xgrep -Fq 'local: Renamed mailbox root to newroot' <"$STDERR"
+xgrep -Fq 'remote: Renamed mailbox root to newroot' <"$STDERR"
+xgrep -Fq 'database: Renamed mailbox root to newroot' <"$STDERR"
+
+check_mailbox_list
+check_mailboxes_status "from.root" "from.root.child" "from.root.child2" "from.root.child.grandchild" \
+ "newroot.sibbling" "newroot.sibbling.grandchild" "root2" "INBOX"
+
+before2="$(printf "%s" "root" | xxd -u -ps)"
+after2="$(printf "%s" "newroot" | xxd -ps)"
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" >"$TMPDIR/mailboxes3.csv" <<-EOF
+ .mode csv
+ SELECT idx,
+ CASE
+ WHEN mailbox = x'$after' OR hex(mailbox) LIKE '${after}00%'
+ THEN '$before' || SUBSTR(hex(mailbox), $((${#after}+1)))
+ WHEN hex(mailbox) LIKE '${after2}00%'
+ THEN '$before2' || SUBSTR(hex(mailbox), $((${#after2}+1)))
+ ELSE hex(mailbox)
+ END
+ FROM mailboxes
+ ORDER BY idx
+EOF
+diff -u --label="a/mailboxes.csv" --label="b/mailboxes.csv" \
+ "$TMPDIR/mailboxes2.csv" "$TMPDIR/mailboxes3.csv"
+
+# vim: set filetype=sh :
--- /dev/null
+namespace inbox {
+ separator = .
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+namespace inbox {
+ separator = ^
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+doveadm -u "local" mailbox create "foo.bar" "foo.bar.baz"
+
+for m in "foo.bar" "foo.bar.baz" "INBOX"; do
+ sample_message | deliver -u "local" -- -m "$m"
+done
+
+interimap
+check_mailbox_list
+check_mailboxes_status "foo.bar" "foo.bar.baz" "INBOX"
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" >"$TMPDIR/dump.sql" <<-EOF
+ .dump
+EOF
+
+# delete non-existent mailbox is a no-op
+interimap --target="local,remote" --target="database" --delete "nonexistent"
+
+check_mailbox_list
+check_mailboxes_status "foo.bar" "foo.bar.baz" "INBOX"
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" >"$TMPDIR/dump2.sql" <<-EOF
+ .dump
+EOF
+diff -u --label="a/dump.sql" --label="b/dump.sql" \
+ "$TMPDIR/dump.sql" "$TMPDIR/dump2.sql"
+
+# foo.bar will become \NoSelect in local, per RFC 3501: "It is permitted
+# to delete a name that has inferior hierarchical names and does not
+# have the \Noselect mailbox name attribute. In this case, all messages
+# in that mailbox are removed, and the name will acquire the \Noselect
+# mailbox name attribute."
+interimap --target="local" --delete "foo.bar"
+
+check_mailbox_list
+check_mailboxes_status "foo.bar.baz" "INBOX"
+
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" >"$TMPDIR/dump2.sql" <<-EOF
+ .dump
+EOF
+diff -u --label="a/dump.sql" --label="b/dump.sql" "$TMPDIR/dump.sql" "$TMPDIR/dump2.sql"
+
+! doveadm -u "local" mailbox status uidvalidity "foo.bar" # gone
+ doveadm -u "remote" mailbox status uidvalidity "foo^bar"
+
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" >"$TMPDIR/mailboxes.csv" <<-EOF
+ SELECT idx, mailbox
+ FROM mailboxes
+ WHERE mailbox != x'$(printf "%s\\0%s" "foo" "bar" | xxd -ps)'
+EOF
+
+
+# now delete from the remote server and the database
+interimap --delete "foo.bar"
+
+! doveadm -u "local" mailbox status uidvalidity "foo.bar"
+! doveadm -u "remote" mailbox status uidvalidity "foo^bar"
+
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" >"$TMPDIR/mailboxes2.csv" <<-EOF
+ SELECT idx, mailbox
+ FROM mailboxes
+ WHERE mailbox != x'$(printf "%s\\0%s" "foo" "bar" | xxd -ps)'
+EOF
+diff -u --label="a/mailboxes.csv" --label="b/mailboxes.csv" \
+ "$TMPDIR/mailboxes.csv" "$TMPDIR/mailboxes2.csv"
+
+check_mailbox_list
+check_mailboxes_status "foo.bar.baz" "INBOX"
+
+# vim: set filetype=sh :
--- /dev/null
+list-mailbox = *
--- /dev/null
+namespace inbox {
+ separator = .
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+namespace inbox {
+ separator = ~
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+# try a bunch of invalid 'list-mailbox' values:
+# empty string, missing space between values, unterminated string
+for v in '""' '"f o o""bar"' '"f o o" "bar" "baz\" x'; do
+ sed -ri "s/^(list-mailbox\\s*=\\s*).*/\\1${v//\\/\\\\}/" "$XDG_CONFIG_HOME/interimap/config"
+ ! interimap
+ xgrep -xF "Invalid value for list-mailbox: $v" <"$STDERR"
+done
+
+# create some mailboxes
+doveadm -u "local" mailbox create "foo" "foo bar" "f\\\"o!o.bar" "f.o.o" "bad"
+for m in "foo" "foo bar" "f\\\"o!o.bar" "f.o.o" "bad" "INBOX"; do
+ sample_message | deliver -u "local" -- -m "$m"
+done
+
+# restrict 'list-mailbox' to the above minus "bad"
+sed -ri 's/^(list-mailbox\s*=\s*).*/\1foo "foo bar" "f\\\\\\"o\\x21o.*" "f\\0o\\0o"/' \
+ "$XDG_CONFIG_HOME/interimap/config"
+
+# run partial sync
+interimap
+check_mailbox_list "foo" "foo bar" "f\\\"o!o.bar" "f.o.o" "INBOX" "f\\\"o!o" "f" "f.o"
+check_mailboxes_status "foo" "foo bar" "f\\\"o!o.bar" "f.o.o"
+
+# check that "bad" isn't in the remote imap server
+! doveadm -u "remote" mailbox status uidvalidity "bad"
+
+# check that "bad" and "INBOX" aren't in the database
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" >"$TMPDIR/count" <<-EOF
+ SELECT COUNT(*)
+ FROM mailboxes
+ WHERE mailbox = x'$(printf "%s" "bad" | xxd -ps)'
+ OR mailbox = x'$(printf "%s" "INBOX" | xxd -ps)'
+EOF
+[ $(< "$TMPDIR/count") -eq 0 ]
+
+
+# run partial sync
+doveadm -u "remote" mailbox create "f\\\"o!o~baz" "f\\\"o!o~bad"
+for m in "f\\\"o!o~baz" "f\\\"o!o~bad"; do
+ sample_message | deliver -u "remote" -- -m "$m"
+done
+interimap "f\\\"o!o.baz"
+
+check_mailbox_list "foo" "foo bar" "f\\\"o!o.bar" "f.o.o" "INBOX" "f\\\"o!o" "f" "f.o" "f\\\"o!o.baz"
+check_mailboxes_status "foo" "foo bar" "f\\\"o!o.bar" "f.o.o" "f\\\"o!o.baz"
+
+# check that "bad", "f\\\"o!o.bad" and "INBOX" aren't in the database
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" >"$TMPDIR/count" <<-EOF
+ SELECT COUNT(*)
+ FROM mailboxes
+ WHERE mailbox = x'$(printf "%s" "bad" | xxd -ps)'
+ OR mailbox = x'$(printf "%s" "INBOX" | xxd -ps)'
+ OR mailbox = x'$(printf "%s\\0%s" "f\\\"o!o" "bad" | xxd -ps)'
+EOF
+[ $(< "$TMPDIR/count") -eq 0 ]
+
+# vim: set filetype=sh :
--- /dev/null
+namespace inbox {
+ separator = /
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+namespace inbox {
+ separator = "\\"
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+# Note: implementation-dependent as the reference name is not a level of
+# mailbox hierarchy nor ends with the hierarchy delimiter
+sed -ri 's#^\[local\]$#&\nlist-reference = foo#; s#^\[remote\]$#&\nlist-reference = bar#' \
+ "$XDG_CONFIG_HOME/interimap/config"
+
+# create a bunch of mailboxes in and out the respective list # references
+doveadm -u "local" mailbox create "foo" "foobar" "foo/bar/baz" "foo/baz" "bar"
+doveadm -u "remote" mailbox create "foo"
+
+# deliver somemessages to these mailboxes
+for m in "foo" "foobar" "foo/bar/baz" "foo/baz" "bar"; do
+ sample_message | deliver -u "local" -- -m "$m"
+done
+sample_message | deliver -u "remote" -- -m "foo"
+
+interimap
+
+# check that the mailbox lists match
+diff -u --label="local/mailboxes" --label="remote/mailboxes" \
+ <( doveadm -u "local" mailbox list | sed -n "s/^foo//p" | sort ) \
+ <( doveadm -u "remote" mailbox list | sed -n "s/^bar//p" | tr '\\' '/' | sort )
+
+for m in "" "bar" "/bar/baz" "/baz"; do
+ blob="x'$(printf "%s" "$m" | tr "/" "\\0" | xxd -c256 -ps)'"
+ check_mailbox_status2 "$blob" "foo$m" "remote" "bar${m//\//\\}"
+done
+
+# vim: set filetype=sh :
--- /dev/null
+namespace inbox {
+ separator = .
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+namespace inbox {
+ separator = ~
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+# pre-create some mailboxes and susbscribe to some
+# foo: present on both, subscribed to both
+# bar: present on both, subscribed to local only
+# baz: present on both, subscribed to remote only
+# foo.bar: present on local only
+# foo.baz: present on remote only
+doveadm -u "local" mailbox create "foo" "bar" "baz" "foo.bar" "fo!o [b*a%r]"
+doveadm -u "local" mailbox subscribe "foo" "bar"
+doveadm -u "remote" mailbox create "foo" "bar" "baz" "foo~baz" "foo]bar"
+doveadm -u "remote" mailbox subscribe "foo" "baz"
+
+interimap
+xgrep -Fx "local: Subscribe to baz" <"$STDERR"
+xgrep -Fx "remote: Subscribe to bar" <"$STDERR"
+xgrep -Fx "local: Created mailbox foo.baz" <"$STDERR"
+xgrep -Fx "remote: Created mailbox foo~bar" <"$STDERR"
+
+# check syncing
+check_mailbox_list
+check_mailboxes_status "foo" "bar" "baz" "foo.bar" "foo.baz" "INBOX" "fo!o [b*a%r]" "foo]bar"
+check_mailbox_list -s
+
+
+# delete a mailbox one server and verify that synchronization fails as it's still in the database
+doveadm -u "remote" mailbox delete "foo~baz"
+! interimap
+xgrep -Fx 'database: ERROR: Mailbox foo.baz exists. Run `interimap --target=database --delete foo.baz` to delete.' <"$STDERR"
+interimap --target="database" --delete "foo.baz"
+xgrep -Fx 'database: Removed mailbox foo.baz' <"$STDERR"
+interimap # create again
+xgrep -Fx 'database: Created mailbox foo.baz' <"$STDERR"
+xgrep -Fx 'remote: Created mailbox foo~baz' <"$STDERR"
+
+doveadm -u "local" mailbox delete "foo.bar"
+! interimap
+xgrep -Fx 'database: ERROR: Mailbox foo.bar exists. Run `interimap --target=database --delete foo.bar` to delete.' <"$STDERR"
+interimap --target="database" --delete "foo.bar"
+xgrep -Fx 'database: Removed mailbox foo.bar' <"$STDERR"
+interimap
+xgrep -Fx 'database: Created mailbox foo.bar' <"$STDERR"
+xgrep -Fx 'local: Created mailbox foo.bar' <"$STDERR"
+
+check_mailbox_list
+check_mailboxes_status "foo" "bar" "baz" "foo.bar" "foo.baz" "INBOX" "fo!o [b*a%r]" "foo]bar"
+check_mailbox_list -s
+
+
+# (un)subscribe from some mailboxes, including a non-existent one
+doveadm -u "local" mailbox unsubscribe "foo"
+doveadm -u "remote" mailbox unsubscribe "bar"
+doveadm -u "local" mailbox subscribe "foo.bar" "foo.nonexistent" "foo.baz"
+doveadm -u "remote" mailbox subscribe "foo~bar" "bar~nonexistent"
+
+interimap
+xgrep -Fx 'remote: Unsubscribe to foo' <"$STDERR"
+xgrep -Fx 'local: Unsubscribe to bar' <"$STDERR"
+xgrep -Fx 'remote: Subscribe to foo~baz' <"$STDERR"
+check_mailbox_list
+check_mailbox_list -s $(doveadm -u "local" mailbox list) # exclude "foo.nonexistent" and "bar~nonexistent"
+
+# check that "baz", "foo.bar" and "foo.baz" are the only subscribed mailboxes
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" >"$TMPDIR/count" <<-EOF
+ SELECT COUNT(*)
+ FROM mailboxes
+ WHERE subscribed <> (mailbox IN (
+ x'$(printf "%s" "baz" | xxd -ps)',
+ x'$(printf "%s\\0%s" "foo" "bar" | xxd -ps)',
+ x'$(printf "%s\\0%s" "foo" "baz" | xxd -ps)'
+ ))
+EOF
+[ $(< "$TMPDIR/count") -eq 0 ]
+
+# vim: set filetype=sh :
--- /dev/null
+namespace inbox {
+ separator = .
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+namespace inbox {
+ separator = ~
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+# create and populate a bunch of mailboxes
+doveadm -u "local" mailbox create "foo" "foo.bar" "baz"
+for ((i = 0; i < 8; i++)); do
+ sample_message | deliver -u "local" -- -m "foo"
+ sample_message | deliver -u "local" -- -m "foo.bar"
+ sample_message | deliver -u "local" -- -m "INBOX"
+done
+interimap
+check_mailbox_list
+check_mailboxes_status "foo" "foo.bar" "baz" "INBOX"
+
+# spoof UIDNEXT in the database
+set_uidnext() {
+ local imap="$1" mailbox="$2" uidnext="$3"
+ sqlite3 "$XDG_DATA_HOME/interimap/remote.db" <<-EOF
+ UPDATE $imap
+ SET UIDNEXT = $uidnext
+ WHERE idx = (
+ SELECT idx
+ FROM mailboxes
+ WHERE mailbox = x'$mailbox'
+ );
+ EOF
+}
+
+# spoof "foo"'s UIDVALIDITY and UIDNEXT values
+uidvalidity="$(doveadm -u "local" -f flow mailbox status uidvalidity "foo" | sed 's/.*=//')"
+[ $uidvalidity -eq 4294967295 ] && uidvalidity2=1 || uidvalidity2=$((uidvalidity+1))
+doveadm -u "local" mailbox update --uid-validity "$uidvalidity2" "foo"
+set_uidnext "local" "$(printf "%s" "foo" | xxd -ps)" 1
+
+# verify that interimap chokes on the UIDVALIDITY change without doing any changes
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" >"$TMPDIR/dump.sql" <<-EOF
+ .dump
+EOF
+doveadm -u "local" mailbox status "all" "foo" >"$TMPDIR/foo.local"
+doveadm -u "remote" mailbox status "all" "foo" >"$TMPDIR/foo.remote"
+
+! interimap
+xgrep -Fx "Resuming interrupted sync for foo" <"$STDERR"
+xgrep -Fx "local(foo): ERROR: UIDVALIDITY changed! ($uidvalidity2 != $uidvalidity) Need to invalidate the UID cache." <"$STDERR"
+
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" >"$TMPDIR/dump2.sql" <<-EOF
+ .dump
+EOF
+doveadm -u "local" mailbox status "all" "foo" >"$TMPDIR/foo.local2"
+doveadm -u "remote" mailbox status "all" "foo" >"$TMPDIR/foo.remote2"
+
+diff -u --label="a/dump.sql" --label="b/dump.sql" "$TMPDIR/dump2.sql" "$TMPDIR/dump.sql"
+diff -u --label="a/foo.local" --label="b/foo.remote" "$TMPDIR/foo.local" "$TMPDIR/foo.local2"
+diff -u --label="a/foo.local" --label="b/foo.remote" "$TMPDIR/foo.remote" "$TMPDIR/foo.remote2"
+
+
+# spoof UIDNEXT values for INBOX (local+remote) and foo.bar (remote)
+set_uidnext "local" "$(printf "%s" "INBOX" | xxd -ps)" 2
+set_uidnext "remote" "$(printf "%s" "INBOX" | xxd -ps)" 2
+set_uidnext "remote" "$(printf "%s\\0%s" "foo" "bar" | xxd -ps)" 0
+
+# set some flags and remove some messages for UIDs >2
+doveadm -u "local" flags add "\\Seen" mailbox "INBOX" 6,7
+doveadm -u "remote" flags add "\\Deleted" mailbox "INBOX" 6,8
+
+doveadm -u "local" expunge mailbox "INBOX" 4,5
+doveadm -u "remote" expunge mailbox "INBOX" 3,4
+doveadm -u "remote" expunge mailbox "foo~bar" 5
+
+# add new messages
+sample_message | deliver -u "local" -- -m "foo.bar"
+sample_message | deliver -u "remote" -- -m "foo~bar"
+sample_message | deliver -u "local" -- -m "baz"
+
+interimap "foo.bar" "InBoX" "baz" # ignore "foo"
+xgrep -Fx "Resuming interrupted sync for foo.bar" <"$STDERR"
+xgrep -Fx "Resuming interrupted sync for INBOX" <"$STDERR"
+check_mailbox_list
+check_mailboxes_status "foo.bar" "INBOX" "baz" # ignore "foo"
+
+
+# count entries in the mapping table
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" >"$TMPDIR/count" <<-EOF
+ SELECT COUNT(*)
+ FROM mapping NATURAL JOIN mailboxes
+ WHERE mailbox != x'$(printf "%s" "foo" | xxd -ps)'
+ GROUP BY idx
+ ORDER BY mailbox;
+EOF
+
+# count messages:
+# INBOX: 8-2-1 = 5
+# baz: 1
+# foo.bar: 8-1+1+1 = 9
+diff -u --label="a/count" --label="b/count" "$TMPDIR/count" - <<-EOF
+ 5
+ 1
+ 9
+EOF
+
+# vim: set filetype=sh :
--- /dev/null
+namespace inbox {
+ separator = .
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+namespace inbox {
+ separator = ~
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+# create some mailboxes and populate them
+doveadm -u "local" mailbox create "foo.bar"
+doveadm -u "remote" mailbox create "foo~bar" "baz"
+for ((i = 0; i < 8; i++)); do
+ sample_message | deliver -u "local" -- -m "foo.bar"
+ sample_message | deliver -u "remote" -- -m "foo~bar"
+done
+for ((i = 0; i < 64; i++)); do
+ sample_message | deliver -u "remote" -- -m "baz"
+done
+
+interimap
+check_mailbox_list
+check_mailboxes_status "foo.bar" "baz" "INBOX"
+
+# make more changes (flag updates, new massages, deletions)
+sample_message | deliver -u "remote" -- -m "INBOX"
+doveadm -u "local" expunge mailbox "baz" 1:10
+doveadm -u "remote" expunge mailbox "baz" "$(seq -s"," 1 2 32),$(seq -s"," 40 2 64)"
+doveadm -u "local" expunge mailbox "foo.bar" 2,3,5:7,10
+doveadm -u "remote" expunge mailbox "foo~bar" 4,5,7,10
+doveadm -u "local" flags add "\\Answered" mailbox "foo.bar" 2,3,5:7,10
+doveadm -u "remote" flags add "\\Seen" mailbox "foo~bar" 4,5,7
+
+# spoof HIGHESTMODSEQ value in the database, to make it look that we recorded the new changes already
+spoof() {
+ local k="$1" v m hex="$(printf "%s\\0%s" "foo" "bar" | xxd -ps)"
+ shift
+ while [ $# -gt 0 ]; do
+ [ "$1" = "local" ] && m="foo.bar" || m="$(printf "%s" "foo.bar" | tr "." "~")"
+ v="$(doveadm -u "$1" -f flow mailbox status "${k,,[A-Z]}" "$m" | sed 's/.*=//')"
+ sqlite3 "$XDG_DATA_HOME/interimap/remote.db" <<-EOF
+ UPDATE \`$1\` SET $k = $v
+ WHERE idx = (SELECT idx FROM mailboxes WHERE mailbox = x'$hex');
+ EOF
+ shift
+ done
+}
+
+spoof HIGHESTMODSEQ "local" "remote"
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" >"$TMPDIR/dump.sql" <<-EOF
+ .dump
+EOF
+doveadm -u "local" mailbox status "all" "foo.bar" >"$TMPDIR/foo-bar.status.local"
+doveadm -u "remote" mailbox status "all" "foo~bar" >"$TMPDIR/foo-bar.status.remote"
+
+
+# verify that without --repair interimap does nothing due to the spoofed HIGHESTMODSEQ values
+interimap "foo.bar"
+
+sqlite3 "$XDG_DATA_HOME/interimap/remote.db" >"$TMPDIR/dump2.sql" <<-EOF
+ .dump
+EOF
+doveadm -u "local" mailbox status all "foo.bar" >"$TMPDIR/foo-bar.status2.local"
+doveadm -u "remote" mailbox status all "foo~bar" >"$TMPDIR/foo-bar.status2.remote"
+diff -u --label="a/dump.sql" --label="b/dump.sql" "$TMPDIR/dump.sql" "$TMPDIR/dump2.sql"
+diff -u --label="a/foo_bar.local" --label="a/foo_bar.local" "$TMPDIR/foo-bar.status.local" "$TMPDIR/foo-bar.status2.local"
+diff -u --label="a/foo_bar.remote" --label="a/foo_bar.remote" "$TMPDIR/foo-bar.status.remote" "$TMPDIR/foo-bar.status2.remote"
+
+
+# deliver more messages and spoof UIDNEXT, on one side only
+sample_message | deliver -u "local" -- -m "foo.bar"
+sample_message | deliver -u "remote" -- -m "foo~bar"
+spoof UIDNEXT "local"
+spoof HIGHESTMODSEQ "local" "remote"
+
+# now repair
+interimap --repair "baz" "foo.bar"
+
+# 6 updates with \Answered (luid 4,8,11:13,16), 2 of which (luid 12,13) vanished from remote
+# 3 updates with \Seen (ruid 6,8,10), 1 of which (uid 10) vanished from remote
+# luid 16 <-> ruid 8 has both \Answered and \Seen
+xcgrep 5 '^WARNING: Missed flag update in foo\.bar for ' <"$STDERR"
+xcgrep 5 '^WARNING: Conflicting flag update in foo\.bar ' <"$STDERR"
+
+# luid 2 <-> ruid 10
+xcgrep 1 -E '^WARNING: Pair \(lUID,rUID\) = \([0-9]+,[0-9]+\) vanished from foo\.bar\. Repairing\.$' <"$STDERR"
+
+# 6-1 (luid 2 <-> ruid 10 is gone from both)
+xcgrep 5 -E '^local\(foo\.bar\): WARNING: UID [0-9]+ disappeared\. Downloading remote UID [0-9]+ again\.$' <"$STDERR"
+
+# 6-1 (luid 2 <-> ruid 10 is gone from both)
+xcgrep 3 -E '^remote\(foo~bar\): WARNING: UID [0-9]+ disappeared\. Downloading local UID [0-9]+ again\.$' <"$STDERR"
+
+xgrep -E '^local\(baz\): Removed 24 UID\(s\) ' <"$STDERR"
+xgrep -E '^remote\(baz\): Removed 5 UID\(s\) ' <"$STDERR"
+
+# pining UIDs here is not very robust...
+xgrep -E '^local\(foo\.bar\): Updated flags \(\\Answered \\Seen\) for UID 16$' <"$STDERR"
+xgrep -E '^local\(foo\.bar\): Updated flags \(\\Seen\) for UID 14$' <"$STDERR"
+xgrep -E '^remote\(foo~bar\): Updated flags \(\\Answered \\Seen\) for UID 8$' <"$STDERR"
+xgrep -E '^remote\(foo~bar\): Updated flags \(\\Answered\) for UID 3,12,16$' <"$STDERR"
+
+# luid 17
+xcgrep 1 -E '^remote\(foo~bar\): WARNING: No match for modified local UID [0-9]+\. Downloading again\.' <"$STDERR"
+
+xgrep -E '^local\(foo\.bar\): Added 5 UID\(s\) ' <"$STDERR"
+xgrep -E '^remote\(foo~bar\): Added 4 UID\(s\) ' <"$STDERR"
+xgrep -E '^local\(foo\.bar\): Added 1 UID\(s\) ' <"$STDERR" # the new message
+
+check_mailbox_list
+check_mailboxes_status "baz" "foo.bar"
+
+interimap
+check_mailboxes_status "baz" "foo.bar" "INBOX"
+
+# vim: set filetype=sh :
--- /dev/null
+namespace inbox {
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+namespace inbox {
+ location = maildir:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+doveadm -u "local" mailbox create "foo" "bar" "baz"
+doveadm -u "remote" mailbox create "foo" "bar" "baz"
+
+doveadm -u "local" mailbox update --uid-validity 1 "INBOX"
+doveadm -u "local" mailbox update --uid-validity 2147483647 "foo" # 2^31-1
+doveadm -u "local" mailbox update --uid-validity 2147483648 "bar" # 2^31
+doveadm -u "local" mailbox update --uid-validity 4294967295 "baz" # 2^32-1
+
+doveadm -u "remote" mailbox update --uid-validity 4294967295 "INBOX" # 2^32-1
+doveadm -u "remote" mailbox update --uid-validity 2147483648 "foo" # 2^31
+doveadm -u "remote" mailbox update --uid-validity 2147483647 "bar" # 2^31-1
+doveadm -u "remote" mailbox update --uid-validity 1 "baz" #
+
+run() {
+ local u m
+ for u in local remote; do
+ for m in "INBOX" "foo" "bar" "baz"; do
+ sample_message | deliver -u "$u" -- -m "$m"
+ done
+ done
+ interimap
+ check_mailbox_status "INBOX" "foo" "bar" "baz"
+}
+run
+
+# raise UIDNEXT AND HIGHESTMODSEQ close to the max values (resp. 2^32-1 och 2^63-1)
+doveadm -u "local" mailbox update --min-next-uid 2147483647 --min-highest-modseq 9223372036854775807 "INBOX" # 2^31-1, 2^63-1
+doveadm -u "local" mailbox update --min-next-uid 2147483647 --min-highest-modseq 9223372036854775807 "foo" # 2^31-1, 2^63-1
+doveadm -u "local" mailbox update --min-next-uid 2147483648 --min-highest-modseq 9223372036854775808 "bar" # 2^31, 2^63
+doveadm -u "local" mailbox update --min-next-uid 2147483648 --min-highest-modseq 9223372036854775808 "baz" # 2^31, 2^63
+
+doveadm -u "remote" mailbox update --min-next-uid 4294967168 --min-highest-modseq 18446744073709551488 "INBOX" # 2^32-128, 2^64-128
+doveadm -u "remote" mailbox update --min-next-uid 2147483776 --min-highest-modseq 9223372036854775936 "foo" # 2^31+128, 2^63+128
+doveadm -u "remote" mailbox update --min-next-uid 2147483648 --min-highest-modseq 9223372036854775808 "bar" # 2^31, 2^63
+
+run
+
+# vim: set filetype=sh :
--- /dev/null
+namespace inbox {
+ separator = /
+ location = dbox:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
+
+namespace foo {
+ separator = /
+ prefix = foo/
+ location = dbox:~/foo:LAYOUT=index
+ inbox = no
+ list = yes
+}
+
+namespace bar {
+ separator = /
+ prefix = bar/
+ location = dbox:~/bar:LAYOUT=index
+ inbox = no
+ list = yes
+}
+
+namespace baz {
+ separator = /
+ prefix = baz/
+ location = dbox:~/baz:LAYOUT=index
+ inbox = no
+ list = yes
+}
--- /dev/null
+namespace inbox {
+ separator = ^
+ location = dbox:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+namespace inbox {
+ separator = "\\"
+ location = dbox:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+namespace inbox {
+ separator = "?"
+ location = dbox:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+# add references to each interimap instance
+sed -ri 's#^\[local\]$#&\nlist-reference = foo/#' "$XDG_CONFIG_HOME/interimap/config"
+sed -ri 's#^\[local\]$#&\nlist-reference = bar/#' "$XDG_CONFIG_HOME/interimap/config2"
+sed -ri 's#^\[local\]$#&\nlist-reference = baz/#' "$XDG_CONFIG_HOME/interimap/config3"
+
+# create databases
+interimap --config="config"
+interimap --config="config2"
+interimap --config="config3"
+
+# start long-lived interimap processes
+interimap --config="config" --watch=1 & pid=$!
+interimap --config="config2" --watch=1 & pid2=$!
+interimap --config="config3" --watch=1 & pid3=$!
+
+abort() {
+ # kill interimap process and its children
+ pkill -P "$pid" -TERM
+ kill -TERM "$pid"
+ pkill -P "$pid2" -TERM
+ kill -TERM "$pid2"
+ pkill -P "$pid3" -TERM
+ kill -TERM "$pid3"
+ wait
+}
+trap abort EXIT INT TERM
+
+
+# mailbox list (as seen on local) and alphabet
+declare -a mailboxes=( "INBOX" ) alphabet=()
+str="#+-0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"
+for ((i=0; i < ${#str}; i++)); do
+ alphabet[i]="${str:i:1}"
+done
+
+declare -a targets=( "local" "remote" "remote2" "remote3" )
+
+timer=$(( $(date +%s) + 30 ))
+while [ $(date +%s) -le $timer ]; do
+ # create new mailbox with 10% probability
+ if [ $(shuf -n1 -i0-9) -eq 0 ]; then
+ u="$(shuf -n1 -e -- "${targets[@]}")" # choose target at random
+ case "$u" in
+ local) ns="$(shuf -n1 -e "foo/" "bar/" "baz/")";;
+ remote) ns="foo/";;
+ remote2) ns="bar/";;
+ remote3) ns="baz/";;
+ *) echo "Uh?" >&2; exit 1;;
+ esac
+
+ m=
+ d=$(shuf -n1 -i1-3) # random depth
+ for (( i=0; i < d; i++)); do
+ l=$(shuf -n1 -i1-16)
+ m="${m:+$m/}$(shuf -n "$l" -e -- "${alphabet[@]}" | tr -d '\n')"
+ done
+ mailboxes+=( "$ns$m" )
+ case "$u" in
+ local) m="$ns$m";;
+ remote) m="${m//\//^}";;
+ remote2) m="${m//\//\\}";;
+ remote3) m="${m//\//\?}";;
+ *) echo "Uh?" >&2; exit 1;;
+ esac
+ doveadm -u "$u" mailbox create -- "$m"
+ fi
+
+ # EXPUNGE some messages
+ u="$(shuf -n1 -e -- "${targets[@]}")" # choose target at random
+ n="$(shuf -n1 -i0-3)"
+ while read guid uid; do
+ doveadm -u "$u" expunge mailbox-guid "$guid" uid "$uid"
+ done < <(doveadm -u "$u" search all | shuf -n "$n")
+
+ # mark some existing messages as read (toggle \Seen flag as unlike other
+ # flags it's easier to query and check_mailboxes_status checks it)
+ u="$(shuf -n1 -e -- "${targets[@]}")" # choose target at random
+ n="$(shuf -n1 -i0-9)"
+ while read guid uid; do
+ a="$(shuf -n1 -e add remove replace)"
+ doveadm -u "$u" flags "$a" "\\Seen" mailbox-guid "$guid" uid "$uid"
+ done < <(doveadm -u "$u" search all | shuf -n "$n")
+
+ # select at random a mailbox where to deliver some messages
+ u="$(shuf -n1 -e "local" "remote")" # choose target at random
+ m="$(shuf -n1 -e -- "${mailboxes[@]}")"
+ if [ "$u" = "remote" ]; then
+ case "$m" in
+ foo/*) u="remote"; m="${m#foo/}"; m="${m//\//^}";;
+ bar/*) u="remote2"; m="${m#bar/}"; m="${m//\//\\}";;
+ baz/*) u="remote3"; m="${m#baz/}"; m="${m//\//\?}";;
+ INBOX) u="$(shuf -n1 -e "remote" "remote2" "remote3")";;
+ *) echo "Uh? $m" >&2; exit 1;;
+ esac
+ fi
+
+ # deliver between 1 and 5 messages to the chosen mailbox
+ n="$(shuf -n1 -i1-5)"
+ for (( i=0; i < n; i++)); do
+ sample_message | deliver -u "$u" -- -m "$m"
+ done
+
+ # sleep a little bit
+ sleep "0.$(shuf -n1 -i1-99)"
+done
+
+# wait a little longer so interimap has time to run loop() again and
+# synchronize outstanding changes, then terminate the processes we
+# started above
+sleep 2
+
+abort
+trap - EXIT INT TERM
+
+# check that the mailbox lists match
+diff -u --label="local/mailboxes" --label="remote/mailboxes" \
+ <( doveadm -u "local" mailbox list | sed -n "s,^foo/,,p" | sort ) \
+ <( doveadm -u "remote" mailbox list | tr '^' '/' | sort )
+diff -u --label="local/mailboxes" --label="remote2/mailboxes" \
+ <( doveadm -u "local" mailbox list | sed -n "s,^bar/,,p" | sort ) \
+ <( doveadm -u "remote2" mailbox list | tr '\\' '/' | sort )
+diff -u --label="local/mailboxes" --label="remote3/mailboxes" \
+ <( doveadm -u "local" mailbox list | sed -n "s,^baz/,,p" | sort ) \
+ <( doveadm -u "remote3" mailbox list | tr '?' '/' | sort )
+
+for m in "${mailboxes[@]}"; do
+ case "$m" in
+ foo/*) u="remote"; mb="${m#foo/}"; mr="${mb//\//^}";;
+ bar/*) u="remote2"; mb="${m#bar/}"; mr="${mb//\//\\}";;
+ baz/*) u="remote3"; mb="${m#baz/}"; mr="${mb//\//\?}";;
+ INBOX) continue;;
+ *) echo "Uh? $m" >&2; exit 1;;
+ esac
+ blob="x'$(printf "%s" "$mb" | tr "/" "\\0" | xxd -c256 -ps)'"
+ check_mailbox_status2 "$blob" "$m" "$u" "$mr"
+done
+
+# vim: set filetype=sh :
--- /dev/null
+namespace inbox {
+ separator = .
+ location = dbox:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+namespace inbox {
+ separator = ^
+ location = dbox:~/inbox:LAYOUT=index
+ inbox = yes
+ list = yes
+}
--- /dev/null
+# create database
+interimap
+
+# start a long-lived interimap process
+interimap --watch=1 & pid=$!
+
+abort() {
+ # kill interimap process and its children
+ pkill -P "$pid" -TERM
+ kill -TERM "$pid"
+ wait
+}
+trap abort EXIT INT TERM
+
+# mailbox list and alphabet (exclude &, / and ~, which dovecot treats specially)
+declare -a mailboxes=( "INBOX" ) alphabet=()
+str="!\"#\$'()+,-0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ\[\\\]_\`abcdefghijklmnopqrstuvwxyz{|}"
+for ((i=0; i < ${#str}; i++)); do
+ alphabet[i]="${str:i:1}"
+done
+
+timer=$(( $(date +%s) + 30 ))
+while [ $(date +%s) -le $timer ]; do
+ # create new mailbox with 10% probability
+ if [ $(shuf -n1 -i0-9) -eq 0 ]; then
+ m=
+ d=$(shuf -n1 -i1-3) # random depth
+ for (( i=0; i < d; i++)); do
+ l=$(shuf -n1 -i1-16)
+ m="${m:+$m.}$(shuf -n "$l" -e -- "${alphabet[@]}" | tr -d '\n')"
+ done
+ mailboxes+=( "$m" )
+ u="$(shuf -n1 -e "local" "remote")" # choose target at random
+ [ "$u" = "local" ] || m="${m//./^}"
+ doveadm -u "$u" mailbox create -- "$m"
+ fi
+
+ # EXPUNGE some messages
+ u="$(shuf -n1 -e "local" "remote")" # choose target at random
+ n="$(shuf -n1 -i0-3)"
+ while read guid uid; do
+ doveadm -u "$u" expunge mailbox-guid "$guid" uid "$uid"
+ done < <(doveadm -u "$u" search all | shuf -n "$n")
+
+ # mark some existing messages as read (toggle \Seen flag as unlike other
+ # flags it's easier to query and check_mailboxes_status checks it)
+ u="$(shuf -n1 -e "local" "remote")" # choose target at random
+ n="$(shuf -n1 -i0-9)"
+ while read guid uid; do
+ a="$(shuf -n1 -e add remove replace)"
+ doveadm -u "$u" flags "$a" "\\Seen" mailbox-guid "$guid" uid "$uid"
+ done < <(doveadm -u "$u" search all | shuf -n "$n")
+
+ # select at random a mailbox where to deliver some messages
+ u="$(shuf -n1 -e "local" "remote")" # choose target at random
+ m="$(shuf -n1 -e -- "${mailboxes[@]}")"
+ [ "$u" = "local" ] || m="${m//./^}"
+
+ # deliver between 1 and 5 messages to the chosen mailbox
+ n="$(shuf -n1 -i1-5)"
+ for (( i=0; i < n; i++)); do
+ sample_message | deliver -u "$u" -- -m "$m"
+ done
+
+ # sleep a little bit
+ sleep "0.$(shuf -n1 -i1-99)"
+done
+
+# wait a little longer so interimap has time to run loop() again and
+# synchronize outstanding changes, then terminate the process we started
+# above
+sleep 2
+
+abort
+trap - EXIT INT TERM
+
+check_mailbox_list
+check_mailboxes_status "${mailboxes[@]}"
+
+# vim: set filetype=sh :
--- /dev/null
+#!/bin/bash
+
+#----------------------------------------------------------------------
+# Test suite for InterIMAP
+# Copyright © 2019 Guilhem Moulin <guilhem@fripost.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#----------------------------------------------------------------------
+
+set -ue
+PATH=/usr/bin:/bin
+export PATH
+
+if [ $# -ne 1 ]; then
+ printf "Usage: %s TESTNAME\\n" "$0" >&2
+ exit 1
+fi
+
+TEST="${1%/}"
+TEST="${TEST##*/}"
+NAME="${TEST#[0-9]*-}"
+TESTDIR="$(dirname -- "$0")/$TEST"
+if [ ! -d "$TESTDIR" ]; then
+ printf "ERROR: Not a directory: %s\\n" "$TESTDIR" >&2
+ exit 1
+fi
+
+ROOTDIR="$(mktemp --tmpdir=/dev/shm --directory "$NAME.XXXXXXXXXX")"
+trap 'rm -rf -- "$ROOTDIR"' EXIT INT TERM
+
+STDOUT="$ROOTDIR/stdout"
+STDERR="$ROOTDIR/stderr"
+TMPDIR="$ROOTDIR/tmp"
+mkdir -- "$TMPDIR" "$ROOTDIR/home"
+
+# Set environment for the given user
+environ_set() {
+ local user="$1" home
+ eval home="\$HOME_$user"
+ ENVIRON=(
+ PATH="$PATH"
+ USER="$user"
+ HOME="$home"
+ XDG_CONFIG_HOME="$home/.config"
+ XDG_DATA_HOME="$home/.local/share"
+ )
+}
+
+# Prepare the test harness
+prepare() {
+ declare -a ENVIRON=()
+ local src cfg target u home
+ # copy dovecot config
+ for src in "$TESTDIR/local.conf" "$TESTDIR"/remote*.conf; do
+ [ -r "$src" ] || continue
+ u="${src#"$TESTDIR/"}"
+ u="${u%.conf}"
+ home="$ROOTDIR/home/$u"
+ export "HOME_$u"="$home"
+ mkdir -pm0755 -- "$home/.local/bin"
+ mkdir -pm0700 -- "$home/.config/dovecot"
+ cat >"$home/.config/dovecot/config" <<-EOF
+ log_path = /dev/null
+ mail_home = $ROOTDIR/home/%u
+ ssl = no
+ EOF
+ cat >>"$home/.config/dovecot/config" <"$src"
+ environ_set "$u"
+ cat >"$home/.local/bin/doveadm" <<-EOF
+ #!/bin/sh
+ exec env -i ${ENVIRON[@]@Q} \\
+ doveadm -c ${home@Q}/.config/dovecot/config "\$@"
+ EOF
+ chmod +x -- "$home/.local/bin/doveadm"
+ done
+
+ # copy interimap config
+ mkdir -pm0700 -- "$HOME_local/.local/share/interimap"
+ mkdir -pm0700 -- "$HOME_local/.config/interimap"
+ for cfg in "$TESTDIR"/remote*.conf; do
+ cfg="${cfg#"$TESTDIR/remote"}"
+ cfg="${cfg%.conf}"
+ u="remote$cfg"
+ eval home="\$HOME_$u"
+ if [ -f "$TESTDIR/interimap.conf" ]; then
+ cat <"$TESTDIR/interimap.conf" >>"$HOME_local/.config/interimap/config$cfg"
+ fi
+ cat >>"$HOME_local/.config/interimap/config$cfg" <<-EOF
+ database = $u.db
+
+ [local]
+ type = tunnel
+ command = exec ${HOME_local@Q}/.local/bin/doveadm exec imap
+ null-stderr = YES
+
+ [remote]
+ type = tunnel
+ command = exec ${home@Q}/.local/bin/doveadm exec imap
+ null-stderr = YES
+ EOF
+ done
+}
+prepare
+
+# Wrappers for interimap(1) and doveadm(1)
+interimap() {
+ declare -a ENVIRON=()
+ environ_set "local"
+ env -i "${ENVIRON[@]}" perl -I./lib -T ./interimap "$@"
+}
+doveadm() {
+ if [ $# -le 2 ] || [ "$1" != "-u" ]; then
+ echo "Usage: doveadm -u USER ..." >&2
+ exit 1
+ fi
+ local u="$2" home
+ eval home="\$HOME_$u"
+ shift 2
+ "$home/.local/bin/doveadm" "$@"
+}
+
+# Sample (random) message
+sample_message() {
+ cat <<-EOF
+ From: <sender@example.net>
+ To: <recipient@example.net>
+ Date: $(date -R)
+ Message-ID: <$(< /proc/sys/kernel/random/uuid)@example.net>
+
+ EOF
+ local len="$(shuf -i1-4096 -n1)"
+ xxd -ps -c30 -l"$len" /dev/urandom # 3 to 8329 bytes
+}
+
+# Wrapper for dovecot-lda(1)
+deliver() {
+ local -a argv
+ while [ $# -gt 0 ] && [ "$1" != "--" ]; do
+ argv+=( "$1" )
+ shift
+ done
+ if [ $# -gt 0 ] && [ "$1" = "--" ]; then
+ shift
+ fi
+ doveadm "${argv[@]}" exec dovecot-lda -e "$@"
+}
+
+# Dump test results
+dump_test_result() {
+ local below=">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
+ local above="<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"
+ local src u home
+ declare -a ENVIRON=()
+ for src in "$TESTDIR/local.conf" "$TESTDIR"/remote*.conf; do
+ u="${src#"$TESTDIR/"}"
+ u="${u%.conf}"
+ environ_set "$u"
+ eval home="\$HOME_$u"
+ printf "%s dovecot configuration:\\n%s\\n" "$u" "$below"
+ env -i "${ENVIRON[@]}" doveconf -c "$home/.config/dovecot/config" -n
+ printf "%s\\n\\n" "$above"
+ done
+
+ printf "(local) interimap configuration:\\n%s\\n" "$below"
+ cat <"$HOME_local/.config/interimap/config"
+ printf "%s\\n\\n" "$above"
+
+ printf "standard output was:\\n%s\\n" "$below"
+ cat <"$STDOUT"
+ printf "%s\\n\\n" "$above"
+
+ printf "standard error was:\\n%s\\n" "$below"
+ cat <"$STDERR"
+ printf "%s\\n\\n" "$above"
+}
+
+# Check mailbox consistency between the local/remote server and interimap's database
+check_mailbox_status() {
+ local mailbox="$1" lns="inbox" lsep lprefix rns="inbox" rsep rprefix
+ lsep="$(doveconf -c "$HOME_local/.config/dovecot/config" -h "namespace/$lns/separator")"
+ lprefix="$(doveconf -c "$HOME_local/.config/dovecot/config" -h "namespace/$lns/prefix")"
+ rsep="$(doveconf -c "$HOME_remote/.config/dovecot/config" -h "namespace/$lns/separator")"
+ rprefix="$(doveconf -c "$HOME_remote/.config/dovecot/config" -h "namespace/$lns/prefix")"
+
+ local blob="x'$(printf "%s" "$mailbox" | tr "$lsep" "\\0" | xxd -c256 -ps)'"
+ local rmailbox="$(printf "%s" "$mailbox" | tr "$lsep" "$rsep")"
+ check_mailbox_status2 "$blob" "$lprefix$mailbox" "remote" "$rprefix$rmailbox"
+}
+check_mailbox_status2() {
+ local blob="$1" lmailbox="$2" u="$3" rmailbox="$4"
+ local lUIDVALIDITY lUIDNEXT lHIGHESTMODSEQ rUIDVALIDITY rUIDNEXT rHIGHESTMODSEQ
+ read lUIDVALIDITY lUIDNEXT lHIGHESTMODSEQ rUIDVALIDITY rUIDNEXT rHIGHESTMODSEQ < <(
+ sqlite3 "$XDG_DATA_HOME/interimap/$u.db" <<-EOF
+ .mode csv
+ .separator " " "\\n"
+ SELECT l.UIDVALIDITY, l.UIDNEXT, l.HIGHESTMODSEQ, r.UIDVALIDITY, r.UIDNEXT, r.HIGHESTMODSEQ
+ FROM mailboxes m JOIN local l ON m.idx = l.idx JOIN remote r ON m.idx = r.idx
+ WHERE mailbox = $blob
+ EOF
+ )
+ lHIGHESTMODSEQ="$(printf "%llu" "$lHIGHESTMODSEQ")"
+ rHIGHESTMODSEQ="$(printf "%llu" "$rHIGHESTMODSEQ")"
+ local MESSAGES
+ read MESSAGES < <( sqlite3 "$XDG_DATA_HOME/interimap/$u.db" <<-EOF
+ .mode csv
+ .separator " " "\\n"
+ SELECT COUNT(*)
+ FROM mailboxes a JOIN mapping b ON a.idx = b.idx
+ WHERE mailbox = $blob
+ EOF
+ )
+ check_mailbox_status_values "local" "$lmailbox" $lUIDVALIDITY $lUIDNEXT $lHIGHESTMODSEQ $MESSAGES
+ check_mailbox_status_values "$u" "$rmailbox" $rUIDVALIDITY $rUIDNEXT $rHIGHESTMODSEQ $MESSAGES
+
+ local a b
+ a="$(doveadm -u "local" -f "flow" mailbox status "messages unseen vsize" -- "$lmailbox" | \
+ sed -nr '/.*\s+(\w+=[0-9]+\s+\w+=[0-9]+\s+\w+=[0-9]+)$/ {s//\1/p;q}')"
+ b="$(doveadm -u "$u" -f "flow" mailbox status "messages unseen vsize" -- "$rmailbox" | \
+ sed -nr '/.*\s+(\w+=[0-9]+\s+\w+=[0-9]+\s+\w+=[0-9]+)$/ {s//\1/p;q}')"
+ if [ "$a" != "$b" ]; then
+ echo "Mailbox $lmailbox status differs: \"$a\" != \"$b\"" >&2
+ exit 1
+ fi
+}
+check_mailbox_status_values() {
+ local user="$1" mailbox="$2" UIDVALIDITY="$3" UIDNEXT="$4" HIGHESTMODSEQ="$5" MESSAGES="$6" x xs v k
+ xs="$(doveadm -u "$user" -f "flow" mailbox status "uidvalidity uidnext highestmodseq messages" -- "$mailbox" | \
+ sed -nr '/.*\s+(\w+=[0-9]+\s+\w+=[0-9]+\s+\w+=[0-9]+\s+\w+=[0-9]+)$/ {s//\1/p;q}')"
+ [ -n "$xs" ] || exit 1
+ for x in $xs; do
+ k="${x%%=*}"
+ case "${k^^[a-z]}" in
+ UIDVALIDITY) v="$UIDVALIDITY";;
+ UIDNEXT) v="$UIDNEXT";;
+ HIGHESTMODSEQ) v="$HIGHESTMODSEQ";;
+ MESSAGES) v="$MESSAGES";;
+ *) echo "Uh? $x" >&2; exit 1
+ esac
+ if [ "${x#*=}" != "$v" ]; then
+ echo "$user($mailbox): ${k^^[a-z]} doesn't match! ${x#*=} != $v" >&2
+ exit 1
+ fi
+ done
+}
+check_mailboxes_status() {
+ local mailbox
+ for mailbox in "$@"; do
+ check_mailbox_status "$mailbox"
+ done
+}
+
+# Check mailbox list constency between the local and remote servers
+check_mailbox_list() {
+ local m i lns="inbox" lsep lprefix rns="inbox" rsep rprefix sub=
+ lsep="$(doveconf -c "$HOME_local/.config/dovecot/config" -h "namespace/$lns/separator")"
+ lprefix="$(doveconf -c "$HOME_local/.config/dovecot/config" -h "namespace/$lns/prefix")"
+ rsep="$(doveconf -c "$HOME_remote/.config/dovecot/config" -h "namespace/$lns/separator")"
+ rprefix="$(doveconf -c "$HOME_remote/.config/dovecot/config" -h "namespace/$lns/prefix")"
+ if [ $# -gt 0 ] && [ "$1" = "-s" ]; then
+ sub="-s"
+ shift
+ fi
+
+ declare -a lmailboxes=() rmailboxes=()
+ if [ $# -eq 0 ]; then
+ lmailboxes=( "${lprefix}*" )
+ rmailboxes=( "${rprefix}*" )
+ else
+ for m in "$@"; do
+ lmailboxes+=( "$lprefix$m" )
+ rmailboxes+=( "$rprefix${m//"$lsep"/"$rsep"}" )
+ done
+ fi
+
+ mapfile -t lmailboxes < <( doveadm -u "local" mailbox list $sub -- "${lmailboxes[@]}" )
+ for ((i = 0; i < ${#lmailboxes[@]}; i++)); do
+ lmailboxes[i]="${lmailboxes[i]#"$lprefix"}"
+ done
+
+ mapfile -t rmailboxes < <( doveadm -u "remote" mailbox list $sub -- "${rmailboxes[@]}" )
+ for ((i = 0; i < ${#rmailboxes[@]}; i++)); do
+ rmailboxes[i]="${rmailboxes[i]#"$rprefix"}"
+ rmailboxes[i]="${rmailboxes[i]//"$rsep"/"$lsep"}"
+ done
+
+ local IFS=$'\n'
+ diff -u --label="local/mailboxes" --label="remote/mailboxes" \
+ <( printf "%s" "${lmailboxes[*]}" | sort ) <( printf "%s" "${rmailboxes[*]}" | sort )
+}
+
+# Wrappers for grep(1) and `grep -C`
+xgrep() {
+ if ! grep -q "$@"; then
+ printf "\`grep %s\` failed on line %d\\n" "${*@Q}" ${BASH_LINENO[0]} >&2
+ exit 1
+ fi
+}
+xcgrep() {
+ local m="$1" n
+ shift
+ if ! n="$(grep -c "$@")" || [ $m -ne $n ]; then
+ printf "\`grep -c %s\` failed on line %d: %d != %d\\n" "${*@Q}" ${BASH_LINENO[0]} "$m" "$n" >&2
+ exit 1
+ fi
+}
+
+# Run test in a sub-shell
+declare -a ENVIRON=()
+environ_set "local"
+export TMPDIR TESTDIR STDOUT STDERR "${ENVIRON[@]}"
+export -f environ_set doveadm interimap sample_message deliver
+export -f check_mailbox_status check_mailbox_status_values check_mailbox_status2
+export -f check_mailboxes_status check_mailbox_list xgrep xcgrep
+printf "%s..." "$TEST"
+if ! bash -ue "$TESTDIR/run" >"$STDOUT" 2>"$STDERR"; then
+ echo " FAILED"
+ dump_test_result
+ exit 1
+else
+ echo " OK"
+ if grep -Paq "\\x00" -- "$STDOUT" "$STDERR"; then
+ printf "\\tWarn: binary output (outstanding \\0)!\\n"
+ fi
+ exit 0
+fi