]> git.g-eek.se Git - interimap.git/commitdiff
Add test-suite (requires dovecot-imapd).
authorGuilhem Moulin <guilhem@fripost.org>
Sun, 26 May 2019 21:28:04 +0000 (23:28 +0200)
committerGuilhem Moulin <guilhem@fripost.org>
Sun, 26 May 2019 22:39:49 +0000 (00:39 +0200)
60 files changed:
Changelog
Makefile
tests/00-db-exclusive/local.conf [new file with mode: 0644]
tests/00-db-exclusive/remote.conf [new file with mode: 0644]
tests/00-db-exclusive/run [new file with mode: 0644]
tests/00-db-migration-0-to-1-delim-mismatch/before.sql [new symlink]
tests/00-db-migration-0-to-1-delim-mismatch/local.conf [new file with mode: 0644]
tests/00-db-migration-0-to-1-delim-mismatch/remote.conf [new file with mode: 0644]
tests/00-db-migration-0-to-1-delim-mismatch/run [new file with mode: 0644]
tests/00-db-migration-0-to-1-foreign-key-violation/local.conf [new file with mode: 0644]
tests/00-db-migration-0-to-1-foreign-key-violation/remote.conf [new file with mode: 0644]
tests/00-db-migration-0-to-1-foreign-key-violation/run [new file with mode: 0644]
tests/00-db-migration-0-to-1/after.sql [new file with mode: 0644]
tests/00-db-migration-0-to-1/before.sql [new file with mode: 0644]
tests/00-db-migration-0-to-1/local.conf [new file with mode: 0644]
tests/00-db-migration-0-to-1/remote.conf [new file with mode: 0644]
tests/00-db-migration-0-to-1/run [new file with mode: 0644]
tests/01-rename-exists-db/local.conf [new file with mode: 0644]
tests/01-rename-exists-db/remote.conf [new file with mode: 0644]
tests/01-rename-exists-db/run [new file with mode: 0644]
tests/01-rename-exists-local/local.conf [new file with mode: 0644]
tests/01-rename-exists-local/remote.conf [new file with mode: 0644]
tests/01-rename-exists-local/run [new file with mode: 0644]
tests/01-rename-exists-remote/local.conf [new file with mode: 0644]
tests/01-rename-exists-remote/remote.conf [new file with mode: 0644]
tests/01-rename-exists-remote/run [new file with mode: 0644]
tests/01-rename/local.conf [new file with mode: 0644]
tests/01-rename/remote.conf [new file with mode: 0644]
tests/01-rename/run [new file with mode: 0644]
tests/02-delete/local.conf [new file with mode: 0644]
tests/02-delete/remote.conf [new file with mode: 0644]
tests/02-delete/run [new file with mode: 0644]
tests/03-sync-mailbox-list-partial/interimap.conf [new file with mode: 0644]
tests/03-sync-mailbox-list-partial/local.conf [new file with mode: 0644]
tests/03-sync-mailbox-list-partial/remote.conf [new file with mode: 0644]
tests/03-sync-mailbox-list-partial/run [new file with mode: 0644]
tests/03-sync-mailbox-list-ref/local.conf [new file with mode: 0644]
tests/03-sync-mailbox-list-ref/remote.conf [new file with mode: 0644]
tests/03-sync-mailbox-list-ref/run [new file with mode: 0644]
tests/03-sync-mailbox-list/local.conf [new file with mode: 0644]
tests/03-sync-mailbox-list/remote.conf [new file with mode: 0644]
tests/03-sync-mailbox-list/run [new file with mode: 0644]
tests/04-resume/local.conf [new file with mode: 0644]
tests/04-resume/remote.conf [new file with mode: 0644]
tests/04-resume/run [new file with mode: 0644]
tests/05-repair/local.conf [new file with mode: 0644]
tests/05-repair/remote.conf [new file with mode: 0644]
tests/05-repair/run [new file with mode: 0644]
tests/06-largeint/local.conf [new file with mode: 0644]
tests/06-largeint/remote.conf [new file with mode: 0644]
tests/06-largeint/run [new file with mode: 0644]
tests/07-sync-live-multi/local.conf [new file with mode: 0644]
tests/07-sync-live-multi/remote.conf [new file with mode: 0644]
tests/07-sync-live-multi/remote2.conf [new file with mode: 0644]
tests/07-sync-live-multi/remote3.conf [new file with mode: 0644]
tests/07-sync-live-multi/run [new file with mode: 0644]
tests/07-sync-live/local.conf [new file with mode: 0644]
tests/07-sync-live/remote.conf [new file with mode: 0644]
tests/07-sync-live/run [new file with mode: 0644]
tests/run [new file with mode: 0755]

index a9f1ae3d1dc220534564d9db433321dc3f6a0990..a13801a2ebfffdb9a84d4c2b1ddbf87194925cd5 100644 (file)
--- a/Changelog
+++ b/Changelog
@@ -18,6 +18,7 @@ interimap (0.5) upstream;
    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
index d7b7133614fea3d71af92ca8d49599bc8ac73bbc..ec35011dd01ba935c32b7963ed826aa8b4d70e50 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -31,7 +31,10 @@ all: pullimap.1 interimap.1
 
 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
diff --git a/tests/00-db-exclusive/local.conf b/tests/00-db-exclusive/local.conf
new file mode 100644 (file)
index 0000000..9c838fd
--- /dev/null
@@ -0,0 +1,5 @@
+namespace inbox {
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/00-db-exclusive/remote.conf b/tests/00-db-exclusive/remote.conf
new file mode 100644 (file)
index 0000000..9c838fd
--- /dev/null
@@ -0,0 +1,5 @@
+namespace inbox {
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/00-db-exclusive/run b/tests/00-db-exclusive/run
new file mode 100644 (file)
index 0000000..1ae27b6
--- /dev/null
@@ -0,0 +1,25 @@
+# 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 :
diff --git a/tests/00-db-migration-0-to-1-delim-mismatch/before.sql b/tests/00-db-migration-0-to-1-delim-mismatch/before.sql
new file mode 120000 (symlink)
index 0000000..0abb9bf
--- /dev/null
@@ -0,0 +1 @@
+../00-db-migration-0-to-1/before.sql
\ No newline at end of file
diff --git a/tests/00-db-migration-0-to-1-delim-mismatch/local.conf b/tests/00-db-migration-0-to-1-delim-mismatch/local.conf
new file mode 100644 (file)
index 0000000..08438cb
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = "\""
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/00-db-migration-0-to-1-delim-mismatch/remote.conf b/tests/00-db-migration-0-to-1-delim-mismatch/remote.conf
new file mode 100644 (file)
index 0000000..cc6781d
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = ^
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/00-db-migration-0-to-1-delim-mismatch/run b/tests/00-db-migration-0-to-1-delim-mismatch/run
new file mode 100644 (file)
index 0000000..434c678
--- /dev/null
@@ -0,0 +1,8 @@
+# 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 :
diff --git a/tests/00-db-migration-0-to-1-foreign-key-violation/local.conf b/tests/00-db-migration-0-to-1-foreign-key-violation/local.conf
new file mode 100644 (file)
index 0000000..93497d9
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = .
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/00-db-migration-0-to-1-foreign-key-violation/remote.conf b/tests/00-db-migration-0-to-1-foreign-key-violation/remote.conf
new file mode 100644 (file)
index 0000000..93497d9
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = .
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/00-db-migration-0-to-1-foreign-key-violation/run b/tests/00-db-migration-0-to-1-foreign-key-violation/run
new file mode 100644 (file)
index 0000000..f2d12a9
--- /dev/null
@@ -0,0 +1,23 @@
+# 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 :
diff --git a/tests/00-db-migration-0-to-1/after.sql b/tests/00-db-migration-0-to-1/after.sql
new file mode 100644 (file)
index 0000000..18b0ad7
--- /dev/null
@@ -0,0 +1,14 @@
+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;
diff --git a/tests/00-db-migration-0-to-1/before.sql b/tests/00-db-migration-0-to-1/before.sql
new file mode 100644 (file)
index 0000000..333a1dc
--- /dev/null
@@ -0,0 +1,14 @@
+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;
diff --git a/tests/00-db-migration-0-to-1/local.conf b/tests/00-db-migration-0-to-1/local.conf
new file mode 100644 (file)
index 0000000..93497d9
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = .
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/00-db-migration-0-to-1/remote.conf b/tests/00-db-migration-0-to-1/remote.conf
new file mode 100644 (file)
index 0000000..93497d9
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = .
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/00-db-migration-0-to-1/run b/tests/00-db-migration-0-to-1/run
new file mode 100644 (file)
index 0000000..e4eb770
--- /dev/null
@@ -0,0 +1,26 @@
+# 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 :
diff --git a/tests/01-rename-exists-db/local.conf b/tests/01-rename-exists-db/local.conf
new file mode 100644 (file)
index 0000000..93497d9
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = .
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/01-rename-exists-db/remote.conf b/tests/01-rename-exists-db/remote.conf
new file mode 100644 (file)
index 0000000..61e3d0d
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = "\\"
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/01-rename-exists-db/run b/tests/01-rename-exists-db/run
new file mode 100644 (file)
index 0000000..29cb075
--- /dev/null
@@ -0,0 +1,14 @@
+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 :
diff --git a/tests/01-rename-exists-local/local.conf b/tests/01-rename-exists-local/local.conf
new file mode 100644 (file)
index 0000000..93497d9
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = .
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/01-rename-exists-local/remote.conf b/tests/01-rename-exists-local/remote.conf
new file mode 100644 (file)
index 0000000..61e3d0d
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = "\\"
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/01-rename-exists-local/run b/tests/01-rename-exists-local/run
new file mode 100644 (file)
index 0000000..17d8fcc
--- /dev/null
@@ -0,0 +1,13 @@
+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 :
diff --git a/tests/01-rename-exists-remote/local.conf b/tests/01-rename-exists-remote/local.conf
new file mode 100644 (file)
index 0000000..93497d9
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = .
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/01-rename-exists-remote/remote.conf b/tests/01-rename-exists-remote/remote.conf
new file mode 100644 (file)
index 0000000..61e3d0d
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = "\\"
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/01-rename-exists-remote/run b/tests/01-rename-exists-remote/run
new file mode 100644 (file)
index 0000000..c867a77
--- /dev/null
@@ -0,0 +1,13 @@
+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 :
diff --git a/tests/01-rename/local.conf b/tests/01-rename/local.conf
new file mode 100644 (file)
index 0000000..93497d9
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = .
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/01-rename/remote.conf b/tests/01-rename/remote.conf
new file mode 100644 (file)
index 0000000..cc6781d
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = ^
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/01-rename/run b/tests/01-rename/run
new file mode 100644 (file)
index 0000000..6541c5c
--- /dev/null
@@ -0,0 +1,84 @@
+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 :
diff --git a/tests/02-delete/local.conf b/tests/02-delete/local.conf
new file mode 100644 (file)
index 0000000..93497d9
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = .
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/02-delete/remote.conf b/tests/02-delete/remote.conf
new file mode 100644 (file)
index 0000000..cc6781d
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = ^
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/02-delete/run b/tests/02-delete/run
new file mode 100644 (file)
index 0000000..f63c52c
--- /dev/null
@@ -0,0 +1,67 @@
+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 :
diff --git a/tests/03-sync-mailbox-list-partial/interimap.conf b/tests/03-sync-mailbox-list-partial/interimap.conf
new file mode 100644 (file)
index 0000000..4970867
--- /dev/null
@@ -0,0 +1 @@
+list-mailbox = *
diff --git a/tests/03-sync-mailbox-list-partial/local.conf b/tests/03-sync-mailbox-list-partial/local.conf
new file mode 100644 (file)
index 0000000..93497d9
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = .
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/03-sync-mailbox-list-partial/remote.conf b/tests/03-sync-mailbox-list-partial/remote.conf
new file mode 100644 (file)
index 0000000..352cdd4
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = ~
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/03-sync-mailbox-list-partial/run b/tests/03-sync-mailbox-list-partial/run
new file mode 100644 (file)
index 0000000..449115d
--- /dev/null
@@ -0,0 +1,57 @@
+# 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 :
diff --git a/tests/03-sync-mailbox-list-ref/local.conf b/tests/03-sync-mailbox-list-ref/local.conf
new file mode 100644 (file)
index 0000000..6eccf43
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = /
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/03-sync-mailbox-list-ref/remote.conf b/tests/03-sync-mailbox-list-ref/remote.conf
new file mode 100644 (file)
index 0000000..61e3d0d
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = "\\"
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/03-sync-mailbox-list-ref/run b/tests/03-sync-mailbox-list-ref/run
new file mode 100644 (file)
index 0000000..3ead25d
--- /dev/null
@@ -0,0 +1,28 @@
+# 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 :
diff --git a/tests/03-sync-mailbox-list/local.conf b/tests/03-sync-mailbox-list/local.conf
new file mode 100644 (file)
index 0000000..93497d9
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = .
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/03-sync-mailbox-list/remote.conf b/tests/03-sync-mailbox-list/remote.conf
new file mode 100644 (file)
index 0000000..352cdd4
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = ~
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/03-sync-mailbox-list/run b/tests/03-sync-mailbox-list/run
new file mode 100644 (file)
index 0000000..e9fda06
--- /dev/null
@@ -0,0 +1,73 @@
+# 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 :
diff --git a/tests/04-resume/local.conf b/tests/04-resume/local.conf
new file mode 100644 (file)
index 0000000..93497d9
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = .
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/04-resume/remote.conf b/tests/04-resume/remote.conf
new file mode 100644 (file)
index 0000000..352cdd4
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = ~
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/04-resume/run b/tests/04-resume/run
new file mode 100644 (file)
index 0000000..22d66bc
--- /dev/null
@@ -0,0 +1,98 @@
+# 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 :
diff --git a/tests/05-repair/local.conf b/tests/05-repair/local.conf
new file mode 100644 (file)
index 0000000..93497d9
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = .
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/05-repair/remote.conf b/tests/05-repair/remote.conf
new file mode 100644 (file)
index 0000000..352cdd4
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = ~
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/05-repair/run b/tests/05-repair/run
new file mode 100644 (file)
index 0000000..747b974
--- /dev/null
@@ -0,0 +1,107 @@
+# 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 :
diff --git a/tests/06-largeint/local.conf b/tests/06-largeint/local.conf
new file mode 100644 (file)
index 0000000..9c838fd
--- /dev/null
@@ -0,0 +1,5 @@
+namespace inbox {
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/06-largeint/remote.conf b/tests/06-largeint/remote.conf
new file mode 100644 (file)
index 0000000..9c838fd
--- /dev/null
@@ -0,0 +1,5 @@
+namespace inbox {
+    location  = maildir:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/06-largeint/run b/tests/06-largeint/run
new file mode 100644 (file)
index 0000000..edcbd31
--- /dev/null
@@ -0,0 +1,38 @@
+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 :
diff --git a/tests/07-sync-live-multi/local.conf b/tests/07-sync-live-multi/local.conf
new file mode 100644 (file)
index 0000000..baae39d
--- /dev/null
@@ -0,0 +1,30 @@
+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
+}
diff --git a/tests/07-sync-live-multi/remote.conf b/tests/07-sync-live-multi/remote.conf
new file mode 100644 (file)
index 0000000..3267182
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = ^
+    location  = dbox:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/07-sync-live-multi/remote2.conf b/tests/07-sync-live-multi/remote2.conf
new file mode 100644 (file)
index 0000000..062429e
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = "\\"
+    location  = dbox:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/07-sync-live-multi/remote3.conf b/tests/07-sync-live-multi/remote3.conf
new file mode 100644 (file)
index 0000000..a4b9b1c
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = "?"
+    location  = dbox:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/07-sync-live-multi/run b/tests/07-sync-live-multi/run
new file mode 100644 (file)
index 0000000..bf0d2f5
--- /dev/null
@@ -0,0 +1,138 @@
+# 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 :
diff --git a/tests/07-sync-live/local.conf b/tests/07-sync-live/local.conf
new file mode 100644 (file)
index 0000000..1333540
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = .
+    location  = dbox:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/07-sync-live/remote.conf b/tests/07-sync-live/remote.conf
new file mode 100644 (file)
index 0000000..3267182
--- /dev/null
@@ -0,0 +1,6 @@
+namespace inbox {
+    separator = ^
+    location  = dbox:~/inbox:LAYOUT=index
+    inbox     = yes
+    list      = yes
+}
diff --git a/tests/07-sync-live/run b/tests/07-sync-live/run
new file mode 100644 (file)
index 0000000..1950e0b
--- /dev/null
@@ -0,0 +1,80 @@
+# 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 :
diff --git a/tests/run b/tests/run
new file mode 100755 (executable)
index 0000000..31af03e
--- /dev/null
+++ b/tests/run
@@ -0,0 +1,336 @@
+#!/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