Merge pull request 'Make Vandelay into inbuxa-migrate' (#1) from rename/inbuxa-migrate into main
github/ci (branch) GitHub Actions
github/ci (branch) GitHub Actions
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
#
|
||||
# Announce each published release on the community forum, in this project's
|
||||
# Announcements category (coffey-labs/actions discourse-release; the repo ->
|
||||
# category map is its release-map.json). Safe to re-run: one topic per tag.
|
||||
name: announce
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
announce:
|
||||
runs-on: light
|
||||
steps:
|
||||
- uses: coffey-labs/actions/discourse-release@baedbb0e89336e49dd5105a22f8ea4f712b453ad
|
||||
with:
|
||||
api-key: ${{ secrets.DISCOURSE_RELEASE_KEY }}
|
||||
discord-webhook: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
|
||||
@@ -0,0 +1,99 @@
|
||||
# SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
#
|
||||
# CI on the self-hosted Gitea. Gitea reads .gitea/workflows and ignores
|
||||
# .github/ once this directory exists; .github/workflows is GitHub's side of
|
||||
# the switch below.
|
||||
#
|
||||
# Every job runs in an image pinned by digest (tag in the trailing comment),
|
||||
# and the only actions used are coffey-labs/actions ones pinned by SHA. The
|
||||
# instance resolves short `uses:` against itself, never GitHub, so nothing
|
||||
# unreviewed can be pulled in.
|
||||
#
|
||||
# BUILD ON GITHUB. The org variable BUILD_ON decides which forge builds.
|
||||
# Set to 'github', the test job below skips and .github/workflows/ci.yml does
|
||||
# the work on GitHub's runners -- GitHub holds a push mirror of this
|
||||
# repository, updated on every commit -- and reports back as the commit status
|
||||
# "github/ci (branch)" or "github/ci (tag)". The `github` job waits for that
|
||||
# status and passes or fails with it, so a run here still says whether the
|
||||
# commit is good. Unset (or anything but 'github'), the tests run here.
|
||||
#
|
||||
# Releases are built only on GitHub: each architecture is compiled on a
|
||||
# native runner there, and these runners are amd64 only. With BUILD_ON unset
|
||||
# a v* tag is tested here but not released.
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# The unit and mock tests; the #[ignore]d ones need live servers or
|
||||
# containers and run by hand (README, "Testing").
|
||||
test:
|
||||
if: ${{ vars.BUILD_ON != 'github' }}
|
||||
runs-on: light
|
||||
container:
|
||||
image: rust:1-bookworm@sha256:93ce27a88655056a51dbdd8f5f2d7ddc071c7b0070fb288a37b5a285fc83971e # 1-bookworm
|
||||
steps:
|
||||
- uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec
|
||||
- run: cargo test --locked
|
||||
|
||||
# Stands in for test and release while GitHub builds: waits for the status
|
||||
# GitHub's ci.yml posts on this commit and passes or fails with it. A pull
|
||||
# request is checked at its head commit, which is what GitHub built when
|
||||
# the branch reached the mirror. Two and a half hours covers a slow queue;
|
||||
# a timeout here with BUILD_ON=github usually means GitHub never got the
|
||||
# push -- check the mirror's last error in the repository settings.
|
||||
github:
|
||||
if: ${{ vars.BUILD_ON == 'github' }}
|
||||
# Its own runner label with plenty of slots: this job only polls, but holds a slot
|
||||
# for as long as the GitHub build takes, and must not starve the build runners.
|
||||
runs-on: wait
|
||||
timeout-minutes: 150
|
||||
container:
|
||||
image: rust:1-bookworm@sha256:93ce27a88655056a51dbdd8f5f2d7ddc071c7b0070fb288a37b5a285fc83971e # 1-bookworm
|
||||
steps:
|
||||
- shell: bash
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
IS_TAG: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
apt-get update -qq && apt-get install -y -qq --no-install-recommends jq >/dev/null
|
||||
ctx="github/ci (branch)"; [ "$IS_TAG" = true ] && ctx="github/ci (tag)"
|
||||
echo "waiting for '$ctx' on $SHA"
|
||||
while :; do
|
||||
s="$(curl -fsS -H "Authorization: token $TOKEN" \
|
||||
"$CI_SERVER_INTERNAL/api/v1/repos/$REPO/commits/$SHA/statuses?limit=50" \
|
||||
| jq -c --arg c "$ctx" '[.[] | select(.context == $c)] | sort_by(.id) | last // empty')" || s=""
|
||||
state="$(printf '%s' "$s" | jq -r '.status // .state // empty')"
|
||||
case "$state" in
|
||||
success) echo "GitHub: success"; exit 0 ;;
|
||||
failure|error) echo "GitHub: $state -- $(jq -r '.target_url' <<<"$s")"; exit 1 ;;
|
||||
esac
|
||||
sleep 20
|
||||
done
|
||||
|
||||
# GitHub makes the Release and publishes it only once its files are
|
||||
# attached; this waits for GitHub's success and announces then. The action
|
||||
# makes one topic per tag, so announce.yml firing for the same Release is a
|
||||
# no-op.
|
||||
announce:
|
||||
needs: [github]
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') && needs.github.result == 'success' }}
|
||||
runs-on: light
|
||||
steps:
|
||||
- uses: coffey-labs/actions/discourse-release@baedbb0e89336e49dd5105a22f8ea4f712b453ad
|
||||
with:
|
||||
api-key: ${{ secrets.DISCOURSE_RELEASE_KEY }}
|
||||
discord-webhook: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
|
||||
tag: ${{ github.ref_name }}
|
||||
@@ -0,0 +1,257 @@
|
||||
# SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
#
|
||||
# CI on GitHub Actions, for the GitHub copy of this repository.
|
||||
#
|
||||
# The repository lives on the self-hosted Gitea; GitHub holds a push mirror
|
||||
# that Gitea updates on every commit. Nothing is merged here -- pull requests
|
||||
# happen on Gitea, and their branches reach GitHub as ordinary pushes, which is
|
||||
# why this workflow runs on `push` and not on `pull_request`.
|
||||
#
|
||||
# Which forge does the building is one switch, the variable BUILD_ON, set on
|
||||
# both forges at the organization level:
|
||||
#
|
||||
# BUILD_ON=github every job here runs; .gitea/workflows/ci.yml skips its
|
||||
# test job and waits for the status this workflow reports
|
||||
# back instead.
|
||||
# unset / other every job here skips; Gitea runs the tests. Releases need
|
||||
# GitHub: they are built on native amd64 and arm64 runners.
|
||||
#
|
||||
# The result goes back to Gitea as one commit status, "github/ci (branch)" or
|
||||
# "github/ci (tag)", which is what Gitea's `github` job waits on.
|
||||
#
|
||||
# v* tags build a release: the tag must be on main and name the version in
|
||||
# Cargo.toml; each architecture is built on its own native runner by
|
||||
# scripts/build-release.sh, and the archives plus SHA256SUMS are attached to
|
||||
# the Gitea Release -- Gitea is where the install guide points. The Release is
|
||||
# created as a draft, filled, then published, so it is never visible with
|
||||
# assets missing; if an upload fails, a Release this run created is deleted.
|
||||
# Gitea announces the release once this run has reported success.
|
||||
#
|
||||
# The binaries link glibc dynamically, so they are built on Ubuntu 22.04: the
|
||||
# oldest glibc GitHub offers, and so the widest range of servers they run on.
|
||||
#
|
||||
# Configuration, all at the organization level on GitHub: variables BUILD_ON,
|
||||
# GITEA_URL; secret GITEA_TOKEN (write:repository on Gitea).
|
||||
#
|
||||
# Every `uses:` is pinned to a full commit SHA, with the release it was in the
|
||||
# trailing comment. Nothing schedules here: schedules belong to Gitea.
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
tags: ['**']
|
||||
# For a run GitHub queued and then orphaned. Run it from a tag to redo that
|
||||
# tag's release; attaching skips any file already on the Release.
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref_type != 'tag' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
GITEA_URL: ${{ vars.GITEA_URL }}
|
||||
CONTEXT: github/ci (${{ github.ref_type }})
|
||||
CARGO_TERM_COLOR: never
|
||||
|
||||
jobs:
|
||||
# Tells Gitea a result is on its way, so a status that is still missing
|
||||
# reads as "running" rather than "never started".
|
||||
pending:
|
||||
if: vars.BUILD_ON == 'github'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
curl -fsS -o /dev/null -X POST -H "Authorization: token $GITEA_TOKEN" -H 'Content-Type: application/json' \
|
||||
"$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" \
|
||||
-d "$(jq -n --arg c "$CONTEXT" --arg u "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
|
||||
'{state:"pending", context:$c, target_url:$u, description:"GitHub Actions"}')"
|
||||
|
||||
# The unit and mock tests. The #[ignore]d ones need live servers or
|
||||
# containers and run by hand (README, "Testing").
|
||||
test:
|
||||
if: vars.BUILD_ON == 'github'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: cargo-test-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('Cargo.lock', 'rust-toolchain.toml') }}
|
||||
restore-keys: cargo-test-${{ runner.os }}-${{ runner.arch }}-
|
||||
# rustup is on the runner; rust-toolchain.toml picks the toolchain.
|
||||
- run: rustup show active-toolchain || rustup toolchain install
|
||||
- run: cargo test --locked
|
||||
|
||||
# Guards shared by both architectures, checked once.
|
||||
version:
|
||||
if: vars.BUILD_ON == 'github' && github.ref_type == 'tag' && startsWith(github.ref_name, 'v')
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TAG: ${{ github.ref_name }}
|
||||
steps:
|
||||
# Full history: the ancestry check cannot be answered from a shallow
|
||||
# clone.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# A release can never describe code that was not reviewed onto main,
|
||||
# and its tag must be the version the binary reports.
|
||||
- run: |
|
||||
git fetch --quiet origin main
|
||||
git merge-base --is-ancestor "$(git rev-parse "${TAG}^{commit}")" origin/main \
|
||||
|| { echo "::error::$TAG is not on main"; exit 1; }
|
||||
want="v$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')"
|
||||
[ "$TAG" = "$want" ] || { echo "::error::$TAG does not match Cargo.toml ($want)"; exit 1; }
|
||||
|
||||
build:
|
||||
if: vars.BUILD_ON == 'github' && github.ref_type == 'tag' && startsWith(github.ref_name, 'v')
|
||||
needs: [test, version]
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
runner: ubuntu-22.04
|
||||
- arch: arm64
|
||||
runner: ubuntu-22.04-arm
|
||||
env:
|
||||
TAG: ${{ github.ref_name }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: rustup show active-toolchain || rustup toolchain install
|
||||
- run: |
|
||||
SOURCE_DATE_EPOCH="$(git log -1 --format=%ct "$TAG")" scripts/build-release.sh "$TAG" dist
|
||||
sha256sum dist/*.tar.gz
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: archive-${{ matrix.arch }}
|
||||
path: dist/*.tar.gz
|
||||
retention-days: 7
|
||||
overwrite: true
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
if: vars.BUILD_ON == 'github' && github.ref_type == 'tag' && startsWith(github.ref_name, 'v')
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TAG: ${{ github.ref_name }}
|
||||
steps:
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
path: dist
|
||||
pattern: archive-*
|
||||
merge-multiple: true
|
||||
- run: |
|
||||
(cd dist && sha256sum ./*.tar.gz | sed 's| \./| |' > SHA256SUMS)
|
||||
cat dist/SHA256SUMS
|
||||
- env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
API="$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY"
|
||||
auth=(-H "Authorization: token $GITEA_TOKEN")
|
||||
# Reuse the Release if the tag already has one (a re-run), else make a
|
||||
# draft of our own.
|
||||
created=0
|
||||
id="$(curl -fsS "${auth[@]}" "$API/releases/tags/$TAG" 2>/dev/null | jq -r '.id // empty' || true)"
|
||||
if [ -z "$id" ]; then
|
||||
id="$(curl -fsS "${auth[@]}" -H 'Content-Type: application/json' \
|
||||
-d "$(jq -n --arg t "$TAG" '{tag_name:$t, name:$t, draft:true,
|
||||
body:"Binaries for linux/amd64 and linux/arm64. Verify with SHA256SUMS."}')" \
|
||||
"$API/releases" | jq -r '.id // empty')"
|
||||
[ -n "$id" ] || { echo "::error::could not create the release"; exit 1; }
|
||||
created=1
|
||||
fi
|
||||
have="$(curl -fsS "${auth[@]}" "$API/releases/$id/assets" | jq -r '.[].name')"
|
||||
for f in dist/*; do
|
||||
n="$(basename "$f")"
|
||||
if grep -qxF "$n" <<<"$have"; then echo "already attached: $n"; continue; fi
|
||||
echo "uploading $n"
|
||||
curl -fsS -o /dev/null "${auth[@]}" --form "attachment=@$f" "$API/releases/$id/assets?name=$n" || {
|
||||
[ "$created" = 1 ] && curl -sS -o /dev/null "${auth[@]}" -X DELETE "$API/releases/$id"
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
if [ "$created" = 1 ]; then
|
||||
curl -fsS -o /dev/null "${auth[@]}" -H 'Content-Type: application/json' -X PATCH \
|
||||
-d '{"draft":false}' "$API/releases/$id"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------- github release ------
|
||||
# Copies this tag's Gitea release -- notes and files -- to a GitHub release,
|
||||
# so the replica's Releases page, and anyone watching it, keeps up. Gitea's
|
||||
# release is the real one; this is left out of the report to Gitea, so a
|
||||
# failure here cannot fail a release. PR and issue numbers in the notes are
|
||||
# rewritten to Gitea links: on GitHub a bare #16 is some other PR.
|
||||
github-release:
|
||||
if: ${{ vars.BUILD_ON == 'github' && github.ref_type == 'tag' }}
|
||||
needs: [release]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
GITEA_URL: ${{ vars.GITEA_URL }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
steps:
|
||||
- run: |
|
||||
set -euo pipefail
|
||||
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
echo "GitHub already has a release for $TAG"; exit 0
|
||||
fi
|
||||
# The Gitea release exists by now if this run made it; allow a few
|
||||
# minutes either way.
|
||||
code=0
|
||||
for _ in $(seq 1 15); do
|
||||
code="$(curl -sS -o rel.json -w '%{http_code}' "$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY/releases/tags/$TAG")"
|
||||
[ "$code" = 200 ] && break
|
||||
sleep 20
|
||||
done
|
||||
if [ "$code" != 200 ]; then echo "No Gitea release for $TAG; nothing to copy"; exit 0; fi
|
||||
if [ "$(jq -r .draft rel.json)" = true ]; then echo "The Gitea release is a draft; not copying"; exit 0; fi
|
||||
export BASE="$(jq -r '.html_url | sub("/releases/tag/.*$"; "")' rel.json)"
|
||||
jq -r '.body // ""' rel.json | perl -pe 's{(?<![\w/&\[])#(\d+)\b}{[#$1]($ENV{BASE}/pulls/$1)}g' > notes.md
|
||||
printf '\n\n_Mirrored from [the Gitea release](%s); report issues on [Gitea](%s/issues)._\n' \
|
||||
"$(jq -r .html_url rel.json)" "$BASE" >> notes.md
|
||||
files=()
|
||||
mkdir -p files
|
||||
while IFS=$'\t' read -r name url; do
|
||||
curl -fsSL -o "files/$name" "$url"; files+=("files/$name")
|
||||
done < <(jq -r '.assets[]? | [.name, .browser_download_url] | @tsv' rel.json)
|
||||
title="$(jq -r '.name // ""' rel.json)"; [ -n "$title" ] || title="$TAG"
|
||||
if [ "$(jq -r .prerelease rel.json)" = true ]; then kind=--prerelease; else kind=--latest; fi
|
||||
gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --verify-tag --title "$title" \
|
||||
--notes-file notes.md "$kind" "${files[@]}"
|
||||
echo "created the GitHub release for $TAG with ${#files[@]} file(s)"
|
||||
|
||||
report:
|
||||
if: always() && vars.BUILD_ON == 'github'
|
||||
needs: [pending, test, version, build, release]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
STATE: ${{ contains(needs.*.result, 'failure') && 'failure' || (contains(needs.*.result, 'cancelled') && 'cancelled' || 'success') }}
|
||||
run: |
|
||||
# A cancelled run was superseded by a newer run for the same commit (the
|
||||
# mirror can push one commit twice); that run reports. Posting "failure"
|
||||
# here would fail the Gitea check while the real build is still going.
|
||||
if [ "$STATE" = cancelled ]; then echo "cancelled: leaving the result to the newer run"; exit 0; fi
|
||||
curl -fsS -o /dev/null -X POST -H "Authorization: token $GITEA_TOKEN" -H 'Content-Type: application/json' \
|
||||
"$GITEA_URL/api/v1/repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" \
|
||||
-d "$(jq -n --arg s "$STATE" --arg c "$CONTEXT" --arg u "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
|
||||
'{state:$s, context:$c, target_url:$u, description:"GitHub Actions"}')"
|
||||
@@ -1,342 +0,0 @@
|
||||
name: Release
|
||||
permissions:
|
||||
"contents": "write"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- '**[0-9]+.[0-9]+.[0-9]+*'
|
||||
|
||||
jobs:
|
||||
# Run 'dist plan' (or host) to determine what tasks we need to do
|
||||
plan:
|
||||
runs-on: "ubuntu-22.04"
|
||||
outputs:
|
||||
val: ${{ steps.plan.outputs.manifest }}
|
||||
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
|
||||
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
|
||||
publishing: ${{ !github.event.pull_request }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install dist
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.31.0/cargo-dist-installer.sh | sh"
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/dist
|
||||
- id: plan
|
||||
run: |
|
||||
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
cat plan-dist-manifest.json
|
||||
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: artifacts-plan-dist-manifest
|
||||
path: plan-dist-manifest.json
|
||||
|
||||
build-local-artifacts:
|
||||
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
|
||||
needs:
|
||||
- plan
|
||||
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
container: ${{ matrix.container && matrix.container.image || null }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
|
||||
AWS_LC_SYS_PREBUILT_NASM: 1
|
||||
permissions:
|
||||
"attestations": "write"
|
||||
"contents": "read"
|
||||
"id-token": "write"
|
||||
steps:
|
||||
- name: enable windows longpaths
|
||||
run: |
|
||||
git config --global core.longpaths true
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install Rust non-interactively if not already installed
|
||||
if: ${{ matrix.container }}
|
||||
run: |
|
||||
if ! command -v cargo > /dev/null 2>&1; then
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
fi
|
||||
- name: Install dist
|
||||
run: ${{ matrix.install_dist.run }}
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
${{ matrix.packages_install }}
|
||||
- name: Update Rust toolchain to current stable
|
||||
shell: bash
|
||||
run: |
|
||||
if command -v rustup > /dev/null 2>&1; then
|
||||
rustup update stable
|
||||
rustup default stable
|
||||
fi
|
||||
- name: Build artifacts
|
||||
run: |
|
||||
# Actually do builds and make zips and whatnot
|
||||
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
- name: Attest
|
||||
uses: actions/attest-build-provenance@v4
|
||||
with:
|
||||
subject-path: "target/distrib/*${{ join(matrix.targets, ', ') }}*"
|
||||
- id: cargo-dist
|
||||
name: Post-build
|
||||
shell: bash
|
||||
run: |
|
||||
# Parse out what we just built and upload it to scratch storage
|
||||
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||
- name: "Upload artifacts"
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
|
||||
path: |
|
||||
${{ steps.cargo-dist.outputs.paths }}
|
||||
${{ env.BUILD_MANIFEST_NAME }}
|
||||
|
||||
build-global-artifacts:
|
||||
needs:
|
||||
- plan
|
||||
- build-local-artifacts
|
||||
runs-on: "ubuntu-22.04"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
- id: cargo-dist
|
||||
shell: bash
|
||||
run: |
|
||||
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
|
||||
# Parse out what we just built and upload it to scratch storage
|
||||
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||
- name: "Upload artifacts"
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: artifacts-build-global
|
||||
path: |
|
||||
${{ steps.cargo-dist.outputs.paths }}
|
||||
${{ env.BUILD_MANIFEST_NAME }}
|
||||
|
||||
host:
|
||||
needs:
|
||||
- plan
|
||||
- build-local-artifacts
|
||||
- build-global-artifacts
|
||||
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
runs-on: "ubuntu-22.04"
|
||||
outputs:
|
||||
val: ${{ steps.host.outputs.manifest }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
- name: Fetch artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
- id: host
|
||||
shell: bash
|
||||
run: |
|
||||
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
|
||||
echo "artifacts uploaded and released successfully"
|
||||
cat dist-manifest.json
|
||||
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: artifacts-dist-manifest
|
||||
path: dist-manifest.json
|
||||
- name: "Download GitHub Artifacts"
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
- name: Cleanup
|
||||
run: |
|
||||
# Remove the granular manifests
|
||||
rm -f artifacts/*-dist-manifest.json
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
|
||||
RELEASE_COMMIT: "${{ github.sha }}"
|
||||
run: |
|
||||
awk '/^## \[/{c++} c==1' CHANGELOG.md > $RUNNER_TEMP/notes.txt
|
||||
|
||||
# Tag-push releases are created as drafts; the `announce` job un-drafts
|
||||
# them only after the build succeeds, so watcher notifications don't
|
||||
# fire on broken builds.
|
||||
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --draft --title "${{ needs.plan.outputs.tag }}" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
|
||||
|
||||
publish-homebrew-formula:
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
runs-on: "ubuntu-22.04"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PLAN: ${{ needs.plan.outputs.val }}
|
||||
GITHUB_USER: "axo bot"
|
||||
GITHUB_EMAIL: "[email protected]"
|
||||
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: true
|
||||
repository: "stalwartlabs/homebrew-tap"
|
||||
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
- name: Fetch homebrew formulae
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: Formula/
|
||||
merge-multiple: true
|
||||
- name: Commit formula files
|
||||
run: |
|
||||
git config --global user.name "${GITHUB_USER}"
|
||||
git config --global user.email "${GITHUB_EMAIL}"
|
||||
|
||||
for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith(".rb")] | any)'); do
|
||||
filename=$(echo "$release" | jq '.artifacts[] | select(endswith(".rb"))' --raw-output)
|
||||
name=$(echo "$filename" | sed "s/\.rb$//")
|
||||
version=$(echo "$release" | jq .app_version --raw-output)
|
||||
|
||||
export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"
|
||||
brew update
|
||||
# We avoid reformatting user-provided data such as the app description and homepage.
|
||||
brew style --except-cops FormulaAudit/Homepage,FormulaAudit/Desc,FormulaAuditStrict --fix "Formula/${filename}" || true
|
||||
|
||||
git add "Formula/${filename}"
|
||||
git commit -m "${name} ${version}"
|
||||
done
|
||||
git push
|
||||
|
||||
publish-npm:
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
runs-on: "ubuntu-22.04"
|
||||
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
|
||||
permissions:
|
||||
"contents": "read"
|
||||
"id-token": "write"
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Fetch npm tarball
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: artifacts/
|
||||
merge-multiple: true
|
||||
- name: Upgrade npm for trusted publishing
|
||||
run: npm install -g npm@latest
|
||||
- name: Publish to npm
|
||||
env:
|
||||
IS_PRERELEASE: ${{ fromJson(needs.plan.outputs.val).announcement_is_prerelease }}
|
||||
run: |
|
||||
TAG_FLAG=""
|
||||
if [ "$IS_PRERELEASE" = "true" ]; then
|
||||
TAG_FLAG="--tag next"
|
||||
fi
|
||||
npm publish ./artifacts/vandelay-npm-package.tar.gz --provenance --access public $TAG_FLAG
|
||||
|
||||
announce:
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
- publish-homebrew-formula
|
||||
- publish-npm
|
||||
if: ${{ always() && needs.host.result == 'success' && (needs.publish-homebrew-formula.result == 'skipped' || needs.publish-homebrew-formula.result == 'success') && (needs.publish-npm.result == 'skipped' || needs.publish-npm.result == 'success') }}
|
||||
runs-on: "ubuntu-22.04"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Un-draft release
|
||||
run: gh release edit "${{ needs.plan.outputs.tag }}" --draft=false --repo "${{ github.repository }}"
|
||||
|
||||
cleanup:
|
||||
name: Cleanup failed release
|
||||
needs:
|
||||
- plan
|
||||
- build-local-artifacts
|
||||
- build-global-artifacts
|
||||
- host
|
||||
- publish-homebrew-formula
|
||||
- publish-npm
|
||||
- announce
|
||||
if: ${{ failure() && needs.plan.outputs.publishing == 'true' }}
|
||||
runs-on: "ubuntu-22.04"
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Delete draft release and tag
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh release delete "${{ needs.plan.outputs.tag }}" --yes --cleanup-tag --repo "${{ github.repository }}" || true
|
||||
@@ -9,3 +9,5 @@ target
|
||||
!CHANGELOG.md
|
||||
.*
|
||||
!.github
|
||||
!.gitea
|
||||
!/docs/*.md
|
||||
|
||||
+21
-1
@@ -1,6 +1,26 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
All notable changes to this project are recorded here. Versions are dates:
|
||||
release `v2026.9.30` is version `2026.9.30`.
|
||||
|
||||
## [Unreleased] -- 2026.9.30
|
||||
|
||||
### Changed
|
||||
- Renamed to inbuxa-migrate: the binary, the crate, and the credential
|
||||
variables, now `INBUXA_MIGRATE_PASSWORD`, `INBUXA_MIGRATE_TOKEN`,
|
||||
`INBUXA_MIGRATE_EWS_CLIENT_SECRET` and `INBUXA_MIGRATE_GRAPH_TOKEN`. The old
|
||||
names are not read. Archives written by Vandelay are not read either.
|
||||
- Registry calls (`x:Account`, `x:Domain` and the other `x:` types) use the
|
||||
capability the server advertises: `urn:inbuxa:jmap:registry` on inbuxa,
|
||||
`urn:stalwart:jmap` on a Stalwart source. A server that advertises neither
|
||||
gets a clear error instead of a request it never offered to accept.
|
||||
- Released as Linux archives for amd64 and arm64 on the Gitea release page,
|
||||
with `SHA256SUMS`. The npm, Homebrew, shell and PowerShell installers and
|
||||
the MSI are gone.
|
||||
|
||||
---
|
||||
|
||||
# Earlier releases (upstream Vandelay)
|
||||
|
||||
## [1.0.11] - 2026-09-28
|
||||
|
||||
|
||||
Generated
+32
-32
@@ -1323,6 +1323,38 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inbuxa-migrate"
|
||||
version = "2026.9.30"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"calcard",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"clap",
|
||||
"crossbeam-channel",
|
||||
"encodify",
|
||||
"flate2",
|
||||
"indexmap 2.14.2",
|
||||
"mail-parser",
|
||||
"mockito",
|
||||
"num_cpus",
|
||||
"psl",
|
||||
"quick-xml",
|
||||
"regex",
|
||||
"rusqlite",
|
||||
"rustls",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"testcontainers",
|
||||
"thiserror",
|
||||
"time",
|
||||
"ureq",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
@@ -2936,38 +2968,6 @@ dependencies = [
|
||||
"sha1_smol",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vandelay"
|
||||
version = "1.0.11"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"calcard",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"clap",
|
||||
"crossbeam-channel",
|
||||
"encodify",
|
||||
"flate2",
|
||||
"indexmap 2.14.2",
|
||||
"mail-parser",
|
||||
"mockito",
|
||||
"num_cpus",
|
||||
"psl",
|
||||
"quick-xml",
|
||||
"regex",
|
||||
"rusqlite",
|
||||
"rustls",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"testcontainers",
|
||||
"thiserror",
|
||||
"time",
|
||||
"ureq",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
|
||||
+7
-17
@@ -1,22 +1,16 @@
|
||||
[package]
|
||||
name = "vandelay"
|
||||
description = "JMAP account migration utility"
|
||||
version = "1.0.11"
|
||||
authors = ["Stalwart Labs LLC <[email protected]>"]
|
||||
name = "inbuxa-migrate"
|
||||
description = "Account migration and backup for inbuxa"
|
||||
version = "2026.9.30"
|
||||
authors = ["John Coffey <[email protected]>"]
|
||||
license = "Apache-2.0 OR MIT"
|
||||
repository = "https://github.com/stalwartlabs/vandelay"
|
||||
homepage = "https://github.com/stalwartlabs/vandelay"
|
||||
repository = "https://git.coffeylabs.org/inbuxa/inbuxa-migrate"
|
||||
homepage = "https://git.coffeylabs.org/inbuxa/inbuxa-migrate"
|
||||
edition = "2024"
|
||||
readme = "README.md"
|
||||
|
||||
[package.metadata.wix]
|
||||
upgrade-guid = "5C8148E7-34A9-4216-A802-FFB8345AECC4"
|
||||
path-guid = "FA9E97EB-5E5C-4809-9018-9DC1EDB0E93D"
|
||||
license = false
|
||||
eula = false
|
||||
|
||||
[[bin]]
|
||||
name = "vandelay"
|
||||
name = "inbuxa-migrate"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
@@ -52,7 +46,3 @@ psl = "2.1.226"
|
||||
mockito = "1.7.2"
|
||||
tempfile = "3.13"
|
||||
testcontainers = { version = "0.28", features = ["blocking"] }
|
||||
|
||||
[profile.dist]
|
||||
inherits = "release"
|
||||
lto = "thin"
|
||||
|
||||
@@ -1,355 +1,103 @@
|
||||
<h1 align="center">Vandelay</h1>
|
||||
# inbuxa-migrate
|
||||
|
||||
<h3 align="center">
|
||||
The JMAP importer-exporter
|
||||
<br/>
|
||||
<sub>One-shot account migration and backup across JMAP, IMAP, CalDAV, CardDAV, WebDAV, ManageSieve, Maildir, Google Takeout and Microsoft Exchange.</sub>
|
||||
</h3>
|
||||
> [!NOTE]
|
||||
> Development happens on [git.coffeylabs.org/inbuxa/inbuxa-migrate](https://git.coffeylabs.org/inbuxa/inbuxa-migrate); the copy on GitHub is a read-only mirror.
|
||||
> Report issues at **[git.coffeylabs.org/inbuxa/inbuxa-migrate/issues](https://git.coffeylabs.org/inbuxa/inbuxa-migrate/issues)**, and join discussions at **[community.coffeylabs.org](https://community.coffeylabs.org)**.
|
||||
|
||||
<br>
|
||||
One program that moves an account into **inbuxa**: its mail, calendars,
|
||||
contacts, identities, Sieve scripts and files.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/stalwartlabs/vandelay/actions/workflows/release.yml"><img src="https://img.shields.io/github/actions/workflow/status/stalwartlabs/vandelay/release.yml?style=flat-square" alt="build"></a>
|
||||
|
||||
<a href="https://github.com/stalwartlabs/vandelay/releases/latest"><img src="https://img.shields.io/github/v/release/stalwartlabs/vandelay?style=flat-square" alt="release"></a>
|
||||
|
||||
<a href="https://www.npmjs.com/package/@stalwartlabs/vandelay"><img src="https://img.shields.io/npm/v/@stalwartlabs/vandelay?style=flat-square&logo=npm" alt="npm"></a>
|
||||
|
||||
<a href="https://github.com/stalwartlabs/vandelay/releases"><img src="https://img.shields.io/github/downloads/stalwartlabs/vandelay/total?style=flat-square" alt="downloads"></a>
|
||||
|
||||
<img src="https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20Windows-blue?style=flat-square" alt="platforms">
|
||||
|
||||
<a href="#license"><img src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue?style=flat-square" alt="license"></a>
|
||||
</p>
|
||||
inbuxa-migrate import imap … alice.sqlite an account into an archive
|
||||
inbuxa-migrate inspect alice.sqlite what landed
|
||||
inbuxa-migrate export … alice.sqlite the archive into inbuxa
|
||||
|
||||
<br>
|
||||
Between the two sides sits an archive: one SQLite file holding one account.
|
||||
`import` fills it from the old server; `export` writes it into the new one.
|
||||
Import and export never talk to each other, only to the archive, so the old
|
||||
server can be gone before the new one exists.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
Both commands converge. An interrupted run picks up where it stopped, and a
|
||||
later `import` into the same archive adds what arrived since. Every command
|
||||
takes `--dry-run` and then only reports the plan. `inspect` reads an archive
|
||||
and changes nothing.
|
||||
|
||||
- Jerry: Well, what does **he** do?
|
||||
- George: He's an importer.
|
||||
- Jerry: Just imports, no exports?
|
||||
- George: He's an **importer/exporter**, okay?
|
||||
The archive is also a backup. Import on a schedule, keep the file, and
|
||||
restore it later with `export`. An archive remembers which account filled it
|
||||
and refuses a different one unless told otherwise.
|
||||
|
||||
</td>
|
||||
<td>
|
||||
<img src="assets/importer-exporter.jpg" alt="He's an importer-exporter.">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
Export speaks JMAP only. It writes into inbuxa, or into any JMAP server that
|
||||
advertises the types being written. The full command reference is in
|
||||
[docs/usage.md](docs/usage.md).
|
||||
|
||||
|
||||
## Sources
|
||||
|
||||
## About
|
||||
- **JMAP** -- any JMAP account, including an inbuxa or Stalwart server.
|
||||
- **IMAP** -- mail only. Folders are chosen by name or by pattern.
|
||||
- **CalDAV, CardDAV** -- calendars and events, address books and contacts.
|
||||
- **WebDAV** -- a plain file collection, kept as a file tree.
|
||||
- **ManageSieve** -- Sieve scripts only, with the active one recorded.
|
||||
- **Maildir** -- a local Maildir++ tree. No network.
|
||||
- **Google Takeout** -- the `.mbox`, `.ics` and `.vcf` files in a Takeout
|
||||
export, or in any directory tree laid out that way.
|
||||
- **Exchange Server** -- an on-premises mailbox, through EWS.
|
||||
- **Exchange Online** -- through Microsoft Graph. EWS is being retired in
|
||||
Exchange Online: blocked from October 1, 2026 unless an administrator allows
|
||||
the client, and switched off on April 1, 2027. Public folders are only
|
||||
reachable through EWS.
|
||||
|
||||
Vandelay is a one-shot account-migration utility for JMAP, the JMAP analogue of `imapsync` generalised to every JMAP data type (mail, contacts, calendars, identities, sieve scripts, file storage). It imports an account from a wide range of source protocols into a local SQLite "archive", then exports that archive into a target JMAP server. Import and export never talk to each other, only to the SQLite archive. One archive holds exactly one account.
|
||||
## Credentials
|
||||
|
||||
Because the archive is a self-contained SQLite file that fully describes one account, vandelay doubles as a per-account backup tool: run an import on a schedule to capture a fresh snapshot, keep the resulting SQLite file as your backup, and restore it later by running an export against a JMAP target.
|
||||
Secrets are read from the environment, or asked for at a prompt:
|
||||
|
||||
## Features
|
||||
INBUXA_MIGRATE_PASSWORD a password for --auth-basic
|
||||
INBUXA_MIGRATE_TOKEN a bearer token for --auth-bearer
|
||||
INBUXA_MIGRATE_EWS_CLIENT_SECRET the OAuth client secret for app-only EWS
|
||||
INBUXA_MIGRATE_GRAPH_TOKEN a Microsoft Graph access token
|
||||
|
||||
- **Many source protocols:**
|
||||
- JMAP
|
||||
- IMAP
|
||||
- CalDAV
|
||||
- CardDAV
|
||||
- WebDAV
|
||||
- ManageSieve
|
||||
- Maildir++
|
||||
- Google Takeout
|
||||
- Microsoft Exchange Server (on-premises) via EWS
|
||||
- Microsoft Exchange Online via Graph
|
||||
- **One target protocol:** JMAP, with type-by-type stateless re-matching on every run.
|
||||
- **Convergent:** Re-running an interrupted import or export picks up where it left off without bookkeeping flags.
|
||||
- **Multi-threaded, no async runtime:** Blocking HTTP with per-server concurrency caps respected automatically.
|
||||
- **Content-addressed blobs:** Emails, sieve scripts and file-storage payloads are stored once by BLAKE3 hash and deduplicated across the archive.
|
||||
- **Dry-run everywhere:** Every command supports `--dry-run` to compute the full plan without writing.
|
||||
- **Source-change protection:** An archive remembers which account it was filled from; pointing it at a different one fails unless explicitly permitted.
|
||||
- **Read-only inspection:** A built-in `inspect` command dumps any object type from an archive for verification.
|
||||
The command line takes them too, but a secret there ends up in the shell's
|
||||
history and in the process list. Use the environment or the prompt.
|
||||
|
||||
## Install
|
||||
## Installing
|
||||
|
||||
```sh
|
||||
# macOS / Linux
|
||||
curl --proto '=https' --tlsv1.2 -LsSf \
|
||||
https://github.com/stalwartlabs/vandelay/releases/latest/download/vandelay-installer.sh | sh
|
||||
Linux, on amd64 or arm64. Each release carries one archive per architecture
|
||||
and a `SHA256SUMS` file:
|
||||
|
||||
# Homebrew
|
||||
brew install stalwartlabs/tap/vandelay
|
||||
base=https://git.coffeylabs.org/inbuxa/inbuxa-migrate/releases/latest/download
|
||||
curl -fLO "$base/inbuxa-migrate-linux-amd64.tar.gz"
|
||||
curl -fLO "$base/SHA256SUMS"
|
||||
sha256sum --check --ignore-missing SHA256SUMS
|
||||
tar -xzf inbuxa-migrate-linux-amd64.tar.gz
|
||||
./inbuxa-migrate --version
|
||||
|
||||
# Windows
|
||||
powershell -ExecutionPolicy Bypass -c "irm https://github.com/stalwartlabs/vandelay/releases/latest/download/vandelay-installer.ps1 | iex"
|
||||
On arm64, fetch `inbuxa-migrate-linux-arm64.tar.gz` instead. The binary is
|
||||
the whole program; put it anywhere on the path.
|
||||
|
||||
# npm
|
||||
npm install -g @stalwartlabs/vandelay
|
||||
## Building and testing
|
||||
|
||||
# From source
|
||||
cargo install --path .
|
||||
```
|
||||
cargo build --release the binary is target/release/inbuxa-migrate
|
||||
cargo test the default suite
|
||||
|
||||
A signed `.msi` is also published with each release.
|
||||
The default suite needs no network and no Docker. Unit tests and scripted
|
||||
mocks stand in for JMAP, DAV, EWS and Graph servers.
|
||||
|
||||
## Quick start
|
||||
The live tests are marked `#[ignore]`. Each boots a throwaway container --
|
||||
Stalwart, Dovecot, Cyrus, Radicale, Baikal or Apache `mod_dav` -- so they
|
||||
need a working Docker daemon. Run them one binary at a time, and always with
|
||||
one test thread:
|
||||
|
||||
A typical run is two commands, one to capture a source account into a local SQLite archive and one to push that archive into a JMAP target.
|
||||
cargo test --test sync_jmap -- --ignored --test-threads=1
|
||||
cargo test --test integration_dovecot -- --ignored --test-threads=1
|
||||
|
||||
```sh
|
||||
# 1. Import an IMAP mailbox into a fresh archive.
|
||||
export VANDELAY_PASSWORD='source-app-password'
|
||||
vandelay import imap \
|
||||
--url imaps://imap.example.com \
|
||||
--auth-basic [email protected] \
|
||||
alice.sqlite
|
||||
|
||||
# 2. Peek at what landed.
|
||||
vandelay inspect alice.sqlite # per-type summary
|
||||
vandelay inspect alice.sqlite mailbox # mailbox tree
|
||||
vandelay inspect alice.sqlite email --limit 5
|
||||
|
||||
# 3. Push the archive into a target JMAP server.
|
||||
export VANDELAY_PASSWORD='target-password'
|
||||
vandelay export \
|
||||
--url https://jmap.example.org \
|
||||
--auth-basic [email protected] \
|
||||
--account-name [email protected] \
|
||||
alice.sqlite
|
||||
```
|
||||
|
||||
Both commands are convergent: rerun either to resume an interrupted run, or rerun `import` later to pick up new mail since the last snapshot. Use `--dry-run` on either side to compute the full plan without writing.
|
||||
|
||||
## CLI quick reference
|
||||
|
||||
```
|
||||
vandelay <import|export|inspect> [args...]
|
||||
```
|
||||
|
||||
All actions accept a set of global flags (verbosity, worker pool size, retry policy, TLS handling) in addition to action-specific ones. The most useful are:
|
||||
|
||||
| Flag | Purpose |
|
||||
| --- | --- |
|
||||
| `-j, --threads <N>` | Worker pool size (default: logical CPUs). |
|
||||
| `--dry-run` | Compute the full plan; perform no writes. |
|
||||
| `-v`, `-vv`, `-vvv` | Increase log verbosity. |
|
||||
| `-q, --quiet` | Warnings and errors only. |
|
||||
| `--max-retries <N>` | Max retries per request on transient failures (default 5). |
|
||||
| `--allow-invalid-certs` | Accept self-signed / invalid TLS certs. |
|
||||
|
||||
Credentials should be supplied via the `VANDELAY_PASSWORD` / `VANDELAY_TOKEN` / `VANDELAY_EWS_CLIENT_SECRET` / `VANDELAY_GRAPH_TOKEN` environment variables, or via an interactive prompt; passing them on the command line is supported but not recommended.
|
||||
|
||||
### Import
|
||||
|
||||
```
|
||||
vandelay import <source> [source-args...] <ARCHIVE>
|
||||
```
|
||||
|
||||
Reads a source account into the local SQLite `ARCHIVE` (created if absent). Every importer accepts `--allow-source-change` to override the archive's source-identity guard.
|
||||
|
||||
#### JMAP
|
||||
|
||||
```
|
||||
vandelay import jmap \
|
||||
--url <URL> \
|
||||
(--auth-basic <USER> [--auth-password <PASS>] | --auth-bearer [TOKEN]) \
|
||||
(--account-id <ID> | --account-name <NAME>) \
|
||||
[--objects <list>] \
|
||||
<ARCHIVE>
|
||||
```
|
||||
|
||||
Imports a single JMAP account. `--objects` accepts a comma-separated list of object tokens (`mailbox,email,calendar,calendarevent,addressbook,contactcard,identity,sievescript,participantidentity,filenode`); default is everything the server advertises.
|
||||
|
||||
#### IMAP
|
||||
|
||||
```
|
||||
vandelay import imap \
|
||||
--url imap(s)://host[:port] \
|
||||
(--auth-basic <USER> [--auth-password <PASS>] | --auth-bearer [TOKEN] --auth-user <USER>) \
|
||||
[--include <REGEX>...] [--exclude <REGEX>...] [--exclude-special <ROLE>...] \
|
||||
[--folder <NAME>...] [--subscribed-only] [--noautomap] \
|
||||
[--include-deleted] [--allow-cleartext] [--compress] \
|
||||
[--fetch-batch <N>] [--imap-connections <1..8>] \
|
||||
<ARCHIVE>
|
||||
```
|
||||
|
||||
Imports mail (and only mail) from any IMAP server. Folder selection is via `--include`/`--exclude` regexes (mutually exclusive with the exact-match `--folder`); `--exclude-special` drops by SPECIAL-USE role.
|
||||
|
||||
#### CalDAV
|
||||
|
||||
```
|
||||
vandelay import caldav \
|
||||
--url <http(s)://host[/path]> \
|
||||
(--auth-basic <USER> [--auth-password <PASS>] | --auth-bearer [TOKEN]) \
|
||||
[--allow-cleartext] [--dav-connections <1..8>] [--multiget-batch <N>] \
|
||||
<ARCHIVE>
|
||||
```
|
||||
|
||||
Discovers the user's CalDAV principal (or accepts a URL pointing straight at a calendar-home or calendar), then imports calendars and events.
|
||||
|
||||
#### CardDAV
|
||||
|
||||
```
|
||||
vandelay import carddav \
|
||||
--url <http(s)://host[/path]> \
|
||||
(--auth-basic <USER> [--auth-password <PASS>] | --auth-bearer [TOKEN]) \
|
||||
[--allow-cleartext] [--dav-connections <1..8>] [--multiget-batch <N>] \
|
||||
<ARCHIVE>
|
||||
```
|
||||
|
||||
Same shape as `caldav`, but for address books and contacts.
|
||||
|
||||
#### WebDAV
|
||||
|
||||
```
|
||||
vandelay import webdav \
|
||||
--url <http(s)://host[/path]> \
|
||||
(--auth-basic <USER> [--auth-password <PASS>] | --auth-bearer [TOKEN]) \
|
||||
[--allow-cleartext] [--dav-connections <1..8>] [--multiget-batch <N>] \
|
||||
<ARCHIVE>
|
||||
```
|
||||
|
||||
Imports a plain WebDAV file collection as a JMAP `FileNode` tree.
|
||||
|
||||
#### ManageSieve
|
||||
|
||||
```
|
||||
vandelay import managesieve \
|
||||
--url sieve(s)://host[:port] \
|
||||
(--auth-basic <USER> [--auth-password <PASS>] | --auth-bearer [TOKEN] --auth-user <USER>) \
|
||||
[--allow-cleartext] \
|
||||
<ARCHIVE>
|
||||
```
|
||||
|
||||
Imports sieve scripts only. Each script is content-addressed in the blob table; the active script is recorded.
|
||||
|
||||
#### Maildir
|
||||
|
||||
```
|
||||
vandelay import maildir <MAILDIR> <ARCHIVE> \
|
||||
[--include <REGEX>...] [--exclude <REGEX>...] [--folder <NAME>...] \
|
||||
[--noautomap] [--include-deleted]
|
||||
```
|
||||
|
||||
Reads a local Maildir++ tree (a directory with `cur/`, `new/`, `tmp/`). No network. Folder selection mirrors the IMAP importer.
|
||||
|
||||
#### Google Takeout
|
||||
|
||||
```
|
||||
vandelay import takeout <PATH> <ARCHIVE> [--noautomap]
|
||||
```
|
||||
|
||||
Scans a directory tree recursively for `.mbox`, `.ics` and `.vcf` files and imports them. Tailored to Google Takeout layouts but works on any such tree; system-label role assignment can be disabled with `--noautomap`.
|
||||
|
||||
#### Microsoft Exchange (EWS)
|
||||
|
||||
```
|
||||
vandelay import exchange-ews \
|
||||
[--url <EWS-ENDPOINT>] [--mailbox <SMTP>] \
|
||||
[--mailbox-kind primary|archive|public-folders] \
|
||||
(--auth-basic <USER> [--auth-password <PASS>] \
|
||||
| --auth-bearer [TOKEN] [--ews-tenant <T> --ews-client-id <ID> \
|
||||
(--ews-device-code | --ews-client-secret <SECRET>)]) \
|
||||
[--ews-connections <1..8>] [--ews-getitem-batch <N>] [--ews-attachment-batch <N>] \
|
||||
[--ews-no-syncfolderitems] \
|
||||
<ARCHIVE>
|
||||
```
|
||||
|
||||
Imports a mailbox via EWS from an on-premises Exchange Server. Autodiscover is used when `--url` is omitted (a `--mailbox` SMTP address is then required). Supports Basic, pre-acquired bearer, interactive device-code OAuth, and app-only client-credentials OAuth.
|
||||
|
||||
For Exchange Online, use `exchange-graph` instead. Microsoft is retiring EWS in Exchange Online: from 1 October 2026 it is blocked unless a tenant administrator sets `EwsEnabled` to `True` and adds the client id to `EwsAllowedAppIDs`, and on 1 April 2027 it is switched off for every tenant. On-premises Exchange Server is not affected.
|
||||
|
||||
#### Microsoft Exchange (Graph)
|
||||
|
||||
```
|
||||
vandelay import exchange-graph \
|
||||
(--client-id <UUID> [--tenant <ID>] | --access-token [TOKEN]) \
|
||||
[--user <UPN|UUID>] \
|
||||
[--mailbox-kind primary|archive] \
|
||||
[--objects mail,calendar,contacts] \
|
||||
[--event-body-format text|html] \
|
||||
[--graph-connections <1..16>] [--top <1..1000>] \
|
||||
<ARCHIVE>
|
||||
```
|
||||
|
||||
Imports a mailbox from Exchange Online via Microsoft Graph. Without `--access-token`, the interactive device-code flow is used. `public-folders` is rejected here: Graph does not expose public folders, so they can only be imported with `exchange-ews`, which for Exchange Online is subject to the retirement described above.
|
||||
|
||||
### Export
|
||||
|
||||
```
|
||||
vandelay export \
|
||||
--url <URL> \
|
||||
(--auth-basic <USER> [--auth-password <PASS>] | --auth-bearer [TOKEN]) \
|
||||
(--account-id <ID> | --account-name <NAME>) \
|
||||
[--objects <list>] [--prune [--yes]] \
|
||||
<ARCHIVE>
|
||||
```
|
||||
|
||||
Stateless re-export of `ARCHIVE` into a target JMAP server account. The default behaviour is upsert-only: matched items are updated, unmatched local items are created, but pre-existing target items not covered by the archive are left alone.
|
||||
|
||||
`--prune` enables destructive reconciliation: target objects that do not match anything in the archive are deleted. The confirmation prompt can be skipped with `--yes` for automation. Export speaks JMAP only; no other target protocols are currently supported.
|
||||
|
||||
### Inspect
|
||||
|
||||
```
|
||||
vandelay inspect <ARCHIVE> [TYPE] [--limit <N>] [--offset <N>]
|
||||
```
|
||||
|
||||
Read-only dump of a local archive. This command never opens a network connection and never writes to the archive.
|
||||
|
||||
- Omit `TYPE` for a per-type summary (counts of every object kind plus blob storage stats).
|
||||
- Pass an object type to dump it: `mailbox`, `email`, `identity`, `sievescript`, `addressbook`, `contactcard`, `calendar`, `calendarevent`, `participantidentity`, `filenode`.
|
||||
- `mailbox` and `filenode` render as a tree (`--limit`/`--offset` are ignored); all other types use a paginated list and respect `--limit` and `--offset`.
|
||||
|
||||
## Testing
|
||||
|
||||
The default suite is hermetic (unit tests plus `mockito`-scripted JMAP/DAV/EWS/Graph behaviours) and needs no network or Docker:
|
||||
|
||||
```sh
|
||||
cargo build
|
||||
cargo clippy --all-targets
|
||||
cargo test
|
||||
```
|
||||
|
||||
### Live and integration tests (Docker required)
|
||||
|
||||
Live integration tests against a Stalwart server and container-based tests against third-party servers (Dovecot, Cyrus, Radicale, Baikal, Apache `mod_dav`) are gated behind `--ignored`. **They require a running Docker daemon**: each test binary boots its own throwaway container via `testcontainers` (images are pulled automatically on first run), so Docker must be installed and `docker info` must succeed before invoking them.
|
||||
|
||||
Run them per binary, and always with `--test-threads=1`:
|
||||
|
||||
```sh
|
||||
cargo test --test sync_jmap -- --ignored --test-threads=1 # live JMAP import/export/convergence/prune
|
||||
cargo test --test sync_imap -- --ignored --test-threads=1
|
||||
cargo test --test sync_managesieve -- --ignored --test-threads=1
|
||||
cargo test --test sync_maildir -- --ignored --test-threads=1
|
||||
cargo test --test sync_caldav -- --ignored --test-threads=1
|
||||
cargo test --test sync_carddav -- --ignored --test-threads=1
|
||||
cargo test --test sync_webdav -- --ignored --test-threads=1
|
||||
cargo test --test live_stalwart -- --ignored --test-threads=1
|
||||
cargo test --test seed_smoke -- --ignored --test-threads=1
|
||||
cargo test --test seed_only -- --ignored --test-threads=1
|
||||
|
||||
# Third-party-server tests (one container each):
|
||||
cargo test --test integration_radicale -- --ignored --test-threads=1
|
||||
cargo test --test integration_baikal -- --ignored --test-threads=1
|
||||
cargo test --test integration_webdav -- --ignored --test-threads=1
|
||||
cargo test --test integration_dovecot -- --ignored --test-threads=1
|
||||
cargo test --test integration_cyrus -- --ignored --test-threads=1
|
||||
|
||||
# Slow tests
|
||||
cargo test --test mock_jmap -- --ignored
|
||||
```
|
||||
|
||||
`--test-threads=1` is mandatory, not just advisory: within a binary every test shares a single per-binary container, and each test provisions then tears down the same disposable `vandelay.org` domain (and opens the archive with SQLite `EXCLUSIVE` locking). Separate binaries are isolated (each boots its own container on dynamic host ports), so plain `cargo test --test <name>` invocations are safe to run one after another.
|
||||
The tests in a binary share one container and one disposable domain, so a
|
||||
second thread would trip over the first. [docs/usage.md](docs/usage.md) lists
|
||||
every live test binary.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
Apache-2.0 OR MIT, at your option. The texts are in [LICENSES](LICENSES).
|
||||
|
||||
* Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
Copyright (C) 2020, Stalwart Labs LLC<br>
|
||||
Copyright (C) 2026, John Coffey
|
||||
|
||||
at your option.
|
||||
|
||||
## Copyright
|
||||
|
||||
Copyright (C) 2020, Stalwart Labs LLC
|
||||
Forked from Vandelay, originally developed by Stalwart Labs, and distributed
|
||||
under the same Apache-2.0 OR MIT terms.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -1,16 +0,0 @@
|
||||
[workspace]
|
||||
members = ["cargo:."]
|
||||
|
||||
[dist]
|
||||
cargo-dist-version = "0.31.0"
|
||||
ci = "github"
|
||||
installers = ["shell", "powershell", "npm", "homebrew", "msi"]
|
||||
unix-archive = ".tar.gz"
|
||||
tap = "stalwartlabs/homebrew-tap"
|
||||
npm-scope = "@stalwartlabs"
|
||||
targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-musl", "armv7-unknown-linux-gnueabihf", "armv7-unknown-linux-musleabihf", "arm-unknown-linux-gnueabihf", "arm-unknown-linux-musleabihf", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"]
|
||||
install-path = "CARGO_HOME"
|
||||
publish-jobs = ["homebrew", "npm"]
|
||||
install-updater = false
|
||||
github-attestations = true
|
||||
allow-dirty = ["ci", "msi"]
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
-->
|
||||
|
||||
# Using inbuxa-migrate
|
||||
|
||||
The full command reference. The [README](../README.md) says what the tool is
|
||||
and how to install it.
|
||||
|
||||
## Commands and global flags
|
||||
|
||||
```
|
||||
inbuxa-migrate <import|export|inspect> [args...]
|
||||
```
|
||||
|
||||
Every command takes a few global flags -- verbosity, worker pool size, retry
|
||||
policy, TLS handling -- besides its own. The ones that matter most:
|
||||
|
||||
| Flag | Purpose |
|
||||
| --- | --- |
|
||||
| `-j, --threads <N>` | Worker pool size (default: logical CPUs). |
|
||||
| `--dry-run` | Compute the full plan; perform no writes. |
|
||||
| `-v`, `-vv`, `-vvv` | Increase log verbosity. |
|
||||
| `-q, --quiet` | Warnings and errors only. |
|
||||
| `--max-retries <N>` | Max retries per request on transient failures (default 5). |
|
||||
| `--allow-invalid-certs` | Accept self-signed / invalid TLS certs. |
|
||||
|
||||
Secrets come from the `INBUXA_MIGRATE_*` environment variables or a prompt;
|
||||
see [Credentials](../README.md#credentials). The command line takes them too,
|
||||
but should not be given them.
|
||||
|
||||
## Import
|
||||
|
||||
```
|
||||
inbuxa-migrate import <source> [source-args...] <ARCHIVE>
|
||||
```
|
||||
|
||||
Reads a source account into the SQLite `ARCHIVE`, creating it if it is absent.
|
||||
An archive remembers which account filled it; every importer takes
|
||||
`--allow-source-change` to fill it from a different one anyway.
|
||||
|
||||
### JMAP
|
||||
|
||||
```
|
||||
inbuxa-migrate import jmap \
|
||||
--url <URL> \
|
||||
(--auth-basic <USER> [--auth-password <PASS>] | --auth-bearer [TOKEN]) \
|
||||
(--account-id <ID> | --account-name <NAME>) \
|
||||
[--objects <list>] \
|
||||
<ARCHIVE>
|
||||
```
|
||||
|
||||
Imports one JMAP account, from inbuxa, Stalwart or any other JMAP server. `--objects` accepts a comma-separated list of object tokens (`mailbox,email,calendar,calendarevent,addressbook,contactcard,identity,sievescript,participantidentity,filenode`). The default is everything the server advertises.
|
||||
|
||||
### IMAP
|
||||
|
||||
```
|
||||
inbuxa-migrate import imap \
|
||||
--url imap(s)://host[:port] \
|
||||
(--auth-basic <USER> [--auth-password <PASS>] | --auth-bearer [TOKEN] --auth-user <USER>) \
|
||||
[--include <REGEX>...] [--exclude <REGEX>...] [--exclude-special <ROLE>...] \
|
||||
[--folder <NAME>...] [--subscribed-only] [--noautomap] \
|
||||
[--include-deleted] [--allow-cleartext] [--compress] \
|
||||
[--fetch-batch <N>] [--imap-connections <1..8>] \
|
||||
<ARCHIVE>
|
||||
```
|
||||
|
||||
Imports mail, and only mail, from any IMAP server. Folders are chosen with
|
||||
`--include` and `--exclude` patterns, or by exact name with `--folder`, but
|
||||
not both. `--exclude-special` drops folders by SPECIAL-USE role.
|
||||
|
||||
### CalDAV
|
||||
|
||||
```
|
||||
inbuxa-migrate import caldav \
|
||||
--url <http(s)://host[/path]> \
|
||||
(--auth-basic <USER> [--auth-password <PASS>] | --auth-bearer [TOKEN]) \
|
||||
[--allow-cleartext] [--dav-connections <1..8>] [--multiget-batch <N>] \
|
||||
<ARCHIVE>
|
||||
```
|
||||
|
||||
Discovers the user's CalDAV principal (or accepts a URL pointing straight at a calendar-home or calendar), then imports calendars and events.
|
||||
|
||||
### CardDAV
|
||||
|
||||
```
|
||||
inbuxa-migrate import carddav \
|
||||
--url <http(s)://host[/path]> \
|
||||
(--auth-basic <USER> [--auth-password <PASS>] | --auth-bearer [TOKEN]) \
|
||||
[--allow-cleartext] [--dav-connections <1..8>] [--multiget-batch <N>] \
|
||||
<ARCHIVE>
|
||||
```
|
||||
|
||||
Same shape as `caldav`, but for address books and contacts.
|
||||
|
||||
### WebDAV
|
||||
|
||||
```
|
||||
inbuxa-migrate import webdav \
|
||||
--url <http(s)://host[/path]> \
|
||||
(--auth-basic <USER> [--auth-password <PASS>] | --auth-bearer [TOKEN]) \
|
||||
[--allow-cleartext] [--dav-connections <1..8>] [--multiget-batch <N>] \
|
||||
<ARCHIVE>
|
||||
```
|
||||
|
||||
Imports a plain WebDAV file collection as a JMAP `FileNode` tree.
|
||||
|
||||
### ManageSieve
|
||||
|
||||
```
|
||||
inbuxa-migrate import managesieve \
|
||||
--url sieve(s)://host[:port] \
|
||||
(--auth-basic <USER> [--auth-password <PASS>] | --auth-bearer [TOKEN] --auth-user <USER>) \
|
||||
[--allow-cleartext] \
|
||||
<ARCHIVE>
|
||||
```
|
||||
|
||||
Imports Sieve scripts only. Each script is stored once by content, and the
|
||||
active one is recorded.
|
||||
|
||||
### Maildir
|
||||
|
||||
```
|
||||
inbuxa-migrate import maildir <MAILDIR> <ARCHIVE> \
|
||||
[--include <REGEX>...] [--exclude <REGEX>...] [--folder <NAME>...] \
|
||||
[--noautomap] [--include-deleted]
|
||||
```
|
||||
|
||||
Reads a local Maildir++ tree: a directory with `cur/`, `new/` and `tmp/`. No
|
||||
network. Folders are chosen as for IMAP.
|
||||
|
||||
### Google Takeout
|
||||
|
||||
```
|
||||
inbuxa-migrate import takeout <PATH> <ARCHIVE> [--noautomap]
|
||||
```
|
||||
|
||||
Finds every `.mbox`, `.ics` and `.vcf` file under a directory and imports it.
|
||||
It is shaped for Google Takeout but reads any such tree. `--noautomap` stops
|
||||
it giving Gmail's system labels their mailbox roles.
|
||||
|
||||
### Microsoft Exchange (EWS)
|
||||
|
||||
```
|
||||
inbuxa-migrate import exchange-ews \
|
||||
[--url <EWS-ENDPOINT>] [--mailbox <SMTP>] \
|
||||
[--mailbox-kind primary|archive|public-folders] \
|
||||
(--auth-basic <USER> [--auth-password <PASS>] \
|
||||
| --auth-bearer [TOKEN] [--ews-tenant <T> --ews-client-id <ID> \
|
||||
(--ews-device-code | --ews-client-secret <SECRET>)]) \
|
||||
[--ews-connections <1..8>] [--ews-getitem-batch <N>] [--ews-attachment-batch <N>] \
|
||||
[--ews-no-syncfolderitems] \
|
||||
<ARCHIVE>
|
||||
```
|
||||
|
||||
Imports a mailbox from an on-premises Exchange Server through EWS. Without
|
||||
`--url` it uses Autodiscover, and then needs `--mailbox`. It signs in with
|
||||
Basic, with a bearer token acquired beforehand, with OAuth's interactive
|
||||
device-code flow, or with app-only client credentials.
|
||||
|
||||
For Exchange Online, use `exchange-graph` instead. Microsoft is retiring EWS in Exchange Online: from October 1, 2026 it is blocked unless a tenant administrator sets `EwsEnabled` to `True` and adds the client id to `EwsAllowedAppIDs`, and on April 1, 2027 it is switched off for every tenant. On-premises Exchange Server is not affected.
|
||||
|
||||
### Microsoft Exchange (Graph)
|
||||
|
||||
```
|
||||
inbuxa-migrate import exchange-graph \
|
||||
(--client-id <UUID> [--tenant <ID>] | --access-token [TOKEN]) \
|
||||
[--user <UPN|UUID>] \
|
||||
[--mailbox-kind primary|archive] \
|
||||
[--objects mail,calendar,contacts] \
|
||||
[--event-body-format text|html] \
|
||||
[--graph-connections <1..16>] [--top <1..1000>] \
|
||||
<ARCHIVE>
|
||||
```
|
||||
|
||||
Imports a mailbox from Exchange Online through Microsoft Graph. Without
|
||||
`--access-token` it signs in with the interactive device-code flow. `public-folders` is rejected here: Graph does not expose public folders, so they can only be imported with `exchange-ews`, which for Exchange Online is subject to the retirement described above.
|
||||
|
||||
## Export
|
||||
|
||||
```
|
||||
inbuxa-migrate export \
|
||||
--url <URL> \
|
||||
(--auth-basic <USER> [--auth-password <PASS>] | --auth-bearer [TOKEN]) \
|
||||
(--account-id <ID> | --account-name <NAME>) \
|
||||
[--objects <list>] [--prune [--yes]] \
|
||||
<ARCHIVE>
|
||||
```
|
||||
|
||||
Writes `ARCHIVE` into an account on a JMAP server, usually inbuxa. It keeps no
|
||||
state of its own: every run matches the archive against the target afresh.
|
||||
By default it only adds and updates -- items that match are updated, the
|
||||
rest are created, and anything already on the target that the archive does
|
||||
not cover is left alone.
|
||||
|
||||
`--prune` also deletes what is on the target and not in the archive. It asks
|
||||
first; `--yes` answers for it, for scripts. Export speaks JMAP only.
|
||||
|
||||
## Inspect
|
||||
|
||||
```
|
||||
inbuxa-migrate inspect <ARCHIVE> [TYPE] [--limit <N>] [--offset <N>]
|
||||
```
|
||||
|
||||
Read-only dump of a local archive. This command never opens a network connection and never writes to the archive.
|
||||
|
||||
- Omit `TYPE` for a per-type summary (counts of every object kind plus blob storage stats).
|
||||
- Pass an object type to dump it: `mailbox`, `email`, `identity`, `sievescript`, `addressbook`, `contactcard`, `calendar`, `calendarevent`, `participantidentity`, `filenode`.
|
||||
- `mailbox` and `filenode` render as a tree (`--limit`/`--offset` are ignored); all other types use a paginated list and respect `--limit` and `--offset`.
|
||||
|
||||
|
||||
## Live tests
|
||||
|
||||
The tests against real servers -- a Stalwart server as a JMAP source, and
|
||||
Dovecot, Cyrus, Radicale, Baikal and Apache `mod_dav` -- are marked
|
||||
`#[ignore]`. Each test binary boots its own throwaway container through
|
||||
`testcontainers`, pulling the image on first use, so Docker must be running
|
||||
(`docker info` succeeds) before they start.
|
||||
|
||||
Run them one binary at a time, always with one test thread:
|
||||
|
||||
```sh
|
||||
cargo test --test sync_jmap -- --ignored --test-threads=1 # live JMAP import/export/convergence/prune
|
||||
cargo test --test sync_imap -- --ignored --test-threads=1
|
||||
cargo test --test sync_managesieve -- --ignored --test-threads=1
|
||||
cargo test --test sync_maildir -- --ignored --test-threads=1
|
||||
cargo test --test sync_caldav -- --ignored --test-threads=1
|
||||
cargo test --test sync_carddav -- --ignored --test-threads=1
|
||||
cargo test --test sync_webdav -- --ignored --test-threads=1
|
||||
cargo test --test live_stalwart -- --ignored --test-threads=1
|
||||
cargo test --test seed_smoke -- --ignored --test-threads=1
|
||||
cargo test --test seed_only -- --ignored --test-threads=1
|
||||
|
||||
# Third-party-server tests (one container each):
|
||||
cargo test --test integration_radicale -- --ignored --test-threads=1
|
||||
cargo test --test integration_baikal -- --ignored --test-threads=1
|
||||
cargo test --test integration_webdav -- --ignored --test-threads=1
|
||||
cargo test --test integration_dovecot -- --ignored --test-threads=1
|
||||
cargo test --test integration_cyrus -- --ignored --test-threads=1
|
||||
|
||||
# Slow tests
|
||||
cargo test --test mock_jmap -- --ignored
|
||||
```
|
||||
|
||||
One thread is required, not advised. The tests in a binary share its
|
||||
container, and each one creates and removes the same disposable domain,
|
||||
`inbuxa-migrate.org`, and opens its archive with SQLite's `EXCLUSIVE` lock.
|
||||
Separate binaries each boot their own container on their own ports, so
|
||||
running them one after another is safe.
|
||||
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
# SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
#
|
||||
# Build one release archive for the machine this runs on.
|
||||
#
|
||||
# Usage: scripts/build-release.sh VERSION OUTDIR
|
||||
# scripts/build-release.sh v2026.9.30 dist
|
||||
#
|
||||
# CI runs exactly this on one native runner per architecture (amd64 and
|
||||
# arm64) and joins the results with SHA256SUMS, so a release can be checked
|
||||
# before tagging on any Linux machine with cargo. The archive name carries no
|
||||
# version, so .../releases/latest/download/<name> always means the newest.
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:?usage: $0 VERSION OUTDIR}"
|
||||
OUT="${2:?usage: $0 VERSION OUTDIR}"
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
case "$(uname -m)" in
|
||||
x86_64) arch=amd64 ;;
|
||||
aarch64 | arm64) arch=arm64 ;;
|
||||
*) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
# The tag names the version the binary reports, so the two cannot disagree.
|
||||
want="v$(cd "$ROOT" && cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')"
|
||||
[ "$VERSION" = "$want" ] || { echo "tag $VERSION does not match Cargo.toml ($want)" >&2; exit 1; }
|
||||
|
||||
name="inbuxa-migrate-linux-$arch"
|
||||
mkdir -p "$OUT"
|
||||
OUT="$(cd "$OUT" && pwd)"
|
||||
STAGE="$(mktemp -d)"
|
||||
trap 'rm -rf "$STAGE"' EXIT
|
||||
|
||||
echo "==> building $name ($VERSION)"
|
||||
(cd "$ROOT" && cargo build --release --locked)
|
||||
mkdir -p "$STAGE/$name"
|
||||
cp "$ROOT/target/release/inbuxa-migrate" "$ROOT/README.md" "$STAGE/$name/"
|
||||
cp -r "$ROOT/LICENSES" "$STAGE/$name/"
|
||||
# Fixed owner, order and time, so the archive's layout and metadata do not
|
||||
# change from one build of a tag to the next.
|
||||
tar --sort=name --owner=0 --group=0 --numeric-owner --mtime="@${SOURCE_DATE_EPOCH:-0}" \
|
||||
-C "$STAGE/$name" -czf "$OUT/$name.tar.gz" inbuxa-migrate LICENSES README.md
|
||||
echo "==> $OUT/$name.tar.gz"
|
||||
@@ -1,4 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
# SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
#
|
||||
|
||||
import argparse
|
||||
import json
|
||||
@@ -136,7 +142,7 @@ def export_mail(conn, root):
|
||||
if directory is None:
|
||||
continue
|
||||
sequence += 1
|
||||
name = f"{stamp}.M{email_id}P{sequence}.vandelay:2,{info}"
|
||||
name = f"{stamp}.M{email_id}P{sequence}.inbuxa-migrate:2,{info}"
|
||||
with open(os.path.join(directory, "cur", name), "wb") as handle:
|
||||
handle.write(data)
|
||||
written += 1
|
||||
@@ -320,7 +326,7 @@ def export_files(conn, root):
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Export a Vandelay SQLite archive to a directory tree."
|
||||
description="Export an inbuxa-migrate SQLite archive to a directory tree."
|
||||
)
|
||||
parser.add_argument("archive")
|
||||
parser.add_argument("target")
|
||||
|
||||
+43
-38
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -30,9 +31,9 @@ use crate::types::{ObjectType, parse_object_list};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "vandelay",
|
||||
name = "inbuxa-migrate",
|
||||
version,
|
||||
about = "Vandelay: the JMAP importer-exporter"
|
||||
about = "inbuxa-migrate: account migration and backup for inbuxa"
|
||||
)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
@@ -138,7 +139,7 @@ struct JmapImportArgs {
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PASS",
|
||||
help = "Basic password (prefer $VANDELAY_PASSWORD or prompt)"
|
||||
help = "Basic password (prefer $INBUXA_MIGRATE_PASSWORD or prompt)"
|
||||
)]
|
||||
auth_password: Option<String>,
|
||||
|
||||
@@ -146,7 +147,7 @@ struct JmapImportArgs {
|
||||
long,
|
||||
value_name = "TOKEN",
|
||||
num_args = 0..=1,
|
||||
help = "Bearer token (value optional: falls back to $VANDELAY_TOKEN or prompt)"
|
||||
help = "Bearer token (value optional: falls back to $INBUXA_MIGRATE_TOKEN or prompt)"
|
||||
)]
|
||||
auth_bearer: Option<Option<String>>,
|
||||
|
||||
@@ -199,7 +200,7 @@ struct ImapImportArgs {
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PASS",
|
||||
help = "Basic password (prefer $VANDELAY_PASSWORD or prompt)"
|
||||
help = "Basic password (prefer $INBUXA_MIGRATE_PASSWORD or prompt)"
|
||||
)]
|
||||
auth_password: Option<String>,
|
||||
|
||||
@@ -207,7 +208,7 @@ struct ImapImportArgs {
|
||||
long,
|
||||
value_name = "TOKEN",
|
||||
num_args = 0..=1,
|
||||
help = "Bearer token for XOAUTH2 (value optional: falls back to $VANDELAY_TOKEN or prompt)"
|
||||
help = "Bearer token for XOAUTH2 (value optional: falls back to $INBUXA_MIGRATE_TOKEN or prompt)"
|
||||
)]
|
||||
auth_bearer: Option<Option<String>>,
|
||||
|
||||
@@ -309,7 +310,7 @@ struct ExportArgs {
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PASS",
|
||||
help = "Basic password (prefer $VANDELAY_PASSWORD or prompt)"
|
||||
help = "Basic password (prefer $INBUXA_MIGRATE_PASSWORD or prompt)"
|
||||
)]
|
||||
auth_password: Option<String>,
|
||||
|
||||
@@ -317,7 +318,7 @@ struct ExportArgs {
|
||||
long,
|
||||
value_name = "TOKEN",
|
||||
num_args = 0..=1,
|
||||
help = "Bearer token (value optional: falls back to $VANDELAY_TOKEN or prompt)"
|
||||
help = "Bearer token (value optional: falls back to $INBUXA_MIGRATE_TOKEN or prompt)"
|
||||
)]
|
||||
auth_bearer: Option<Option<String>>,
|
||||
|
||||
@@ -407,7 +408,7 @@ pub struct ManageSieveImportArgs {
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PASS",
|
||||
help = "Basic password (prefer $VANDELAY_PASSWORD or prompt)"
|
||||
help = "Basic password (prefer $INBUXA_MIGRATE_PASSWORD or prompt)"
|
||||
)]
|
||||
auth_password: Option<String>,
|
||||
|
||||
@@ -415,7 +416,7 @@ pub struct ManageSieveImportArgs {
|
||||
long,
|
||||
value_name = "TOKEN",
|
||||
num_args = 0..=1,
|
||||
help = "Bearer token for OAUTHBEARER (value optional: $VANDELAY_TOKEN or prompt)"
|
||||
help = "Bearer token for OAUTHBEARER (value optional: $INBUXA_MIGRATE_TOKEN or prompt)"
|
||||
)]
|
||||
auth_bearer: Option<Option<String>>,
|
||||
|
||||
@@ -457,7 +458,7 @@ pub struct DavImportArgs {
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PASS",
|
||||
help = "Basic password (prefer $VANDELAY_PASSWORD or prompt)"
|
||||
help = "Basic password (prefer $INBUXA_MIGRATE_PASSWORD or prompt)"
|
||||
)]
|
||||
auth_password: Option<String>,
|
||||
|
||||
@@ -465,7 +466,7 @@ pub struct DavImportArgs {
|
||||
long,
|
||||
value_name = "TOKEN",
|
||||
num_args = 0..=1,
|
||||
help = "Bearer token (value optional: $VANDELAY_TOKEN or prompt)"
|
||||
help = "Bearer token (value optional: $INBUXA_MIGRATE_TOKEN or prompt)"
|
||||
)]
|
||||
auth_bearer: Option<Option<String>>,
|
||||
|
||||
@@ -560,7 +561,7 @@ fn resolve_managesieve_auth(
|
||||
"--auth-user is only valid with --auth-bearer".to_owned(),
|
||||
));
|
||||
}
|
||||
let password = secret::resolve(auth_password, "VANDELAY_PASSWORD", "password")?;
|
||||
let password = secret::resolve(auth_password, "INBUXA_MIGRATE_PASSWORD", "password")?;
|
||||
return Ok(ManageSieveAuth::Basic {
|
||||
user: user.to_owned(),
|
||||
password,
|
||||
@@ -577,7 +578,7 @@ fn resolve_managesieve_auth(
|
||||
let user = auth_user
|
||||
.ok_or_else(|| Error::Usage("--auth-user is required with --auth-bearer".to_owned()))?
|
||||
.to_owned();
|
||||
let token = secret::resolve(bearer.as_deref(), "VANDELAY_TOKEN", "bearer token")?;
|
||||
let token = secret::resolve(bearer.as_deref(), "INBUXA_MIGRATE_TOKEN", "bearer token")?;
|
||||
Ok(ManageSieveAuth::Bearer { user, token })
|
||||
}
|
||||
|
||||
@@ -726,7 +727,7 @@ fn resolve_dav_auth(
|
||||
auth_bearer: Option<&Option<String>>,
|
||||
) -> Result<DavAuth, Error> {
|
||||
if let Some(user) = auth_basic {
|
||||
let password = secret::resolve(auth_password, "VANDELAY_PASSWORD", "password")?;
|
||||
let password = secret::resolve(auth_password, "INBUXA_MIGRATE_PASSWORD", "password")?;
|
||||
return Ok(DavAuth::Basic {
|
||||
user: user.to_owned(),
|
||||
password,
|
||||
@@ -740,7 +741,7 @@ fn resolve_dav_auth(
|
||||
let bearer = auth_bearer.ok_or_else(|| {
|
||||
Error::Usage("exactly one of --auth-basic / --auth-bearer is required".to_owned())
|
||||
})?;
|
||||
let token = secret::resolve(bearer.as_deref(), "VANDELAY_TOKEN", "bearer token")?;
|
||||
let token = secret::resolve(bearer.as_deref(), "INBUXA_MIGRATE_TOKEN", "bearer token")?;
|
||||
Ok(DavAuth::Bearer { token })
|
||||
}
|
||||
|
||||
@@ -828,7 +829,7 @@ fn resolve_imap_auth(
|
||||
"--auth-user is only valid with --auth-bearer".to_owned(),
|
||||
));
|
||||
}
|
||||
let password = secret::resolve(auth_password, "VANDELAY_PASSWORD", "password")?;
|
||||
let password = secret::resolve(auth_password, "INBUXA_MIGRATE_PASSWORD", "password")?;
|
||||
return Ok(ImapAuth::Basic {
|
||||
user: user.to_owned(),
|
||||
password,
|
||||
@@ -845,7 +846,7 @@ fn resolve_imap_auth(
|
||||
let user = auth_user
|
||||
.ok_or_else(|| Error::Usage("--auth-user is required with --auth-bearer".to_owned()))?
|
||||
.to_owned();
|
||||
let token = secret::resolve(bearer.as_deref(), "VANDELAY_TOKEN", "bearer token")?;
|
||||
let token = secret::resolve(bearer.as_deref(), "INBUXA_MIGRATE_TOKEN", "bearer token")?;
|
||||
Ok(ImapAuth::Bearer { user, token })
|
||||
}
|
||||
|
||||
@@ -921,7 +922,7 @@ pub struct ExchangeEwsImportArgs {
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PASS",
|
||||
help = "Basic password (prefer $VANDELAY_PASSWORD or prompt)"
|
||||
help = "Basic password (prefer $INBUXA_MIGRATE_PASSWORD or prompt)"
|
||||
)]
|
||||
auth_password: Option<String>,
|
||||
|
||||
@@ -929,7 +930,7 @@ pub struct ExchangeEwsImportArgs {
|
||||
long,
|
||||
value_name = "TOKEN",
|
||||
num_args = 0..=1,
|
||||
help = "Bearer token (value optional: $VANDELAY_TOKEN or device-code flow)"
|
||||
help = "Bearer token (value optional: $INBUXA_MIGRATE_TOKEN or device-code flow)"
|
||||
)]
|
||||
auth_bearer: Option<Option<String>>,
|
||||
|
||||
@@ -946,7 +947,7 @@ pub struct ExchangeEwsImportArgs {
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "SECRET",
|
||||
help = "OAuth client secret (app-only flow; prefer $VANDELAY_EWS_CLIENT_SECRET)"
|
||||
help = "OAuth client secret (app-only flow; prefer $INBUXA_MIGRATE_EWS_CLIENT_SECRET)"
|
||||
)]
|
||||
ews_client_secret: Option<String>,
|
||||
|
||||
@@ -1029,7 +1030,7 @@ fn resolve_exchange_ews_auth(args: &ExchangeEwsImportArgs) -> Result<EwsAuth, Er
|
||||
if let Some(user) = args.auth_basic.as_deref() {
|
||||
let password = secret::resolve(
|
||||
args.auth_password.as_deref(),
|
||||
"VANDELAY_PASSWORD",
|
||||
"INBUXA_MIGRATE_PASSWORD",
|
||||
"password",
|
||||
)?;
|
||||
return Ok(EwsAuth::Basic {
|
||||
@@ -1066,7 +1067,7 @@ fn resolve_exchange_ews_auth(args: &ExchangeEwsImportArgs) -> Result<EwsAuth, Er
|
||||
if let Some(secret) = args.ews_client_secret.clone() {
|
||||
let resolved = secret::resolve(
|
||||
Some(secret.as_str()),
|
||||
"VANDELAY_EWS_CLIENT_SECRET",
|
||||
"INBUXA_MIGRATE_EWS_CLIENT_SECRET",
|
||||
"client secret",
|
||||
)?;
|
||||
return Ok(EwsAuth::OAuth(OAuthFlow::ClientCredentials {
|
||||
@@ -1075,7 +1076,7 @@ fn resolve_exchange_ews_auth(args: &ExchangeEwsImportArgs) -> Result<EwsAuth, Er
|
||||
client_secret: resolved,
|
||||
}));
|
||||
}
|
||||
if let Ok(env_secret) = std::env::var("VANDELAY_EWS_CLIENT_SECRET")
|
||||
if let Ok(env_secret) = std::env::var("INBUXA_MIGRATE_EWS_CLIENT_SECRET")
|
||||
&& !env_secret.is_empty()
|
||||
{
|
||||
return Ok(EwsAuth::OAuth(OAuthFlow::ClientCredentials {
|
||||
@@ -1084,7 +1085,7 @@ fn resolve_exchange_ews_auth(args: &ExchangeEwsImportArgs) -> Result<EwsAuth, Er
|
||||
client_secret: env_secret,
|
||||
}));
|
||||
}
|
||||
let token = secret::resolve(None, "VANDELAY_TOKEN", "bearer token")?;
|
||||
let token = secret::resolve(None, "INBUXA_MIGRATE_TOKEN", "bearer token")?;
|
||||
Ok(EwsAuth::Bearer { token })
|
||||
}
|
||||
|
||||
@@ -1125,7 +1126,7 @@ pub struct ExchangeGraphImportArgs {
|
||||
long,
|
||||
value_name = "TOKEN",
|
||||
num_args = 0..=1,
|
||||
help = "Pre-acquired bearer token (skip device-code flow). Resolves: flag value -> $VANDELAY_GRAPH_TOKEN -> prompt."
|
||||
help = "Pre-acquired bearer token (skip device-code flow). Resolves: flag value -> $INBUXA_MIGRATE_GRAPH_TOKEN -> prompt."
|
||||
)]
|
||||
access_token: Option<Option<String>>,
|
||||
|
||||
@@ -1168,7 +1169,7 @@ pub struct ExchangeGraphImportArgs {
|
||||
help = "Years either side of today to scan for edited recurrence occurrences (max 5)",
|
||||
long_help = "Years either side of today to scan for edited recurrence occurrences.\n \
|
||||
Graph reports a series exception only inside an expanded calendar view, and caps\n \
|
||||
any single view at five years, so vandelay scans [today - N, today] and\n \
|
||||
any single view at five years, so inbuxa-migrate scans [today - N, today] and\n \
|
||||
[today, today + N]. An occurrence edited outside that span imports with the\n \
|
||||
series default instead of its edit."
|
||||
)]
|
||||
@@ -1177,7 +1178,7 @@ pub struct ExchangeGraphImportArgs {
|
||||
#[arg(
|
||||
long,
|
||||
help = "Skip contact photos (saves one request per contact on large address books)",
|
||||
long_help = "Skip contact photos.\n Graph will not say which contacts have a photo: $expand=photo comes back empty\n even when one exists, so the only way to find out is to ask for the bytes.\n vandelay therefore spends one extra request per contact. Pass this to skip\n photos entirely on a large address book."
|
||||
long_help = "Skip contact photos.\n Graph will not say which contacts have a photo: $expand=photo comes back empty\n even when one exists, so the only way to find out is to ask for the bytes.\n inbuxa-migrate therefore spends one extra request per contact. Pass this to skip\n photos entirely on a large address book."
|
||||
)]
|
||||
skip_contact_photos: bool,
|
||||
|
||||
@@ -1265,7 +1266,11 @@ fn resolve_graph_auth(
|
||||
tenant: &str,
|
||||
) -> Result<GraphAuth, Error> {
|
||||
if let Some(slot) = access_token {
|
||||
let token = secret::resolve(slot.as_deref(), "VANDELAY_GRAPH_TOKEN", "bearer token")?;
|
||||
let token = secret::resolve(
|
||||
slot.as_deref(),
|
||||
"INBUXA_MIGRATE_GRAPH_TOKEN",
|
||||
"bearer token",
|
||||
)?;
|
||||
return Ok(GraphAuth::PreAcquired { token });
|
||||
}
|
||||
let client_id = client_id
|
||||
@@ -1305,7 +1310,7 @@ fn resolve_auth(
|
||||
auth_bearer: Option<&Option<String>>,
|
||||
) -> Result<Auth, Error> {
|
||||
if let Some(user) = auth_basic {
|
||||
let password = secret::resolve(auth_password, "VANDELAY_PASSWORD", "password")?;
|
||||
let password = secret::resolve(auth_password, "INBUXA_MIGRATE_PASSWORD", "password")?;
|
||||
return Ok(Auth::Basic {
|
||||
user: user.to_owned(),
|
||||
password,
|
||||
@@ -1319,7 +1324,7 @@ fn resolve_auth(
|
||||
let bearer = auth_bearer.ok_or_else(|| {
|
||||
Error::Usage("exactly one of --auth-basic / --auth-bearer is required".to_owned())
|
||||
})?;
|
||||
let token = secret::resolve(bearer.as_deref(), "VANDELAY_TOKEN", "bearer token")?;
|
||||
let token = secret::resolve(bearer.as_deref(), "INBUXA_MIGRATE_TOKEN", "bearer token")?;
|
||||
Ok(Auth::Bearer { token })
|
||||
}
|
||||
|
||||
@@ -1350,8 +1355,8 @@ mod tests {
|
||||
fn auth_password_without_basic_is_usage_error() {
|
||||
let _g = lock();
|
||||
unsafe {
|
||||
env::remove_var("VANDELAY_TOKEN");
|
||||
env::remove_var("VANDELAY_PASSWORD");
|
||||
env::remove_var("INBUXA_MIGRATE_TOKEN");
|
||||
env::remove_var("INBUXA_MIGRATE_PASSWORD");
|
||||
}
|
||||
let bearer: Option<String> = Some("t".to_owned());
|
||||
let r = resolve_auth(None, Some("hunter2"), Some(&bearer));
|
||||
@@ -1367,8 +1372,8 @@ mod tests {
|
||||
fn auth_basic_with_password_resolves() {
|
||||
let _g = lock();
|
||||
unsafe {
|
||||
env::remove_var("VANDELAY_TOKEN");
|
||||
env::remove_var("VANDELAY_PASSWORD");
|
||||
env::remove_var("INBUXA_MIGRATE_TOKEN");
|
||||
env::remove_var("INBUXA_MIGRATE_PASSWORD");
|
||||
}
|
||||
let auth = resolve_auth(Some("alice"), Some("pw"), None).unwrap();
|
||||
match auth {
|
||||
@@ -1384,8 +1389,8 @@ mod tests {
|
||||
fn auth_bearer_inline_token_resolves() {
|
||||
let _g = lock();
|
||||
unsafe {
|
||||
env::remove_var("VANDELAY_TOKEN");
|
||||
env::remove_var("VANDELAY_PASSWORD");
|
||||
env::remove_var("INBUXA_MIGRATE_TOKEN");
|
||||
env::remove_var("INBUXA_MIGRATE_PASSWORD");
|
||||
}
|
||||
let bearer: Option<String> = Some("t".to_owned());
|
||||
let auth = resolve_auth(None, None, Some(&bearer)).unwrap();
|
||||
@@ -1394,7 +1399,7 @@ mod tests {
|
||||
|
||||
fn graph_config(objects: &[&str]) -> Result<GraphImportConfig, Error> {
|
||||
let mut argv = vec![
|
||||
"vandelay",
|
||||
"inbuxa-migrate",
|
||||
"import",
|
||||
"exchange-graph",
|
||||
"--access-token",
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -87,7 +88,7 @@ impl DavClient {
|
||||
log_level: AtomicU8::new(LEVEL_DEFAULT),
|
||||
retries_total: AtomicU64::new(0),
|
||||
retry_after_sleeps: AtomicU64::new(0),
|
||||
user_agent: format!("vandelay/{}", env!("CARGO_PKG_VERSION")),
|
||||
user_agent: format!("inbuxa-migrate/{}", env!("CARGO_PKG_VERSION")),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -36,7 +37,7 @@ const CALENDAR_ADDRESS_DEPENDENTS: &[&str] = &[
|
||||
|
||||
pub fn synthetic_attendee_address(identifier: &str) -> String {
|
||||
format!(
|
||||
"urn:x-vandelay:attendee:{}",
|
||||
"urn:x-inbuxa-migrate:attendee:{}",
|
||||
blake3::hash(identifier.as_bytes()).to_hex()
|
||||
)
|
||||
}
|
||||
@@ -146,7 +147,7 @@ mod tests {
|
||||
let event = json!({
|
||||
"@type": "Event",
|
||||
"uid": "uid-1",
|
||||
"prodId": "vandelay",
|
||||
"prodId": "inbuxa-migrate",
|
||||
"privacy": "public",
|
||||
"organizerCalendarAddress": "mailto:[email protected]",
|
||||
"recurrenceRule": {"frequency": "daily"},
|
||||
@@ -173,7 +174,7 @@ mod tests {
|
||||
let addr = synthetic_attendee_address("Jane Doe");
|
||||
assert_eq!(addr, synthetic_attendee_address("Jane Doe"));
|
||||
assert_ne!(addr, synthetic_attendee_address("John Doe"));
|
||||
assert!(addr.starts_with("urn:x-vandelay:attendee:"));
|
||||
assert!(addr.starts_with("urn:x-inbuxa-migrate:attendee:"));
|
||||
assert!(
|
||||
!addr.starts_with("mailto:"),
|
||||
"export must never invite a fabricated address"
|
||||
@@ -186,7 +187,7 @@ mod tests {
|
||||
"1": {
|
||||
"@type": "Participant",
|
||||
"name": "Room 4",
|
||||
"calendarAddress": "urn:x-vandelay:attendee:abc",
|
||||
"calendarAddress": "urn:x-inbuxa-migrate:attendee:abc",
|
||||
"email": "[email protected]",
|
||||
"roles": {"required": true},
|
||||
"kind": "resource",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -53,7 +54,7 @@ fn build_event_map(raw: &CalendarItemRaw, iana: Option<&str>) -> Map<String, Val
|
||||
event.insert(
|
||||
"uid".to_owned(),
|
||||
Value::String(format!(
|
||||
"vandelay-ews-event-{}",
|
||||
"inbuxa-migrate-ews-event-{}",
|
||||
blake3::hash(raw.id.id.as_bytes()).to_hex()
|
||||
)),
|
||||
);
|
||||
@@ -723,7 +724,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let addr = p["calendarAddress"].as_str().unwrap();
|
||||
assert!(
|
||||
addr.starts_with("urn:x-vandelay:attendee:"),
|
||||
addr.starts_with("urn:x-inbuxa-migrate:attendee:"),
|
||||
"a name-only attendee gets a stable synthetic calendarAddress, got {addr}"
|
||||
);
|
||||
assert!(
|
||||
@@ -771,7 +772,7 @@ mod tests {
|
||||
let v = to_jscalendar(&raw).data;
|
||||
let org_addr = v["organizerCalendarAddress"].as_str().unwrap();
|
||||
assert!(
|
||||
org_addr.starts_with("urn:x-vandelay:attendee:") && !org_addr.contains("mailto:"),
|
||||
org_addr.starts_with("urn:x-inbuxa-migrate:attendee:") && !org_addr.contains("mailto:"),
|
||||
"an EX organizer must not become mailto:/o=.../cn=...; got {org_addr}"
|
||||
);
|
||||
let att = v["participants"]
|
||||
@@ -921,7 +922,7 @@ mod tests {
|
||||
let v = to_jscalendar(&raw).data;
|
||||
let addr = v["organizerCalendarAddress"].as_str().unwrap();
|
||||
assert!(
|
||||
addr.starts_with("urn:x-vandelay:attendee:"),
|
||||
addr.starts_with("urn:x-inbuxa-migrate:attendee:"),
|
||||
"an unresolvable directory reference stays synthetic, got {addr}"
|
||||
);
|
||||
let participants = v["participants"].as_object().unwrap();
|
||||
@@ -947,7 +948,7 @@ mod tests {
|
||||
v["organizerCalendarAddress"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.starts_with("urn:x-vandelay:attendee:")
|
||||
.starts_with("urn:x-inbuxa-migrate:attendee:")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -81,7 +82,7 @@ impl EwsClient {
|
||||
retries_total: AtomicU64::new(0),
|
||||
retry_after_sleeps: AtomicU64::new(0),
|
||||
soap_calls: AtomicU64::new(0),
|
||||
user_agent: format!("vandelay/{}", env!("CARGO_PKG_VERSION")),
|
||||
user_agent: format!("inbuxa-migrate/{}", env!("CARGO_PKG_VERSION")),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -10,7 +11,7 @@ use crate::exchange_ews::parse::{ContactItemRaw, RawContactAddress};
|
||||
|
||||
pub fn synthetic_uid(item_id: &str) -> String {
|
||||
let hash = blake3::hash(item_id.as_bytes()).to_hex();
|
||||
format!("vandelay-ews-syn-{hash}")
|
||||
format!("inbuxa-migrate-ews-syn-{hash}")
|
||||
}
|
||||
|
||||
pub fn to_jscontact(raw: &ContactItemRaw) -> Value {
|
||||
@@ -389,7 +390,7 @@ mod tests {
|
||||
let u2 = synthetic_uid("ITEM-1");
|
||||
assert_eq!(u1, u2);
|
||||
assert_ne!(u1, synthetic_uid("ITEM-2"));
|
||||
assert!(u1.starts_with("vandelay-ews-syn-"));
|
||||
assert!(u1.starts_with("inbuxa-migrate-ews-syn-"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -724,7 +725,7 @@ fn wrap_inner_with_namespaces(inner: &str) -> String {
|
||||
return String::new();
|
||||
}
|
||||
format!(
|
||||
"<vandelay-inner xmlns=\"{NS_TYPES}\" xmlns:t=\"{NS_TYPES}\" xmlns:m=\"{NS_MESSAGES}\">{inner}</vandelay-inner>",
|
||||
"<inbuxa-migrate-inner xmlns=\"{NS_TYPES}\" xmlns:t=\"{NS_TYPES}\" xmlns:m=\"{NS_MESSAGES}\">{inner}</inbuxa-migrate-inner>",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -701,7 +702,7 @@ mod tests {
|
||||
let p = &conv.data["participants"]["att-1"];
|
||||
let addr = p["calendarAddress"].as_str().unwrap();
|
||||
assert!(
|
||||
addr.starts_with("urn:x-vandelay:attendee:"),
|
||||
addr.starts_with("urn:x-inbuxa-migrate:attendee:"),
|
||||
"a hybrid X500 reference must not become a malformed mailto, got {addr}"
|
||||
);
|
||||
assert!(
|
||||
@@ -755,7 +756,7 @@ mod tests {
|
||||
let p = &conv.data["participants"]["att-1"];
|
||||
let addr = p["calendarAddress"].as_str().unwrap();
|
||||
assert!(
|
||||
addr.starts_with("urn:x-vandelay:attendee:"),
|
||||
addr.starts_with("urn:x-inbuxa-migrate:attendee:"),
|
||||
"a name-only attendee needs a synthetic calendarAddress, got {addr}"
|
||||
);
|
||||
assert!(
|
||||
@@ -799,7 +800,7 @@ mod tests {
|
||||
v["organizer"] = json!({"emailAddress": {"name": "Alice Example"}});
|
||||
let conv = convert_event(&v, None).unwrap();
|
||||
let organizer_address = conv.data["organizerCalendarAddress"].as_str().unwrap();
|
||||
assert!(organizer_address.starts_with("urn:x-vandelay:attendee:"));
|
||||
assert!(organizer_address.starts_with("urn:x-inbuxa-migrate:attendee:"));
|
||||
assert_eq!(
|
||||
conv.data["participants"]["organizer"]["calendarAddress"],
|
||||
organizer_address
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -112,7 +113,7 @@ impl GraphClient {
|
||||
retries_total: AtomicU64::new(0),
|
||||
retry_after_sleeps: AtomicU64::new(0),
|
||||
requests_total: AtomicU64::new(0),
|
||||
user_agent: format!("vandelay/{}", env!("CARGO_PKG_VERSION")),
|
||||
user_agent: format!("inbuxa-migrate/{}", env!("CARGO_PKG_VERSION")),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -354,7 +355,7 @@ fn has_uri_scheme(value: &str) -> bool {
|
||||
|
||||
pub fn synthetic_uid(graph_id: &str) -> String {
|
||||
let hash = blake3::hash(graph_id.as_bytes());
|
||||
format!("vandelay-graph-{}", hash.to_hex())
|
||||
format!("inbuxa-migrate-graph-{}", hash.to_hex())
|
||||
}
|
||||
|
||||
fn normalise_utc_datetime(raw: &str) -> String {
|
||||
@@ -465,7 +466,7 @@ mod tests {
|
||||
fn minimal_contact_yields_card_with_synthetic_uid() {
|
||||
let c = json!({"id": "GRAPH-1", "displayName": "Alice"});
|
||||
let conv = convert_contact(&c).unwrap();
|
||||
assert!(conv.uid.starts_with("vandelay-graph-"));
|
||||
assert!(conv.uid.starts_with("inbuxa-migrate-graph-"));
|
||||
assert_eq!(conv.data["@type"], "Card");
|
||||
assert_eq!(conv.data["kind"], "individual");
|
||||
assert_eq!(conv.data["name"]["full"], "Alice");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -18,7 +19,7 @@ impl MailboxKind {
|
||||
"primary" => Ok(MailboxKind::Primary),
|
||||
"archive" => Ok(MailboxKind::Archive),
|
||||
"public-folders" => Err(Error::Usage(
|
||||
"Microsoft Graph does not expose public folders. Run `vandelay import exchange-ews --mailbox-kind public-folders` instead.".to_owned(),
|
||||
"Microsoft Graph does not expose public folders. Run `inbuxa-migrate import exchange-ews --mailbox-kind public-folders` instead.".to_owned(),
|
||||
)),
|
||||
other => Err(Error::Usage(format!(
|
||||
"--mailbox-kind must be primary | archive, got {other:?}"
|
||||
|
||||
+7
-1
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -50,6 +51,9 @@ pub enum JmapError {
|
||||
#[error("malformed jmap response: {0}")]
|
||||
Malformed(String),
|
||||
|
||||
#[error("server lacks a required capability: {0}")]
|
||||
MissingCapability(String),
|
||||
|
||||
#[error("json error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
@@ -69,7 +73,9 @@ impl JmapError {
|
||||
impl From<JmapError> for Error {
|
||||
fn from(value: JmapError) -> Self {
|
||||
match value {
|
||||
JmapError::Connect(m) | JmapError::Transport(m) => Error::Connection(m),
|
||||
JmapError::Connect(m) | JmapError::Transport(m) | JmapError::MissingCapability(m) => {
|
||||
Error::Connection(m)
|
||||
}
|
||||
JmapError::Auth(m) => Error::Connection(format!("authentication rejected: {m}")),
|
||||
JmapError::Sqlite(e) => Error::Db(OpenError::Sqlite(e)),
|
||||
reached @ (JmapError::HttpStatus { .. } | JmapError::RetriesExhausted(_)) => {
|
||||
|
||||
+86
-18
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -17,9 +18,28 @@ use crate::logging::Logger;
|
||||
|
||||
pub const URN_CORE: &str = "urn:ietf:params:jmap:core";
|
||||
|
||||
pub fn using_urn(method: &str) -> &'static str {
|
||||
/// The capability inbuxa advertises for its registry types (`x:Account`,
|
||||
/// `x:Domain` and the rest of the `x:` objects).
|
||||
pub const URN_INBUXA_REGISTRY: &str = "urn:inbuxa:jmap:registry";
|
||||
|
||||
/// The same registry as Stalwart advertises it, so a Stalwart server can be
|
||||
/// migrated from.
|
||||
pub const URN_STALWART_REGISTRY: &str = "urn:stalwart:jmap";
|
||||
|
||||
/// The capability a method needs. Registry (`x:`) methods have no fixed
|
||||
/// capability: the connected server names it, and `registry` carries that
|
||||
/// name (see `Session::registry_urn`). Without one they cannot be sent.
|
||||
pub fn using_urn(method: &str, registry: Option<&'static str>) -> Result<&'static str, JmapError> {
|
||||
let prefix = method.split('/').next().unwrap_or(method);
|
||||
match prefix {
|
||||
if prefix.starts_with("x:") {
|
||||
return registry.ok_or_else(|| {
|
||||
JmapError::MissingCapability(format!(
|
||||
"{method} needs the server's registry capability, but it advertises neither \
|
||||
{URN_INBUXA_REGISTRY} nor {URN_STALWART_REGISTRY}"
|
||||
))
|
||||
});
|
||||
}
|
||||
Ok(match prefix {
|
||||
"Mailbox" | "Email" => "urn:ietf:params:jmap:mail",
|
||||
"Identity" => "urn:ietf:params:jmap:submission",
|
||||
"SieveScript" => "urn:ietf:params:jmap:sieve",
|
||||
@@ -27,9 +47,8 @@ pub fn using_urn(method: &str) -> &'static str {
|
||||
"Calendar" | "CalendarEvent" | "ParticipantIdentity" => "urn:ietf:params:jmap:calendars",
|
||||
"FileNode" => "urn:ietf:params:jmap:filenode",
|
||||
"Principal" => "urn:ietf:params:jmap:principals",
|
||||
"x:Account" | "x:Domain" => "urn:stalwart:jmap",
|
||||
_ => URN_CORE,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -42,11 +61,19 @@ pub struct MethodCall {
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Request {
|
||||
calls: Vec<MethodCall>,
|
||||
registry: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
pub fn new() -> Request {
|
||||
Request { calls: Vec::new() }
|
||||
Request::default()
|
||||
}
|
||||
|
||||
/// The registry capability to use for any `x:` call in this request,
|
||||
/// normally `session.registry_urn()`.
|
||||
pub fn registry(&mut self, urn: Option<&'static str>) -> &mut Request {
|
||||
self.registry = urn;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn call(
|
||||
@@ -71,29 +98,29 @@ impl Request {
|
||||
self.calls.is_empty()
|
||||
}
|
||||
|
||||
pub fn using(&self) -> Vec<String> {
|
||||
pub fn using(&self) -> Result<Vec<String>, JmapError> {
|
||||
let mut set: IndexSet<String> = IndexSet::new();
|
||||
set.insert(URN_CORE.to_owned());
|
||||
for c in &self.calls {
|
||||
set.insert(using_urn(&c.name).to_owned());
|
||||
set.insert(using_urn(&c.name, self.registry)?.to_owned());
|
||||
}
|
||||
set.into_iter().collect()
|
||||
Ok(set.into_iter().collect())
|
||||
}
|
||||
|
||||
fn envelope(&self) -> Value {
|
||||
fn envelope(&self) -> Result<Value, JmapError> {
|
||||
let method_calls: Vec<Value> = self
|
||||
.calls
|
||||
.iter()
|
||||
.map(|c| json!([c.name, c.args, c.call_id]))
|
||||
.collect();
|
||||
json!({ "using": self.using(), "methodCalls": method_calls })
|
||||
Ok(json!({ "using": self.using()?, "methodCalls": method_calls }))
|
||||
}
|
||||
|
||||
pub fn fits(&self, limits: &Limits) -> Result<(), JmapError> {
|
||||
if self.calls.len() as u64 > limits.max_calls_in_request {
|
||||
return Err(JmapError::RequestTooLarge);
|
||||
}
|
||||
let size = serde_json::to_vec(&self.envelope())?.len() as u64;
|
||||
let size = serde_json::to_vec(&self.envelope()?)?.len() as u64;
|
||||
if size > limits.max_size_request {
|
||||
return Err(JmapError::RequestTooLarge);
|
||||
}
|
||||
@@ -101,7 +128,7 @@ impl Request {
|
||||
}
|
||||
|
||||
pub fn send(&self, client: &HttpClient, api_url: &str) -> Result<Response, JmapError> {
|
||||
let value = client.post_json(api_url, &self.envelope())?;
|
||||
let value = client.post_json(api_url, &self.envelope()?)?;
|
||||
Response::parse(value)
|
||||
}
|
||||
}
|
||||
@@ -855,24 +882,65 @@ mod tests {
|
||||
let mut r = Request::new();
|
||||
r.call("Mailbox/get", json!({}), "a");
|
||||
r.call("Email/query", json!({}), "b");
|
||||
let u = r.using();
|
||||
let u = r.using().unwrap();
|
||||
assert!(u.contains(&URN_CORE.to_owned()));
|
||||
assert!(u.contains(&"urn:ietf:params:jmap:mail".to_owned()));
|
||||
assert_eq!(u.iter().filter(|x| x.as_str() == URN_CORE).count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn using_maps_principal_and_stalwart() {
|
||||
fn using_maps_standard_types() {
|
||||
assert_eq!(
|
||||
using_urn("Principal/query"),
|
||||
using_urn("Principal/query", None).unwrap(),
|
||||
"urn:ietf:params:jmap:principals"
|
||||
);
|
||||
assert_eq!(using_urn("x:Account/set"), "urn:stalwart:jmap");
|
||||
assert_eq!(
|
||||
using_urn("CalendarEvent/set"),
|
||||
using_urn("CalendarEvent/set", None).unwrap(),
|
||||
"urn:ietf:params:jmap:calendars"
|
||||
);
|
||||
assert_eq!(using_urn("Identity/get"), "urn:ietf:params:jmap:submission");
|
||||
assert_eq!(
|
||||
using_urn("Identity/get", None).unwrap(),
|
||||
"urn:ietf:params:jmap:submission"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_calls_use_the_inbuxa_capability() {
|
||||
let mut r = Request::new();
|
||||
r.registry(Some(URN_INBUXA_REGISTRY));
|
||||
r.call("x:Account/set", json!({}), "a");
|
||||
r.call("x:Domain/get", json!({}), "b");
|
||||
assert_eq!(r.using().unwrap(), vec![URN_CORE, URN_INBUXA_REGISTRY]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_calls_use_the_stalwart_capability_from_a_stalwart_source() {
|
||||
let mut r = Request::new();
|
||||
r.registry(Some(URN_STALWART_REGISTRY));
|
||||
r.call("x:Account/get", json!({}), "a");
|
||||
assert_eq!(r.using().unwrap(), vec![URN_CORE, URN_STALWART_REGISTRY]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_calls_without_a_registry_capability_fail_clearly() {
|
||||
let mut r = Request::new();
|
||||
r.call("Mailbox/get", json!({}), "a");
|
||||
r.call("x:Domain/set", json!({}), "b");
|
||||
let err = r.using().unwrap_err();
|
||||
let msg = err.to_string();
|
||||
assert!(matches!(err, JmapError::MissingCapability(_)));
|
||||
assert!(msg.contains("x:Domain/set"), "{msg}");
|
||||
assert!(
|
||||
msg.contains(URN_INBUXA_REGISTRY) && msg.contains(URN_STALWART_REGISTRY),
|
||||
"{msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requests_without_registry_calls_need_no_registry() {
|
||||
let mut r = Request::new();
|
||||
r.call("Email/get", json!({}), "a");
|
||||
assert!(r.using().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+44
-1
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -11,6 +12,7 @@ use serde_json::Value;
|
||||
use crate::error::Error;
|
||||
use crate::jmap::error::JmapError;
|
||||
use crate::jmap::http::HttpClient;
|
||||
use crate::jmap::request::{URN_INBUXA_REGISTRY, URN_STALWART_REGISTRY};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -139,7 +141,7 @@ impl std::fmt::Display for OriginMismatch {
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
" on {} (connected to {}); vandelay must use the advertised URL. \
|
||||
" on {} (connected to {}); inbuxa-migrate must use the advertised URL. \
|
||||
If this is wrong, fix the server's advertised HTTP URL setting.",
|
||||
self.session_origin, self.connect_origin
|
||||
)
|
||||
@@ -233,6 +235,14 @@ impl Session {
|
||||
.map_err(|e| Error::Connection(format!("session core capability is malformed: {e}")))
|
||||
}
|
||||
|
||||
/// The capability to use for registry (`x:`) calls on this server:
|
||||
/// inbuxa's name when advertised, else Stalwart's, else none.
|
||||
pub fn registry_urn(&self) -> Option<&'static str> {
|
||||
[URN_INBUXA_REGISTRY, URN_STALWART_REGISTRY]
|
||||
.into_iter()
|
||||
.find(|urn| self.capabilities.contains_key(*urn))
|
||||
}
|
||||
|
||||
pub fn account(&self, account_id: &str) -> Option<&Account> {
|
||||
self.accounts.get(account_id)
|
||||
}
|
||||
@@ -315,6 +325,39 @@ mod tests {
|
||||
}"#
|
||||
}
|
||||
|
||||
fn session_with(extra_caps: &[&str]) -> Session {
|
||||
let mut v: serde_json::Value = serde_json::from_str(raw_session()).unwrap();
|
||||
for urn in extra_caps {
|
||||
v["capabilities"][*urn] = serde_json::json!({});
|
||||
}
|
||||
serde_json::from_value(v).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_urn_prefers_inbuxa() {
|
||||
assert_eq!(
|
||||
session_with(&[URN_INBUXA_REGISTRY]).registry_urn(),
|
||||
Some(URN_INBUXA_REGISTRY)
|
||||
);
|
||||
assert_eq!(
|
||||
session_with(&[URN_STALWART_REGISTRY, URN_INBUXA_REGISTRY]).registry_urn(),
|
||||
Some(URN_INBUXA_REGISTRY)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_urn_falls_back_to_stalwart() {
|
||||
assert_eq!(
|
||||
session_with(&[URN_STALWART_REGISTRY]).registry_urn(),
|
||||
Some(URN_STALWART_REGISTRY)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_urn_is_none_without_either() {
|
||||
assert_eq!(session_with(&[]).registry_urn(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_a_session_and_reads_limits() {
|
||||
let session = parse_session(raw_session()).unwrap();
|
||||
|
||||
+5
-4
@@ -1,15 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use vandelay::cli::{Action, Cli};
|
||||
use vandelay::error::Error;
|
||||
use vandelay::inspect;
|
||||
use vandelay::sync::{self, RunOutcome, Summary};
|
||||
use inbuxa_migrate::cli::{Action, Cli};
|
||||
use inbuxa_migrate::error::Error;
|
||||
use inbuxa_migrate::inspect;
|
||||
use inbuxa_migrate::sync::{self, RunOutcome, Summary};
|
||||
|
||||
fn main() {
|
||||
let code = run();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -52,7 +53,7 @@ pub fn vcard_to_jscontact(text: &str, item_href: &str) -> Result<JsContact, Calc
|
||||
|
||||
pub fn synthesise_uid(item_href: &str) -> String {
|
||||
let hash = blake3::hash(item_href.as_bytes());
|
||||
format!("vandelay-syn-{}", hash.to_hex())
|
||||
format!("inbuxa-migrate-syn-{}", hash.to_hex())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -208,7 +209,7 @@ END:VCALENDAR\r\n";
|
||||
let href = "/dav/card/u/d/no-uid.vcf";
|
||||
let jc = vcard_to_jscontact(no_uid, href).expect("synthetic uid");
|
||||
assert_eq!(jc.uid, synthesise_uid(href));
|
||||
assert!(jc.uid.starts_with("vandelay-syn-"));
|
||||
assert!(jc.uid.starts_with("inbuxa-migrate-syn-"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -385,7 +386,7 @@ fn spawn_token_refresher(
|
||||
let client = client.clone();
|
||||
let initial_refresh = initial.as_ref().and_then(|t| t.refresh_token.clone());
|
||||
std::thread::Builder::new()
|
||||
.name("vandelay-ews-token-refresh".to_owned())
|
||||
.name("inbuxa-migrate-ews-token-refresh".to_owned())
|
||||
.spawn(move || {
|
||||
let mut next_exp = exp_secs;
|
||||
let mut refresh_token = initial_refresh;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -395,7 +396,7 @@ fn spawn_token_refresher(
|
||||
let shutdown = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let shutdown_signal = shutdown.clone();
|
||||
let handle = std::thread::Builder::new()
|
||||
.name("vandelay-graph-token-refresh".to_owned())
|
||||
.name("inbuxa-migrate-graph-token-refresh".to_owned())
|
||||
.spawn(move || {
|
||||
while !shutdown_signal.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
let now = unix_now();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -199,7 +200,7 @@ pub fn run(common: CommonConfig, config: ManageSieveImportConfig) -> Result<Summ
|
||||
}
|
||||
Disposition::Referral => {
|
||||
return Err(Error::Connection(format!(
|
||||
"ManageSieve referral on GETSCRIPT {name:?}: {e}; vandelay does not follow referrals"
|
||||
"ManageSieve referral on GETSCRIPT {name:?}: {e}; inbuxa-migrate does not follow referrals"
|
||||
)));
|
||||
}
|
||||
Disposition::TransportDrop => {
|
||||
@@ -536,7 +537,7 @@ fn listscripts_with_retry(
|
||||
}
|
||||
Disposition::Referral => {
|
||||
return Err(Error::Connection(format!(
|
||||
"LISTSCRIPTS referral: {e}; vandelay does not follow referrals"
|
||||
"LISTSCRIPTS referral: {e}; inbuxa-migrate does not follow referrals"
|
||||
)));
|
||||
}
|
||||
_ => return Err(Error::Connection(format!("LISTSCRIPTS failed: {e}"))),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -34,7 +35,7 @@ const BAIKAL_YAML: &str = "system:
|
||||
timezone: 'UTC'
|
||||
card_enabled: true
|
||||
cal_enabled: true
|
||||
invite_from: 'noreply@vandelay.test'
|
||||
invite_from: 'noreply@inbuxa-migrate.test'
|
||||
dav_auth_type: 'Basic'
|
||||
admin_passwordhash: '004b53ae84286ba6003b34e8ef404826'
|
||||
failed_access_message: 'user %u authentication failure for Baikal'
|
||||
@@ -64,7 +65,7 @@ impl Baikal {
|
||||
pub fn start() -> ContainerResult<Self> {
|
||||
let db_bytes = build_sqlite_db()?;
|
||||
|
||||
let image: GenericImage = GenericBuildableImage::new("vandelay-baikal", "test")
|
||||
let image: GenericImage = GenericBuildableImage::new("inbuxa-migrate-baikal", "test")
|
||||
.with_dockerfile_string(DOCKERFILE.to_owned())
|
||||
.with_data(BAIKAL_YAML.as_bytes().to_vec(), "baikal.yaml")
|
||||
.with_data(db_bytes, "db.sqlite")
|
||||
@@ -325,8 +326,8 @@ fn build_sqlite_db() -> ContainerResult<Vec<u8>> {
|
||||
)
|
||||
.map_err(|e| ContainerError::Seed(format!("baikal user {name}: {e}")))?;
|
||||
let uri = format!("principals/{name}");
|
||||
let email = format!("{name}@vandelay.test");
|
||||
let display = format!("Vandelay {name}");
|
||||
let email = format!("{name}@inbuxa-migrate.test");
|
||||
let display = format!("Inbuxa {name}");
|
||||
conn.execute(
|
||||
"INSERT INTO principals (uri, email, displayname) VALUES (?1, ?2, ?3)",
|
||||
params![uri, email, display],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -142,7 +143,7 @@ pub struct Cyrus {
|
||||
|
||||
impl Cyrus {
|
||||
pub fn start() -> ContainerResult<Self> {
|
||||
let image: GenericImage = GenericBuildableImage::new("vandelay-cyrus", "test")
|
||||
let image: GenericImage = GenericBuildableImage::new("inbuxa-migrate-cyrus", "test")
|
||||
.with_dockerfile_string(DOCKERFILE.to_owned())
|
||||
.with_data(IMAPD_CONF.as_bytes().to_vec(), "imapd.conf")
|
||||
.with_data(CYRUS_CONF.as_bytes().to_vec(), "cyrus.conf")
|
||||
@@ -313,10 +314,13 @@ impl Cyrus {
|
||||
) -> ContainerResult<(Vec<u8>, String)> {
|
||||
let mut client = ImapSeed::connect(&self.imap.host, self.imap.port)?;
|
||||
client.login(&account.username, &account.password)?;
|
||||
let message_id = format!("<cyrus-added-{tag}-{}@vandelay.test>", account.username);
|
||||
let message_id = format!(
|
||||
"<cyrus-added-{tag}-{}@inbuxa-migrate.test>",
|
||||
account.username
|
||||
);
|
||||
let body = format!(
|
||||
"From: cyrus-added-{tag}@vandelay.test\r\n\
|
||||
To: {}@vandelay.test\r\n\
|
||||
"From: cyrus-added-{tag}@inbuxa-migrate.test\r\n\
|
||||
To: {}@inbuxa-migrate.test\r\n\
|
||||
Subject: Cyrus added probe {tag}\r\n\
|
||||
Message-ID: {message_id}\r\n\
|
||||
Date: Wed, 01 Jan 2025 12:00:00 +0000\r\n\
|
||||
@@ -372,10 +376,10 @@ impl Cyrus {
|
||||
}
|
||||
|
||||
pub fn flag_probe_message() -> Vec<u8> {
|
||||
let body = "From: probe-flags@vandelay.test\r\n\
|
||||
To: user@vandelay.test\r\n\
|
||||
let body = "From: probe-flags@inbuxa-migrate.test\r\n\
|
||||
To: user@inbuxa-migrate.test\r\n\
|
||||
Subject: Cyrus flag probe\r\n\
|
||||
Message-ID: <cyrus-flag-probe@vandelay.test>\r\n\
|
||||
Message-ID: <cyrus-flag-probe@inbuxa-migrate.test>\r\n\
|
||||
Date: Wed, 01 Jan 2025 12:00:00 +0000\r\n\
|
||||
\r\n\
|
||||
Cyrus flag probe body.\r\n";
|
||||
|
||||
+45
-44
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -89,15 +90,15 @@ fn synth_messages(n: usize) -> Vec<MboxMessage> {
|
||||
let mut out = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let body = format!(
|
||||
"From: sender{i}@vandelay.test\r\n\
|
||||
To: user@vandelay.test\r\n\
|
||||
"From: sender{i}@inbuxa-migrate.test\r\n\
|
||||
To: user@inbuxa-migrate.test\r\n\
|
||||
Subject: Synthetic test message {i}\r\n\
|
||||
Message-ID: <synth-{i}@vandelay.test>\r\n\
|
||||
Message-ID: <synth-{i}@inbuxa-migrate.test>\r\n\
|
||||
Date: Wed, 01 Jan 2025 12:00:00 +0000\r\n\
|
||||
MIME-Version: 1.0\r\n\
|
||||
Content-Type: text/plain; charset=utf-8\r\n\
|
||||
\r\n\
|
||||
This is synthetic seed message #{i} for vandelay container tests.\r\n",
|
||||
This is synthetic seed message #{i} for inbuxa-migrate container tests.\r\n",
|
||||
);
|
||||
out.push(MboxMessage {
|
||||
raw: body.into_bytes(),
|
||||
@@ -135,10 +136,10 @@ fn vcard_v3_basic(i: usize) -> String {
|
||||
format!(
|
||||
"BEGIN:VCARD\r\n\
|
||||
VERSION:3.0\r\n\
|
||||
UID:vandelay-card-basic-{i}\r\n\
|
||||
UID:inbuxa-migrate-card-basic-{i}\r\n\
|
||||
FN:Basic Contact {i}\r\n\
|
||||
N:Contact{i};Basic;;;\r\n\
|
||||
EMAIL;TYPE=INTERNET:basic-{i}@vandelay.test\r\n\
|
||||
EMAIL;TYPE=INTERNET:basic-{i}@inbuxa-migrate.test\r\n\
|
||||
END:VCARD\r\n"
|
||||
)
|
||||
}
|
||||
@@ -147,14 +148,14 @@ fn vcard_v3_full(i: usize) -> String {
|
||||
format!(
|
||||
"BEGIN:VCARD\r\n\
|
||||
VERSION:3.0\r\n\
|
||||
UID:vandelay-card-full-{i}\r\n\
|
||||
UID:inbuxa-migrate-card-full-{i}\r\n\
|
||||
FN:Full Contact {i}\r\n\
|
||||
N:Contact{i};Full;Middle;;\r\n\
|
||||
EMAIL;TYPE=INTERNET;TYPE=WORK:full-{i}@vandelay.test\r\n\
|
||||
EMAIL;TYPE=INTERNET;TYPE=WORK:full-{i}@inbuxa-migrate.test\r\n\
|
||||
TEL;TYPE=CELL:+15550010{i:03}\r\n\
|
||||
ORG:Vandelay Test\r\n\
|
||||
ORG:Inbuxa Test\r\n\
|
||||
TITLE:Senior Test Engineer\r\n\
|
||||
URL:https://vandelay.test/{i}\r\n\
|
||||
URL:https://inbuxa-migrate.test/{i}\r\n\
|
||||
NOTE:Synthetic full v3 contact #{i}\r\n\
|
||||
END:VCARD\r\n"
|
||||
)
|
||||
@@ -164,11 +165,11 @@ fn vcard_v3_multi_email(i: usize) -> String {
|
||||
format!(
|
||||
"BEGIN:VCARD\r\n\
|
||||
VERSION:3.0\r\n\
|
||||
UID:vandelay-card-multi-{i}\r\n\
|
||||
UID:inbuxa-migrate-card-multi-{i}\r\n\
|
||||
FN:Multi Email {i}\r\n\
|
||||
N:Contact{i};MultiEmail;;;\r\n\
|
||||
EMAIL;TYPE=INTERNET;TYPE=HOME:multi-{i}-home@vandelay.test\r\n\
|
||||
EMAIL;TYPE=INTERNET;TYPE=WORK:multi-{i}-work@vandelay.test\r\n\
|
||||
EMAIL;TYPE=INTERNET;TYPE=HOME:multi-{i}-home@inbuxa-migrate.test\r\n\
|
||||
EMAIL;TYPE=INTERNET;TYPE=WORK:multi-{i}-work@inbuxa-migrate.test\r\n\
|
||||
END:VCARD\r\n"
|
||||
)
|
||||
}
|
||||
@@ -177,12 +178,12 @@ fn vcard_v3_org_and_title(i: usize) -> String {
|
||||
format!(
|
||||
"BEGIN:VCARD\r\n\
|
||||
VERSION:3.0\r\n\
|
||||
UID:vandelay-card-org-{i}\r\n\
|
||||
UID:inbuxa-migrate-card-org-{i}\r\n\
|
||||
FN:Org Contact {i}\r\n\
|
||||
N:Contact{i};Org;;Dr.;PhD\r\n\
|
||||
ORG:Vandelay Industries;Engineering;Test Group\r\n\
|
||||
ORG:Inbuxa Industries;Engineering;Test Group\r\n\
|
||||
TITLE:Principal Researcher\r\n\
|
||||
EMAIL;TYPE=INTERNET:org-{i}@vandelay.test\r\n\
|
||||
EMAIL;TYPE=INTERNET:org-{i}@inbuxa-migrate.test\r\n\
|
||||
END:VCARD\r\n"
|
||||
)
|
||||
}
|
||||
@@ -194,7 +195,7 @@ fn vcard_v4_basic(i: usize) -> String {
|
||||
UID:urn:uuid:00000000-0000-4000-a000-0000{i:08x}\r\n\
|
||||
FN:V4 Basic {i}\r\n\
|
||||
N:Contact{i};V4Basic;;;\r\n\
|
||||
EMAIL:v4-basic-{i}@vandelay.test\r\n\
|
||||
EMAIL:v4-basic-{i}@inbuxa-migrate.test\r\n\
|
||||
END:VCARD\r\n"
|
||||
)
|
||||
}
|
||||
@@ -206,7 +207,7 @@ fn vcard_v4_with_address(i: usize) -> String {
|
||||
UID:urn:uuid:00000000-0000-4000-b000-0000{i:08x}\r\n\
|
||||
FN:V4 Address {i}\r\n\
|
||||
N:Contact{i};V4Address;;;\r\n\
|
||||
EMAIL:v4-address-{i}@vandelay.test\r\n\
|
||||
EMAIL:v4-address-{i}@inbuxa-migrate.test\r\n\
|
||||
ADR;TYPE=home:;;{i} Test Street;Springfield;OR;97477;US\r\n\
|
||||
END:VCARD\r\n"
|
||||
)
|
||||
@@ -220,7 +221,7 @@ fn vcard_v4_nickname(i: usize) -> String {
|
||||
FN:V4 Nick {i}\r\n\
|
||||
N:Contact{i};V4Nick;;;\r\n\
|
||||
NICKNAME:Nicky{i}\r\n\
|
||||
EMAIL:v4-nick-{i}@vandelay.test\r\n\
|
||||
EMAIL:v4-nick-{i}@inbuxa-migrate.test\r\n\
|
||||
END:VCARD\r\n"
|
||||
)
|
||||
}
|
||||
@@ -236,7 +237,7 @@ fn vcard_v4_birthday(i: usize) -> String {
|
||||
FN:V4 Birthday {i}\r\n\
|
||||
N:Contact{i};V4Birthday;;;\r\n\
|
||||
BDAY:{year:04}{month:02}{day:02}\r\n\
|
||||
EMAIL:v4-bday-{i}@vandelay.test\r\n\
|
||||
EMAIL:v4-bday-{i}@inbuxa-migrate.test\r\n\
|
||||
END:VCARD\r\n"
|
||||
)
|
||||
}
|
||||
@@ -245,10 +246,10 @@ fn vcard_v3_apple_item_labels(i: usize) -> String {
|
||||
format!(
|
||||
"BEGIN:VCARD\r\n\
|
||||
VERSION:3.0\r\n\
|
||||
UID:vandelay-card-ablabels-{i}\r\n\
|
||||
UID:inbuxa-migrate-card-ablabels-{i}\r\n\
|
||||
FN:Apple Labels {i}\r\n\
|
||||
N:Contact{i};AppleLabels;;;\r\n\
|
||||
EMAIL;TYPE=INTERNET:ablabels-{i}@vandelay.test\r\n\
|
||||
EMAIL;TYPE=INTERNET:ablabels-{i}@inbuxa-migrate.test\r\n\
|
||||
ITEM1.X-ABLABEL:Name1\r\n\
|
||||
ITEM2.X-ABLABEL:Name2\r\n\
|
||||
ITEM1.X-ABDATE:20171111\r\n\
|
||||
@@ -287,9 +288,9 @@ fn ical_simple_dt(i: usize) -> String {
|
||||
format!(
|
||||
"BEGIN:VCALENDAR\r\n\
|
||||
VERSION:2.0\r\n\
|
||||
PRODID:-//vandelay//container-test//EN\r\n\
|
||||
PRODID:-//inbuxa-migrate//container-test//EN\r\n\
|
||||
BEGIN:VEVENT\r\n\
|
||||
UID:vandelay-simple-{i}@vandelay.test\r\n\
|
||||
UID:inbuxa-migrate-simple-{i}@inbuxa-migrate.test\r\n\
|
||||
DTSTAMP:20250101T120000Z\r\n\
|
||||
DTSTART:2025{month:02}{day:02}T{hour:02}0000Z\r\n\
|
||||
DTEND:2025{month:02}{day:02}T{hour:02}3000Z\r\n\
|
||||
@@ -307,9 +308,9 @@ fn ical_all_day(i: usize) -> String {
|
||||
format!(
|
||||
"BEGIN:VCALENDAR\r\n\
|
||||
VERSION:2.0\r\n\
|
||||
PRODID:-//vandelay//container-test//EN\r\n\
|
||||
PRODID:-//inbuxa-migrate//container-test//EN\r\n\
|
||||
BEGIN:VEVENT\r\n\
|
||||
UID:vandelay-allday-{i}@vandelay.test\r\n\
|
||||
UID:inbuxa-migrate-allday-{i}@inbuxa-migrate.test\r\n\
|
||||
DTSTAMP:20250101T120000Z\r\n\
|
||||
DTSTART;VALUE=DATE:2025{month:02}{day:02}\r\n\
|
||||
DTEND;VALUE=DATE:2025{month:02}{next_day:02}\r\n\
|
||||
@@ -325,9 +326,9 @@ fn ical_multi_day_dt(i: usize) -> String {
|
||||
format!(
|
||||
"BEGIN:VCALENDAR\r\n\
|
||||
VERSION:2.0\r\n\
|
||||
PRODID:-//vandelay//container-test//EN\r\n\
|
||||
PRODID:-//inbuxa-migrate//container-test//EN\r\n\
|
||||
BEGIN:VEVENT\r\n\
|
||||
UID:vandelay-multiday-{i}@vandelay.test\r\n\
|
||||
UID:inbuxa-migrate-multiday-{i}@inbuxa-migrate.test\r\n\
|
||||
DTSTAMP:20250101T120000Z\r\n\
|
||||
DTSTART:2025{month:02}{day:02}T090000Z\r\n\
|
||||
DTEND:2025{month:02}{day:02}T170000Z\r\n\
|
||||
@@ -344,16 +345,16 @@ fn ical_with_organizer_attendees(i: usize) -> String {
|
||||
format!(
|
||||
"BEGIN:VCALENDAR\r\n\
|
||||
VERSION:2.0\r\n\
|
||||
PRODID:-//vandelay//container-test//EN\r\n\
|
||||
PRODID:-//inbuxa-migrate//container-test//EN\r\n\
|
||||
BEGIN:VEVENT\r\n\
|
||||
UID:vandelay-meeting-{i}@vandelay.test\r\n\
|
||||
UID:inbuxa-migrate-meeting-{i}@inbuxa-migrate.test\r\n\
|
||||
DTSTAMP:20250101T120000Z\r\n\
|
||||
DTSTART:2025{month:02}{day:02}T140000Z\r\n\
|
||||
DTEND:2025{month:02}{day:02}T150000Z\r\n\
|
||||
SUMMARY:Sync meeting {i}\r\n\
|
||||
ORGANIZER;CN=Organiser:mailto:organiser-{i}@vandelay.test\r\n\
|
||||
ATTENDEE;CN=Alice;PARTSTAT=ACCEPTED:mailto:alice-{i}@vandelay.test\r\n\
|
||||
ATTENDEE;CN=Bob;PARTSTAT=NEEDS-ACTION:mailto:bob-{i}@vandelay.test\r\n\
|
||||
ORGANIZER;CN=Organiser:mailto:organiser-{i}@inbuxa-migrate.test\r\n\
|
||||
ATTENDEE;CN=Alice;PARTSTAT=ACCEPTED:mailto:alice-{i}@inbuxa-migrate.test\r\n\
|
||||
ATTENDEE;CN=Bob;PARTSTAT=NEEDS-ACTION:mailto:bob-{i}@inbuxa-migrate.test\r\n\
|
||||
END:VEVENT\r\n\
|
||||
END:VCALENDAR\r\n"
|
||||
)
|
||||
@@ -365,15 +366,15 @@ fn ical_with_categories_location(i: usize) -> String {
|
||||
format!(
|
||||
"BEGIN:VCALENDAR\r\n\
|
||||
VERSION:2.0\r\n\
|
||||
PRODID:-//vandelay//container-test//EN\r\n\
|
||||
PRODID:-//inbuxa-migrate//container-test//EN\r\n\
|
||||
BEGIN:VEVENT\r\n\
|
||||
UID:vandelay-cat-{i}@vandelay.test\r\n\
|
||||
UID:inbuxa-migrate-cat-{i}@inbuxa-migrate.test\r\n\
|
||||
DTSTAMP:20250101T120000Z\r\n\
|
||||
DTSTART:2025{month:02}{day:02}T100000Z\r\n\
|
||||
DTEND:2025{month:02}{day:02}T110000Z\r\n\
|
||||
SUMMARY:Categorised event {i}\r\n\
|
||||
LOCATION:Test Lab\\, Room {i}\r\n\
|
||||
CATEGORIES:VANDELAY,TEST,FIXTURE\r\n\
|
||||
CATEGORIES:INBUXA,TEST,FIXTURE\r\n\
|
||||
END:VEVENT\r\n\
|
||||
END:VCALENDAR\r\n"
|
||||
)
|
||||
@@ -385,9 +386,9 @@ fn ical_recurring_daily(i: usize) -> String {
|
||||
format!(
|
||||
"BEGIN:VCALENDAR\r\n\
|
||||
VERSION:2.0\r\n\
|
||||
PRODID:-//vandelay//container-test//EN\r\n\
|
||||
PRODID:-//inbuxa-migrate//container-test//EN\r\n\
|
||||
BEGIN:VEVENT\r\n\
|
||||
UID:vandelay-daily-{i}@vandelay.test\r\n\
|
||||
UID:inbuxa-migrate-daily-{i}@inbuxa-migrate.test\r\n\
|
||||
DTSTAMP:20250101T120000Z\r\n\
|
||||
DTSTART:2025{month:02}{day:02}T070000Z\r\n\
|
||||
DTEND:2025{month:02}{day:02}T073000Z\r\n\
|
||||
@@ -405,9 +406,9 @@ fn ical_recurring_weekly_until(i: usize) -> String {
|
||||
format!(
|
||||
"BEGIN:VCALENDAR\r\n\
|
||||
VERSION:2.0\r\n\
|
||||
PRODID:-//vandelay//container-test//EN\r\n\
|
||||
PRODID:-//inbuxa-migrate//container-test//EN\r\n\
|
||||
BEGIN:VEVENT\r\n\
|
||||
UID:vandelay-weekly-{i}@vandelay.test\r\n\
|
||||
UID:inbuxa-migrate-weekly-{i}@inbuxa-migrate.test\r\n\
|
||||
DTSTAMP:20250101T120000Z\r\n\
|
||||
DTSTART:2025{month:02}{day:02}T160000Z\r\n\
|
||||
DTEND:2025{month:02}{day:02}T170000Z\r\n\
|
||||
@@ -424,9 +425,9 @@ fn ical_with_alarm(i: usize) -> String {
|
||||
format!(
|
||||
"BEGIN:VCALENDAR\r\n\
|
||||
VERSION:2.0\r\n\
|
||||
PRODID:-//vandelay//container-test//EN\r\n\
|
||||
PRODID:-//inbuxa-migrate//container-test//EN\r\n\
|
||||
BEGIN:VEVENT\r\n\
|
||||
UID:vandelay-alarm-{i}@vandelay.test\r\n\
|
||||
UID:inbuxa-migrate-alarm-{i}@inbuxa-migrate.test\r\n\
|
||||
DTSTAMP:20250101T120000Z\r\n\
|
||||
DTSTART:2025{month:02}{day:02}T080000Z\r\n\
|
||||
DTEND:2025{month:02}{day:02}T083000Z\r\n\
|
||||
@@ -445,9 +446,9 @@ pub fn malformed_ical(name: &str) -> RawFixture {
|
||||
let body = format!(
|
||||
"BEGIN:VCALENDAR\r\n\
|
||||
VERSION:2.0\r\n\
|
||||
PRODID:-//vandelay//broken//EN\r\n\
|
||||
PRODID:-//inbuxa-migrate//broken//EN\r\n\
|
||||
BEGIN:VEVENT\r\n\
|
||||
UID:broken-{name}@vandelay.test\r\n\
|
||||
UID:broken-{name}@inbuxa-migrate.test\r\n\
|
||||
DTSTAMP:20250101T120000Z\r\n\
|
||||
DTSTART:NOT-A-DATE\r\n\
|
||||
RRULE:FREQ=BOGUS;INTERVAL=oops\r\n\
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -126,7 +127,7 @@ pub struct Dovecot {
|
||||
|
||||
impl Dovecot {
|
||||
pub fn start() -> ContainerResult<Self> {
|
||||
let image: GenericImage = GenericBuildableImage::new("vandelay-dovecot", "test")
|
||||
let image: GenericImage = GenericBuildableImage::new("inbuxa-migrate-dovecot", "test")
|
||||
.with_dockerfile_string(DOCKERFILE.to_owned())
|
||||
.with_data(DOVECOT_CONF.as_bytes().to_vec(), "dovecot.conf")
|
||||
.with_data(users_file().into_bytes(), "users")
|
||||
@@ -374,10 +375,10 @@ impl Dovecot {
|
||||
) -> ContainerResult<(Vec<u8>, String)> {
|
||||
let mut client = ImapSeed::connect(&self.imap.host, self.imap.port)?;
|
||||
client.login(&account.username, &account.password)?;
|
||||
let message_id = format!("<added-{tag}-{}@vandelay.test>", account.username);
|
||||
let message_id = format!("<added-{tag}-{}@inbuxa-migrate.test>", account.username);
|
||||
let body = format!(
|
||||
"From: added-{tag}@vandelay.test\r\n\
|
||||
To: {}@vandelay.test\r\n\
|
||||
"From: added-{tag}@inbuxa-migrate.test\r\n\
|
||||
To: {}@inbuxa-migrate.test\r\n\
|
||||
Subject: Added probe {tag}\r\n\
|
||||
Message-ID: {message_id}\r\n\
|
||||
Date: Wed, 01 Jan 2025 12:00:00 +0000\r\n\
|
||||
@@ -446,10 +447,10 @@ fn histogram_total(h: &HashMap<String, usize>) -> usize {
|
||||
}
|
||||
|
||||
pub fn flag_probe_message() -> Vec<u8> {
|
||||
let body = "From: probe-flags@vandelay.test\r\n\
|
||||
To: user@vandelay.test\r\n\
|
||||
let body = "From: probe-flags@inbuxa-migrate.test\r\n\
|
||||
To: user@inbuxa-migrate.test\r\n\
|
||||
Subject: Flag probe\r\n\
|
||||
Message-ID: <flag-probe@vandelay.test>\r\n\
|
||||
Message-ID: <flag-probe@inbuxa-migrate.test>\r\n\
|
||||
Date: Wed, 01 Jan 2025 12:00:00 +0000\r\n\
|
||||
\r\n\
|
||||
Flag probe body.\r\n";
|
||||
@@ -457,8 +458,8 @@ pub fn flag_probe_message() -> Vec<u8> {
|
||||
}
|
||||
|
||||
pub fn no_message_id_probe() -> Vec<u8> {
|
||||
let body = "From: nomid@vandelay.test\r\n\
|
||||
To: user@vandelay.test\r\n\
|
||||
let body = "From: nomid@inbuxa-migrate.test\r\n\
|
||||
To: user@inbuxa-migrate.test\r\n\
|
||||
Subject: No Message-ID probe\r\n\
|
||||
Date: Wed, 01 Jan 2025 12:00:00 +0000\r\n\
|
||||
\r\n\
|
||||
@@ -467,10 +468,10 @@ pub fn no_message_id_probe() -> Vec<u8> {
|
||||
}
|
||||
|
||||
pub fn shared_message_id_probe() -> Vec<u8> {
|
||||
let body = "From: shared@vandelay.test\r\n\
|
||||
To: user@vandelay.test\r\n\
|
||||
let body = "From: shared@inbuxa-migrate.test\r\n\
|
||||
To: user@inbuxa-migrate.test\r\n\
|
||||
Subject: Shared MID probe\r\n\
|
||||
Message-ID: <shared-mid@vandelay.test>\r\n\
|
||||
Message-ID: <shared-mid@inbuxa-migrate.test>\r\n\
|
||||
Date: Wed, 01 Jan 2025 12:00:00 +0000\r\n\
|
||||
\r\n\
|
||||
Shared MID body.\r\n";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -63,7 +64,7 @@ pub const ACCOUNT1: &str = "user1";
|
||||
pub const ACCOUNT2: &str = "user2";
|
||||
pub const ACCOUNT3: &str = "user3";
|
||||
|
||||
pub const PASSWORD: &str = "VandelayUser#2026";
|
||||
pub const PASSWORD: &str = "InbuxaUser#2026";
|
||||
|
||||
pub static MAILBOXES_DEEP: &[MailboxSpec] = &[
|
||||
mailbox!("proj", "Projects", None),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -22,7 +23,7 @@ pub mod webdav;
|
||||
|
||||
pub use error::{ContainerError, ContainerResult};
|
||||
|
||||
pub const OWNER_LABEL: &str = "art.stalw.vandelay.itest";
|
||||
pub const OWNER_LABEL: &str = "art.stalw.inbuxa-migrate.itest";
|
||||
|
||||
use std::sync::Once;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -9,13 +10,13 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use rusqlite::Connection;
|
||||
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::CommonConfig;
|
||||
use inbuxa_migrate::logging::Logger;
|
||||
use inbuxa_migrate::sync::CommonConfig;
|
||||
|
||||
pub fn tmp_archive(tag: &str) -> PathBuf {
|
||||
let mut p = std::env::temp_dir();
|
||||
p.push(format!(
|
||||
"vandelay-container-{tag}-{}-{}.sqlite",
|
||||
"inbuxa-migrate-container-{tag}-{}-{}.sqlite",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -137,7 +138,7 @@ pub struct WebDav {
|
||||
|
||||
impl WebDav {
|
||||
pub fn start() -> ContainerResult<Self> {
|
||||
let image: GenericImage = GenericBuildableImage::new("vandelay-webdav", "test")
|
||||
let image: GenericImage = GenericBuildableImage::new("inbuxa-migrate-webdav", "test")
|
||||
.with_dockerfile_string(DOCKERFILE.to_owned())
|
||||
.with_data(APACHE_CONF.as_bytes().to_vec(), "apache.conf")
|
||||
.with_data(htpasswd_for_test().into_bytes(), "htpasswd")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -16,11 +17,11 @@ use integration::validate::{
|
||||
contact_card_count, count, open_archive, tmp_archive,
|
||||
};
|
||||
|
||||
use inbuxa_migrate::error::Error;
|
||||
use inbuxa_migrate::sync::import_dav;
|
||||
use inbuxa_migrate::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg};
|
||||
use rusqlite::Connection;
|
||||
use serde_json::Value;
|
||||
use vandelay::error::Error;
|
||||
use vandelay::sync::import_dav;
|
||||
use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg};
|
||||
|
||||
fn dav_config(kind: DavKindArg, account: &Account, dav_root: &str) -> DavImportConfig {
|
||||
let path = match kind {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -16,9 +17,9 @@ use integration::validate::{
|
||||
};
|
||||
use integration::{Account, Endpoint};
|
||||
|
||||
use vandelay::error::Error;
|
||||
use vandelay::sync::import_imap;
|
||||
use vandelay::sync::import_imap::{ImapAuth, ImapImportConfig};
|
||||
use inbuxa_migrate::error::Error;
|
||||
use inbuxa_migrate::sync::import_imap;
|
||||
use inbuxa_migrate::sync::import_imap::{ImapAuth, ImapImportConfig};
|
||||
|
||||
fn imap_config(account: &Account, imap: &Endpoint) -> ImapImportConfig {
|
||||
ImapImportConfig {
|
||||
@@ -327,10 +328,10 @@ fn cyrus_non_ascii_mailbox_names_round_trip() {
|
||||
|
||||
let mutf7_name = "Envoy&AOk-s";
|
||||
let utf8_name = "Envoyés";
|
||||
let message = b"From: accents@vandelay.test\r\n\
|
||||
To: user1@vandelay.test\r\n\
|
||||
let message = b"From: accents@inbuxa-migrate.test\r\n\
|
||||
To: user1@inbuxa-migrate.test\r\n\
|
||||
Subject: Accents\r\n\
|
||||
Message-ID: <accents-1@vandelay.test>\r\n\
|
||||
Message-ID: <accents-1@inbuxa-migrate.test>\r\n\
|
||||
Date: Wed, 01 Jan 2025 12:00:00 +0000\r\n\
|
||||
\r\n\
|
||||
Accented folder probe.\r\n";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -16,14 +17,14 @@ use integration::validate::{
|
||||
mailbox_id_by_path, mailbox_path, open_archive, tmp_archive,
|
||||
};
|
||||
|
||||
use inbuxa_migrate::error::Error;
|
||||
use inbuxa_migrate::imap::client::{ConnectMode, ImapClient};
|
||||
use inbuxa_migrate::imap::transport::Connector;
|
||||
use inbuxa_migrate::logging::Logger;
|
||||
use inbuxa_migrate::sync::import_imap::{ImapAuth, ImapImportConfig};
|
||||
use inbuxa_migrate::sync::import_managesieve::{ManageSieveAuth, ManageSieveImportConfig};
|
||||
use inbuxa_migrate::sync::{import_imap, import_managesieve};
|
||||
use rusqlite::Connection;
|
||||
use vandelay::error::Error;
|
||||
use vandelay::imap::client::{ConnectMode, ImapClient};
|
||||
use vandelay::imap::transport::Connector;
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::import_imap::{ImapAuth, ImapImportConfig};
|
||||
use vandelay::sync::import_managesieve::{ManageSieveAuth, ManageSieveImportConfig};
|
||||
use vandelay::sync::{import_imap, import_managesieve};
|
||||
|
||||
fn imap_config(account: &Account, imap: &integration::Endpoint) -> ImapImportConfig {
|
||||
ImapImportConfig {
|
||||
@@ -526,10 +527,10 @@ fn dovecot_non_ascii_mailbox_names_round_trip() {
|
||||
|
||||
let mutf7_name = "Envoy&AOk-s";
|
||||
let utf8_name = "Envoyés";
|
||||
let message = b"From: accents@vandelay.test\r\n\
|
||||
To: user1@vandelay.test\r\n\
|
||||
let message = b"From: accents@inbuxa-migrate.test\r\n\
|
||||
To: user1@inbuxa-migrate.test\r\n\
|
||||
Subject: Accents\r\n\
|
||||
Message-ID: <accents-1@vandelay.test>\r\n\
|
||||
Message-ID: <accents-1@inbuxa-migrate.test>\r\n\
|
||||
Date: Wed, 01 Jan 2025 12:00:00 +0000\r\n\
|
||||
\r\n\
|
||||
Accented folder probe.\r\n";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -16,11 +17,11 @@ use integration::validate::{
|
||||
};
|
||||
use integration::{Account, Endpoint};
|
||||
|
||||
use inbuxa_migrate::error::Error;
|
||||
use inbuxa_migrate::sync::import_dav;
|
||||
use inbuxa_migrate::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg};
|
||||
use rusqlite::Connection;
|
||||
use serde_json::Value;
|
||||
use vandelay::error::Error;
|
||||
use vandelay::sync::import_dav;
|
||||
use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg};
|
||||
|
||||
fn dav_config(kind: DavKindArg, account: &Account, http: &Endpoint) -> DavImportConfig {
|
||||
DavImportConfig {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -15,9 +16,9 @@ use integration::validate::{
|
||||
use integration::webdav::{AccountSeed, WebDav};
|
||||
use integration::{Account, Endpoint};
|
||||
|
||||
use vandelay::error::Error;
|
||||
use vandelay::sync::import_dav;
|
||||
use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg};
|
||||
use inbuxa_migrate::error::Error;
|
||||
use inbuxa_migrate::sync::import_dav;
|
||||
use inbuxa_migrate::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg};
|
||||
|
||||
fn webdav_config(account: &Account, http: &Endpoint) -> DavImportConfig {
|
||||
DavImportConfig {
|
||||
@@ -319,7 +320,7 @@ fn webdav_starts_seeds_and_imports() {
|
||||
drop(conn);
|
||||
|
||||
let new_payload = format!(
|
||||
"added-{} content for vandelay webdav add-after-import probe\n",
|
||||
"added-{} content for inbuxa-migrate webdav add-after-import probe\n",
|
||||
seed.username
|
||||
);
|
||||
let new_payload_bytes = new_payload.as_bytes().to_vec();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -7,10 +8,10 @@
|
||||
mod integration;
|
||||
mod seeder;
|
||||
|
||||
use inbuxa_migrate::jmap::account::{self, AccountSelector};
|
||||
use inbuxa_migrate::jmap::http::{Auth, HttpClient, RetryPolicy};
|
||||
use inbuxa_migrate::jmap::session::Session;
|
||||
use integration::stalwart::shared as shared_stalwart;
|
||||
use vandelay::jmap::account::{self, AccountSelector};
|
||||
use vandelay::jmap::http::{Auth, HttpClient, RetryPolicy};
|
||||
use vandelay::jmap::session::Session;
|
||||
|
||||
fn admin_client() -> HttpClient {
|
||||
HttpClient::new(
|
||||
|
||||
+66
-51
@@ -1,17 +1,18 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
use vandelay::dav::client::DavClient;
|
||||
use vandelay::dav::discover::{DavKind, discover};
|
||||
use vandelay::dav::parse::{parse_multistatus, strip_ascii_control_chars};
|
||||
use vandelay::dav::xml;
|
||||
use vandelay::jmap::error::JmapError;
|
||||
use vandelay::jmap::http::{Auth, RetryPolicy};
|
||||
use inbuxa_migrate::dav::client::DavClient;
|
||||
use inbuxa_migrate::dav::discover::{DavKind, discover};
|
||||
use inbuxa_migrate::dav::parse::{parse_multistatus, strip_ascii_control_chars};
|
||||
use inbuxa_migrate::dav::xml;
|
||||
use inbuxa_migrate::jmap::error::JmapError;
|
||||
use inbuxa_migrate::jmap::http::{Auth, RetryPolicy};
|
||||
|
||||
fn client(retries: u32) -> DavClient {
|
||||
DavClient::new(
|
||||
@@ -75,7 +76,7 @@ fn discovery_resolves_per_user_principal_even_when_url_lists_collections() {
|
||||
r#"<?xml version="1.0"?>
|
||||
<d:multistatus xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||
<d:response>
|
||||
<d:href>{url}/dav/cal/secondary@vandelay.org/default/</d:href>
|
||||
<d:href>{url}/dav/cal/secondary@inbuxa-migrate.org/default/</d:href>
|
||||
<d:propstat>
|
||||
<d:prop>
|
||||
<d:resourcetype><d:collection/><c:calendar/></d:resourcetype>
|
||||
@@ -101,7 +102,7 @@ fn discovery_resolves_per_user_principal_even_when_url_lists_collections() {
|
||||
<d:href>{url}/dav/cal/</d:href>
|
||||
<d:propstat>
|
||||
<d:prop>
|
||||
<d:current-user-principal><d:href>{url}/dav/principals/secondary@vandelay.org/</d:href></d:current-user-principal>
|
||||
<d:current-user-principal><d:href>{url}/dav/principals/secondary@inbuxa-migrate.org/</d:href></d:current-user-principal>
|
||||
</d:prop>
|
||||
<d:status>HTTP/1.1 200 OK</d:status>
|
||||
</d:propstat>
|
||||
@@ -121,7 +122,7 @@ fn discovery_resolves_per_user_principal_even_when_url_lists_collections() {
|
||||
assert_eq!(disc.collections.len(), 1);
|
||||
assert_eq!(
|
||||
disc.principal_url.as_deref(),
|
||||
Some(format!("{url}/dav/principals/secondary@vandelay.org/").as_str()),
|
||||
Some(format!("{url}/dav/principals/secondary@inbuxa-migrate.org/").as_str()),
|
||||
"account identity must be the per-user principal, not the shared base DAV root"
|
||||
);
|
||||
}
|
||||
@@ -212,7 +213,7 @@ fn discovery_returns_not_found_when_no_collections() {
|
||||
let err = discover(&c, DavKind::Caldav, &server.url()).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
vandelay::dav::discover::DiscoveryError::NotFound { .. }
|
||||
inbuxa_migrate::dav::discover::DiscoveryError::NotFound { .. }
|
||||
));
|
||||
}
|
||||
|
||||
@@ -489,14 +490,14 @@ fn webdav_listing_returns_collection_and_file() {
|
||||
|
||||
#[test]
|
||||
fn google_usage_limits_403_classified_retryable() {
|
||||
use vandelay::dav::retry::{DavOutcome, classify};
|
||||
use inbuxa_migrate::dav::retry::{DavOutcome, classify};
|
||||
let body = br#"{"error":{"errors":[{"domain":"usageLimits"}]}}"#;
|
||||
assert_eq!(classify(403, body), DavOutcome::Retryable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_quota_403_is_auth() {
|
||||
use vandelay::dav::retry::{DavOutcome, classify};
|
||||
use inbuxa_migrate::dav::retry::{DavOutcome, classify};
|
||||
assert_eq!(classify(403, b"forbidden"), DavOutcome::Auth);
|
||||
}
|
||||
|
||||
@@ -801,7 +802,7 @@ fn webdav_cycle_breaks_on_lex_smallest() {
|
||||
|
||||
#[test]
|
||||
fn multiget_405_method_not_allowed_triggers_get_fallback() {
|
||||
use vandelay::dav::retry::{DavOutcome, classify};
|
||||
use inbuxa_migrate::dav::retry::{DavOutcome, classify};
|
||||
assert_eq!(classify(405, b""), DavOutcome::Fatal);
|
||||
}
|
||||
|
||||
@@ -893,8 +894,8 @@ fn streaming_propfind_404_returns_empty_responses() {
|
||||
|
||||
#[test]
|
||||
fn source_change_protection_rejects_different_account_id() {
|
||||
use inbuxa_migrate::db::{init, sources};
|
||||
use rusqlite::Connection;
|
||||
use vandelay::db::{init, sources};
|
||||
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
init::apply_schema(&conn).unwrap();
|
||||
@@ -1049,15 +1050,15 @@ fn webdav_discovery_keeps_only_self_row_as_root() {
|
||||
|
||||
#[test]
|
||||
fn webdav_root_collection_is_not_materialised_children_map_to_target_root() {
|
||||
use inbuxa_migrate::dav::discover::DiscoveredCollection;
|
||||
use inbuxa_migrate::dav::href::Href;
|
||||
use inbuxa_migrate::dav::parse::ResourceProps;
|
||||
use inbuxa_migrate::db;
|
||||
use inbuxa_migrate::db::sources::SourceKey;
|
||||
use inbuxa_migrate::logging::Logger;
|
||||
use inbuxa_migrate::sync::TypeCounts;
|
||||
use inbuxa_migrate::sync::import_dav::tree::{WebDavCtx, reconcile_filenodes};
|
||||
use rusqlite::Connection;
|
||||
use vandelay::dav::discover::DiscoveredCollection;
|
||||
use vandelay::dav::href::Href;
|
||||
use vandelay::dav::parse::ResourceProps;
|
||||
use vandelay::db;
|
||||
use vandelay::db::sources::SourceKey;
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::TypeCounts;
|
||||
use vandelay::sync::import_dav::tree::{WebDavCtx, reconcile_filenodes};
|
||||
|
||||
let mut server = mockito::Server::new();
|
||||
let url = server.url();
|
||||
@@ -1336,10 +1337,10 @@ fn webdav_display_or_basename_test_via_parser_keeps_basename_semantics() {
|
||||
|
||||
#[test]
|
||||
fn dry_run_writes_nothing_but_emits_per_collection_counts() {
|
||||
use inbuxa_migrate::logging::Logger;
|
||||
use inbuxa_migrate::sync::CommonConfig;
|
||||
use inbuxa_migrate::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg, run};
|
||||
use std::path::PathBuf;
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::CommonConfig;
|
||||
use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg, run};
|
||||
|
||||
let mut server = mockito::Server::new();
|
||||
let url = server.url();
|
||||
@@ -1381,7 +1382,7 @@ fn dry_run_writes_nothing_but_emits_per_collection_counts() {
|
||||
let _items = multistatus_response(&mut server, "PROPFIND", "/dav/cal/u/default/", &items_body);
|
||||
|
||||
let archive: PathBuf = std::env::temp_dir().join(format!(
|
||||
"vandelay-dryrun-{}-{}.sqlite",
|
||||
"inbuxa-migrate-dryrun-{}-{}.sqlite",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
@@ -1449,10 +1450,10 @@ fn dry_run_writes_nothing_but_emits_per_collection_counts() {
|
||||
#[test]
|
||||
fn dav_source_change_protection_fires_across_users_on_same_root() {
|
||||
use encodify::base64::STANDARD;
|
||||
use inbuxa_migrate::logging::Logger;
|
||||
use inbuxa_migrate::sync::CommonConfig;
|
||||
use inbuxa_migrate::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg, run};
|
||||
use std::path::PathBuf;
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::CommonConfig;
|
||||
use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg, run};
|
||||
|
||||
let mut server = mockito::Server::new();
|
||||
let url = server.url();
|
||||
@@ -1499,8 +1500,14 @@ fn dav_source_change_protection_fires_across_users_on_same_root() {
|
||||
.with_body(&empty_items)
|
||||
.create();
|
||||
|
||||
let auth_a = format!("Basic {}", STANDARD.encode("[email protected]:passA"));
|
||||
let auth_b = format!("Basic {}", STANDARD.encode("[email protected]:passB"));
|
||||
let auth_a = format!(
|
||||
"Basic {}",
|
||||
STANDARD.encode("[email protected]:passA")
|
||||
);
|
||||
let auth_b = format!(
|
||||
"Basic {}",
|
||||
STANDARD.encode("[email protected]:passB")
|
||||
);
|
||||
let principal = |who: &str| {
|
||||
format!(
|
||||
r#"<?xml version="1.0"?>
|
||||
@@ -1521,7 +1528,7 @@ fn dav_source_change_protection_fires_across_users_on_same_root() {
|
||||
.match_header("authorization", auth_a.as_str())
|
||||
.with_status(207)
|
||||
.with_header("content-type", "application/xml; charset=utf-8")
|
||||
.with_body(principal("secondary@vandelay.org"))
|
||||
.with_body(principal("secondary@inbuxa-migrate.org"))
|
||||
.create();
|
||||
let _pb = server
|
||||
.mock("PROPFIND", "/dav/cal/")
|
||||
@@ -1529,11 +1536,11 @@ fn dav_source_change_protection_fires_across_users_on_same_root() {
|
||||
.match_header("authorization", auth_b.as_str())
|
||||
.with_status(207)
|
||||
.with_header("content-type", "application/xml; charset=utf-8")
|
||||
.with_body(principal("tertiary@vandelay.org"))
|
||||
.with_body(principal("tertiary@inbuxa-migrate.org"))
|
||||
.create();
|
||||
|
||||
let archive: PathBuf = std::env::temp_dir().join(format!(
|
||||
"vandelay-dav-srcchange-{}-{}.sqlite",
|
||||
"inbuxa-migrate-dav-srcchange-{}-{}.sqlite",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
@@ -1563,19 +1570,27 @@ fn dav_source_change_protection_fires_across_users_on_same_root() {
|
||||
allow_source_change: false,
|
||||
};
|
||||
|
||||
run(common(&archive), config("[email protected]", "passA")).expect("user A import ok");
|
||||
let err = run(common(&archive), config("[email protected]", "passB")).unwrap_err();
|
||||
run(
|
||||
common(&archive),
|
||||
config("[email protected]", "passA"),
|
||||
)
|
||||
.expect("user A import ok");
|
||||
let err = run(
|
||||
common(&archive),
|
||||
config("[email protected]", "passB"),
|
||||
)
|
||||
.unwrap_err();
|
||||
let _ = std::fs::remove_file(&archive);
|
||||
assert!(
|
||||
matches!(err, vandelay::error::Error::SourceChange(_)),
|
||||
matches!(err, inbuxa_migrate::error::Error::SourceChange(_)),
|
||||
"importing a different user into the same archive must trigger source-change protection; got {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_change_protection_rejects_different_session_url_same_account() {
|
||||
use inbuxa_migrate::db::{init, sources};
|
||||
use rusqlite::Connection;
|
||||
use vandelay::db::{init, sources};
|
||||
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
init::apply_schema(&conn).unwrap();
|
||||
@@ -1627,8 +1642,8 @@ fn etag_missing_in_enumeration_propagates_to_storage_layer() {
|
||||
|
||||
#[test]
|
||||
fn mixed_source_archive_allows_distinct_kinds_against_same_url() {
|
||||
use inbuxa_migrate::db::{init, sources};
|
||||
use rusqlite::Connection;
|
||||
use vandelay::db::{init, sources};
|
||||
|
||||
let conn = Connection::open_in_memory().unwrap();
|
||||
init::apply_schema(&conn).unwrap();
|
||||
@@ -1663,7 +1678,7 @@ fn dav_archive() -> std::path::PathBuf {
|
||||
let n = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let mut p = std::env::temp_dir();
|
||||
p.push(format!(
|
||||
"vandelay-mockdav-{}-{:?}-{n}.sqlite",
|
||||
"inbuxa-migrate-mockdav-{}-{:?}-{n}.sqlite",
|
||||
std::process::id(),
|
||||
std::thread::current().id(),
|
||||
));
|
||||
@@ -1671,11 +1686,11 @@ fn dav_archive() -> std::path::PathBuf {
|
||||
p
|
||||
}
|
||||
|
||||
fn caldav_import_config(url: &str) -> vandelay::sync::import_dav::DavImportConfig {
|
||||
vandelay::sync::import_dav::DavImportConfig {
|
||||
kind: vandelay::sync::import_dav::DavKindArg::Caldav,
|
||||
fn caldav_import_config(url: &str) -> inbuxa_migrate::sync::import_dav::DavImportConfig {
|
||||
inbuxa_migrate::sync::import_dav::DavImportConfig {
|
||||
kind: inbuxa_migrate::sync::import_dav::DavKindArg::Caldav,
|
||||
url: url.to_owned(),
|
||||
auth: vandelay::sync::import_dav::DavAuth::Basic {
|
||||
auth: inbuxa_migrate::sync::import_dav::DavAuth::Basic {
|
||||
user: "u".to_owned(),
|
||||
password: "p".to_owned(),
|
||||
},
|
||||
@@ -1686,14 +1701,14 @@ fn caldav_import_config(url: &str) -> vandelay::sync::import_dav::DavImportConfi
|
||||
}
|
||||
}
|
||||
|
||||
fn dav_common(archive: &std::path::Path) -> vandelay::sync::CommonConfig {
|
||||
vandelay::sync::CommonConfig {
|
||||
fn dav_common(archive: &std::path::Path) -> inbuxa_migrate::sync::CommonConfig {
|
||||
inbuxa_migrate::sync::CommonConfig {
|
||||
archive: archive.to_path_buf(),
|
||||
threads: 1,
|
||||
dry_run: false,
|
||||
max_retries: 0,
|
||||
allow_invalid_certs: false,
|
||||
logger: vandelay::logging::Logger::from_flags(true, 0),
|
||||
logger: inbuxa_migrate::logging::Logger::from_flags(true, 0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1788,7 +1803,7 @@ END:VCALENDAR
|
||||
.create();
|
||||
|
||||
let archive = dav_archive();
|
||||
let summary = vandelay::sync::import_dav::run(
|
||||
let summary = inbuxa_migrate::sync::import_dav::run(
|
||||
dav_common(&archive),
|
||||
caldav_import_config(&format!("{url}/dav/cal/u/")),
|
||||
)
|
||||
@@ -1828,14 +1843,14 @@ fn credentials_rejected_during_discovery_still_aborts_the_run() {
|
||||
.create();
|
||||
|
||||
let archive = dav_archive();
|
||||
let err = vandelay::sync::import_dav::run(
|
||||
let err = inbuxa_migrate::sync::import_dav::run(
|
||||
dav_common(&archive),
|
||||
caldav_import_config(&format!("{url}/dav/cal/u/")),
|
||||
)
|
||||
.expect_err("credentials rejected for the principal aborts the run");
|
||||
|
||||
assert!(
|
||||
matches!(err, vandelay::error::Error::Connection(_)),
|
||||
matches!(err, inbuxa_migrate::error::Error::Connection(_)),
|
||||
"discovery-level auth rejection is a whole-run failure, got {err:?}"
|
||||
);
|
||||
assert_eq!(err.exit_code(), 2);
|
||||
@@ -1871,7 +1886,7 @@ fn dav_import_reports_both_phases_when_every_collection_fails() {
|
||||
.create();
|
||||
|
||||
let archive = dav_archive();
|
||||
let outcome = vandelay::sync::import_dav::run_reporting(
|
||||
let outcome = inbuxa_migrate::sync::import_dav::run_reporting(
|
||||
dav_common(&archive),
|
||||
caldav_import_config(&format!("{url}/dav/cal/u/")),
|
||||
);
|
||||
|
||||
+40
-37
@@ -1,25 +1,28 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
|
||||
use encodify::base64::{LENIENT, STANDARD};
|
||||
use mockito::Matcher;
|
||||
use vandelay::exchange_ews::EwsClient;
|
||||
use vandelay::exchange_ews::autodiscover::{DiscoverySource, discover};
|
||||
use vandelay::exchange_ews::error::EwsError;
|
||||
use vandelay::exchange_ews::parse::{
|
||||
use inbuxa_migrate::exchange_ews::EwsClient;
|
||||
use inbuxa_migrate::exchange_ews::autodiscover::{DiscoverySource, discover};
|
||||
use inbuxa_migrate::exchange_ews::error::EwsError;
|
||||
use inbuxa_migrate::exchange_ews::parse::{
|
||||
EnvelopeKind, parse_find_folder_response, parse_find_item_response,
|
||||
parse_get_attachment_inline, parse_response_messages, parse_sync_folder_items_response,
|
||||
read_envelope_summary,
|
||||
};
|
||||
use vandelay::exchange_ews::types::{FolderClass, FolderId, ItemId, ResponseCode, ServerVersion};
|
||||
use vandelay::exchange_ews::xml::{
|
||||
use inbuxa_migrate::exchange_ews::types::{
|
||||
FolderClass, FolderId, ItemId, ResponseCode, ServerVersion,
|
||||
};
|
||||
use inbuxa_migrate::exchange_ews::xml::{
|
||||
FolderRef, ItemShape, Traversal, find_folder_body, find_item_body, get_attachment_body,
|
||||
get_item_body, sync_folder_items_body,
|
||||
};
|
||||
use vandelay::jmap::http::{Auth, RetryPolicy};
|
||||
use inbuxa_migrate::jmap::http::{Auth, RetryPolicy};
|
||||
use mockito::Matcher;
|
||||
|
||||
const TXT_XML: &str = "text/xml; charset=utf-8";
|
||||
const APP_JSON: &str = "application/json";
|
||||
@@ -59,7 +62,7 @@ fn autodiscover_v2_returns_global_endpoint() {
|
||||
.create();
|
||||
let _ = server;
|
||||
let url = "https://outlook.office365.com/EWS/Exchange.asmx";
|
||||
assert!(vandelay::exchange_ews::autodiscover::is_fully_qualified_ews_url(url));
|
||||
assert!(inbuxa_migrate::exchange_ews::autodiscover::is_fully_qualified_ews_url(url));
|
||||
let r = discover(Some(url), None, None, false).unwrap();
|
||||
assert_eq!(r.source, DiscoverySource::SuppliedUrl);
|
||||
assert_eq!(r.ews_url, url);
|
||||
@@ -265,7 +268,7 @@ fn server_busy_with_back_off_triggers_retry_and_eventually_succeeds() {
|
||||
let c = client(3);
|
||||
let body = find_folder_body(
|
||||
FolderRef::Distinguished(
|
||||
vandelay::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot,
|
||||
inbuxa_migrate::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot,
|
||||
),
|
||||
Traversal::Deep,
|
||||
);
|
||||
@@ -290,7 +293,7 @@ fn http_401_surfaces_as_auth_error() {
|
||||
let c = client(0);
|
||||
let body = find_folder_body(
|
||||
FolderRef::Distinguished(
|
||||
vandelay::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot,
|
||||
inbuxa_migrate::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot,
|
||||
),
|
||||
Traversal::Deep,
|
||||
);
|
||||
@@ -313,7 +316,7 @@ fn mime_content_round_trips_through_base64_decode() {
|
||||
</m:ResponseMessages></m:GetItemResponse>"
|
||||
));
|
||||
let r = parse_response_messages(body.as_bytes(), "GetItemResponseMessage").unwrap();
|
||||
let item = vandelay::exchange_ews::parse::parse_message_item(&r[0].inner_xml).unwrap();
|
||||
let item = inbuxa_migrate::exchange_ews::parse::parse_message_item(&r[0].inner_xml).unwrap();
|
||||
let s = item.mime_content.unwrap();
|
||||
let bytes = LENIENT.decode(&s).unwrap();
|
||||
assert_eq!(bytes, original);
|
||||
@@ -341,7 +344,7 @@ fn get_attachment_inline_decodes_photo_blob() {
|
||||
|
||||
#[test]
|
||||
fn calendar_item_master_inlines_modified_and_deleted_occurrences() {
|
||||
let body = "<vandelay-inner xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">\
|
||||
let body = "<inbuxa-migrate-inner xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">\
|
||||
<t:CalendarItem>\
|
||||
<t:ItemId Id=\"M1\" ChangeKey=\"K1\"/>\
|
||||
<t:Subject>Daily</t:Subject>\
|
||||
@@ -359,8 +362,8 @@ fn calendar_item_master_inlines_modified_and_deleted_occurrences() {
|
||||
<t:DeletedOccurrences>\
|
||||
<t:DeletedOccurrence><t:Start>2025-06-17T14:00:00Z</t:Start></t:DeletedOccurrence>\
|
||||
</t:DeletedOccurrences>\
|
||||
</t:CalendarItem></vandelay-inner>";
|
||||
let item = vandelay::exchange_ews::parse::parse_calendar_item(body).unwrap();
|
||||
</t:CalendarItem></inbuxa-migrate-inner>";
|
||||
let item = inbuxa_migrate::exchange_ews::parse::parse_calendar_item(body).unwrap();
|
||||
assert_eq!(item.id.id, "M1");
|
||||
assert_eq!(item.uid.as_deref(), Some("uid-1"));
|
||||
assert_eq!(item.modified_occurrences.len(), 1);
|
||||
@@ -373,7 +376,7 @@ fn calendar_item_master_inlines_modified_and_deleted_occurrences() {
|
||||
|
||||
#[test]
|
||||
fn organizer_and_attendee_addresses_survive_an_ex_routing_type() {
|
||||
let body = "<vandelay-inner xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">\
|
||||
let body = "<inbuxa-migrate-inner xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">\
|
||||
<t:CalendarItem>\
|
||||
<t:ItemId Id=\"M3\" ChangeKey=\"K1\"/>\
|
||||
<t:Subject>Review</t:Subject>\
|
||||
@@ -386,9 +389,9 @@ fn organizer_and_attendee_addresses_survive_an_ex_routing_type() {
|
||||
<t:RequiredAttendees><t:Attendee><t:Mailbox><t:Name>Kristina Morgental</t:Name>\
|
||||
<t:EmailAddress>/o=ExchangeLabs/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=bdc77b18152647a29d28ce1188376dc9-kristina</t:EmailAddress>\
|
||||
<t:RoutingType>EX</t:RoutingType></t:Mailbox><t:ResponseType>Unknown</t:ResponseType></t:Attendee></t:RequiredAttendees>\
|
||||
</t:CalendarItem></vandelay-inner>";
|
||||
let item = vandelay::exchange_ews::parse::parse_calendar_item(body).unwrap();
|
||||
let event = vandelay::exchange_ews::calendar_map::to_jscalendar(&item);
|
||||
</t:CalendarItem></inbuxa-migrate-inner>";
|
||||
let item = inbuxa_migrate::exchange_ews::parse::parse_calendar_item(body).unwrap();
|
||||
let event = inbuxa_migrate::exchange_ews::calendar_map::to_jscalendar(&item);
|
||||
|
||||
assert_eq!(
|
||||
event.data["organizerCalendarAddress"], "mailto:[email protected]",
|
||||
@@ -409,7 +412,7 @@ fn organizer_and_attendee_addresses_survive_an_ex_routing_type() {
|
||||
attendee["calendarAddress"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.starts_with("urn:x-vandelay:attendee:"),
|
||||
.starts_with("urn:x-inbuxa-migrate:attendee:"),
|
||||
"a legacy directory reference cannot be resolved and stays synthetic"
|
||||
);
|
||||
assert!(
|
||||
@@ -420,7 +423,7 @@ fn organizer_and_attendee_addresses_survive_an_ex_routing_type() {
|
||||
|
||||
#[test]
|
||||
fn recurrence_end_date_with_a_timezone_offset_yields_a_bounded_series() {
|
||||
let body = "<vandelay-inner xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">\
|
||||
let body = "<inbuxa-migrate-inner xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\">\
|
||||
<t:CalendarItem>\
|
||||
<t:ItemId Id=\"M2\" ChangeKey=\"K1\"/>\
|
||||
<t:Subject>Biweekly</t:Subject>\
|
||||
@@ -432,9 +435,9 @@ fn recurrence_end_date_with_a_timezone_offset_yields_a_bounded_series() {
|
||||
<t:WeeklyRecurrence><t:Interval>2</t:Interval><t:DaysOfWeek>Wednesday</t:DaysOfWeek></t:WeeklyRecurrence>\
|
||||
<t:EndDateRecurrence><t:StartDate>2021-08-04-06:00</t:StartDate><t:EndDate>2021-09-30-06:00</t:EndDate></t:EndDateRecurrence>\
|
||||
</t:Recurrence>\
|
||||
</t:CalendarItem></vandelay-inner>";
|
||||
let item = vandelay::exchange_ews::parse::parse_calendar_item(body).unwrap();
|
||||
let event = vandelay::exchange_ews::calendar_map::to_jscalendar(&item);
|
||||
</t:CalendarItem></inbuxa-migrate-inner>";
|
||||
let item = inbuxa_migrate::exchange_ews::parse::parse_calendar_item(body).unwrap();
|
||||
let event = inbuxa_migrate::exchange_ews::calendar_map::to_jscalendar(&item);
|
||||
let rule = &event.data["recurrenceRule"];
|
||||
assert_eq!(rule["frequency"], "weekly");
|
||||
assert_eq!(
|
||||
@@ -486,8 +489,8 @@ fn invalid_sync_state_data_fault_is_surfaced() {
|
||||
|
||||
#[test]
|
||||
fn coordinator_diff_classifies_new_vanished_changed_unchanged() {
|
||||
use vandelay::db::exchange_ews_ids::ItemRow;
|
||||
use vandelay::sync::import_exchange_ews::items::{EnumeratedItem, diff};
|
||||
use inbuxa_migrate::db::exchange_ews_ids::ItemRow;
|
||||
use inbuxa_migrate::sync::import_exchange_ews::items::{EnumeratedItem, diff};
|
||||
|
||||
let server = vec![
|
||||
EnumeratedItem {
|
||||
@@ -532,9 +535,9 @@ fn coordinator_diff_classifies_new_vanished_changed_unchanged() {
|
||||
|
||||
#[test]
|
||||
fn source_change_protection_refuses_different_account() {
|
||||
use inbuxa_migrate::db;
|
||||
use inbuxa_migrate::db::sources::SourceKey;
|
||||
use rusqlite::Connection;
|
||||
use vandelay::db;
|
||||
use vandelay::db::sources::SourceKey;
|
||||
|
||||
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
let conn = Connection::open(tmp.path()).unwrap();
|
||||
@@ -557,9 +560,9 @@ fn source_change_protection_refuses_different_account() {
|
||||
|
||||
#[test]
|
||||
fn mailbox_kinds_are_three_separate_sources() {
|
||||
use inbuxa_migrate::db;
|
||||
use inbuxa_migrate::db::sources::SourceKey;
|
||||
use rusqlite::Connection;
|
||||
use vandelay::db;
|
||||
use vandelay::db::sources::SourceKey;
|
||||
|
||||
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
let conn = Connection::open(tmp.path()).unwrap();
|
||||
@@ -653,7 +656,7 @@ fn http_500_with_server_busy_body_is_treated_as_fault_and_retried() {
|
||||
let c = client(3);
|
||||
let body = find_folder_body(
|
||||
FolderRef::Distinguished(
|
||||
vandelay::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot,
|
||||
inbuxa_migrate::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot,
|
||||
),
|
||||
Traversal::Deep,
|
||||
);
|
||||
@@ -710,7 +713,7 @@ fn invalid_server_version_fault_downgrades_and_succeeds() {
|
||||
assert_eq!(c.server_version(), ServerVersion::Exchange2013Sp1);
|
||||
let body = find_folder_body(
|
||||
FolderRef::Distinguished(
|
||||
vandelay::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot,
|
||||
inbuxa_migrate::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot,
|
||||
),
|
||||
Traversal::Deep,
|
||||
);
|
||||
@@ -748,7 +751,7 @@ fn unsupported_version_floor_surfaces_as_soap_fault() {
|
||||
let c = client(0);
|
||||
let body = find_folder_body(
|
||||
FolderRef::Distinguished(
|
||||
vandelay::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot,
|
||||
inbuxa_migrate::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot,
|
||||
),
|
||||
Traversal::Deep,
|
||||
);
|
||||
@@ -801,7 +804,7 @@ fn server_affinity_cookie_is_captured_and_resent() {
|
||||
let c = client(0);
|
||||
let body = find_folder_body(
|
||||
FolderRef::Distinguished(
|
||||
vandelay::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot,
|
||||
inbuxa_migrate::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot,
|
||||
),
|
||||
Traversal::Deep,
|
||||
);
|
||||
@@ -823,7 +826,7 @@ fn http_456_surfaces_as_account_locked_auth_error() {
|
||||
let c = client(0);
|
||||
let body = find_folder_body(
|
||||
FolderRef::Distinguished(
|
||||
vandelay::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot,
|
||||
inbuxa_migrate::exchange_ews::types::DistinguishedFolderId::MsgFolderRoot,
|
||||
),
|
||||
Traversal::Deep,
|
||||
);
|
||||
@@ -836,8 +839,8 @@ fn http_456_surfaces_as_account_locked_auth_error() {
|
||||
|
||||
#[test]
|
||||
fn for_each_fetched_item_streams_every_id_across_windows() {
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::import_exchange_ews::items::{ItemRunCtx, for_each_fetched_item};
|
||||
use inbuxa_migrate::logging::Logger;
|
||||
use inbuxa_migrate::sync::import_exchange_ews::items::{ItemRunCtx, for_each_fetched_item};
|
||||
|
||||
let mut server = mockito::Server::new();
|
||||
let url = format!("{}/EWS/Exchange.asmx", server.url());
|
||||
|
||||
+151
-147
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -7,21 +8,21 @@
|
||||
use std::sync::Once;
|
||||
|
||||
use encodify::base64::URL_SAFE_NO_PAD;
|
||||
use mockito::{Matcher, Server};
|
||||
use serde_json::json;
|
||||
use vandelay::exchange_graph::api::{Endpoints, collect_all_ids, paged_collect};
|
||||
use vandelay::exchange_graph::calendar_map::{EventType, classify_event_type, convert_event};
|
||||
use vandelay::exchange_graph::client::{Accept, GraphClient};
|
||||
use vandelay::exchange_graph::contact_map::convert_contact;
|
||||
use vandelay::exchange_graph::error::GraphError;
|
||||
use vandelay::exchange_graph::oauth::{
|
||||
use inbuxa_migrate::exchange_graph::api::{Endpoints, collect_all_ids, paged_collect};
|
||||
use inbuxa_migrate::exchange_graph::calendar_map::{EventType, classify_event_type, convert_event};
|
||||
use inbuxa_migrate::exchange_graph::client::{Accept, GraphClient};
|
||||
use inbuxa_migrate::exchange_graph::contact_map::convert_contact;
|
||||
use inbuxa_migrate::exchange_graph::error::GraphError;
|
||||
use inbuxa_migrate::exchange_graph::oauth::{
|
||||
DeviceCodeResponse, TokenResponse, parse_device_code_response, parse_token_response,
|
||||
run_device_code_polling_against,
|
||||
};
|
||||
use vandelay::exchange_graph::recurrence::convert_patterned_recurrence;
|
||||
use vandelay::exchange_graph::retry::{HttpClass, classify_http_status};
|
||||
use vandelay::exchange_graph::types::Surfaces;
|
||||
use vandelay::jmap::http::RetryPolicy;
|
||||
use inbuxa_migrate::exchange_graph::recurrence::convert_patterned_recurrence;
|
||||
use inbuxa_migrate::exchange_graph::retry::{HttpClass, classify_http_status};
|
||||
use inbuxa_migrate::exchange_graph::types::Surfaces;
|
||||
use inbuxa_migrate::jmap::http::RetryPolicy;
|
||||
use mockito::{Matcher, Server};
|
||||
use serde_json::json;
|
||||
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
@@ -525,7 +526,7 @@ fn contact_minimal_yields_jscontact_card() {
|
||||
"emailAddresses": [{"name": "Alice", "address": "[email protected]"}]
|
||||
});
|
||||
let out = convert_contact(&c).unwrap();
|
||||
assert!(out.uid.starts_with("vandelay-graph-"));
|
||||
assert!(out.uid.starts_with("inbuxa-migrate-graph-"));
|
||||
assert_eq!(out.data["@type"], "Card");
|
||||
assert_eq!(out.data["name"]["full"], "Alice Liddell");
|
||||
let emails = out.data["emails"].as_object().unwrap();
|
||||
@@ -569,23 +570,23 @@ fn integration_dry_run_against_mock_server_lists_three_surfaces() {
|
||||
.with_body(r#"{"value":[{"id":"CON1","displayName":"Contacts"}]}"#)
|
||||
.create();
|
||||
let base = server.url();
|
||||
let common = vandelay::sync::CommonConfig {
|
||||
let common = inbuxa_migrate::sync::CommonConfig {
|
||||
archive: tempfile::NamedTempFile::new().unwrap().path().to_owned(),
|
||||
threads: 2,
|
||||
dry_run: true,
|
||||
max_retries: 0,
|
||||
allow_invalid_certs: false,
|
||||
logger: vandelay::logging::Logger::new(0),
|
||||
logger: inbuxa_migrate::logging::Logger::new(0),
|
||||
};
|
||||
let config = vandelay::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
let config = inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
token: make_jwt(9999999999, "[email protected]"),
|
||||
},
|
||||
api_base: base.clone(),
|
||||
user_target: None,
|
||||
mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary,
|
||||
mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary,
|
||||
surfaces: Surfaces::ALL,
|
||||
event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text,
|
||||
event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text,
|
||||
graph_connections: 2,
|
||||
top: 100,
|
||||
exception_window_years: 5,
|
||||
@@ -593,7 +594,7 @@ fn integration_dry_run_against_mock_server_lists_three_surfaces() {
|
||||
event_attachments: true,
|
||||
allow_source_change: false,
|
||||
};
|
||||
let summary = vandelay::sync::import_exchange_graph::run(common, config).unwrap();
|
||||
let summary = inbuxa_migrate::sync::import_exchange_graph::run(common, config).unwrap();
|
||||
let mailbox = summary
|
||||
.per_type
|
||||
.iter()
|
||||
@@ -681,23 +682,23 @@ fn integration_full_run_mail_only_imports_mime_via_value() {
|
||||
|
||||
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
let archive_path = tmp.path().to_owned();
|
||||
let common = vandelay::sync::CommonConfig {
|
||||
let common = inbuxa_migrate::sync::CommonConfig {
|
||||
archive: archive_path.clone(),
|
||||
threads: 2,
|
||||
dry_run: false,
|
||||
max_retries: 0,
|
||||
allow_invalid_certs: false,
|
||||
logger: vandelay::logging::Logger::new(0),
|
||||
logger: inbuxa_migrate::logging::Logger::new(0),
|
||||
};
|
||||
let config = vandelay::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
let config = inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
token: make_jwt(9999999999, "[email protected]"),
|
||||
},
|
||||
api_base: server.url(),
|
||||
user_target: None,
|
||||
mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary,
|
||||
mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary,
|
||||
surfaces: surfaces("mail"),
|
||||
event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text,
|
||||
event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text,
|
||||
graph_connections: 2,
|
||||
top: 100,
|
||||
exception_window_years: 5,
|
||||
@@ -706,7 +707,7 @@ fn integration_full_run_mail_only_imports_mime_via_value() {
|
||||
allow_source_change: false,
|
||||
};
|
||||
drop(tmp);
|
||||
let summary = vandelay::sync::import_exchange_graph::run(common, config).unwrap();
|
||||
let summary = inbuxa_migrate::sync::import_exchange_graph::run(common, config).unwrap();
|
||||
let emails = summary
|
||||
.per_type
|
||||
.iter()
|
||||
@@ -780,23 +781,23 @@ fn integration_duplicate_message_id_does_not_abort_run() {
|
||||
|
||||
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
let archive_path = tmp.path().to_owned();
|
||||
let common = vandelay::sync::CommonConfig {
|
||||
let common = inbuxa_migrate::sync::CommonConfig {
|
||||
archive: archive_path.clone(),
|
||||
threads: 2,
|
||||
dry_run: false,
|
||||
max_retries: 0,
|
||||
allow_invalid_certs: false,
|
||||
logger: vandelay::logging::Logger::new(0),
|
||||
logger: inbuxa_migrate::logging::Logger::new(0),
|
||||
};
|
||||
let config = vandelay::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
let config = inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
token: make_jwt(9999999999, "[email protected]"),
|
||||
},
|
||||
api_base: server.url(),
|
||||
user_target: None,
|
||||
mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary,
|
||||
mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary,
|
||||
surfaces: surfaces("mail"),
|
||||
event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text,
|
||||
event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text,
|
||||
graph_connections: 2,
|
||||
top: 100,
|
||||
exception_window_years: 5,
|
||||
@@ -805,7 +806,7 @@ fn integration_duplicate_message_id_does_not_abort_run() {
|
||||
allow_source_change: false,
|
||||
};
|
||||
drop(tmp);
|
||||
let summary = vandelay::sync::import_exchange_graph::run(common, config).unwrap();
|
||||
let summary = inbuxa_migrate::sync::import_exchange_graph::run(common, config).unwrap();
|
||||
let emails = summary
|
||||
.per_type
|
||||
.iter()
|
||||
@@ -880,15 +881,15 @@ fn integration_full_run_is_convergent_on_second_invocation() {
|
||||
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
let api_base = server.url();
|
||||
let make_config = || vandelay::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
let make_config = || inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
token: make_jwt(9999999999, "[email protected]"),
|
||||
},
|
||||
api_base: api_base.clone(),
|
||||
user_target: None,
|
||||
mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary,
|
||||
mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary,
|
||||
surfaces: surfaces("mail"),
|
||||
event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text,
|
||||
event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text,
|
||||
graph_connections: 2,
|
||||
top: 100,
|
||||
exception_window_years: 5,
|
||||
@@ -896,18 +897,20 @@ fn integration_full_run_is_convergent_on_second_invocation() {
|
||||
event_attachments: true,
|
||||
allow_source_change: false,
|
||||
};
|
||||
let make_common = |path: std::path::PathBuf| vandelay::sync::CommonConfig {
|
||||
let make_common = |path: std::path::PathBuf| inbuxa_migrate::sync::CommonConfig {
|
||||
archive: path,
|
||||
threads: 2,
|
||||
dry_run: false,
|
||||
max_retries: 0,
|
||||
allow_invalid_certs: false,
|
||||
logger: vandelay::logging::Logger::new(0),
|
||||
logger: inbuxa_migrate::logging::Logger::new(0),
|
||||
};
|
||||
|
||||
let first =
|
||||
vandelay::sync::import_exchange_graph::run(make_common(archive.clone()), make_config())
|
||||
.unwrap();
|
||||
let first = inbuxa_migrate::sync::import_exchange_graph::run(
|
||||
make_common(archive.clone()),
|
||||
make_config(),
|
||||
)
|
||||
.unwrap();
|
||||
let created_first = first
|
||||
.per_type
|
||||
.iter()
|
||||
@@ -917,7 +920,8 @@ fn integration_full_run_is_convergent_on_second_invocation() {
|
||||
assert_eq!(created_first, 1);
|
||||
|
||||
let second =
|
||||
vandelay::sync::import_exchange_graph::run(make_common(archive), make_config()).unwrap();
|
||||
inbuxa_migrate::sync::import_exchange_graph::run(make_common(archive), make_config())
|
||||
.unwrap();
|
||||
let created_second = second
|
||||
.per_type
|
||||
.iter()
|
||||
@@ -930,10 +934,10 @@ fn integration_full_run_is_convergent_on_second_invocation() {
|
||||
#[test]
|
||||
fn source_change_protection_refuses_a_different_account() {
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
let conn = vandelay::db::init::open(&archive).unwrap();
|
||||
vandelay::db::sources::upsert_source(
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
inbuxa_migrate::db::sources::upsert_source(
|
||||
&conn,
|
||||
&vandelay::db::sources::SourceKey {
|
||||
&inbuxa_migrate::db::sources::SourceKey {
|
||||
kind: "exchange_graph".to_owned(),
|
||||
session_url:
|
||||
"https://login.microsoftonline.com/common|https://graph.microsoft.com/v1.0"
|
||||
@@ -954,23 +958,23 @@ fn source_change_protection_refuses_a_different_account() {
|
||||
.with_body(r#"{"id":"new-user-id","userPrincipalName":"[email protected]"}"#)
|
||||
.create();
|
||||
|
||||
let common = vandelay::sync::CommonConfig {
|
||||
let common = inbuxa_migrate::sync::CommonConfig {
|
||||
archive,
|
||||
threads: 2,
|
||||
dry_run: false,
|
||||
max_retries: 0,
|
||||
allow_invalid_certs: false,
|
||||
logger: vandelay::logging::Logger::new(0),
|
||||
logger: inbuxa_migrate::logging::Logger::new(0),
|
||||
};
|
||||
let config = vandelay::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
let config = inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
token: make_jwt(9999999999, "[email protected]"),
|
||||
},
|
||||
api_base: server.url(),
|
||||
user_target: None,
|
||||
mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary,
|
||||
mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary,
|
||||
surfaces: Surfaces::ALL,
|
||||
event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text,
|
||||
event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text,
|
||||
graph_connections: 2,
|
||||
top: 100,
|
||||
exception_window_years: 5,
|
||||
@@ -978,9 +982,9 @@ fn source_change_protection_refuses_a_different_account() {
|
||||
event_attachments: true,
|
||||
allow_source_change: false,
|
||||
};
|
||||
let err = vandelay::sync::import_exchange_graph::run(common, config).unwrap_err();
|
||||
let err = inbuxa_migrate::sync::import_exchange_graph::run(common, config).unwrap_err();
|
||||
assert!(
|
||||
matches!(err, vandelay::error::Error::SourceChange(_)),
|
||||
matches!(err, inbuxa_migrate::error::Error::SourceChange(_)),
|
||||
"expected SourceChange error, got {err:?}"
|
||||
);
|
||||
}
|
||||
@@ -1011,35 +1015,35 @@ fn stub_well_known_folders(server: &mut Server, inbox_id: &str) {
|
||||
.create();
|
||||
}
|
||||
|
||||
fn make_common(archive: std::path::PathBuf) -> vandelay::sync::CommonConfig {
|
||||
vandelay::sync::CommonConfig {
|
||||
fn make_common(archive: std::path::PathBuf) -> inbuxa_migrate::sync::CommonConfig {
|
||||
inbuxa_migrate::sync::CommonConfig {
|
||||
archive,
|
||||
threads: 2,
|
||||
dry_run: false,
|
||||
max_retries: 0,
|
||||
allow_invalid_certs: false,
|
||||
logger: vandelay::logging::Logger::new(0),
|
||||
logger: inbuxa_migrate::logging::Logger::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn surfaces(list: &str) -> vandelay::exchange_graph::types::Surfaces {
|
||||
vandelay::exchange_graph::types::Surfaces::parse_list(list).unwrap()
|
||||
fn surfaces(list: &str) -> inbuxa_migrate::exchange_graph::types::Surfaces {
|
||||
inbuxa_migrate::exchange_graph::types::Surfaces::parse_list(list).unwrap()
|
||||
}
|
||||
|
||||
fn make_config(
|
||||
api_base: String,
|
||||
user_target: Option<String>,
|
||||
surfaces: vandelay::exchange_graph::types::Surfaces,
|
||||
) -> vandelay::sync::import_exchange_graph::GraphImportConfig {
|
||||
vandelay::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
surfaces: inbuxa_migrate::exchange_graph::types::Surfaces,
|
||||
) -> inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig {
|
||||
inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
token: make_jwt(9999999999, "[email protected]"),
|
||||
},
|
||||
api_base,
|
||||
user_target,
|
||||
mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary,
|
||||
mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary,
|
||||
surfaces,
|
||||
event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text,
|
||||
event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text,
|
||||
graph_connections: 2,
|
||||
top: 100,
|
||||
exception_window_years: 5,
|
||||
@@ -1097,15 +1101,15 @@ fn users_upn_routes_through_users_segment_not_me() {
|
||||
.expect_at_least(0)
|
||||
.create();
|
||||
let base = server.url();
|
||||
let common = vandelay::sync::CommonConfig {
|
||||
let common = inbuxa_migrate::sync::CommonConfig {
|
||||
archive: tempfile::NamedTempFile::new().unwrap().path().to_owned(),
|
||||
threads: 2,
|
||||
dry_run: true,
|
||||
max_retries: 0,
|
||||
allow_invalid_certs: false,
|
||||
logger: vandelay::logging::Logger::new(0),
|
||||
logger: inbuxa_migrate::logging::Logger::new(0),
|
||||
};
|
||||
let summary = vandelay::sync::import_exchange_graph::run(
|
||||
let summary = inbuxa_migrate::sync::import_exchange_graph::run(
|
||||
common,
|
||||
make_config(base, Some("[email protected]".to_owned()), Surfaces::ALL),
|
||||
)
|
||||
@@ -1207,7 +1211,7 @@ fn series_master_with_exception_merges_into_recurrence_overrides() {
|
||||
.create();
|
||||
let base = server.url();
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
let summary = vandelay::sync::import_exchange_graph::run(
|
||||
let summary = inbuxa_migrate::sync::import_exchange_graph::run(
|
||||
make_common(archive.clone()),
|
||||
make_config(base, None, surfaces("calendar")),
|
||||
)
|
||||
@@ -1220,7 +1224,7 @@ fn series_master_with_exception_merges_into_recurrence_overrides() {
|
||||
.unwrap_or(0);
|
||||
assert_eq!(events, 1, "exactly one master row, exception merged in");
|
||||
|
||||
let conn = vandelay::db::init::open(&archive).unwrap();
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
let row: String = conn
|
||||
.query_row("SELECT data FROM calendar_events", [], |row| row.get(0))
|
||||
.unwrap();
|
||||
@@ -1312,7 +1316,7 @@ fn occurrence_event_is_skipped_no_row_no_id_mapping() {
|
||||
.create();
|
||||
let base = server.url();
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
let summary = vandelay::sync::import_exchange_graph::run(
|
||||
let summary = inbuxa_migrate::sync::import_exchange_graph::run(
|
||||
make_common(archive.clone()),
|
||||
make_config(base, None, surfaces("calendar")),
|
||||
)
|
||||
@@ -1326,7 +1330,7 @@ fn occurrence_event_is_skipped_no_row_no_id_mapping() {
|
||||
assert_eq!(events, 0, "occurrence must not be fetched or stored");
|
||||
occ_get.assert();
|
||||
|
||||
let conn = vandelay::db::init::open(&archive).unwrap();
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
let count: i64 = conn
|
||||
.query_row("SELECT COUNT(*) FROM calendar_events", [], |row| row.get(0))
|
||||
.unwrap();
|
||||
@@ -1392,12 +1396,12 @@ fn hidden_mail_folder_has_is_subscribed_zero() {
|
||||
stub_well_known_folders(&mut server, "FVISIBLE");
|
||||
let base = server.url();
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
let _ = vandelay::sync::import_exchange_graph::run(
|
||||
let _ = inbuxa_migrate::sync::import_exchange_graph::run(
|
||||
make_common(archive.clone()),
|
||||
make_config(base, None, surfaces("mail")),
|
||||
)
|
||||
.unwrap();
|
||||
let conn = vandelay::db::init::open(&archive).unwrap();
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
let visible: i64 = conn
|
||||
.query_row(
|
||||
"SELECT is_subscribed FROM mailboxes WHERE name = 'Inbox'",
|
||||
@@ -1486,12 +1490,12 @@ fn well_known_folder_probes_assign_jmap_roles() {
|
||||
}
|
||||
let base = server.url();
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
let _ = vandelay::sync::import_exchange_graph::run(
|
||||
let _ = inbuxa_migrate::sync::import_exchange_graph::run(
|
||||
make_common(archive.clone()),
|
||||
make_config(base, None, surfaces("mail")),
|
||||
)
|
||||
.unwrap();
|
||||
let conn = vandelay::db::init::open(&archive).unwrap();
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT name, role FROM mailboxes ORDER BY name")
|
||||
.unwrap();
|
||||
@@ -1563,7 +1567,7 @@ SGVsbG8=\r\n--X--\r\n";
|
||||
.create();
|
||||
let base = server.url();
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
let summary = vandelay::sync::import_exchange_graph::run(
|
||||
let summary = inbuxa_migrate::sync::import_exchange_graph::run(
|
||||
make_common(archive),
|
||||
make_config(base, None, surfaces("mail")),
|
||||
)
|
||||
@@ -1651,7 +1655,7 @@ fn event_get_carries_outlook_timezone_and_body_content_type_prefer() {
|
||||
.create();
|
||||
let base = server.url();
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
let _ = vandelay::sync::import_exchange_graph::run(
|
||||
let _ = inbuxa_migrate::sync::import_exchange_graph::run(
|
||||
make_common(archive),
|
||||
make_config(base, None, surfaces("calendar")),
|
||||
)
|
||||
@@ -1737,23 +1741,23 @@ fn full_run_records_graph_id_in_sync_id_exchange_graph_with_padding() {
|
||||
|
||||
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
let archive = tmp.path().to_owned();
|
||||
let common = vandelay::sync::CommonConfig {
|
||||
let common = inbuxa_migrate::sync::CommonConfig {
|
||||
archive: archive.clone(),
|
||||
threads: 2,
|
||||
dry_run: false,
|
||||
max_retries: 0,
|
||||
allow_invalid_certs: false,
|
||||
logger: vandelay::logging::Logger::new(0),
|
||||
logger: inbuxa_migrate::logging::Logger::new(0),
|
||||
};
|
||||
let config = vandelay::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
let config = inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
token: make_jwt(9999999999, "[email protected]"),
|
||||
},
|
||||
api_base: server.url(),
|
||||
user_target: None,
|
||||
mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary,
|
||||
mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary,
|
||||
surfaces: surfaces("mail"),
|
||||
event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text,
|
||||
event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text,
|
||||
graph_connections: 2,
|
||||
top: 100,
|
||||
exception_window_years: 5,
|
||||
@@ -1762,8 +1766,8 @@ fn full_run_records_graph_id_in_sync_id_exchange_graph_with_padding() {
|
||||
allow_source_change: false,
|
||||
};
|
||||
drop(tmp);
|
||||
let _ = vandelay::sync::import_exchange_graph::run(common, config).unwrap();
|
||||
let conn = vandelay::db::init::open(&archive).unwrap();
|
||||
let _ = inbuxa_migrate::sync::import_exchange_graph::run(common, config).unwrap();
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
let (graph_id, type_name): (String, String) = conn
|
||||
.query_row(
|
||||
"SELECT graph_id, type_name FROM sync_id_exchange_graph",
|
||||
@@ -1810,23 +1814,23 @@ fn archive_mailbox_kind_encodes_synthetic_suffix_in_account_id() {
|
||||
|
||||
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
let archive = tmp.path().to_owned();
|
||||
let common = vandelay::sync::CommonConfig {
|
||||
let common = inbuxa_migrate::sync::CommonConfig {
|
||||
archive: archive.clone(),
|
||||
threads: 2,
|
||||
dry_run: false,
|
||||
max_retries: 0,
|
||||
allow_invalid_certs: false,
|
||||
logger: vandelay::logging::Logger::new(0),
|
||||
logger: inbuxa_migrate::logging::Logger::new(0),
|
||||
};
|
||||
let config = vandelay::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
let config = inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
token: make_jwt(9999999999, "[email protected]"),
|
||||
},
|
||||
api_base: server.url(),
|
||||
user_target: None,
|
||||
mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Archive,
|
||||
mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Archive,
|
||||
surfaces: surfaces("mail"),
|
||||
event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text,
|
||||
event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text,
|
||||
graph_connections: 2,
|
||||
top: 100,
|
||||
exception_window_years: 5,
|
||||
@@ -1835,8 +1839,8 @@ fn archive_mailbox_kind_encodes_synthetic_suffix_in_account_id() {
|
||||
allow_source_change: false,
|
||||
};
|
||||
drop(tmp);
|
||||
let _ = vandelay::sync::import_exchange_graph::run(common, config).unwrap();
|
||||
let conn = vandelay::db::init::open(&archive).unwrap();
|
||||
let _ = inbuxa_migrate::sync::import_exchange_graph::run(common, config).unwrap();
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
let account_id: String = conn
|
||||
.query_row(
|
||||
"SELECT account_id FROM sources WHERE kind = 'exchange_graph'",
|
||||
@@ -1850,10 +1854,10 @@ fn archive_mailbox_kind_encodes_synthetic_suffix_in_account_id() {
|
||||
#[test]
|
||||
fn allow_source_change_permits_overwriting_a_different_account() {
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
let conn = vandelay::db::init::open(&archive).unwrap();
|
||||
vandelay::db::sources::upsert_source(
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
inbuxa_migrate::db::sources::upsert_source(
|
||||
&conn,
|
||||
&vandelay::db::sources::SourceKey {
|
||||
&inbuxa_migrate::db::sources::SourceKey {
|
||||
kind: "exchange_graph".to_owned(),
|
||||
session_url: "https://login.microsoftonline.com/common|https://x.example".to_owned(),
|
||||
account_id: "different-uid".to_owned(),
|
||||
@@ -1878,23 +1882,23 @@ fn allow_source_change_permits_overwriting_a_different_account() {
|
||||
.with_body(r#"{"value":[]}"#)
|
||||
.create();
|
||||
|
||||
let common = vandelay::sync::CommonConfig {
|
||||
let common = inbuxa_migrate::sync::CommonConfig {
|
||||
archive: archive.clone(),
|
||||
threads: 2,
|
||||
dry_run: false,
|
||||
max_retries: 0,
|
||||
allow_invalid_certs: false,
|
||||
logger: vandelay::logging::Logger::new(0),
|
||||
logger: inbuxa_migrate::logging::Logger::new(0),
|
||||
};
|
||||
let config = vandelay::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
let config = inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
token: make_jwt(9999999999, "[email protected]"),
|
||||
},
|
||||
api_base: server.url(),
|
||||
user_target: None,
|
||||
mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary,
|
||||
mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary,
|
||||
surfaces: surfaces("mail"),
|
||||
event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text,
|
||||
event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text,
|
||||
graph_connections: 2,
|
||||
top: 100,
|
||||
exception_window_years: 5,
|
||||
@@ -1902,7 +1906,7 @@ fn allow_source_change_permits_overwriting_a_different_account() {
|
||||
event_attachments: true,
|
||||
allow_source_change: true,
|
||||
};
|
||||
let result = vandelay::sync::import_exchange_graph::run(common, config);
|
||||
let result = inbuxa_migrate::sync::import_exchange_graph::run(common, config);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"--allow-source-change must permit a different account, got {result:?}"
|
||||
@@ -1962,23 +1966,23 @@ fn dry_run_makes_no_per_item_get_and_no_sqlite_writes() {
|
||||
|
||||
let tmp = tempfile::NamedTempFile::new().unwrap();
|
||||
let archive = tmp.path().to_owned();
|
||||
let common = vandelay::sync::CommonConfig {
|
||||
let common = inbuxa_migrate::sync::CommonConfig {
|
||||
archive: archive.clone(),
|
||||
threads: 2,
|
||||
dry_run: true,
|
||||
max_retries: 0,
|
||||
allow_invalid_certs: false,
|
||||
logger: vandelay::logging::Logger::new(0),
|
||||
logger: inbuxa_migrate::logging::Logger::new(0),
|
||||
};
|
||||
let config = vandelay::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
let config = inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
token: make_jwt(9999999999, "[email protected]"),
|
||||
},
|
||||
api_base: server.url(),
|
||||
user_target: None,
|
||||
mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary,
|
||||
mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary,
|
||||
surfaces: Surfaces::ALL,
|
||||
event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text,
|
||||
event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text,
|
||||
graph_connections: 2,
|
||||
top: 100,
|
||||
exception_window_years: 5,
|
||||
@@ -1987,10 +1991,10 @@ fn dry_run_makes_no_per_item_get_and_no_sqlite_writes() {
|
||||
allow_source_change: false,
|
||||
};
|
||||
drop(tmp);
|
||||
let _ = vandelay::sync::import_exchange_graph::run(common, config).unwrap();
|
||||
let _ = inbuxa_migrate::sync::import_exchange_graph::run(common, config).unwrap();
|
||||
no_messages.assert();
|
||||
no_well_known.assert();
|
||||
let conn = vandelay::db::init::open(&archive).unwrap();
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
let sources: i64 = conn
|
||||
.query_row("SELECT count(*) FROM sources", [], |row| row.get(0))
|
||||
.unwrap();
|
||||
@@ -2025,10 +2029,10 @@ fn token_response_bad_code_recognised() {
|
||||
#[test]
|
||||
fn folder_enumeration_failure_skips_vanished_deletion() {
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
let setup_conn = vandelay::db::init::open(&archive).unwrap();
|
||||
let sid = vandelay::db::sources::upsert_source(
|
||||
let setup_conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
let sid = inbuxa_migrate::db::sources::upsert_source(
|
||||
&setup_conn,
|
||||
&vandelay::db::sources::SourceKey {
|
||||
&inbuxa_migrate::db::sources::SourceKey {
|
||||
kind: "exchange_graph".to_owned(),
|
||||
session_url: "stale".to_owned(),
|
||||
account_id: "stale-uid".to_owned(),
|
||||
@@ -2043,10 +2047,10 @@ fn folder_enumeration_failure_skips_vanished_deletion() {
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
vandelay::db::exchange_graph_ids::insert(
|
||||
inbuxa_migrate::db::exchange_graph_ids::insert(
|
||||
&setup_conn,
|
||||
sid,
|
||||
vandelay::db::exchange_graph_ids::MAILBOX,
|
||||
inbuxa_migrate::db::exchange_graph_ids::MAILBOX,
|
||||
"FMAIL",
|
||||
42,
|
||||
)
|
||||
@@ -2067,10 +2071,10 @@ fn folder_enumeration_failure_skips_vanished_deletion() {
|
||||
rusqlite::params![blob_id],
|
||||
)
|
||||
.unwrap();
|
||||
vandelay::db::exchange_graph_ids::insert(
|
||||
inbuxa_migrate::db::exchange_graph_ids::insert(
|
||||
&setup_conn,
|
||||
sid,
|
||||
vandelay::db::exchange_graph_ids::EMAIL,
|
||||
inbuxa_migrate::db::exchange_graph_ids::EMAIL,
|
||||
"MSG-LOCAL",
|
||||
1,
|
||||
)
|
||||
@@ -2133,23 +2137,23 @@ fn folder_enumeration_failure_skips_vanished_deletion() {
|
||||
.with_body("transient outage")
|
||||
.create();
|
||||
|
||||
let common = vandelay::sync::CommonConfig {
|
||||
let common = inbuxa_migrate::sync::CommonConfig {
|
||||
archive: archive.clone(),
|
||||
threads: 2,
|
||||
dry_run: false,
|
||||
max_retries: 0,
|
||||
allow_invalid_certs: false,
|
||||
logger: vandelay::logging::Logger::new(0),
|
||||
logger: inbuxa_migrate::logging::Logger::new(0),
|
||||
};
|
||||
let config = vandelay::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: vandelay::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
let config = inbuxa_migrate::sync::import_exchange_graph::GraphImportConfig {
|
||||
auth: inbuxa_migrate::sync::import_exchange_graph::GraphAuth::PreAcquired {
|
||||
token: make_jwt(9999999999, "[email protected]"),
|
||||
},
|
||||
api_base: server.url(),
|
||||
user_target: None,
|
||||
mailbox_kind: vandelay::exchange_graph::types::MailboxKind::Primary,
|
||||
mailbox_kind: inbuxa_migrate::exchange_graph::types::MailboxKind::Primary,
|
||||
surfaces: surfaces("mail"),
|
||||
event_body_format: vandelay::exchange_graph::types::EventBodyFormat::Text,
|
||||
event_body_format: inbuxa_migrate::exchange_graph::types::EventBodyFormat::Text,
|
||||
graph_connections: 2,
|
||||
top: 100,
|
||||
exception_window_years: 5,
|
||||
@@ -2157,8 +2161,8 @@ fn folder_enumeration_failure_skips_vanished_deletion() {
|
||||
event_attachments: true,
|
||||
allow_source_change: true,
|
||||
};
|
||||
let _ = vandelay::sync::import_exchange_graph::run(common, config).unwrap();
|
||||
let conn = vandelay::db::init::open(&archive).unwrap();
|
||||
let _ = inbuxa_migrate::sync::import_exchange_graph::run(common, config).unwrap();
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
let remaining: i64 = conn
|
||||
.query_row("SELECT count(*) FROM emails WHERE id = 1", [], |row| {
|
||||
row.get(0)
|
||||
@@ -2324,7 +2328,7 @@ fn contacts_surface_imports_only_address_books_and_cards() {
|
||||
|
||||
let base = server.url();
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
vandelay::sync::import_exchange_graph::run(
|
||||
inbuxa_migrate::sync::import_exchange_graph::run(
|
||||
make_common(archive.clone()),
|
||||
make_config(base, None, surfaces("contacts")),
|
||||
)
|
||||
@@ -2332,7 +2336,7 @@ fn contacts_surface_imports_only_address_books_and_cards() {
|
||||
no_mail.assert();
|
||||
no_calendars.assert();
|
||||
|
||||
let conn = vandelay::db::init::open(&archive).unwrap();
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
assert_eq!(row_count(&conn, "address_books"), 1);
|
||||
assert_eq!(row_count(&conn, "contact_cards"), 1);
|
||||
assert_eq!(row_count(&conn, "mailboxes"), 0);
|
||||
@@ -2368,7 +2372,7 @@ fn mail_surface_imports_only_mailboxes_and_emails() {
|
||||
|
||||
let base = server.url();
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
vandelay::sync::import_exchange_graph::run(
|
||||
inbuxa_migrate::sync::import_exchange_graph::run(
|
||||
make_common(archive.clone()),
|
||||
make_config(base, None, surfaces("mail")),
|
||||
)
|
||||
@@ -2376,7 +2380,7 @@ fn mail_surface_imports_only_mailboxes_and_emails() {
|
||||
no_calendars.assert();
|
||||
no_contact_folders.assert();
|
||||
|
||||
let conn = vandelay::db::init::open(&archive).unwrap();
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
assert_eq!(row_count(&conn, "mailboxes"), 1);
|
||||
assert_eq!(row_count(&conn, "emails"), 1);
|
||||
assert_eq!(row_count(&conn, "address_books"), 0);
|
||||
@@ -2442,7 +2446,7 @@ fn default_contact_folder_is_imported_although_contactfolders_omits_it() {
|
||||
|
||||
let base = server.url();
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
let summary = vandelay::sync::import_exchange_graph::run(
|
||||
let summary = inbuxa_migrate::sync::import_exchange_graph::run(
|
||||
make_common(archive.clone()),
|
||||
make_config(base, None, surfaces("contacts")),
|
||||
)
|
||||
@@ -2468,7 +2472,7 @@ fn default_contact_folder_is_imported_although_contactfolders_omits_it() {
|
||||
"both default-folder contacts import"
|
||||
);
|
||||
|
||||
let conn = vandelay::db::init::open(&archive).unwrap();
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
let is_default: i64 = conn
|
||||
.query_row("SELECT is_default FROM address_books", [], |row| row.get(0))
|
||||
.unwrap();
|
||||
@@ -2518,13 +2522,13 @@ fn default_contact_folder_falls_back_to_parent_of_an_existing_contact() {
|
||||
|
||||
let base = server.url();
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
vandelay::sync::import_exchange_graph::run(
|
||||
inbuxa_migrate::sync::import_exchange_graph::run(
|
||||
make_common(archive.clone()),
|
||||
make_config(base, None, surfaces("contacts")),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let conn = vandelay::db::init::open(&archive).unwrap();
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
let name: String = conn
|
||||
.query_row("SELECT name FROM address_books", [], |row| row.get(0))
|
||||
.unwrap();
|
||||
@@ -2568,7 +2572,7 @@ fn contact_folder_reachable_by_two_paths_is_inserted_once() {
|
||||
|
||||
let base = server.url();
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
let summary = vandelay::sync::import_exchange_graph::run(
|
||||
let summary = inbuxa_migrate::sync::import_exchange_graph::run(
|
||||
make_common(archive.clone()),
|
||||
make_config(base, None, surfaces("contacts")),
|
||||
)
|
||||
@@ -2638,13 +2642,13 @@ fn message_state_becomes_jmap_keywords() {
|
||||
|
||||
let base = server.url();
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
vandelay::sync::import_exchange_graph::run(
|
||||
inbuxa_migrate::sync::import_exchange_graph::run(
|
||||
make_common(archive.clone()),
|
||||
make_config(base, None, surfaces("mail")),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let conn = vandelay::db::init::open(&archive).unwrap();
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT keywords FROM emails ORDER BY id")
|
||||
.unwrap();
|
||||
@@ -2708,7 +2712,7 @@ fn drive_items_import_as_file_nodes_and_skip_facetless_items() {
|
||||
|
||||
let base = server.url();
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
let summary = vandelay::sync::import_exchange_graph::run(
|
||||
let summary = inbuxa_migrate::sync::import_exchange_graph::run(
|
||||
make_common(archive.clone()),
|
||||
make_config(base, None, surfaces("files")),
|
||||
)
|
||||
@@ -2725,7 +2729,7 @@ fn drive_items_import_as_file_nodes_and_skip_facetless_items() {
|
||||
"one directory and two files; the remoteItem has no file or folder facet and is skipped"
|
||||
);
|
||||
|
||||
let conn = vandelay::db::init::open(&archive).unwrap();
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
let vault: i64 = conn
|
||||
.query_row(
|
||||
"SELECT count(*) FROM file_nodes WHERE name = 'Personal Vault'",
|
||||
@@ -2844,13 +2848,13 @@ fn a_deleted_occurrence_becomes_an_excluded_override() {
|
||||
|
||||
let base = server.url();
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
vandelay::sync::import_exchange_graph::run(
|
||||
inbuxa_migrate::sync::import_exchange_graph::run(
|
||||
make_common(archive.clone()),
|
||||
make_config(base, None, surfaces("calendar")),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let conn = vandelay::db::init::open(&archive).unwrap();
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
let row: String = conn
|
||||
.query_row("SELECT data FROM calendar_events", [], |row| row.get(0))
|
||||
.unwrap();
|
||||
@@ -2910,13 +2914,13 @@ fn event_file_attachments_become_enclosure_links() {
|
||||
|
||||
let base = server.url();
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
vandelay::sync::import_exchange_graph::run(
|
||||
inbuxa_migrate::sync::import_exchange_graph::run(
|
||||
make_common(archive.clone()),
|
||||
make_config(base, None, surfaces("calendar")),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let conn = vandelay::db::init::open(&archive).unwrap();
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
let row: String = conn
|
||||
.query_row("SELECT data FROM calendar_events", [], |row| row.get(0))
|
||||
.unwrap();
|
||||
@@ -2985,9 +2989,9 @@ fn contact_photo_categories_and_im_addresses_are_imported() {
|
||||
let archive = tempfile::NamedTempFile::new().unwrap().path().to_owned();
|
||||
let mut config = make_config(base, None, surfaces("contacts"));
|
||||
config.contact_photos = true;
|
||||
vandelay::sync::import_exchange_graph::run(make_common(archive.clone()), config).unwrap();
|
||||
inbuxa_migrate::sync::import_exchange_graph::run(make_common(archive.clone()), config).unwrap();
|
||||
|
||||
let conn = vandelay::db::init::open(&archive).unwrap();
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).unwrap();
|
||||
let row: String = conn
|
||||
.query_row("SELECT data FROM contact_cards", [], |row| row.get(0))
|
||||
.unwrap();
|
||||
|
||||
+16
-15
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -13,14 +14,14 @@ use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use encodify::base64::STANDARD;
|
||||
use inbuxa_migrate::db;
|
||||
use inbuxa_migrate::imap::client::{ConnectMode, ImapClient};
|
||||
use inbuxa_migrate::imap::error::ImapError;
|
||||
use inbuxa_migrate::imap::transport::Connector;
|
||||
use inbuxa_migrate::logging::Logger;
|
||||
use inbuxa_migrate::sync::CommonConfig;
|
||||
use inbuxa_migrate::sync::import_imap::{ImapAuth, ImapImportConfig, run};
|
||||
use rusqlite::Connection;
|
||||
use vandelay::db;
|
||||
use vandelay::imap::client::{ConnectMode, ImapClient};
|
||||
use vandelay::imap::error::ImapError;
|
||||
use vandelay::imap::transport::Connector;
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::CommonConfig;
|
||||
use vandelay::sync::import_imap::{ImapAuth, ImapImportConfig, run};
|
||||
|
||||
type Script = Box<dyn FnOnce(&mut MockConn) -> std::io::Result<()> + Send + 'static>;
|
||||
|
||||
@@ -129,7 +130,7 @@ fn connect_mock(server: &MockImap) -> Result<ImapClient, ImapError> {
|
||||
fn tempfile(label: &str) -> PathBuf {
|
||||
let counter = COUNTER.fetch_add(1, Ordering::SeqCst);
|
||||
let mut p = std::env::temp_dir();
|
||||
p.push(format!("vandelay_mock_imap_{label}_{counter}.sqlite"));
|
||||
p.push(format!("inbuxa_migrate_mock_imap_{label}_{counter}.sqlite"));
|
||||
if p.exists() {
|
||||
let _ = std::fs::remove_file(&p);
|
||||
}
|
||||
@@ -143,7 +144,7 @@ fn run_import(
|
||||
user: &str,
|
||||
archive: PathBuf,
|
||||
config_tweak: impl FnOnce(&mut ImapImportConfig),
|
||||
) -> Result<vandelay::sync::Summary, vandelay::error::Error> {
|
||||
) -> Result<inbuxa_migrate::sync::Summary, inbuxa_migrate::error::Error> {
|
||||
let common = CommonConfig {
|
||||
archive: archive.clone(),
|
||||
threads: 1,
|
||||
@@ -364,14 +365,14 @@ fn run_collect_parses_fetch_with_literal_body() {
|
||||
.run_collect("UID FETCH 5 (UID BODY.PEEK[])")
|
||||
.expect("fetch");
|
||||
let body = r.untagged.iter().find_map(|u| match u {
|
||||
vandelay::imap::response::Untagged::Fetch { items, .. } => Some(items.clone()),
|
||||
inbuxa_migrate::imap::response::Untagged::Fetch { items, .. } => Some(items.clone()),
|
||||
_ => None,
|
||||
});
|
||||
let items = body.expect("fetch items");
|
||||
let body_item = items.iter().find(|(n, _)| n == "BODY[]").unwrap();
|
||||
match &body_item.1 {
|
||||
vandelay::imap::response::Value::Str(s) => assert_eq!(s, "Hello world"),
|
||||
vandelay::imap::response::Value::Bytes(b) => assert_eq!(b, b"Hello world"),
|
||||
inbuxa_migrate::imap::response::Value::Str(s) => assert_eq!(s, "Hello world"),
|
||||
inbuxa_migrate::imap::response::Value::Bytes(b) => assert_eq!(b, b"Hello world"),
|
||||
other => panic!("unexpected body shape: {other:?}"),
|
||||
}
|
||||
}
|
||||
@@ -1094,7 +1095,7 @@ fn coordinator_source_change_detected_on_different_url() {
|
||||
let server2 = MockImap::start_scripts(vec![control2]);
|
||||
|
||||
let err = run_import(&server2, "alice", archive.clone(), |_| {}).unwrap_err();
|
||||
assert!(matches!(err, vandelay::error::Error::SourceChange(_)));
|
||||
assert!(matches!(err, inbuxa_migrate::error::Error::SourceChange(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1483,7 +1484,7 @@ fn coordinator_auth_plain_and_login_both_refused_aborts_run() {
|
||||
});
|
||||
let archive = tempfile("dual_refuse");
|
||||
let err = run_import(&server, "alice", archive, |_| {}).unwrap_err();
|
||||
assert!(matches!(err, vandelay::error::Error::Connection(_)));
|
||||
assert!(matches!(err, inbuxa_migrate::error::Error::Connection(_)));
|
||||
assert_eq!(err.exit_code(), 2);
|
||||
}
|
||||
|
||||
@@ -1812,7 +1813,7 @@ fn coordinator_authenticationfailed_yields_exit2_connection_error() {
|
||||
});
|
||||
let archive = tempfile("authfail");
|
||||
let err = run_import(&server, "alice", archive, |_| {}).unwrap_err();
|
||||
assert!(matches!(err, vandelay::error::Error::Connection(_)));
|
||||
assert!(matches!(err, inbuxa_migrate::error::Error::Connection(_)));
|
||||
assert_eq!(err.exit_code(), 2);
|
||||
}
|
||||
|
||||
|
||||
+22
-21
@@ -1,22 +1,23 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use serde_json::json;
|
||||
use vandelay::jmap::account::{self, AccountSelector};
|
||||
use vandelay::jmap::error::JmapError;
|
||||
use vandelay::jmap::http::{Auth, HttpClient, RetryPolicy};
|
||||
use vandelay::jmap::request::{
|
||||
use inbuxa_migrate::jmap::account::{self, AccountSelector};
|
||||
use inbuxa_migrate::jmap::error::JmapError;
|
||||
use inbuxa_migrate::jmap::http::{Auth, HttpClient, RetryPolicy};
|
||||
use inbuxa_migrate::jmap::request::{
|
||||
self, SetRequest, get_all, get_changes, get_objects, get_state, set_call,
|
||||
};
|
||||
use vandelay::jmap::session::{Limits, Session};
|
||||
use vandelay::jmap::wire::JmapId;
|
||||
use vandelay::jmap::wire::identity::Identity;
|
||||
use vandelay::jmap::wire::mailbox::Mailbox;
|
||||
use inbuxa_migrate::jmap::session::{Limits, Session};
|
||||
use inbuxa_migrate::jmap::wire::JmapId;
|
||||
use inbuxa_migrate::jmap::wire::identity::Identity;
|
||||
use inbuxa_migrate::jmap::wire::mailbox::Mailbox;
|
||||
use serde_json::json;
|
||||
|
||||
fn client(retries: u32) -> HttpClient {
|
||||
HttpClient::new(
|
||||
@@ -672,7 +673,7 @@ fn retries_counter_increments_on_429() {
|
||||
|
||||
#[test]
|
||||
fn upload_size_precheck_blocks_before_contacting_server() {
|
||||
use vandelay::jmap::blobxfer;
|
||||
use inbuxa_migrate::jmap::blobxfer;
|
||||
|
||||
let mut server = mockito::Server::new();
|
||||
let no_hits = server
|
||||
@@ -739,7 +740,7 @@ fn concurrent_requests_400_problem_is_retried_then_succeeds() {
|
||||
|
||||
#[test]
|
||||
fn concurrent_uploads_400_problem_is_retried_then_succeeds() {
|
||||
use vandelay::jmap::blobxfer;
|
||||
use inbuxa_migrate::jmap::blobxfer;
|
||||
|
||||
let mut server = mockito::Server::new();
|
||||
server
|
||||
@@ -772,7 +773,7 @@ fn concurrent_uploads_400_problem_is_retried_then_succeeds() {
|
||||
|
||||
#[test]
|
||||
fn blob_upload_quota_429_with_retry_after_is_honoured() {
|
||||
use vandelay::jmap::blobxfer;
|
||||
use inbuxa_migrate::jmap::blobxfer;
|
||||
|
||||
let mut server = mockito::Server::new();
|
||||
server
|
||||
@@ -883,7 +884,7 @@ fn ratelimit_headers_without_retry_after_fall_back_to_backoff() {
|
||||
|
||||
#[test]
|
||||
fn upload_extracts_blob_id_and_download_streams_via_download_url() {
|
||||
use vandelay::jmap::blobxfer;
|
||||
use inbuxa_migrate::jmap::blobxfer;
|
||||
|
||||
let mut server = mockito::Server::new();
|
||||
let base = server.url();
|
||||
@@ -974,7 +975,7 @@ fn persistent_problem_json_429_without_retry_after_eventually_succeeds_via_share
|
||||
|
||||
#[test]
|
||||
fn persistent_429_on_download_uses_shared_throttle() {
|
||||
use vandelay::jmap::blobxfer;
|
||||
use inbuxa_migrate::jmap::blobxfer;
|
||||
|
||||
let mut server = mockito::Server::new();
|
||||
let session = upload_session(&server.url());
|
||||
@@ -1207,7 +1208,7 @@ fn archive_path() -> std::path::PathBuf {
|
||||
let n = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let mut p = std::env::temp_dir();
|
||||
p.push(format!(
|
||||
"vandelay-mockjmap-{}-{n}.sqlite",
|
||||
"inbuxa-migrate-mockjmap-{}-{n}.sqlite",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_file(&p);
|
||||
@@ -1264,17 +1265,17 @@ fn import_aborts_with_exit_two_when_the_advertised_api_url_is_unreachable() {
|
||||
.create();
|
||||
|
||||
let archive = archive_path();
|
||||
let err = vandelay::sync::import_jmap::run(
|
||||
vandelay::sync::CommonConfig {
|
||||
let err = inbuxa_migrate::sync::import_jmap::run(
|
||||
inbuxa_migrate::sync::CommonConfig {
|
||||
archive: archive.clone(),
|
||||
threads: 1,
|
||||
dry_run: false,
|
||||
max_retries: 0,
|
||||
allow_invalid_certs: false,
|
||||
logger: vandelay::logging::Logger::from_flags(true, 0),
|
||||
logger: inbuxa_migrate::logging::Logger::from_flags(true, 0),
|
||||
},
|
||||
vandelay::sync::ImportConfig {
|
||||
connect: vandelay::sync::ConnectConfig {
|
||||
inbuxa_migrate::sync::ImportConfig {
|
||||
connect: inbuxa_migrate::sync::ConnectConfig {
|
||||
url: session_base.clone(),
|
||||
auth: Auth::Basic {
|
||||
user: "u".into(),
|
||||
@@ -1289,7 +1290,7 @@ fn import_aborts_with_exit_two_when_the_advertised_api_url_is_unreachable() {
|
||||
.expect_err("an unreachable apiUrl must abort the run");
|
||||
|
||||
assert!(
|
||||
matches!(err, vandelay::error::Error::Connection(_)),
|
||||
matches!(err, inbuxa_migrate::error::Error::Connection(_)),
|
||||
"a 404 from the advertised apiUrl is a whole-run connection failure, got {err:?}"
|
||||
);
|
||||
assert_eq!(err.exit_code(), 2, "must not report a partial failure");
|
||||
|
||||
+11
-10
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -8,16 +9,16 @@ use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use inbuxa_migrate::logging::Logger;
|
||||
use inbuxa_migrate::sync::CommonConfig;
|
||||
use inbuxa_migrate::sync::import_maildir::{MaildirImportConfig, run};
|
||||
use rusqlite::Connection;
|
||||
use tempfile::TempDir;
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::CommonConfig;
|
||||
use vandelay::sync::import_maildir::{MaildirImportConfig, run};
|
||||
|
||||
fn tmp_archive(tag: &str) -> PathBuf {
|
||||
let mut p = std::env::temp_dir();
|
||||
p.push(format!(
|
||||
"vandelay-mock-maildir-{tag}-{}-{}.sqlite",
|
||||
"inbuxa-migrate-mock-maildir-{tag}-{}-{}.sqlite",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
@@ -530,7 +531,7 @@ fn rejects_path_without_cur_subdir() {
|
||||
fs::create_dir_all(td.path().join("new")).unwrap();
|
||||
let archive = tmp_archive("not-a-maildir");
|
||||
let err = run(common(&archive), base_cfg(td.path())).expect_err("should refuse");
|
||||
assert!(matches!(err, vandelay::error::Error::Usage(_)));
|
||||
assert!(matches!(err, inbuxa_migrate::error::Error::Usage(_)));
|
||||
let _ = fs::remove_file(&archive);
|
||||
}
|
||||
|
||||
@@ -544,7 +545,7 @@ fn rejects_dovecot_layout_fs_tree() {
|
||||
let archive = tmp_archive("layout-fs");
|
||||
let err = run(common(&archive), base_cfg(td.path())).expect_err("should refuse");
|
||||
match err {
|
||||
vandelay::error::Error::Usage(msg) => {
|
||||
inbuxa_migrate::error::Error::Usage(msg) => {
|
||||
assert!(msg.contains("Maildir++"), "msg was: {msg}");
|
||||
}
|
||||
other => panic!("expected Usage error, got {other:?}"),
|
||||
@@ -558,7 +559,7 @@ fn rejects_nonexistent_path() {
|
||||
let mut cfg = base_cfg(Path::new("/definitely/not/a/real/maildir"));
|
||||
cfg.maildir = PathBuf::from("/definitely/not/a/real/maildir");
|
||||
let err = run(common(&archive), cfg).expect_err("should refuse");
|
||||
assert!(matches!(err, vandelay::error::Error::Usage(_)));
|
||||
assert!(matches!(err, inbuxa_migrate::error::Error::Usage(_)));
|
||||
let _ = fs::remove_file(&archive);
|
||||
}
|
||||
|
||||
@@ -572,7 +573,7 @@ fn source_change_protection_refuses_second_path() {
|
||||
let archive = tmp_archive("source-change");
|
||||
run(common(&archive), base_cfg(td_a.path())).expect("first import");
|
||||
let err = run(common(&archive), base_cfg(td_b.path())).expect_err("second import refused");
|
||||
assert!(matches!(err, vandelay::error::Error::SourceChange(_)));
|
||||
assert!(matches!(err, inbuxa_migrate::error::Error::SourceChange(_)));
|
||||
let _ = fs::remove_file(&archive);
|
||||
}
|
||||
|
||||
@@ -601,7 +602,7 @@ fn folder_and_include_are_mutually_exclusive() {
|
||||
cfg.include = vec![regex::Regex::new("^Sent$").unwrap()];
|
||||
let err = run(common(&archive), cfg).expect_err("mutex");
|
||||
match err {
|
||||
vandelay::error::Error::Usage(msg) => {
|
||||
inbuxa_migrate::error::Error::Usage(msg) => {
|
||||
assert!(
|
||||
msg.contains("--folder") && msg.contains("--include"),
|
||||
"msg was: {msg}"
|
||||
@@ -621,7 +622,7 @@ fn folder_and_exclude_are_mutually_exclusive() {
|
||||
cfg.folder = vec!["INBOX".to_owned()];
|
||||
cfg.exclude = vec![regex::Regex::new("^Trash$").unwrap()];
|
||||
let err = run(common(&archive), cfg).expect_err("mutex");
|
||||
assert!(matches!(err, vandelay::error::Error::Usage(_)));
|
||||
assert!(matches!(err, inbuxa_migrate::error::Error::Usage(_)));
|
||||
let _ = fs::remove_file(&archive);
|
||||
}
|
||||
|
||||
|
||||
+11
-10
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -12,10 +13,10 @@ use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use inbuxa_migrate::logging::Logger;
|
||||
use inbuxa_migrate::sync::CommonConfig;
|
||||
use inbuxa_migrate::sync::import_managesieve::{ManageSieveAuth, ManageSieveImportConfig, run};
|
||||
use rusqlite::Connection;
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::CommonConfig;
|
||||
use vandelay::sync::import_managesieve::{ManageSieveAuth, ManageSieveImportConfig, run};
|
||||
|
||||
type Script = Box<dyn FnOnce(&mut MockConn) -> std::io::Result<()> + Send + 'static>;
|
||||
|
||||
@@ -117,7 +118,7 @@ fn tempfile(label: &str) -> PathBuf {
|
||||
let counter = COUNTER.fetch_add(1, Ordering::SeqCst);
|
||||
let mut p = std::env::temp_dir();
|
||||
p.push(format!(
|
||||
"vandelay_mock_managesieve_{label}_{counter}.sqlite"
|
||||
"inbuxa_migrate_mock_managesieve_{label}_{counter}.sqlite"
|
||||
));
|
||||
if p.exists() {
|
||||
let _ = std::fs::remove_file(&p);
|
||||
@@ -133,7 +134,7 @@ fn count(conn: &Connection, table: &str) -> i64 {
|
||||
fn run_basic(
|
||||
server: &MockSieveServer,
|
||||
archive: &Path,
|
||||
) -> Result<vandelay::sync::Summary, vandelay::error::Error> {
|
||||
) -> Result<inbuxa_migrate::sync::Summary, inbuxa_migrate::error::Error> {
|
||||
let common = CommonConfig {
|
||||
archive: archive.to_path_buf(),
|
||||
threads: 1,
|
||||
@@ -256,7 +257,7 @@ fn auth_no_translates_to_connection_error() {
|
||||
let archive = tempfile("auth_fail");
|
||||
let err = run_basic(&server, &archive).unwrap_err();
|
||||
match err {
|
||||
vandelay::error::Error::Connection(msg) => assert!(
|
||||
inbuxa_migrate::error::Error::Connection(msg) => assert!(
|
||||
msg.contains("auth failed") || msg.contains("LOGIN"),
|
||||
"got {msg}"
|
||||
),
|
||||
@@ -274,7 +275,7 @@ fn server_lacking_plain_login_for_basic_is_refused() {
|
||||
});
|
||||
let archive = tempfile("no_basic_mech");
|
||||
let err = run_basic(&server, &archive).unwrap_err();
|
||||
assert!(matches!(err, vandelay::error::Error::Connection(_)));
|
||||
assert!(matches!(err, inbuxa_migrate::error::Error::Connection(_)));
|
||||
let _ = std::fs::remove_file(&archive);
|
||||
}
|
||||
|
||||
@@ -423,7 +424,7 @@ fn referral_aborts_run_with_connection_error() {
|
||||
let archive = tempfile("referral");
|
||||
let err = run_basic(&server, &archive).unwrap_err();
|
||||
match err {
|
||||
vandelay::error::Error::Connection(msg) => assert!(msg.contains("referral"), "{msg}"),
|
||||
inbuxa_migrate::error::Error::Connection(msg) => assert!(msg.contains("referral"), "{msg}"),
|
||||
other => panic!("expected Connection, got {other:?}"),
|
||||
}
|
||||
let _ = std::fs::remove_file(&archive);
|
||||
@@ -438,7 +439,7 @@ fn seed_then_run(seed: Script, second: Script) -> (MockSieveServer, PathBuf) {
|
||||
fn run_against(
|
||||
server: &MockSieveServer,
|
||||
archive: &Path,
|
||||
) -> Result<vandelay::sync::Summary, vandelay::error::Error> {
|
||||
) -> Result<inbuxa_migrate::sync::Summary, inbuxa_migrate::error::Error> {
|
||||
run_basic(server, archive)
|
||||
}
|
||||
|
||||
@@ -935,7 +936,7 @@ fn plain_then_login_both_refused_aborts_run_with_combined_message() {
|
||||
let archive = tempfile("plain_and_login_refused");
|
||||
let err = run_basic(&server, &archive).unwrap_err();
|
||||
match err {
|
||||
vandelay::error::Error::Connection(msg) => {
|
||||
inbuxa_migrate::error::Error::Connection(msg) => {
|
||||
assert!(
|
||||
msg.contains("LOGIN") && msg.contains("PLAIN"),
|
||||
"expected combined LOGIN/PLAIN error text, got {msg}"
|
||||
|
||||
+15
-14
@@ -1,26 +1,27 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use inbuxa_migrate::db;
|
||||
use inbuxa_migrate::jmap::account::AccountSelector;
|
||||
use inbuxa_migrate::jmap::http::Auth;
|
||||
use inbuxa_migrate::logging::Logger;
|
||||
use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig};
|
||||
use inbuxa_migrate::types::ObjectType;
|
||||
use mockito::Matcher;
|
||||
use serde_json::{Value, json};
|
||||
use vandelay::db;
|
||||
use vandelay::jmap::account::AccountSelector;
|
||||
use vandelay::jmap::http::Auth;
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig};
|
||||
use vandelay::types::ObjectType;
|
||||
|
||||
fn tmp() -> PathBuf {
|
||||
static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
let n = SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let mut p = std::env::temp_dir();
|
||||
p.push(format!(
|
||||
"vandelay-mocksync-{}-{:?}-{n}.sqlite",
|
||||
"inbuxa-migrate-mocksync-{}-{:?}-{n}.sqlite",
|
||||
std::process::id(),
|
||||
std::thread::current().id(),
|
||||
));
|
||||
@@ -1585,8 +1586,8 @@ fn export_missing_target_email_is_created_on_rerun() {
|
||||
let raw =
|
||||
format!("From: a@x\r\nSubject: m{n}\r\nMessage-ID: <m-{n}@h>\r\n\r\nbody {n}",);
|
||||
let blob = db::blobs::intern_blob(&conn, raw.as_bytes()).unwrap();
|
||||
let mm = vandelay::sync::keys::index_to_json(
|
||||
&vandelay::sync::emailmeta::email_index_from_blob(raw.as_bytes()),
|
||||
let mm = inbuxa_migrate::sync::keys::index_to_json(
|
||||
&inbuxa_migrate::sync::emailmeta::email_index_from_blob(raw.as_bytes()),
|
||||
);
|
||||
conn.execute(
|
||||
"INSERT INTO emails (blob_id,received_at,mailbox_ids,keywords,message_match)
|
||||
@@ -1700,8 +1701,8 @@ fn export_email_blake3_fallback_matches_when_no_message_id() {
|
||||
)
|
||||
.unwrap();
|
||||
let blob = db::blobs::intern_blob(&conn, raw.as_bytes()).unwrap();
|
||||
let mm = vandelay::sync::keys::index_to_json(
|
||||
&vandelay::sync::emailmeta::email_index_from_blob(raw.as_bytes()),
|
||||
let mm = inbuxa_migrate::sync::keys::index_to_json(
|
||||
&inbuxa_migrate::sync::emailmeta::email_index_from_blob(raw.as_bytes()),
|
||||
);
|
||||
conn.execute(
|
||||
"INSERT INTO emails (blob_id,received_at,mailbox_ids,keywords,message_match)
|
||||
@@ -1711,7 +1712,7 @@ fn export_email_blake3_fallback_matches_when_no_message_id() {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let local_idx = vandelay::sync::emailmeta::email_index_from_blob(raw.as_bytes());
|
||||
let local_idx = inbuxa_migrate::sync::emailmeta::email_index_from_blob(raw.as_bytes());
|
||||
assert!(local_idx.mids.is_empty(), "blob must lack Message-ID");
|
||||
|
||||
let _root = server.mock("GET", "/").with_status(404).create();
|
||||
@@ -4144,7 +4145,7 @@ fn export_aborts_with_exit_two_when_the_advertised_api_url_is_unreachable() {
|
||||
.expect_err("an unreachable apiUrl must abort the run");
|
||||
|
||||
assert!(
|
||||
matches!(err, vandelay::error::Error::Connection(_)),
|
||||
matches!(err, inbuxa_migrate::error::Error::Connection(_)),
|
||||
"a 404 from the advertised apiUrl is a whole-run connection failure, got {err:?}"
|
||||
);
|
||||
assert_eq!(err.exit_code(), 2, "must not report a partial failure");
|
||||
@@ -4207,7 +4208,7 @@ fn aborting_import_still_reports_the_types_that_finished() {
|
||||
.as_ref()
|
||||
.expect("a 404 from the api endpoint aborts the run");
|
||||
assert!(
|
||||
matches!(err, vandelay::error::Error::Connection(_)),
|
||||
matches!(err, inbuxa_migrate::error::Error::Connection(_)),
|
||||
"the abort must keep its own exit code, got {err:?}"
|
||||
);
|
||||
assert_eq!(err.exit_code(), 2);
|
||||
|
||||
+4
-1
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -98,7 +99,9 @@ fn verify_account(label: &str, user: &Jmap, account_id: &str, s: &SeedStats) {
|
||||
assert_eq!(list.len(), 2, "{label}: sieve script count");
|
||||
let primary = list
|
||||
.iter()
|
||||
.find(|s| s.get("name").and_then(Value::as_str) == Some("vandelay-test-filter"))
|
||||
.find(|s| {
|
||||
s.get("name").and_then(Value::as_str) == Some("inbuxa-migrate-test-filter")
|
||||
})
|
||||
.expect("primary sieve script present");
|
||||
assert_eq!(
|
||||
primary.get("isActive").and_then(Value::as_bool),
|
||||
|
||||
+5
-4
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -17,14 +18,14 @@ use seed::{FileSpec, Layout, MailboxSpec};
|
||||
|
||||
pub const ADMIN_USER: &str = "admin";
|
||||
pub const ADMIN_PASSWORD: &str = "admin";
|
||||
pub const DOMAIN: &str = "vandelay.org";
|
||||
pub const USER_PASSWORD: &str = "VandelayUser#2026";
|
||||
pub const ADMIN_ACCOUNT_PASSWORD: &str = "VandelayAdmin#2026";
|
||||
pub const DOMAIN: &str = "inbuxa-migrate.org";
|
||||
pub const USER_PASSWORD: &str = "InbuxaUser#2026";
|
||||
pub const ADMIN_ACCOUNT_PASSWORD: &str = "InbuxaAdmin#2026";
|
||||
|
||||
pub const SYNC_IN: [&str; 3] = ["test1", "test2", "test3"];
|
||||
pub const SYNC_OUT: [&str; 3] = ["test4", "test5", "test6"];
|
||||
pub const ADMIN_LOCALPART: &str = "vandeladmin";
|
||||
pub const CUSTOM_IDENTITY_NAME: &str = "Vandelay Test (Custom)";
|
||||
pub const CUSTOM_IDENTITY_NAME: &str = "Inbuxa Test (Custom)";
|
||||
|
||||
macro_rules! mailbox {
|
||||
($k:expr, $n:expr, $p:expr, $r:expr) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -230,8 +231,8 @@ fn seed_sieve(jmap: &Jmap, account_id: &str, active: bool) -> SeedResult<()> {
|
||||
"SieveScript/set",
|
||||
account_id,
|
||||
json!({
|
||||
"s0": { "name": "vandelay-test-filter", "blobId": primary_blob },
|
||||
"s1": { "name": "vandelay-vacation", "blobId": secondary_blob }
|
||||
"s0": { "name": "inbuxa-migrate-test-filter", "blobId": primary_blob },
|
||||
"s1": { "name": "inbuxa-migrate-vacation", "blobId": secondary_blob }
|
||||
}),
|
||||
&extra,
|
||||
)?;
|
||||
@@ -247,8 +248,8 @@ fn seed_identity(jmap: &Jmap, account_id: &str, account_email: &str) -> SeedResu
|
||||
"i0": {
|
||||
"name": super::CUSTOM_IDENTITY_NAME,
|
||||
"email": account_email,
|
||||
"textSignature": "-- \nVandelay Industries",
|
||||
"htmlSignature": "<p>Vandelay Industries</p>"
|
||||
"textSignature": "-- \nInbuxa Industries",
|
||||
"htmlSignature": "<p>Inbuxa Industries</p>"
|
||||
}
|
||||
}),
|
||||
&[],
|
||||
@@ -339,7 +340,7 @@ fn seed_contacts_calendars(
|
||||
&[CORE, CONTACTS],
|
||||
"AddressBook/set",
|
||||
account_id,
|
||||
json!({ "ab": { "name": "Vandelay Work Contacts" } }),
|
||||
json!({ "ab": { "name": "Inbuxa Work Contacts" } }),
|
||||
&[],
|
||||
)?;
|
||||
if let Some(id) = response
|
||||
@@ -359,7 +360,7 @@ fn seed_contacts_calendars(
|
||||
&[CORE, CALENDARS],
|
||||
"Calendar/set",
|
||||
account_id,
|
||||
json!({ "cal": { "name": "Vandelay Team Calendar", "color": "#3366cc" } }),
|
||||
json!({ "cal": { "name": "Inbuxa Team Calendar", "color": "#3366cc" } }),
|
||||
&[],
|
||||
)?;
|
||||
if let Some(id) = response
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -16,16 +17,16 @@ use rusqlite::Connection;
|
||||
fn base_url() -> &'static str {
|
||||
shared_stalwart().base_url()
|
||||
}
|
||||
use vandelay::jmap::account::AccountSelector;
|
||||
use vandelay::jmap::http::Auth;
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg};
|
||||
use vandelay::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig};
|
||||
use inbuxa_migrate::jmap::account::AccountSelector;
|
||||
use inbuxa_migrate::jmap::http::Auth;
|
||||
use inbuxa_migrate::logging::Logger;
|
||||
use inbuxa_migrate::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg};
|
||||
use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig};
|
||||
|
||||
fn tmp_archive(tag: &str) -> PathBuf {
|
||||
let mut p = std::env::temp_dir();
|
||||
p.push(format!(
|
||||
"vandelay-{tag}-{}-{}.sqlite",
|
||||
"inbuxa-migrate-{tag}-{}-{}.sqlite",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -10,22 +11,22 @@ mod seeder;
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use inbuxa_migrate::jmap::account::AccountSelector;
|
||||
use inbuxa_migrate::jmap::http::Auth;
|
||||
use inbuxa_migrate::logging::Logger;
|
||||
use integration::stalwart::shared as shared_stalwart;
|
||||
use rusqlite::Connection;
|
||||
use vandelay::jmap::account::AccountSelector;
|
||||
use vandelay::jmap::http::Auth;
|
||||
use vandelay::logging::Logger;
|
||||
|
||||
fn base_url() -> &'static str {
|
||||
shared_stalwart().base_url()
|
||||
}
|
||||
use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg};
|
||||
use vandelay::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig};
|
||||
use inbuxa_migrate::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg};
|
||||
use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig};
|
||||
|
||||
fn tmp_archive(tag: &str) -> PathBuf {
|
||||
let mut p = std::env::temp_dir();
|
||||
p.push(format!(
|
||||
"vandelay-{tag}-{}-{}.sqlite",
|
||||
"inbuxa-migrate-{tag}-{}-{}.sqlite",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -9,15 +10,15 @@ mod seeder;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use inbuxa_migrate::exchange_graph::types::{EventBodyFormat, MailboxKind, Surfaces};
|
||||
use inbuxa_migrate::jmap::account::AccountSelector;
|
||||
use inbuxa_migrate::jmap::http::Auth;
|
||||
use inbuxa_migrate::logging::Logger;
|
||||
use inbuxa_migrate::sync::import_exchange_graph::{GraphAuth, GraphImportConfig};
|
||||
use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ExportConfig};
|
||||
use integration::stalwart::shared as shared_stalwart;
|
||||
use mockito::{Matcher, Server};
|
||||
use serde_json::{Value, json};
|
||||
use vandelay::exchange_graph::types::{EventBodyFormat, MailboxKind, Surfaces};
|
||||
use vandelay::jmap::account::AccountSelector;
|
||||
use vandelay::jmap::http::Auth;
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::import_exchange_graph::{GraphAuth, GraphImportConfig};
|
||||
use vandelay::sync::{self, CommonConfig, ConnectConfig, ExportConfig};
|
||||
|
||||
fn base_url() -> &'static str {
|
||||
shared_stalwart().base_url()
|
||||
@@ -26,7 +27,7 @@ fn base_url() -> &'static str {
|
||||
fn tmp_archive(tag: &str) -> PathBuf {
|
||||
let mut p = std::env::temp_dir();
|
||||
p.push(format!(
|
||||
"vandelay-{tag}-{}-{}.sqlite",
|
||||
"inbuxa-migrate-{tag}-{}-{}.sqlite",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
@@ -96,9 +97,9 @@ fn graph_fixture(server: &mut Server) {
|
||||
.with_status(200)
|
||||
.with_header("content-type", "text/plain")
|
||||
.with_body(
|
||||
"From: art@vandelay.example\r\nTo: [email protected]\r\n\
|
||||
"From: art@inbuxa-migrate.example\r\nTo: [email protected]\r\n\
|
||||
Subject: Graph round trip\r\nDate: Tue, 01 Sep 2026 10:00:00 +0000\r\n\
|
||||
Message-ID: <graph-rt-1@vandelay.example>\r\n\
|
||||
Message-ID: <graph-rt-1@inbuxa-migrate.example>\r\n\
|
||||
MIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n\
|
||||
Grüße aus Köln 🎉\r\n",
|
||||
)
|
||||
@@ -219,8 +220,8 @@ fn graph_fixture(server: &mut Server) {
|
||||
server,
|
||||
"/me/contacts/C1",
|
||||
r#"{"id":"C1","displayName":"Graph Contact","givenName":"Graph","surname":"Contact",
|
||||
"categories":["Work","VIP"],"imAddresses":["sip:graph@vandelay.example"],
|
||||
"emailAddresses":[{"address":"graph@vandelay.example"}]}"#,
|
||||
"categories":["Work","VIP"],"imAddresses":["sip:graph@inbuxa-migrate.example"],
|
||||
"emailAddresses":[{"address":"graph@inbuxa-migrate.example"}]}"#,
|
||||
);
|
||||
server
|
||||
.mock("GET", "/me/contacts/C1/photo/$value")
|
||||
@@ -393,7 +394,7 @@ fn graph_import_survives_a_full_export_to_stalwart() {
|
||||
.as_object()
|
||||
.and_then(|m| m.values().next())
|
||||
.and_then(|s| s["uri"].as_str()),
|
||||
Some("sip:graph@vandelay.example"),
|
||||
Some("sip:graph@inbuxa-migrate.example"),
|
||||
"a URI-shaped IM address must land in uri: Stalwart drops an OnlineService \
|
||||
that carries only user, even though RFC 9553 permits it"
|
||||
);
|
||||
|
||||
+7
-6
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -22,16 +23,16 @@ fn imaps_url() -> String {
|
||||
format!("imaps://{}:{}", s.host, s.imaps_port)
|
||||
}
|
||||
|
||||
use vandelay::jmap::account::AccountSelector;
|
||||
use vandelay::jmap::http::Auth;
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::import_imap::{ImapAuth, ImapImportConfig};
|
||||
use vandelay::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig};
|
||||
use inbuxa_migrate::jmap::account::AccountSelector;
|
||||
use inbuxa_migrate::jmap::http::Auth;
|
||||
use inbuxa_migrate::logging::Logger;
|
||||
use inbuxa_migrate::sync::import_imap::{ImapAuth, ImapImportConfig};
|
||||
use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig};
|
||||
|
||||
fn tmp_archive(tag: &str) -> PathBuf {
|
||||
let mut p = std::env::temp_dir();
|
||||
p.push(format!(
|
||||
"vandelay-imap-{tag}-{}-{}.sqlite",
|
||||
"inbuxa-migrate-imap-{tag}-{}-{}.sqlite",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
|
||||
+40
-39
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -10,15 +11,15 @@ mod seeder;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use encodify::base64::STANDARD;
|
||||
use inbuxa_migrate::jmap::account::{self, AccountSelector};
|
||||
use inbuxa_migrate::jmap::http::{Auth, HttpClient, RetryPolicy};
|
||||
use inbuxa_migrate::jmap::request::Request;
|
||||
use inbuxa_migrate::jmap::session::Session;
|
||||
use inbuxa_migrate::logging::Logger;
|
||||
use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig};
|
||||
use integration::stalwart::shared as shared_stalwart;
|
||||
use rusqlite::Connection;
|
||||
use serde_json::{Map, Value, json};
|
||||
use vandelay::jmap::account::{self, AccountSelector};
|
||||
use vandelay::jmap::http::{Auth, HttpClient, RetryPolicy};
|
||||
use vandelay::jmap::request::Request;
|
||||
use vandelay::jmap::session::Session;
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig};
|
||||
|
||||
fn base_url() -> &'static str {
|
||||
shared_stalwart().base_url()
|
||||
@@ -27,7 +28,7 @@ fn base_url() -> &'static str {
|
||||
fn tmp_archive(tag: &str) -> PathBuf {
|
||||
let mut p = std::env::temp_dir();
|
||||
p.push(format!(
|
||||
"vandelay-{tag}-{}-{}.sqlite",
|
||||
"inbuxa-migrate-{tag}-{}-{}.sqlite",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
@@ -181,7 +182,7 @@ fn import_resolves_account_via_admin_principal_path() {
|
||||
},
|
||||
account: AccountSelector::Name(target.email.clone()),
|
||||
},
|
||||
objects: Some(vec![vandelay::types::ObjectType::Mailbox]),
|
||||
objects: Some(vec![inbuxa_migrate::types::ObjectType::Mailbox]),
|
||||
allow_source_change: false,
|
||||
};
|
||||
let summary = sync::import_jmap::run(common(&archive, false), cfg)
|
||||
@@ -390,7 +391,7 @@ fn import_export_deeply_nested_mailbox_tree() {
|
||||
auth: basic("test4"),
|
||||
account: AccountSelector::Id(src.account_id.clone()),
|
||||
},
|
||||
objects: Some(vec![vandelay::types::ObjectType::Mailbox]),
|
||||
objects: Some(vec![inbuxa_migrate::types::ObjectType::Mailbox]),
|
||||
allow_source_change: false,
|
||||
};
|
||||
let s = sync::import_jmap::run(common(&archive, false), imp).expect("import");
|
||||
@@ -432,7 +433,7 @@ fn import_export_deeply_nested_mailbox_tree() {
|
||||
auth: basic("test5"),
|
||||
account: AccountSelector::Id(tgt.account_id.clone()),
|
||||
},
|
||||
objects: Some(vec![vandelay::types::ObjectType::Mailbox]),
|
||||
objects: Some(vec![inbuxa_migrate::types::ObjectType::Mailbox]),
|
||||
allow_source_change: false,
|
||||
};
|
||||
sync::import_jmap::run(common(&again_archive, false), imp_again)
|
||||
@@ -470,7 +471,7 @@ fn import_export_deeply_nested_filenode_tree() {
|
||||
auth: basic("test4"),
|
||||
account: AccountSelector::Id(src.account_id.clone()),
|
||||
},
|
||||
objects: Some(vec![vandelay::types::ObjectType::FileNode]),
|
||||
objects: Some(vec![inbuxa_migrate::types::ObjectType::FileNode]),
|
||||
allow_source_change: false,
|
||||
};
|
||||
let s = sync::import_jmap::run(common(&archive, false), imp).expect("import");
|
||||
@@ -514,7 +515,7 @@ fn import_export_deeply_nested_filenode_tree() {
|
||||
auth: basic("test5"),
|
||||
account: AccountSelector::Id(tgt.account_id.clone()),
|
||||
},
|
||||
objects: Some(vec![vandelay::types::ObjectType::FileNode]),
|
||||
objects: Some(vec![inbuxa_migrate::types::ObjectType::FileNode]),
|
||||
allow_source_change: false,
|
||||
},
|
||||
)
|
||||
@@ -554,7 +555,7 @@ fn prune_multilevel_mailbox_tree_destroys_leaf_first() {
|
||||
auth: basic("test4"),
|
||||
account: AccountSelector::Id(src.account_id.clone()),
|
||||
},
|
||||
objects: Some(vec![vandelay::types::ObjectType::Mailbox]),
|
||||
objects: Some(vec![inbuxa_migrate::types::ObjectType::Mailbox]),
|
||||
allow_source_change: false,
|
||||
},
|
||||
)
|
||||
@@ -567,7 +568,7 @@ fn prune_multilevel_mailbox_tree_destroys_leaf_first() {
|
||||
|
||||
let arch_empty = tmp_archive("prune_deep_empty");
|
||||
{
|
||||
let _ = vandelay::db::init::open(&arch_empty).expect("init empty archive");
|
||||
let _ = inbuxa_migrate::db::init::open(&arch_empty).expect("init empty archive");
|
||||
}
|
||||
|
||||
let small_jmap = jmap_for(&fx.account("test6").expect("test6").localpart);
|
||||
@@ -595,7 +596,7 @@ fn prune_multilevel_mailbox_tree_destroys_leaf_first() {
|
||||
auth: basic("test6"),
|
||||
account: AccountSelector::Id(fx.account("test6").unwrap().account_id.clone()),
|
||||
},
|
||||
objects: Some(vec![vandelay::types::ObjectType::Mailbox]),
|
||||
objects: Some(vec![inbuxa_migrate::types::ObjectType::Mailbox]),
|
||||
allow_source_change: false,
|
||||
},
|
||||
)
|
||||
@@ -644,8 +645,8 @@ fn prune_non_tree_type_contact_card() {
|
||||
account: AccountSelector::Id(src.account_id.clone()),
|
||||
},
|
||||
objects: Some(vec![
|
||||
vandelay::types::ObjectType::AddressBook,
|
||||
vandelay::types::ObjectType::ContactCard,
|
||||
inbuxa_migrate::types::ObjectType::AddressBook,
|
||||
inbuxa_migrate::types::ObjectType::ContactCard,
|
||||
]),
|
||||
allow_source_change: false,
|
||||
},
|
||||
@@ -659,7 +660,7 @@ fn prune_non_tree_type_contact_card() {
|
||||
|
||||
let arch_empty = tmp_archive("prune_cc_empty");
|
||||
{
|
||||
let conn = vandelay::db::init::open(&arch_empty).expect("init empty archive");
|
||||
let conn = inbuxa_migrate::db::init::open(&arch_empty).expect("init empty archive");
|
||||
conn.execute(
|
||||
"INSERT INTO address_books (id,name,is_default) VALUES (1,'AB',1)",
|
||||
[],
|
||||
@@ -690,8 +691,8 @@ fn prune_non_tree_type_contact_card() {
|
||||
seeder::teardown(base_url()).expect("teardown");
|
||||
}
|
||||
|
||||
const ISSUE30_CARD_UID: &str = "vandelay-issue30-card";
|
||||
const ISSUE30_EVENT_UID: &str = "vandelay-issue30-event";
|
||||
const ISSUE30_CARD_UID: &str = "inbuxa-migrate-issue30-card";
|
||||
const ISSUE30_EVENT_UID: &str = "inbuxa-migrate-issue30-event";
|
||||
|
||||
fn data_uri_bytes(resource: &Value, uri_key: &str, expect_media_type: &str) -> Vec<u8> {
|
||||
let uri = resource
|
||||
@@ -716,10 +717,10 @@ fn export_inlines_contact_and_event_blobs_instead_of_blob_ids() {
|
||||
let tgt = fx.account("test6").expect("test6");
|
||||
let archive = tmp_archive("issue30");
|
||||
|
||||
let photo: &[u8] = b"\x89PNG\r\n\x1a\nvandelay issue 30 contact photo bytes";
|
||||
let agenda: &[u8] = b"vandelay issue 30 calendar enclosure bytes";
|
||||
let photo: &[u8] = b"\x89PNG\r\n\x1a\ninbuxa-migrate issue 30 contact photo bytes";
|
||||
let agenda: &[u8] = b"inbuxa-migrate issue 30 calendar enclosure bytes";
|
||||
{
|
||||
let conn = vandelay::db::init::open(&archive).expect("init archive");
|
||||
let conn = inbuxa_migrate::db::init::open(&archive).expect("init archive");
|
||||
conn.execute(
|
||||
"INSERT INTO address_books (id,name,is_default) VALUES (1,'Issue30 Book',1)",
|
||||
[],
|
||||
@@ -730,8 +731,8 @@ fn export_inlines_contact_and_event_blobs_instead_of_blob_ids() {
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
let photo_blob = vandelay::db::blobs::intern_blob(&conn, photo).unwrap();
|
||||
let agenda_blob = vandelay::db::blobs::intern_blob(&conn, agenda).unwrap();
|
||||
let photo_blob = inbuxa_migrate::db::blobs::intern_blob(&conn, photo).unwrap();
|
||||
let agenda_blob = inbuxa_migrate::db::blobs::intern_blob(&conn, agenda).unwrap();
|
||||
let card = json!({
|
||||
"@type": "Card",
|
||||
"version": "1.0",
|
||||
@@ -944,7 +945,7 @@ fn export_prune_destroys_unmatched() {
|
||||
auth: basic("test1"),
|
||||
account: AccountSelector::Id(big.account_id.clone()),
|
||||
},
|
||||
objects: Some(vec![vandelay::types::ObjectType::Mailbox]),
|
||||
objects: Some(vec![inbuxa_migrate::types::ObjectType::Mailbox]),
|
||||
allow_source_change: false,
|
||||
},
|
||||
)
|
||||
@@ -964,7 +965,7 @@ fn export_prune_destroys_unmatched() {
|
||||
auth: basic("test3"),
|
||||
account: AccountSelector::Id(small.account_id.clone()),
|
||||
},
|
||||
objects: Some(vec![vandelay::types::ObjectType::Mailbox]),
|
||||
objects: Some(vec![inbuxa_migrate::types::ObjectType::Mailbox]),
|
||||
allow_source_change: false,
|
||||
},
|
||||
)
|
||||
@@ -1004,7 +1005,7 @@ fn import_resume_converges_after_partial() {
|
||||
};
|
||||
|
||||
let mut first = mk();
|
||||
first.objects = Some(vec![vandelay::types::ObjectType::Mailbox]);
|
||||
first.objects = Some(vec![inbuxa_migrate::types::ObjectType::Mailbox]);
|
||||
sync::import_jmap::run(common(&archive, false), first).expect("partial import");
|
||||
|
||||
let conn = Connection::open(&archive).unwrap();
|
||||
@@ -1038,10 +1039,10 @@ fn import_resume_converges_after_partial() {
|
||||
#[test]
|
||||
#[ignore = "requires Docker"]
|
||||
fn live_burst_exceeds_concurrent_requests_and_recovers() {
|
||||
use inbuxa_migrate::jmap::http::{HttpClient, RetryPolicy};
|
||||
use inbuxa_migrate::jmap::request::{Request, check_method_error};
|
||||
use inbuxa_migrate::jmap::session::{Limits, Session};
|
||||
use std::sync::Arc;
|
||||
use vandelay::jmap::http::{HttpClient, RetryPolicy};
|
||||
use vandelay::jmap::request::{Request, check_method_error};
|
||||
use vandelay::jmap::session::{Limits, Session};
|
||||
|
||||
let fx = seeder::provision(base_url()).expect("provision");
|
||||
let acc = fx.account("test1").expect("test1");
|
||||
@@ -1131,7 +1132,7 @@ fn live_burst_exceeds_concurrent_requests_and_recovers() {
|
||||
}
|
||||
|
||||
struct JmapSettingsGuard {
|
||||
admin: vandelay::jmap::http::HttpClient,
|
||||
admin: inbuxa_migrate::jmap::http::HttpClient,
|
||||
admin_api: String,
|
||||
admin_account: String,
|
||||
previous: serde_json::Map<String, serde_json::Value>,
|
||||
@@ -1139,8 +1140,8 @@ struct JmapSettingsGuard {
|
||||
|
||||
impl JmapSettingsGuard {
|
||||
fn override_settings(updates: serde_json::Map<String, serde_json::Value>) -> Self {
|
||||
use vandelay::jmap::http::{Auth, HttpClient, RetryPolicy};
|
||||
use vandelay::jmap::session::Session;
|
||||
use inbuxa_migrate::jmap::http::{Auth, HttpClient, RetryPolicy};
|
||||
use inbuxa_migrate::jmap::session::Session;
|
||||
let admin = HttpClient::new(
|
||||
Auth::Basic {
|
||||
user: seeder::ADMIN_USER.to_owned(),
|
||||
@@ -1188,7 +1189,7 @@ impl Drop for JmapSettingsGuard {
|
||||
}
|
||||
|
||||
fn read_jmap_settings(
|
||||
admin: &vandelay::jmap::http::HttpClient,
|
||||
admin: &inbuxa_migrate::jmap::http::HttpClient,
|
||||
api: &str,
|
||||
account: &str,
|
||||
properties: &[&str],
|
||||
@@ -1217,7 +1218,7 @@ fn read_jmap_settings(
|
||||
}
|
||||
|
||||
fn apply_jmap_settings(
|
||||
admin: &vandelay::jmap::http::HttpClient,
|
||||
admin: &inbuxa_migrate::jmap::http::HttpClient,
|
||||
api: &str,
|
||||
account: &str,
|
||||
updates: &serde_json::Map<String, serde_json::Value>,
|
||||
@@ -1252,9 +1253,9 @@ fn apply_jmap_settings(
|
||||
#[test]
|
||||
#[ignore = "requires Docker"]
|
||||
fn live_blob_quota_429_triggers_retry_after_then_succeeds() {
|
||||
use vandelay::jmap::blobxfer;
|
||||
use vandelay::jmap::http::{HttpClient, RetryPolicy};
|
||||
use vandelay::jmap::session::Session;
|
||||
use inbuxa_migrate::jmap::blobxfer;
|
||||
use inbuxa_migrate::jmap::http::{HttpClient, RetryPolicy};
|
||||
use inbuxa_migrate::jmap::session::Session;
|
||||
|
||||
let fx = seeder::provision(base_url()).expect("provision");
|
||||
let acc = fx.account("test1").expect("test1");
|
||||
|
||||
+11
-10
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -19,16 +20,16 @@ use rusqlite::Connection;
|
||||
fn base_url() -> &'static str {
|
||||
shared_stalwart().base_url()
|
||||
}
|
||||
use vandelay::jmap::account::AccountSelector;
|
||||
use vandelay::jmap::http::Auth;
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::import_maildir::{MaildirImportConfig, run as run_maildir};
|
||||
use vandelay::sync::{self, CommonConfig, ConnectConfig, ImportConfig};
|
||||
use inbuxa_migrate::jmap::account::AccountSelector;
|
||||
use inbuxa_migrate::jmap::http::Auth;
|
||||
use inbuxa_migrate::logging::Logger;
|
||||
use inbuxa_migrate::sync::import_maildir::{MaildirImportConfig, run as run_maildir};
|
||||
use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ImportConfig};
|
||||
|
||||
fn tmp_archive(tag: &str) -> PathBuf {
|
||||
let mut p = std::env::temp_dir();
|
||||
p.push(format!(
|
||||
"vandelay-sync-maildir-{tag}-{}-{}.sqlite",
|
||||
"inbuxa-migrate-sync-maildir-{tag}-{}-{}.sqlite",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
@@ -551,8 +552,8 @@ fn maildir_and_jmap_imports_share_blob_bytes() {
|
||||
account: AccountSelector::Id(acc.account_id.clone()),
|
||||
},
|
||||
objects: Some(vec![
|
||||
vandelay::types::ObjectType::Mailbox,
|
||||
vandelay::types::ObjectType::Email,
|
||||
inbuxa_migrate::types::ObjectType::Mailbox,
|
||||
inbuxa_migrate::types::ObjectType::Email,
|
||||
]),
|
||||
allow_source_change: false,
|
||||
};
|
||||
@@ -627,8 +628,8 @@ fn maildir_message_count_matches_jmap_for_same_corpus() {
|
||||
account: AccountSelector::Id(acc.account_id.clone()),
|
||||
},
|
||||
objects: Some(vec![
|
||||
vandelay::types::ObjectType::Mailbox,
|
||||
vandelay::types::ObjectType::Email,
|
||||
inbuxa_migrate::types::ObjectType::Mailbox,
|
||||
inbuxa_migrate::types::ObjectType::Email,
|
||||
]),
|
||||
allow_source_change: false,
|
||||
};
|
||||
|
||||
+14
-13
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -25,16 +26,16 @@ fn sieves_url() -> String {
|
||||
let s = shared_stalwart();
|
||||
format!("sieves://{}:{}", s.host, s.sieve_port)
|
||||
}
|
||||
use vandelay::jmap::account::AccountSelector;
|
||||
use vandelay::jmap::http::Auth;
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::import_managesieve::{ManageSieveAuth, ManageSieveImportConfig};
|
||||
use vandelay::sync::{self, CommonConfig, ConnectConfig, ImportConfig};
|
||||
use inbuxa_migrate::jmap::account::AccountSelector;
|
||||
use inbuxa_migrate::jmap::http::Auth;
|
||||
use inbuxa_migrate::logging::Logger;
|
||||
use inbuxa_migrate::sync::import_managesieve::{ManageSieveAuth, ManageSieveImportConfig};
|
||||
use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ImportConfig};
|
||||
|
||||
fn tmp_archive(tag: &str) -> PathBuf {
|
||||
let mut p = std::env::temp_dir();
|
||||
p.push(format!(
|
||||
"vandelay-managesieve-{tag}-{}-{}.sqlite",
|
||||
"inbuxa-migrate-managesieve-{tag}-{}-{}.sqlite",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
@@ -226,7 +227,7 @@ fn managesieve_and_jmap_imports_share_blob_bytes() {
|
||||
},
|
||||
account: AccountSelector::Id(acc.account_id.clone()),
|
||||
},
|
||||
objects: Some(vec![vandelay::types::ObjectType::SieveScript]),
|
||||
objects: Some(vec![inbuxa_migrate::types::ObjectType::SieveScript]),
|
||||
allow_source_change: false,
|
||||
};
|
||||
let jmap_common = CommonConfig {
|
||||
@@ -319,7 +320,7 @@ fn managesieve_source_change_protection_refuses_second_account() {
|
||||
|
||||
let err = sync::import_managesieve::run(common(&archive), basic_config(&acc2.localpart))
|
||||
.expect_err("should refuse the second source without override");
|
||||
assert!(matches!(err, vandelay::error::Error::SourceChange(_)));
|
||||
assert!(matches!(err, inbuxa_migrate::error::Error::SourceChange(_)));
|
||||
|
||||
let _ = std::fs::remove_file(&archive);
|
||||
seeder::teardown(base_url()).expect("teardown");
|
||||
@@ -336,7 +337,7 @@ fn managesieve_implicit_tls_path_succeeds_when_offered() {
|
||||
cfg.url = sieves_url();
|
||||
let result = sync::import_managesieve::run(common(&archive), cfg);
|
||||
|
||||
if let Err(vandelay::error::Error::Connection(msg)) = &result {
|
||||
if let Err(inbuxa_migrate::error::Error::Connection(msg)) = &result {
|
||||
eprintln!("(expected on cleartext-only deployments) {msg}");
|
||||
}
|
||||
|
||||
@@ -382,7 +383,7 @@ fn managesieve_round_trip_via_jmap_export_converges() {
|
||||
allow_invalid_certs: true,
|
||||
logger: Logger::from_flags(false, 0),
|
||||
};
|
||||
let export_cfg = vandelay::sync::ExportConfig {
|
||||
let export_cfg = inbuxa_migrate::sync::ExportConfig {
|
||||
connect: ConnectConfig {
|
||||
url: fx.base_url.clone(),
|
||||
auth: Auth::Basic {
|
||||
@@ -391,7 +392,7 @@ fn managesieve_round_trip_via_jmap_export_converges() {
|
||||
},
|
||||
account: AccountSelector::Id(dst.account_id.clone()),
|
||||
},
|
||||
objects: Some(vec![vandelay::types::ObjectType::SieveScript]),
|
||||
objects: Some(vec![inbuxa_migrate::types::ObjectType::SieveScript]),
|
||||
prune: false,
|
||||
yes: false,
|
||||
};
|
||||
@@ -416,7 +417,7 @@ fn managesieve_round_trip_via_jmap_export_converges() {
|
||||
allow_invalid_certs: true,
|
||||
logger: Logger::from_flags(false, 0),
|
||||
};
|
||||
let export_cfg2 = vandelay::sync::ExportConfig {
|
||||
let export_cfg2 = inbuxa_migrate::sync::ExportConfig {
|
||||
connect: ConnectConfig {
|
||||
url: fx.base_url.clone(),
|
||||
auth: Auth::Basic {
|
||||
@@ -425,7 +426,7 @@ fn managesieve_round_trip_via_jmap_export_converges() {
|
||||
},
|
||||
account: AccountSelector::Id(dst.account_id.clone()),
|
||||
},
|
||||
objects: Some(vec![vandelay::types::ObjectType::SieveScript]),
|
||||
objects: Some(vec![inbuxa_migrate::types::ObjectType::SieveScript]),
|
||||
prune: false,
|
||||
yes: false,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -11,9 +12,9 @@ use std::path::{Path, PathBuf};
|
||||
use rusqlite::Connection;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::CommonConfig;
|
||||
use vandelay::sync::import_takeout::{TakeoutImportConfig, run};
|
||||
use inbuxa_migrate::logging::Logger;
|
||||
use inbuxa_migrate::sync::CommonConfig;
|
||||
use inbuxa_migrate::sync::import_takeout::{TakeoutImportConfig, run};
|
||||
|
||||
const RESOURCES: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/resources");
|
||||
|
||||
@@ -230,7 +231,7 @@ fn empty_directory_fails_with_usage() {
|
||||
let archive = td.path().join("a.sqlite");
|
||||
let err = run(common_for(&archive), config_for(td.path().to_path_buf())).unwrap_err();
|
||||
assert!(format!("{err}").contains("no .mbox"));
|
||||
assert!(matches!(err, vandelay::error::Error::Usage(_)));
|
||||
assert!(matches!(err, inbuxa_migrate::error::Error::Usage(_)));
|
||||
assert_eq!(err.exit_code(), 1);
|
||||
}
|
||||
|
||||
@@ -240,7 +241,7 @@ fn nonexistent_path_fails_with_usage() {
|
||||
let archive = td.path().join("a.sqlite");
|
||||
let missing = td.path().join("does-not-exist");
|
||||
let err = run(common_for(&archive), config_for(missing)).unwrap_err();
|
||||
assert!(matches!(err, vandelay::error::Error::Usage(_)));
|
||||
assert!(matches!(err, inbuxa_migrate::error::Error::Usage(_)));
|
||||
assert_eq!(err.exit_code(), 1);
|
||||
}
|
||||
|
||||
@@ -450,7 +451,7 @@ fn contacts_dedup_by_uid_across_two_vcf_files() {
|
||||
assert_eq!(with_uid, 2);
|
||||
let synthetic_uids: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM contact_cards WHERE uid LIKE 'vandelay-syn-%'",
|
||||
"SELECT COUNT(*) FROM contact_cards WHERE uid LIKE 'inbuxa-migrate-syn-%'",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
@@ -543,7 +544,7 @@ fn source_change_protection_refuses_different_path() {
|
||||
let archive = td.path().join("a.sqlite");
|
||||
run(common_for(&archive), config_for(a.clone())).unwrap();
|
||||
let err = run(common_for(&archive), config_for(b.clone())).unwrap_err();
|
||||
assert!(matches!(err, vandelay::error::Error::SourceChange(_)));
|
||||
assert!(matches!(err, inbuxa_migrate::error::Error::SourceChange(_)));
|
||||
|
||||
let mut cfg2 = config_for(b);
|
||||
cfg2.allow_source_change = true;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
* SPDX-FileCopyrightText: 2026 John Coffey <johnellis@linux.com>
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
*/
|
||||
@@ -16,16 +17,16 @@ use rusqlite::Connection;
|
||||
fn base_url() -> &'static str {
|
||||
shared_stalwart().base_url()
|
||||
}
|
||||
use vandelay::jmap::account::AccountSelector;
|
||||
use vandelay::jmap::http::Auth;
|
||||
use vandelay::logging::Logger;
|
||||
use vandelay::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg};
|
||||
use vandelay::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig};
|
||||
use inbuxa_migrate::jmap::account::AccountSelector;
|
||||
use inbuxa_migrate::jmap::http::Auth;
|
||||
use inbuxa_migrate::logging::Logger;
|
||||
use inbuxa_migrate::sync::import_dav::{DavAuth, DavImportConfig, DavKindArg};
|
||||
use inbuxa_migrate::sync::{self, CommonConfig, ConnectConfig, ExportConfig, ImportConfig};
|
||||
|
||||
fn tmp_archive(tag: &str) -> PathBuf {
|
||||
let mut p = std::env::temp_dir();
|
||||
p.push(format!(
|
||||
"vandelay-{tag}-{}-{}.sqlite",
|
||||
"inbuxa-migrate-{tag}-{}-{}.sqlite",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
|
||||
-228
@@ -1,228 +0,0 @@
|
||||
<?xml version='1.0' encoding='windows-1252'?>
|
||||
<!--
|
||||
Copyright (C) 2017 Christopher R. Field.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<!--
|
||||
The "cargo wix" subcommand provides a variety of predefined variables available
|
||||
for customization of this template. The values for each variable are set at
|
||||
installer creation time. The following variables are available:
|
||||
|
||||
TargetTriple = The rustc target triple name.
|
||||
TargetEnv = The rustc target environment. This is typically either
|
||||
"msvc" or "gnu" depending on the toolchain downloaded and
|
||||
installed.
|
||||
TargetVendor = The rustc target vendor. This is typically "pc", but Rust
|
||||
does support other vendors, like "uwp".
|
||||
CargoTargetBinDir = The complete path to the directory containing the
|
||||
binaries (exes) to include. The default would be
|
||||
"target\release\". If an explicit rustc target triple is
|
||||
used, i.e. cross-compiling, then the default path would
|
||||
be "target\<CARGO_TARGET>\<CARGO_PROFILE>",
|
||||
where "<CARGO_TARGET>" is replaced with the "CargoTarget"
|
||||
variable value and "<CARGO_PROFILE>" is replaced with the
|
||||
value from the "CargoProfile" variable. This can also
|
||||
be overridden manually with the "target-bin-dir" flag.
|
||||
CargoTargetDir = The path to the directory for the build artifacts, i.e.
|
||||
"target".
|
||||
CargoProfile = The cargo profile used to build the binaries
|
||||
(usually "debug" or "release").
|
||||
Version = The version for the installer. The default is the
|
||||
"Major.Minor.Fix" semantic versioning number of the Rust
|
||||
package.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Please do not remove these pre-processor If-Else blocks. These are used with
|
||||
the `cargo wix` subcommand to automatically determine the installation
|
||||
destination for 32-bit versus 64-bit installers. Removal of these lines will
|
||||
cause installation errors.
|
||||
-->
|
||||
<?if $(sys.BUILDARCH) = x64 or $(sys.BUILDARCH) = arm64 ?>
|
||||
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
|
||||
<?else ?>
|
||||
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
|
||||
<?endif ?>
|
||||
|
||||
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
|
||||
|
||||
<Product
|
||||
Id='*'
|
||||
Name='vandelay'
|
||||
UpgradeCode='5C8148E7-34A9-4216-A802-FFB8345AECC4'
|
||||
Manufacturer='Stalwart Labs LLC'
|
||||
Language='1033'
|
||||
Codepage='1252'
|
||||
Version='$(var.Version)'>
|
||||
|
||||
<Package Id='*'
|
||||
Keywords='Installer'
|
||||
Description='Vandelay, the JMAP importer-exporter'
|
||||
Manufacturer='Stalwart Labs LLC'
|
||||
InstallerVersion='450'
|
||||
Languages='1033'
|
||||
Compressed='yes'
|
||||
InstallScope='perMachine'
|
||||
SummaryCodepage='1252'
|
||||
/>
|
||||
|
||||
<MajorUpgrade
|
||||
Schedule='afterInstallInitialize'
|
||||
DowngradeErrorMessage='A newer version of [ProductName] is already installed. Setup will now exit.'/>
|
||||
|
||||
<Media Id='1' Cabinet='media1.cab' EmbedCab='yes' DiskPrompt='CD-ROM #1'/>
|
||||
<Property Id='DiskPrompt' Value='vandelay Installation'/>
|
||||
|
||||
<Directory Id='TARGETDIR' Name='SourceDir'>
|
||||
<Directory Id='$(var.PlatformProgramFilesFolder)' Name='PFiles'>
|
||||
<Directory Id='APPLICATIONFOLDER' Name='vandelay'>
|
||||
|
||||
<!--
|
||||
Enabling the license sidecar file in the installer is a four step process:
|
||||
|
||||
1. Uncomment the `Component` tag and its contents.
|
||||
2. Change the value for the `Source` attribute in the `File` tag to a path
|
||||
to the file that should be included as the license sidecar file. The path
|
||||
can, and probably should be, relative to this file.
|
||||
3. Change the value for the `Name` attribute in the `File` tag to the
|
||||
desired name for the file when it is installed alongside the `bin` folder
|
||||
in the installation directory. This can be omitted if the desired name is
|
||||
the same as the file name.
|
||||
4. Uncomment the `ComponentRef` tag with the Id attribute value of "License"
|
||||
further down in this file.
|
||||
-->
|
||||
<!--
|
||||
<Component Id='License' Guid='*'>
|
||||
<File Id='LicenseFile' Name='ChangeMe' DiskId='1' Source='C:\Path\To\File' KeyPath='yes'/>
|
||||
</Component>
|
||||
-->
|
||||
|
||||
<Directory Id='Bin' Name='bin'>
|
||||
<Component Id='Path' Guid='FA9E97EB-5E5C-4809-9018-9DC1EDB0E93D' KeyPath='yes'>
|
||||
<Environment
|
||||
Id='PATH'
|
||||
Name='PATH'
|
||||
Value='[Bin]'
|
||||
Permanent='no'
|
||||
Part='last'
|
||||
Action='set'
|
||||
System='yes'/>
|
||||
</Component>
|
||||
<Component Id='binary0' Guid='*'>
|
||||
<File
|
||||
Id='exe0'
|
||||
Name='vandelay.exe'
|
||||
DiskId='1'
|
||||
Source='$(var.CargoTargetBinDir)\vandelay.exe'
|
||||
KeyPath='yes'/>
|
||||
</Component>
|
||||
</Directory>
|
||||
</Directory>
|
||||
</Directory>
|
||||
</Directory>
|
||||
|
||||
<Feature
|
||||
Id='Binaries'
|
||||
Title='Application'
|
||||
Description='Installs all binaries and the license.'
|
||||
Level='1'
|
||||
ConfigurableDirectory='APPLICATIONFOLDER'
|
||||
AllowAdvertise='no'
|
||||
Display='expand'
|
||||
Absent='disallow'>
|
||||
|
||||
<!--
|
||||
Uncomment the following `ComponentRef` tag to add the license
|
||||
sidecar file to the installer.
|
||||
-->
|
||||
<!--<ComponentRef Id='License'/>-->
|
||||
|
||||
<ComponentRef Id='binary0'/>
|
||||
|
||||
<Feature
|
||||
Id='Environment'
|
||||
Title='PATH Environment Variable'
|
||||
Description='Add the install location of the [ProductName] executable to the PATH system environment variable. This allows the [ProductName] executable to be called from any location.'
|
||||
Level='1'
|
||||
Absent='allow'>
|
||||
<ComponentRef Id='Path'/>
|
||||
</Feature>
|
||||
</Feature>
|
||||
|
||||
<SetProperty Id='ARPINSTALLLOCATION' Value='[APPLICATIONFOLDER]' After='CostFinalize'/>
|
||||
|
||||
|
||||
<!--
|
||||
Uncomment the following `Icon` and `Property` tags to change the product icon.
|
||||
|
||||
The product icon is the graphic that appears in the Add/Remove
|
||||
Programs control panel for the application.
|
||||
-->
|
||||
<!--<Icon Id='ProductICO' SourceFile='wix\Product.ico'/>-->
|
||||
<!--<Property Id='ARPPRODUCTICON' Value='ProductICO' />-->
|
||||
|
||||
<Property Id='ARPHELPLINK' Value='https://github.com/stalwartlabs/vandelay'/>
|
||||
|
||||
<UI>
|
||||
<UIRef Id='WixUI_FeatureTree'/>
|
||||
|
||||
<!--
|
||||
Enabling the EULA dialog in the installer is a three step process:
|
||||
|
||||
1. Comment out or remove the two `Publish` tags that follow the
|
||||
`WixVariable` tag.
|
||||
2. Uncomment the `<WixVariable Id='WixUILicenseRtf' Value='Path\to\Eula.rft'>` tag further down
|
||||
3. Replace the `Value` attribute of the `WixVariable` tag with
|
||||
the path to a RTF file that will be used as the EULA and
|
||||
displayed in the license agreement dialog.
|
||||
-->
|
||||
<Publish Dialog='WelcomeDlg' Control='Next' Event='NewDialog' Value='CustomizeDlg' Order='99'>1</Publish>
|
||||
<Publish Dialog='CustomizeDlg' Control='Back' Event='NewDialog' Value='WelcomeDlg' Order='99'>1</Publish>
|
||||
|
||||
</UI>
|
||||
|
||||
|
||||
<!--
|
||||
Enabling the EULA dialog in the installer requires uncommenting
|
||||
the following `WixUILicenseRTF` tag and changing the `Value`
|
||||
attribute.
|
||||
-->
|
||||
<!-- <WixVariable Id='WixUILicenseRtf' Value='Relative\Path\to\Eula.rtf'/> -->
|
||||
|
||||
|
||||
<!--
|
||||
Uncomment the next `WixVariable` tag to customize the installer's
|
||||
Graphical User Interface (GUI) and add a custom banner image across
|
||||
the top of each screen. See the WiX Toolset documentation for details
|
||||
about customization.
|
||||
|
||||
The banner BMP dimensions are 493 x 58 pixels.
|
||||
-->
|
||||
<!--<WixVariable Id='WixUIBannerBmp' Value='wix\Banner.bmp'/>-->
|
||||
|
||||
|
||||
<!--
|
||||
Uncomment the next `WixVariable` tag to customize the installer's
|
||||
Graphical User Interface (GUI) and add a custom image to the first
|
||||
dialog, or screen. See the WiX Toolset documentation for details about
|
||||
customization.
|
||||
|
||||
The dialog BMP dimensions are 493 x 312 pixels.
|
||||
-->
|
||||
<!--<WixVariable Id='WixUIDialogBmp' Value='wix\Dialog.bmp'/>-->
|
||||
|
||||
</Product>
|
||||
|
||||
</Wix>
|
||||
Reference in New Issue
Block a user