From 9c6c53e21c37b100bec87683fb9c817ce945403d Mon Sep 17 00:00:00 2001 From: mdecimus Date: Tue, 11 Jul 2023 17:16:08 +0200 Subject: [PATCH] CI/CD tweaks. --- .github/FUNDING.yml | 13 + .github/workflows/build.yml | 306 +++++++++ .github/workflows/test.yml | 110 +++ CNAME | 1 + Cargo.lock | 47 ++ Dockerfile | 52 ++ crates/imap/src/op/authenticate.rs | 27 - crates/imap/src/op/list.rs | 78 +-- crates/install/Cargo.toml | 8 +- crates/install/build.rs | 6 + crates/install/src/main.rs | 457 ++++++++++--- crates/jmap/src/push/ece.rs | 26 - crates/store/Cargo.toml | 3 + install.sh | 641 ++++++++++++++++++ resources/config/common.toml | 2 +- resources/config/smtp.toml | 3 +- resources/config/stalwart-config.zip | Bin 10702 -> 0 bytes ...art-smtp.service => stalwart-mail.service} | 8 +- ...talwart.smtp.plist => stalwart.mail.plist} | 8 +- tests/src/imap/basic.rs | 24 + tests/src/imap/mailbox.rs | 73 ++ tests/src/jmap/mod.rs | 16 +- tests/src/jmap/push_subscription.rs | 22 + 23 files changed, 1707 insertions(+), 224 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/test.yml create mode 100644 CNAME create mode 100644 Dockerfile create mode 100644 crates/install/build.rs create mode 100644 install.sh delete mode 100644 resources/config/stalwart-config.zip rename resources/systemd/{stalwart-smtp.service => stalwart-mail.service} (58%) rename resources/systemd/{stalwart.smtp.plist => stalwart.mail.plist} (64%) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..e2b24cc0 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: stalwartlabs +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..3aeefb09 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,306 @@ +name: Build + +on: + workflow_dispatch: + pull_request: + push: + tags: + - '*' + +jobs: + build: + name: Building for ${{ matrix.target }} on ${{ matrix.host_os }} + runs-on: ${{ matrix.host_os }} + if: '!cancelled()' + strategy: + matrix: + include: + - target: x86_64-unknown-linux-gnu + host_os: ubuntu-20.04 + - target: x86_64-apple-darwin + host_os: macos-latest + - target: x86_64-pc-windows-msvc + host_os: windows-2022 + #- target: aarch64-unknown-linux-gnu + # host_os: ubuntu-20.04 + #- target: x86_64-unknown-linux-musl + # host_os: ubuntu-20.04 + #- target: aarch64-unknown-linux-musl + # host_os: ubuntu-20.04 + #- target: aarch64-apple-darwin + # host_os: macos-latest + #- target: aarch64-pc-windows-msvc + # host_os: windows-2022 + #- target: aarch64-pc-windows-msvc + # host_os: windows-2022 + #- target: arm-unknown-linux-musleabihf + # host_os: ubuntu-20.04 + #- target: arm-unknown-linux-gnueabihf + # host_os: ubuntu-20.04 + #- target: armv7-unknown-linux-musleabihf + # host_os: ubuntu-20.04 + #- target: armv7-unknown-linux-gnueabihf + # host_os: ubuntu-20.04 + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + persist-credentials: false + + - name: Install dependencies (Linux) + if: ${{ contains(matrix.host_os, 'ubuntu') }} + run: | + sudo apt-get update -y + sudo apt-get install -yq protobuf-compiler wget + wget https://github.com/apple/foundationdb/releases/download/7.1.0/foundationdb-clients_7.1.0-1_amd64.deb + sudo dpkg -i --force-architecture foundationdb-clients_7.1.0-1_amd64.deb + + - name: Install dependencies (MacOs) + if: ${{ contains(matrix.host_os, 'macos') }} + run: | + brew install protobuf + brew install wget + wget https://github.com/apple/foundationdb/releases/download/7.1.32/FoundationDB-7.1.32_x86_64.pkg + sudo installer -allowUntrusted -dumplog -pkg FoundationDB-7.1.32_x86_64.pkg -target / + + - name: Install dependencies (Windows) + if: ${{ contains(matrix.host_os, 'windows') }} + uses: arduino/setup-protoc@v1 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + override: true + target: ${{ matrix.target }} + toolchain: stable + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.host_os }}-${{ matrix.target }}-mail + + - name: Building binary (Unix version) + if: ${{ !contains(matrix.host_os, 'windows') }} + run: | + cargo build --manifest-path=crates/main/Cargo.toml --target=${{ matrix.target }} --no-default-features --features foundationdb --release + cd target/${{ matrix.target }}/release && tar czvf ../../../stalwart-mail-foundationdb-${{ matrix.target }}.tar.gz stalwart-mail && cd - + cargo build --manifest-path=crates/main/Cargo.toml --target=${{ matrix.target }} --release + cargo build --manifest-path=crates/cli/Cargo.toml --target=${{ matrix.target }} --release + cargo build --manifest-path=crates/install/Cargo.toml --target=${{ matrix.target }} --release + cd target/${{ matrix.target }}/release + tar czvf ../../../stalwart-mail-sqlite-${{ matrix.target }}.tar.gz stalwart-mail + tar czvf ../../../stalwart-cli-${{ matrix.target }}.tar.gz stalwart-cli + tar czvf ../../../stalwart-install-${{ matrix.target }}.tar.gz stalwart-install + cd - + + - name: Building binary (Windows version) + if: ${{ contains(matrix.host_os, 'windows') }} + run: | + cargo build --manifest-path=crates/main/Cargo.toml --target=${{ matrix.target }} --release + cargo build --manifest-path=crates/cli/Cargo.toml --target=${{ matrix.target }} --release + cargo build --manifest-path=crates/install/Cargo.toml --target=${{ matrix.target }} --release + cd target/${{ matrix.target }}/release + 7z a ../../../stalwart-mail-sqlite-${{ matrix.target }}.zip stalwart-mail.exe + 7z a ../../../stalwart-cli-${{ matrix.target }}.zip stalwart-cli.exe + 7z a ../../../stalwart-install-${{ matrix.target }}.zip stalwart-install.exe + cd - + + - name: Publish Release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + files: 'stalwart-*' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + cross_build_tools: + runs-on: ubuntu-latest + name: Building tools for ${{ matrix.target }} on ${{ matrix.distro }} + if: '!cancelled()' + + strategy: + matrix: + include: + - arch: aarch64 + distro: ubuntu20.04 + target: aarch64-unknown-linux-gnu + steps: + - uses: actions/checkout@v3 + - uses: uraimo/run-on-arch-action@v2 + name: Build artifact + id: build + with: + arch: ${{ matrix.arch }} + distro: ${{ matrix.distro }} + + # Not required, but speeds up builds + githubToken: ${{ github.token }} + + # Create an artifacts directory + setup: | + mkdir -p "${PWD}/artifacts" + + # Mount the artifacts directory as /artifacts in the container + dockerRunArgs: | + --volume "${PWD}/artifacts:/artifacts" + + # Pass some environment variables to the container + env: | + target: ${{ matrix.target }} + + # The shell to run commands with in the container + shell: /bin/sh + + install: | + apt-get update -yq + apt-get install -yq build-essential cmake wget curl + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal + + run: | + export PATH="$HOME/.cargo/bin:$PATH" + cargo build --manifest-path=crates/cli/Cargo.toml --target=${target} --release + cargo build --manifest-path=crates/install/Cargo.toml --target=${target} --release + cd target/${target}/release + tar czvf /artifacts/stalwart-cli-${target}.tar.gz stalwart-cli + tar czvf /artifacts/stalwart-install-${target}.tar.gz stalwart-install + cd - + + - name: Move packages + run: | + mv ${PWD}/artifacts/* . + + - name: Publish Release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + files: 'stalwart-*' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + cross_build: + runs-on: ubuntu-latest + name: Building for ${{ matrix.target }} on ${{ matrix.distro }} + if: '!cancelled()' + + strategy: + matrix: + include: + - arch: aarch64 + distro: ubuntu20.04 + target: aarch64-unknown-linux-gnu + steps: + - uses: actions/checkout@v3 + - uses: uraimo/run-on-arch-action@v2 + name: Build artifact + id: build + with: + arch: ${{ matrix.arch }} + distro: ${{ matrix.distro }} + + # Not required, but speeds up builds + githubToken: ${{ github.token }} + + # Create an artifacts directory + setup: | + mkdir -p "${PWD}/artifacts" + + # Mount the artifacts directory as /artifacts in the container + dockerRunArgs: | + --volume "${PWD}/artifacts:/artifacts" + + # Pass some environment variables to the container + env: | + target: ${{ matrix.target }} + + # The shell to run commands with in the container + shell: /bin/sh + + install: | + apt-get update -yq + apt-get install -yq build-essential cmake protobuf-compiler wget curl + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal + + run: | + export PATH="$HOME/.cargo/bin:$PATH" + cargo build --manifest-path=crates/main/Cargo.toml --target=${target} --release + cd target/${target}/release + tar czvf /artifacts/stalwart-mail-sqlite-${target}.tar.gz stalwart-mail + cd - + + - name: Move packages + run: | + mv ${PWD}/artifacts/* . + + - name: Publish Release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + files: 'stalwart-*' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + + docker-amd: + name: Build Docker AMD64 images + if: '!cancelled()' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + push: true + platforms: linux/amd64 + tags: stalwartlabs/mail-server:latest + cache-from: type=registry,ref=stalwartlabs/mail-server:buildcache + cache-to: type=registry,ref=stalwartlabs/mail-server:buildcache,mode=max + #cache-from: type=gha + #cache-to: type=gha,mode=max + + docker-arm: + name: Build Docker ARM64 images + if: '!cancelled()' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + push: true + platforms: linux/arm64 + tags: stalwartlabs/mail-server:latest + cache-from: type=registry,ref=stalwartlabs/mail-server:buildcache + cache-to: type=registry,ref=stalwartlabs/mail-server:buildcache,mode=max + #cache-from: type=gha + #cache-to: type=gha,mode=max diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..e2b2ae46 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,110 @@ +name: Test + +on: + workflow_dispatch: + pull_request: + push: + tags: + - '*' + +jobs: + style: + name: Check Style + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: rustfmt + profile: minimal + override: true + + - name: cargo fmt -- --check + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + test: + name: Test + needs: [style] + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Install dependencies + run: | + sudo apt-get update -y + sudo apt-get install -yq protobuf-compiler + wget https://github.com/glauth/glauth/releases/download/v2.2.0/glauth-linux-arm64 + chmod a+rx glauth-linux-arm64 + nohup ./glauth-linux-arm64 -c tests/resources/ldap.cfg & + wget https://dl.min.io/server/minio/release/linux-amd64/archive/minio_20230629051228.0.0_amd64.deb -O minio.deb + sudo dpkg -i minio.deb + mkdir ~/minio + nohup minio server ~/minio --console-address :9090 & + wget https://dl.min.io/client/mc/release/linux-amd64/mc + chmod a+rx mc + ./mc alias set myminio http://localhost:9000 minioadmin minioadmin + ./mc mb tmp + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + + - name: JMAP Protocol Tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path=crates/jmap-proto/Cargo.toml + + - name: IMAP Protocol Tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path=crates/imap-proto/Cargo.toml + + - name: Full-text search Tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path=crates/store/Cargo.toml + + - name: Store Tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path=tests/Cargo.toml store -- --nocapture + + - name: Directory Tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path=tests/Cargo.toml directory -- --nocapture + + - name: SMTP Tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path=tests/Cargo.toml smtp -- --nocapture + + - name: IMAP Tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path=tests/Cargo.toml imap -- --nocapture + + - name: JMAP Tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path=tests/Cargo.toml jmap -- --nocapture diff --git a/CNAME b/CNAME new file mode 100644 index 00000000..554a1aff --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +get.stalw.art diff --git a/Cargo.lock b/Cargo.lock index deec4070..9e066b08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1296,6 +1296,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "filetime" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.2.16", + "windows-sys 0.48.0", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -4385,7 +4397,9 @@ name = "stalwart-install" version = "0.3.0" dependencies = [ "base64 0.21.2", + "clap", "dialoguer", + "flate2", "indicatif", "libc", "openssl", @@ -4394,6 +4408,8 @@ dependencies = [ "reqwest", "rpassword", "rusqlite", + "tar", + "zip-extract", ] [[package]] @@ -4498,6 +4514,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tar" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.6.0" @@ -5534,6 +5561,15 @@ dependencies = [ "time 0.3.22", ] +[[package]] +name = "xattr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +dependencies = [ + "libc", +] + [[package]] name = "xml-rs" version = "0.8.15" @@ -5572,6 +5608,17 @@ dependencies = [ "zstd", ] +[[package]] +name = "zip-extract" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb654964c003959ed64cbd0d7b329bcdcbd9690facd50c8617748d3622543972" +dependencies = [ + "log", + "thiserror", + "zip", +] + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..55478d45 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,52 @@ +FROM debian:buster-slim AS chef +RUN apt-get update && \ + export DEBIAN_FRONTEND=noninteractive && \ + apt-get install -yq \ + build-essential \ + cmake \ + clang \ + curl \ + protobuf-compiler +ENV RUSTUP_HOME=/opt/rust/rustup \ + PATH=/home/root/.cargo/bin:/opt/rust/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +RUN curl https://sh.rustup.rs -sSf | \ + env CARGO_HOME=/opt/rust/cargo \ + sh -s -- -y --default-toolchain stable --profile minimal --no-modify-path && \ + env CARGO_HOME=/opt/rust/cargo \ + rustup component add rustfmt +RUN env CARGO_HOME=/opt/rust/cargo cargo install cargo-chef && \ + rm -rf /opt/rust/cargo/registry/ +WORKDIR /app + +FROM chef AS planner +COPY Cargo.toml . +COPY Cargo.lock . +COPY crates/ crates/ +COPY resources/ resources/ +COPY tests/ tests/ +RUN cargo chef prepare --recipe-path recipe.json + +FROM chef AS builder +COPY --from=planner /app/recipe.json recipe.json +RUN cargo chef cook --release --recipe-path recipe.json +COPY Cargo.toml . +COPY Cargo.lock . +COPY crates/ crates/ +COPY resources/ resources/ +COPY tests/ tests/ +RUN cargo build --manifest-path=crates/main/Cargo.toml --release +RUN cargo build --manifest-path=crates/install/Cargo.toml --release + +FROM debian:buster-slim AS runtime + +COPY --from=builder /app/target/release/stalwart-mail /usr/local/bin/stalwart-mail +COPY --from=builder /app/target/release/stalwart-install /usr/local/bin/stalwart-install +RUN echo "#\!/bin/sh\n\n/usr/local/bin/stalwart-install -c all-in-one -p /opt/stalwart-mail -d" > /usr/local/bin/configure.sh && \ + chmod +x /usr/local/bin/configure.sh +RUN useradd stalwart-mail -s /sbin/nologin -M +RUN mkdir -p /opt/stalwart-mail +RUN chown stalwart-mail:stalwart-mail /opt/stalwart-mail + +VOLUME [ "/opt/stalwart-mail" ] + +ENTRYPOINT ["/usr/local/bin/stalwart-mail", "--config", "/opt/stalwart-mail/etc/config.toml"] diff --git a/crates/imap/src/op/authenticate.rs b/crates/imap/src/op/authenticate.rs index 92f7bae0..4f73892d 100644 --- a/crates/imap/src/op/authenticate.rs +++ b/crates/imap/src/op/authenticate.rs @@ -276,30 +276,3 @@ pub fn decode_challenge_oauth(challenge: &[u8]) -> Result, & Err("Failed to find 'auth=Bearer' in challenge.") } - -#[cfg(test)] -mod tests { - use mail_parser::decoders::base64::base64_decode; - use mail_send::Credentials; - - #[test] - fn decode_challenge_oauth() { - assert!( - Credentials::OAuthBearer { - token: "vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg==".to_string() - } == super::decode_challenge_oauth( - &base64_decode( - concat!( - "bixhPXVzZXJAZXhhbXBsZS5jb20sAWhv", - "c3Q9c2VydmVyLmV4YW1wbGUuY29tAXBvcnQ9MTQzAWF1dGg9QmVhcmVyI", - "HZGOWRmdDRxbVRjMk52YjNSbGNrQmhiSFJoZG1semRHRXVZMjl0Q2c9PQ", - "EB" - ) - .as_bytes(), - ) - .unwrap(), - ) - .unwrap() - ); - } -} diff --git a/crates/imap/src/op/list.rs b/crates/imap/src/op/list.rs index 7c0346f0..3f7f1013 100644 --- a/crates/imap/src/op/list.rs +++ b/crates/imap/src/op/list.rs @@ -303,7 +303,7 @@ impl SessionData { } #[allow(clippy::while_let_on_iterator)] -fn matches_pattern(patterns: &[String], mailbox_name: &str) -> bool { +pub fn matches_pattern(patterns: &[String], mailbox_name: &str) -> bool { if patterns.is_empty() { return true; } @@ -370,79 +370,3 @@ fn matches_pattern(patterns: &[String], mailbox_name: &str) -> bool { false } - -#[cfg(test)] -mod tests { - - #[test] - fn matches_pattern() { - let mailboxes = [ - "imaptest", - "imaptest/test", - "imaptest/test2", - "imaptest/test3", - "imaptest/test3/test4", - "imaptest/test3/test4/test5", - "foobar/test", - "foobar/test/test", - "foobar/test1/test1", - ]; - - for (pattern, expected_match) in [ - ( - "imaptest/%", - vec!["imaptest/test", "imaptest/test2", "imaptest/test3"], - ), - ("imaptest/%/%", vec!["imaptest/test3/test4"]), - ( - "imaptest/*", - vec![ - "imaptest/test", - "imaptest/test2", - "imaptest/test3", - "imaptest/test3/test4", - "imaptest/test3/test4/test5", - ], - ), - ("imaptest/*test4", vec!["imaptest/test3/test4"]), - ( - "imaptest/*test*", - vec![ - "imaptest/test", - "imaptest/test2", - "imaptest/test3", - "imaptest/test3/test4", - "imaptest/test3/test4/test5", - ], - ), - ("imaptest/%3/%", vec!["imaptest/test3/test4"]), - ("imaptest/%3/%4", vec!["imaptest/test3/test4"]), - ("imaptest/%t*4", vec!["imaptest/test3/test4"]), - ("*st/%3/%4/%5", vec!["imaptest/test3/test4/test5"]), - ( - "*%*%*%", - vec![ - "imaptest", - "imaptest/test", - "imaptest/test2", - "imaptest/test3", - "imaptest/test3/test4", - "imaptest/test3/test4/test5", - "foobar/test", - "foobar/test/test", - "foobar/test1/test1", - ], - ), - ("foobar*test", vec!["foobar/test", "foobar/test/test"]), - ] { - let patterns = vec![pattern.to_string()]; - let mut matched_mailboxes = Vec::new(); - for mailbox in mailboxes { - if super::matches_pattern(&patterns, mailbox) { - matched_mailboxes.push(mailbox); - } - } - assert_eq!(matched_mailboxes, expected_match, "for pattern {}", pattern); - } - } -} diff --git a/crates/install/Cargo.toml b/crates/install/Cargo.toml index b3c7119e..6b1ab4b4 100644 --- a/crates/install/Cargo.toml +++ b/crates/install/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" resolver = "2" [dependencies] -reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-webpki-roots"]} +reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-webpki-roots", "blocking"] } rusqlite = { version = "0.29.0", features = ["bundled"] } rpassword = "7.0" indicatif = "0.17.0" @@ -20,6 +20,12 @@ openssl = { version = "0.10.55", features = ["vendored"] } base64 = "0.21.2" pwhash = "1.0.0" rand = "0.8.5" +clap = { version = "4.1.6", features = ["derive"] } [target.'cfg(not(target_env = "msvc"))'.dependencies] libc = "0.2.147" +flate2 = "1.0.26" +tar = "0.4.38" + +[target.'cfg(target_env = "msvc")'.dependencies] +zip-extract = "0.1.2" diff --git a/crates/install/build.rs b/crates/install/build.rs new file mode 100644 index 00000000..81caa36d --- /dev/null +++ b/crates/install/build.rs @@ -0,0 +1,6 @@ +fn main() { + println!( + "cargo:rustc-env=TARGET={}", + std::env::var("TARGET").unwrap() + ); +} diff --git a/crates/install/src/main.rs b/crates/install/src/main.rs index a06481d0..183553db 100644 --- a/crates/install/src/main.rs +++ b/crates/install/src/main.rs @@ -1,15 +1,20 @@ use std::{ fs, + io::Cursor, path::{Path, PathBuf}, + process::Command, time::SystemTime, }; use base64::{engine::general_purpose, Engine}; +use clap::{Parser, ValueEnum}; use dialoguer::{console::Term, theme::ColorfulTheme, Input, Select}; +use flate2::bufread::GzDecoder; use openssl::rsa::Rsa; use pwhash::sha512_crypt; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use rusqlite::{Connection, OpenFlags}; +use tar::Archive; const CFG_COMMON: &str = include_str!("../../../resources/config/common.toml"); const CFG_DIRECTORY: &str = include_str!("../../../resources/config/directory.toml"); @@ -17,7 +22,25 @@ const CFG_JMAP: &str = include_str!("../../../resources/config/jmap.toml"); const CFG_IMAP: &str = include_str!("../../../resources/config/imap.toml"); const CFG_SMTP: &str = include_str!("../../../resources/config/smtp.toml"); -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg(target_os = "linux")] +const SERVICE: &str = include_str!("../../../resources/systemd/stalwart-mail.service"); +#[cfg(target_os = "macos")] +const SERVICE: &str = include_str!("../../../resources/systemd/stalwart.mail.plist"); + +#[cfg(target_os = "linux")] +const ACCOUNT_NAME: &str = "stalwart-mail"; +#[cfg(target_os = "macos")] +const ACCOUNT_NAME: &str = "_stalwart-mail"; + +#[cfg(not(target_env = "msvc"))] +const PKG_EXTENSION: &str = "tar.gz"; + +#[cfg(target_env = "msvc")] +const PKG_EXTENSION: &str = "zip"; + +static TARGET: &str = env!("TARGET"); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] enum Component { AllInOne, Jmap, @@ -36,23 +59,23 @@ enum Blob { Local, MinIO, S3, - GCS, + Gcs, Azure, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Directory { - SQL, - LDAP, + Sql, + Ldap, None, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum SmtpDirectory { - SQL, - LDAP, - LMTP, - IMAP, + Sql, + Ldap, + Lmtp, + Imap, } const DIRECTORIES: [[&str; 2]; 6] = [ @@ -64,28 +87,63 @@ const DIRECTORIES: [[&str; 2]; 6] = [ ["reports", ""], ]; +#[derive(Debug, Parser)] +#[clap(version, about, long_about = None)] +#[clap(name = "stalwart-cli")] +pub struct Arguments { + #[clap(long, short = 'p')] + path: Option, + #[clap(long, short = 'c')] + component: Option, + #[clap(long, short = 'd')] + docker: bool, +} + fn main() -> std::io::Result<()> { - let c = "fix"; - /*#[cfg(not(target_env = "msvc"))] + let args = Arguments::parse(); + + #[cfg(not(target_env = "msvc"))] unsafe { if libc::getuid() != 0 { eprintln!("This program must be run as root."); std::process::exit(1); } - }*/ + } - println!("\nWelcome to the Stalwart mail server installer\n"); + println!("\nWelcome to the Stalwart Mail Server installer\n"); - let component = select::( - "Which components would you like to install?", - &[ - "All-in-one mail server (JMAP + IMAP + SMTP)", - "JMAP server", - "IMAP server", - "SMTP server", - ], - Component::AllInOne, - )?; + // Obtain component to install + let (component, skip_download) = if let Some(component) = args.component { + (component, true) + } else { + ( + select::( + "Which components would you like to install?", + &[ + "All-in-one mail server (JMAP + IMAP + SMTP)", + "JMAP server", + "IMAP server", + "SMTP server", + ], + Component::AllInOne, + )?, + false, + ) + }; + + // Obtain base path + let base_path = if let Some(base_path) = args.path { + base_path + } else { + PathBuf::from(input( + "Installation directory", + component.default_base_path(), + dir_create_if_missing, + )?) + }; + create_directories(&base_path)?; + + // Build configuration file let mut cfg_file = match component { Component::AllInOne | Component::Imap => { [CFG_COMMON, CFG_DIRECTORY, CFG_JMAP, CFG_IMAP, CFG_SMTP].join("\n") @@ -93,15 +151,46 @@ fn main() -> std::io::Result<()> { Component::Jmap => [CFG_COMMON, CFG_DIRECTORY, CFG_JMAP, CFG_SMTP].join("\n"), Component::Smtp => [CFG_COMMON, CFG_DIRECTORY, CFG_SMTP].join("\n"), }; + let mut download_url = None; + + // Obtain database engine let directory = if component != Component::Smtp { - let backend = select::( - "Which database engine would you like to use?", - &[ - "SQLite (single node, replicated with Litestream)", - "FoundationDB (distributed and fault-tolerant)", - ], - Backend::SQLite, - )?; + if !skip_download { + let backend = select::( + "Which database engine would you like to use?", + &[ + "SQLite (single node, replicated with Litestream)", + "FoundationDB (distributed and fault-tolerant)", + ], + Backend::SQLite, + )?; + + download_url = format!( + concat!( + "https://github.com/stalwartlabs/{}", + "/releases/latest/download/stalwart-{}-{}-{}.{}" + ), + match component { + Component::AllInOne => "mail-server", + Component::Jmap => "jmap-server", + Component::Imap => "imap-server", + Component::Smtp => unreachable!(), + }, + match component { + Component::AllInOne => "mail", + Component::Jmap => "jmap", + Component::Imap => "imap", + Component::Smtp => unreachable!(), + }, + match backend { + Backend::SQLite => "sqlite", + Backend::FoundationDB => "foundationdb", + }, + TARGET, + PKG_EXTENSION + ) + .into(); + } let blob = select::( "Where would you like to store e-mails and blobs?", &[ @@ -135,15 +224,15 @@ fn main() -> std::io::Result<()> { .replace( "__DIRECTORY__", match directory { - Directory::SQL | Directory::None => "sql", - Directory::LDAP => "ldap", + Directory::Sql | Directory::None => "sql", + Directory::Ldap => "ldap", }, ) .replace( "__SMTP_DIRECTORY__", match directory { - Directory::SQL | Directory::None => "sql", - Directory::LDAP => "ldap", + Directory::Sql | Directory::None => "sql", + Directory::Ldap => "ldap", }, ) .replace( @@ -165,40 +254,100 @@ fn main() -> std::io::Result<()> { "LMTP server", "IMAP server", ], - SmtpDirectory::LMTP, + SmtpDirectory::Lmtp, )?; cfg_file = cfg_file .replace("__NEXT_HOP__", "lmtp") .replace( "__SMTP_DIRECTORY__", match smtp_directory { - SmtpDirectory::SQL => "sql", - SmtpDirectory::LDAP => "ldap", - SmtpDirectory::LMTP => "lmtp", - SmtpDirectory::IMAP => "imap", + SmtpDirectory::Sql => "sql", + SmtpDirectory::Ldap => "ldap", + SmtpDirectory::Lmtp => "lmtp", + SmtpDirectory::Imap => "imap", }, ) .replace( "__DIRECTORY__", match smtp_directory { - SmtpDirectory::SQL | SmtpDirectory::LMTP | SmtpDirectory::IMAP => "sql", - SmtpDirectory::LDAP => "ldap", + SmtpDirectory::Sql | SmtpDirectory::Lmtp | SmtpDirectory::Imap => "sql", + SmtpDirectory::Ldap => "ldap", }, ) .replace("__NEXT_HOP__", "lmtp"); + + if !skip_download { + download_url = format!( + concat!( + "https://github.com/stalwartlabs/smtp-server", + "/releases/latest/download/stalwart-smtp-{}.{}" + ), + TARGET, PKG_EXTENSION + ) + .into(); + } match smtp_directory { - SmtpDirectory::SQL => Directory::SQL, - SmtpDirectory::LDAP => Directory::LDAP, - SmtpDirectory::LMTP | SmtpDirectory::IMAP => Directory::None, + SmtpDirectory::Sql => Directory::Sql, + SmtpDirectory::Ldap => Directory::Ldap, + SmtpDirectory::Lmtp | SmtpDirectory::Imap => Directory::None, } }; - let base_path = PathBuf::from(input( - "Installation directory", - component.default_base_path(), - dir_create_if_missing, - )?); - create_directories(&base_path)?; + // Download binary + if let Some(download_url) = download_url { + eprintln!("šŸ“¦ Downloading components..."); + for url in [ + download_url, + format!( + concat!( + "https://github.com/stalwartlabs/mail-server", + "/releases/latest/download/stalwart-cli-{}.{}" + ), + TARGET, PKG_EXTENSION + ), + ] { + match reqwest::blocking::get(&url).and_then(|r| { + if r.status().is_success() { + r.bytes().map(Ok) + } else { + Ok(Err(r)) + } + }) { + Ok(Ok(bytes)) => { + #[cfg(not(target_env = "msvc"))] + if let Err(err) = Archive::new(GzDecoder::new(Cursor::new(bytes))) + .unpack(base_path.join("bin")) + { + eprintln!("āŒ Failed to unpack {}: {}", url, err); + return Ok(()); + } + + #[cfg(target_env = "msvc")] + if let Err(err) = + zip_extract::extract(Cursor::new(bytes), &base_path.join("bin"), true) + { + eprintln!("āŒ Failed to unpack {}: {}", url, err); + return Ok(()); + } + } + Ok(Err(response)) => { + eprintln!( + "āŒ Failed to download {}, make sure your platform is supported: {}", + url, + response.status() + ); + return Ok(()); + } + + Err(err) => { + eprintln!("āŒ Failed to download {}: {}", url, err); + return Ok(()); + } + } + } + } + + // Obtain domain name let domain = input( "What is your main domain name? (you can add others later)", "yourdomain.org", @@ -214,18 +363,37 @@ fn main() -> std::io::Result<()> { .trim() .to_lowercase(); - let cert_path = input( - &format!("Where is the TLS certificate for '{hostname}' located?"), - &format!("/etc/letsencrypt/live/{hostname}/fullchain.pem"), - file_exists, - )?; - let pk_path = input( - &format!("Where is the TLS private key for '{hostname}' located?"), - &format!("/etc/letsencrypt/live/{hostname}/privkey.pem"), - file_exists, - )?; + // Obtain TLS certificate path + let (cert_path, pk_path) = if !args.docker { + ( + input( + &format!("Where is the TLS certificate for '{hostname}' located?"), + &format!("/etc/letsencrypt/live/{hostname}/fullchain.pem"), + file_exists, + )?, + input( + &format!("Where is the TLS private key for '{hostname}' located?"), + &format!("/etc/letsencrypt/live/{hostname}/privkey.pem"), + file_exists, + )?, + ) + } else { + // Create directories + fs::create_dir_all(base_path.join("etc").join("certs").join(&hostname))?; + ( + format!( + "{}/etc/certs/{}/fullchain.pem", + base_path.display(), + hostname + ), + format!("{}/etc/certs/{}/privkey.pem", base_path.display(), hostname), + ) + }; + // Generate DKIM key and instructions let dkim_instructions = generate_dkim(&base_path, &domain, &hostname)?; + + // Create authentication SQLite database let admin_password = if matches!(directory, Directory::None) { create_auth_db(&base_path, &domain)?.into() } else { @@ -245,6 +413,13 @@ fn main() -> std::io::Result<()> { )); fs::rename(&cfg_path, backup_path)?; } + if args.docker { + cfg_file = cfg_file + .replace("127.0.0.1:8686", "0.0.0.0:8686") + .replace("[server.run-as]", "#[server.run-as]") + .replace("user = \"stalwart-mail\"", "#user = \"stalwart-mail\"") + .replace("group = \"stalwart-mail\"", "#group = \"stalwart-mail\""); + } fs::write( cfg_path, cfg_file @@ -255,6 +430,107 @@ fn main() -> std::io::Result<()> { .replace("__PK_PATH__", &pk_path), )?; + // Write service file + if !args.docker { + // Change permissions + #[cfg(not(target_env = "msvc"))] + { + let mut cmd = Command::new("chown"); + cmd.arg("-R") + .arg(format!("{}:{}", ACCOUNT_NAME, ACCOUNT_NAME)) + .arg(&base_path); + if let Err(err) = cmd.status() { + eprintln!("Warning: Failed to set permissions: {}", err); + } + let mut cmd = Command::new("chmod"); + cmd.arg("-R") + .arg("770") + .arg(&format!("{}/etc", base_path.display())) + .arg(&format!("{}/data", base_path.display())) + .arg(&format!("{}/queue", base_path.display())) + .arg(&format!("{}/reports", base_path.display())) + .arg(&format!("{}/logs", base_path.display())); + if let Err(err) = cmd.status() { + eprintln!("Warning: Failed to set permissions: {}", err); + } + } + + #[cfg(target_os = "linux")] + { + let service_file = format!( + "/etc/systemd/system/stalwart-{}.service", + component.binary_name() + ); + let service_name = format!("stalwart-{}", component.binary_name()); + match fs::write( + &service_file, + SERVICE + .replace("__PATH__", base_path.to_str().unwrap()) + .replace("__NAME__", component.binary_name()) + .replace("__TITLE__", component.name()), + ) { + Ok(_) => { + if let Err(err) = Command::new("/bin/systemctl") + .arg("enable") + .arg(service_file) + .status() + .and_then(|_| { + Command::new("/bin/systemctl") + .arg("restart") + .arg(&service_name) + .status() + }) + { + eprintln!("Warning: Failed to start service: {}", err); + } + } + Err(err) => { + eprintln!("Warning: Failed to write service file: {}", err); + } + } + } + #[cfg(target_os = "macos")] + { + let service_file = format!( + "/Library/LaunchDaemons/stalwart.{}.plist", + component.binary_name() + ); + let service_name = format!("system/stalwart.{}", component.binary_name()); + match fs::write( + &service_file, + SERVICE + .replace("__PATH__", base_path.to_str().unwrap()) + .replace("__NAME__", component.binary_name()) + .replace("__TITLE__", component.name()), + ) { + Ok(_) => { + if let Err(err) = Command::new("launchctl") + .arg("load") + .arg(service_file) + .status() + .and_then(|_| { + Command::new("launchctl") + .arg("enable") + .arg(&service_name) + .status() + }) + .and_then(|_| { + Command::new("launchctl") + .arg("start") + .arg(&service_name) + .status() + }) + { + eprintln!("Warning: Failed to start service: {}", err); + } + } + Err(err) => { + eprintln!("Warning: Failed to write service file: {}", err); + } + } + } + } + eprintln!("\nšŸŽ‰ Installation completed!\n\nāœ… {dkim_instructions}\n"); if let Some(admin_password) = admin_password { @@ -419,11 +695,12 @@ fn generate_dkim(path: &Path, domain: &str, hostname: &str) -> std::io::Result (libc::uid_t, libc::gid_t) { - use std::process::Command; - let pw = libc::getpwnam("stalwart-mail".as_ptr() as *const i8); - let gr = libc::getgrnam("stalwart-mail".as_ptr() as *const i8); + use std::{ffi::CString, process::Command}; + let c_str = CString::new("stalwart-mail").unwrap(); + let pw = libc::getpwnam(c_str.as_ptr()); + let gr = libc::getgrnam(c_str.as_ptr()); if pw.is_null() || gr.is_null() { let mut cmd = Command::new("useradd"); @@ -436,13 +713,13 @@ unsafe fn get_uid_gid() -> (libc::uid_t, libc::gid_t) { eprintln!("Failed to create stalwart system account: {}", e); std::process::exit(1); } - let pw = libc::getpwnam("stalwart-mail".as_ptr() as *const i8); - let gr = libc::getgrnam("stalwart-mail".as_ptr() as *const i8); + let pw = libc::getpwnam(c_str.as_ptr()); + let gr = libc::getgrnam(c_str.as_ptr()); (pw.as_ref().unwrap().pw_uid, gr.as_ref().unwrap().gr_gid) } else { ((*pw).pw_uid, ((*gr).gr_gid)) } -} +}*/ trait SelectItem { fn from_index(index: usize) -> Self; @@ -490,8 +767,8 @@ impl SelectItem for Backend { impl SelectItem for Directory { fn from_index(index: usize) -> Self { match index { - 0 => Self::SQL, - 1 => Self::LDAP, + 0 => Self::Sql, + 1 => Self::Ldap, 2 => Self::None, _ => unreachable!(), } @@ -499,8 +776,8 @@ impl SelectItem for Directory { fn to_index(&self) -> usize { match self { - Self::SQL => 0, - Self::LDAP => 1, + Self::Sql => 0, + Self::Ldap => 1, Self::None => 2, } } @@ -509,20 +786,20 @@ impl SelectItem for Directory { impl SelectItem for SmtpDirectory { fn from_index(index: usize) -> Self { match index { - 0 => Self::SQL, - 1 => Self::LDAP, - 2 => Self::LMTP, - 3 => Self::IMAP, + 0 => Self::Sql, + 1 => Self::Ldap, + 2 => Self::Lmtp, + 3 => Self::Imap, _ => unreachable!(), } } fn to_index(&self) -> usize { match self { - SmtpDirectory::SQL => 0, - SmtpDirectory::LDAP => 1, - SmtpDirectory::LMTP => 2, - SmtpDirectory::IMAP => 3, + SmtpDirectory::Sql => 0, + SmtpDirectory::Ldap => 1, + SmtpDirectory::Lmtp => 2, + SmtpDirectory::Imap => 3, } } } @@ -533,7 +810,7 @@ impl SelectItem for Blob { 0 => Blob::Local, 1 => Blob::MinIO, 2 => Blob::S3, - 3 => Blob::GCS, + 3 => Blob::Gcs, 4 => Blob::Azure, _ => unreachable!(), } @@ -544,7 +821,7 @@ impl SelectItem for Blob { Blob::Local => 0, Blob::MinIO => 1, Blob::S3 => 2, - Blob::GCS => 3, + Blob::Gcs => 3, Blob::Azure => 4, } } @@ -559,4 +836,22 @@ impl Component { Self::Smtp => "/opt/stalwart-smtp", } } + + fn binary_name(&self) -> &'static str { + match self { + Self::AllInOne => "mail", + Self::Jmap => "jmap", + Self::Imap => "imap", + Self::Smtp => "smtp", + } + } + + fn name(&self) -> &'static str { + match self { + Self::AllInOne => "Mail", + Self::Jmap => "JMAP", + Self::Imap => "IMAP", + Self::Smtp => "SMTP", + } + } } diff --git a/crates/jmap/src/push/ece.rs b/crates/jmap/src/push/ece.rs index 80d869fe..f493b12b 100644 --- a/crates/jmap/src/push/ece.rs +++ b/crates/jmap/src/push/ece.rs @@ -206,29 +206,3 @@ pub fn generate_iv(nonce: &[u8], counter: usize) -> [u8; ECE_NONCE_LENGTH] { iv[offset..].copy_from_slice(&(mask ^ (counter as u64)).to_be_bytes()); iv } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn ece_roundtrip() { - for len in [1, 2, 5, 16, 256, 1024, 2048, 4096, 1024 * 1024] { - let (keypair, auth_secret) = ece::generate_keypair_and_auth_secret().unwrap(); - - let bytes: Vec = (0..len).map(|_| store::rand::random::()).collect(); - - let encrypted_bytes = - ece_encrypt(&keypair.pub_as_raw().unwrap(), &auth_secret, &bytes).unwrap(); - - let decrypted_bytes = ece::decrypt( - &keypair.raw_components().unwrap(), - &auth_secret, - &encrypted_bytes, - ) - .unwrap(); - - assert_eq!(bytes, decrypted_bytes, "len: {}", len); - } - } -} diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index 329d6b7a..9c4bb149 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -34,6 +34,9 @@ num_cpus = { version = "1.15.0", optional = true } blake3 = "1.3.3" tracing = "0.1" +[dev-dependencies] +tokio = { version = "1.23", features = ["full"] } + [features] rocks = ["rocksdb", "rayon", "is_sync", "backend"] sqlite = ["rusqlite", "rayon", "r2d2", "num_cpus", "is_sync", "backend"] diff --git a/install.sh b/install.sh new file mode 100644 index 00000000..b265ce1e --- /dev/null +++ b/install.sh @@ -0,0 +1,641 @@ +#!/usr/bin/env sh +# shellcheck shell=dash + +# Stalwart SMTP install script -- based on the rustup installation script. + +set -e +set -u + +readonly BASE_URL="https://github.com/stalwartlabs/mail-server/releases/latest/download" + +main() { + downloader --check + need_cmd uname + need_cmd mktemp + need_cmd chmod + need_cmd mkdir + need_cmd rm + need_cmd rmdir + need_cmd tar + + # Make sure we are running as root + if [ "$(id -u)" -ne 0 ] ; then + err "āŒ Install failed: This program needs to run as root." + fi + + # Detect OS + local _os="unknown" + local _uname="$(uname)" + _account="stalwart-mail" + if [ "${_uname}" = "Linux" ]; then + _os="linux" + elif [ "${_uname}" = "Darwin" ]; then + _os="macos" + _account="_stalwart-mail" + fi + + # Start configuration mode + if [ "$#" -eq 1 ] && [ "$1" = "--init" ] ; then + init + configure + return 0 + fi + + # Detect platform architecture + get_architecture || return 1 + local _arch="$RETVAL" + assert_nz "$_arch" "arch" + + # Download latest binary + say "ā³ Downloading installer for ${_arch}..." + local _dir + _dir="$(ensure mktemp -d)" + local _file="${_dir}/stalwart-install.tar.gz" + local _url="${BASE_URL}/stalwart-install-${_arch}.tar.gz" + ensure mkdir -p "$_dir" + ensure downloader "$_url" "$_file" "$_arch" + + # Create system account + if ! id -u ${_account} > /dev/null 2>&1; then + say "šŸ–„ļø Creating '${_account}' account..." + if [ "${_os}" = "macos" ]; then + local _last_uid="$(dscacheutil -q user | grep uid | awk '{print $2}' | sort -n | tail -n 1)" + local _last_gid="$(dscacheutil -q group | grep gid | awk '{print $2}' | sort -n | tail -n 1)" + local _uid="$((_last_uid+1))" + local _gid="$((_last_gid+1))" + + ensure dscl /Local/Default -create Groups/_stalwart-mail + ensure dscl /Local/Default -create Groups/_stalwart-mail Password \* + ensure dscl /Local/Default -create Groups/_stalwart-mail PrimaryGroupID $_gid + ensure dscl /Local/Default -create Groups/_stalwart-mail RealName "Stalwart SMTP service" + ensure dscl /Local/Default -create Groups/_stalwart-mail RecordName _stalwart-mail stalwart-mail + + ensure dscl /Local/Default -create Users/_stalwart-mail + ensure dscl /Local/Default -create Users/_stalwart-mail NFSHomeDirectory /Users/_stalwart-mail + ensure dscl /Local/Default -create Users/_stalwart-mail Password \* + ensure dscl /Local/Default -create Users/_stalwart-mail PrimaryGroupID $_gid + ensure dscl /Local/Default -create Users/_stalwart-mail RealName "Stalwart SMTP service" + ensure dscl /Local/Default -create Users/_stalwart-mail RecordName _stalwart-mail stalwart-mail + ensure dscl /Local/Default -create Users/_stalwart-mail UniqueID $_uid + ensure dscl /Local/Default -create Users/_stalwart-mail UserShell /bin/bash + + ensure dscl /Local/Default -delete /Users/_stalwart-mail AuthenticationAuthority + ensure dscl /Local/Default -delete /Users/_stalwart-mail PasswordPolicyOptions + else + ensure useradd ${_account} -s /sbin/nologin -M + fi + fi + + # Copy binary + say "ā¬‡ļø Running installer..." + ensure tar zxvf "$_file" -C "$_dir" + ignore $_dir/stalwart-install + ignore rm "$_file" + ignore rm "$_dir/stalwart-install" + + return 0 +} + +get_architecture() { + local _ostype _cputype _bitness _arch _clibtype + _ostype="$(uname -s)" + _cputype="$(uname -m)" + _clibtype="gnu" + + if [ "$_ostype" = Linux ]; then + if [ "$(uname -o)" = Android ]; then + _ostype=Android + fi + if ldd --version 2>&1 | grep -q 'musl'; then + _clibtype="musl" + fi + fi + + if [ "$_ostype" = Darwin ] && [ "$_cputype" = i386 ]; then + # Darwin `uname -m` lies + if sysctl hw.optional.x86_64 | grep -q ': 1'; then + _cputype=x86_64 + fi + fi + + if [ "$_ostype" = SunOS ]; then + # Both Solaris and illumos presently announce as "SunOS" in "uname -s" + # so use "uname -o" to disambiguate. We use the full path to the + # system uname in case the user has coreutils uname first in PATH, + # which has historically sometimes printed the wrong value here. + if [ "$(/usr/bin/uname -o)" = illumos ]; then + _ostype=illumos + fi + + # illumos systems have multi-arch userlands, and "uname -m" reports the + # machine hardware name; e.g., "i86pc" on both 32- and 64-bit x86 + # systems. Check for the native (widest) instruction set on the + # running kernel: + if [ "$_cputype" = i86pc ]; then + _cputype="$(isainfo -n)" + fi + fi + + case "$_ostype" in + + Android) + _ostype=linux-android + ;; + + Linux) + check_proc + _ostype=unknown-linux-$_clibtype + _bitness=$(get_bitness) + ;; + + FreeBSD) + _ostype=unknown-freebsd + ;; + + NetBSD) + _ostype=unknown-netbsd + ;; + + DragonFly) + _ostype=unknown-dragonfly + ;; + + Darwin) + _ostype=apple-darwin + ;; + + illumos) + _ostype=unknown-illumos + ;; + + MINGW* | MSYS* | CYGWIN* | Windows_NT) + _ostype=pc-windows-gnu + ;; + + *) + err "unrecognized OS type: $_ostype" + ;; + + esac + + case "$_cputype" in + + i386 | i486 | i686 | i786 | x86) + _cputype=i686 + ;; + + xscale | arm) + _cputype=arm + if [ "$_ostype" = "linux-android" ]; then + _ostype=linux-androideabi + fi + ;; + + armv6l) + _cputype=arm + if [ "$_ostype" = "linux-android" ]; then + _ostype=linux-androideabi + else + _ostype="${_ostype}eabihf" + fi + ;; + + armv7l | armv8l) + _cputype=armv7 + if [ "$_ostype" = "linux-android" ]; then + _ostype=linux-androideabi + else + _ostype="${_ostype}eabihf" + fi + ;; + + aarch64 | arm64) + _cputype=aarch64 + ;; + + x86_64 | x86-64 | x64 | amd64) + _cputype=x86_64 + ;; + + mips) + _cputype=$(get_endianness mips '' el) + ;; + + mips64) + if [ "$_bitness" -eq 64 ]; then + # only n64 ABI is supported for now + _ostype="${_ostype}abi64" + _cputype=$(get_endianness mips64 '' el) + fi + ;; + + ppc) + _cputype=powerpc + ;; + + ppc64) + _cputype=powerpc64 + ;; + + ppc64le) + _cputype=powerpc64le + ;; + + s390x) + _cputype=s390x + ;; + riscv64) + _cputype=riscv64gc + ;; + *) + err "unknown CPU type: $_cputype" + + esac + + # Detect 64-bit linux with 32-bit userland + if [ "${_ostype}" = unknown-linux-gnu ] && [ "${_bitness}" -eq 32 ]; then + case $_cputype in + x86_64) + if [ -n "${RUSTUP_CPUTYPE:-}" ]; then + _cputype="$RUSTUP_CPUTYPE" + else { + # 32-bit executable for amd64 = x32 + if is_host_amd64_elf; then { + echo "This host is running an x32 userland; as it stands, x32 support is poor," 1>&2 + echo "and there isn't a native toolchain -- you will have to install" 1>&2 + echo "multiarch compatibility with i686 and/or amd64, then select one" 1>&2 + echo "by re-running this script with the RUSTUP_CPUTYPE environment variable" 1>&2 + echo "set to i686 or x86_64, respectively." 1>&2 + echo 1>&2 + echo "You will be able to add an x32 target after installation by running" 1>&2 + echo " rustup target add x86_64-unknown-linux-gnux32" 1>&2 + exit 1 + }; else + _cputype=i686 + fi + }; fi + ;; + mips64) + _cputype=$(get_endianness mips '' el) + ;; + powerpc64) + _cputype=powerpc + ;; + aarch64) + _cputype=armv7 + if [ "$_ostype" = "linux-android" ]; then + _ostype=linux-androideabi + else + _ostype="${_ostype}eabihf" + fi + ;; + riscv64gc) + err "riscv64 with 32-bit userland unsupported" + ;; + esac + fi + + # Detect armv7 but without the CPU features Rust needs in that build, + # and fall back to arm. + # See https://github.com/rust-lang/rustup.rs/issues/587. + if [ "$_ostype" = "unknown-linux-gnueabihf" ] && [ "$_cputype" = armv7 ]; then + if ensure grep '^Features' /proc/cpuinfo | grep -q -v neon; then + # At least one processor does not have NEON. + _cputype=arm + fi + fi + + _arch="${_cputype}-${_ostype}" + + RETVAL="$_arch" +} + +check_proc() { + # Check for /proc by looking for the /proc/self/exe link + # This is only run on Linux + if ! test -L /proc/self/exe ; then + err "fatal: Unable to find /proc/self/exe. Is /proc mounted? Installation cannot proceed without /proc." + fi +} + +get_bitness() { + need_cmd head + # Architecture detection without dependencies beyond coreutils. + # ELF files start out "\x7fELF", and the following byte is + # 0x01 for 32-bit and + # 0x02 for 64-bit. + # The printf builtin on some shells like dash only supports octal + # escape sequences, so we use those. + local _current_exe_head + _current_exe_head=$(head -c 5 /proc/self/exe ) + if [ "$_current_exe_head" = "$(printf '\177ELF\001')" ]; then + echo 32 + elif [ "$_current_exe_head" = "$(printf '\177ELF\002')" ]; then + echo 64 + else + err "unknown platform bitness" + fi +} + +is_host_amd64_elf() { + need_cmd head + need_cmd tail + # ELF e_machine detection without dependencies beyond coreutils. + # Two-byte field at offset 0x12 indicates the CPU, + # but we're interested in it being 0x3E to indicate amd64, or not that. + local _current_exe_machine + _current_exe_machine=$(head -c 19 /proc/self/exe | tail -c 1) + [ "$_current_exe_machine" = "$(printf '\076')" ] +} + +get_endianness() { + local cputype=$1 + local suffix_eb=$2 + local suffix_el=$3 + + # detect endianness without od/hexdump, like get_bitness() does. + need_cmd head + need_cmd tail + + local _current_exe_endianness + _current_exe_endianness="$(head -c 6 /proc/self/exe | tail -c 1)" + if [ "$_current_exe_endianness" = "$(printf '\001')" ]; then + echo "${cputype}${suffix_el}" + elif [ "$_current_exe_endianness" = "$(printf '\002')" ]; then + echo "${cputype}${suffix_eb}" + else + err "unknown platform endianness" + fi +} + +say() { + printf 'stalwart-mail: %s\n' "$1" +} + +err() { + say "$1" >&2 + exit 1 +} + +need_cmd() { + if ! check_cmd "$1"; then + err "need '$1' (command not found)" + fi +} + +check_cmd() { + command -v "$1" > /dev/null 2>&1 +} + +assert_nz() { + if [ -z "$1" ]; then err "assert_nz $2"; fi +} + +# Run a command that should never fail. If the command fails execution +# will immediately terminate with an error showing the failing +# command. +ensure() { + if ! "$@"; then err "command failed: $*"; fi +} + +# This wraps curl or wget. Try curl first, if not installed, +# use wget instead. +downloader() { + local _dld + local _ciphersuites + local _err + local _status + local _retry + if check_cmd curl; then + _dld=curl + elif check_cmd wget; then + _dld=wget + else + _dld='curl or wget' # to be used in error message of need_cmd + fi + + if [ "$1" = --check ]; then + need_cmd "$_dld" + elif [ "$_dld" = curl ]; then + check_curl_for_retry_support + _retry="$RETVAL" + get_ciphersuites_for_curl + _ciphersuites="$RETVAL" + if [ -n "$_ciphersuites" ]; then + _err=$(curl $_retry --proto '=https' --tlsv1.2 --ciphers "$_ciphersuites" --silent --show-error --fail --location "$1" --output "$2" 2>&1) + _status=$? + else + echo "Warning: Not enforcing strong cipher suites for TLS, this is potentially less secure" + if ! check_help_for "$3" curl --proto --tlsv1.2; then + echo "Warning: Not enforcing TLS v1.2, this is potentially less secure" + _err=$(curl $_retry --silent --show-error --fail --location "$1" --output "$2" 2>&1) + _status=$? + else + _err=$(curl $_retry --proto '=https' --tlsv1.2 --silent --show-error --fail --location "$1" --output "$2" 2>&1) + _status=$? + fi + fi + if [ -n "$_err" ]; then + if echo "$_err" | grep -q 404; then + err "āŒ Binary for platform '$3' not found, this platform may be unsupported." + else + echo "$_err" >&2 + fi + fi + return $_status + elif [ "$_dld" = wget ]; then + if [ "$(wget -V 2>&1|head -2|tail -1|cut -f1 -d" ")" = "BusyBox" ]; then + echo "Warning: using the BusyBox version of wget. Not enforcing strong cipher suites for TLS or TLS v1.2, this is potentially less secure" + _err=$(wget "$1" -O "$2" 2>&1) + _status=$? + else + get_ciphersuites_for_wget + _ciphersuites="$RETVAL" + if [ -n "$_ciphersuites" ]; then + _err=$(wget --https-only --secure-protocol=TLSv1_2 --ciphers "$_ciphersuites" "$1" -O "$2" 2>&1) + _status=$? + else + echo "Warning: Not enforcing strong cipher suites for TLS, this is potentially less secure" + if ! check_help_for "$3" wget --https-only --secure-protocol; then + echo "Warning: Not enforcing TLS v1.2, this is potentially less secure" + _err=$(wget "$1" -O "$2" 2>&1) + _status=$? + else + _err=$(wget --https-only --secure-protocol=TLSv1_2 "$1" -O "$2" 2>&1) + _status=$? + fi + fi + fi + if [ -n "$_err" ]; then + if echo "$_err" | grep -q ' 404 Not Found'; then + err "āŒ Binary for platform '$3' not found, this platform may be unsupported." + else + echo "$_err" >&2 + fi + fi + return $_status + else + err "Unknown downloader" # should not reach here + fi +} + +# Check if curl supports the --retry flag, then pass it to the curl invocation. +check_curl_for_retry_support() { + local _retry_supported="" + # "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc. + if check_help_for "notspecified" "curl" "--retry"; then + _retry_supported="--retry 3" + fi + + RETVAL="$_retry_supported" + +} + +check_help_for() { + local _arch + local _cmd + local _arg + _arch="$1" + shift + _cmd="$1" + shift + + local _category + if "$_cmd" --help | grep -q 'For all options use the manual or "--help all".'; then + _category="all" + else + _category="" + fi + + case "$_arch" in + + *darwin*) + if check_cmd sw_vers; then + case $(sw_vers -productVersion) in + 10.*) + # If we're running on macOS, older than 10.13, then we always + # fail to find these options to force fallback + if [ "$(sw_vers -productVersion | cut -d. -f2)" -lt 13 ]; then + # Older than 10.13 + echo "Warning: Detected macOS platform older than 10.13" + return 1 + fi + ;; + 11.*) + # We assume Big Sur will be OK for now + ;; + *) + # Unknown product version, warn and continue + echo "Warning: Detected unknown macOS major version: $(sw_vers -productVersion)" + echo "Warning TLS capabilities detection may fail" + ;; + esac + fi + ;; + + esac + + for _arg in "$@"; do + if ! "$_cmd" --help $_category | grep -q -- "$_arg"; then + return 1 + fi + done + + true # not strictly needed +} + +# Return cipher suite string specified by user, otherwise return strong TLS 1.2-1.3 cipher suites +# if support by local tools is detected. Detection currently supports these curl backends: +# GnuTLS and OpenSSL (possibly also LibreSSL and BoringSSL). Return value can be empty. +get_ciphersuites_for_curl() { + if [ -n "${RUSTUP_TLS_CIPHERSUITES-}" ]; then + # user specified custom cipher suites, assume they know what they're doing + RETVAL="$RUSTUP_TLS_CIPHERSUITES" + return + fi + + local _openssl_syntax="no" + local _gnutls_syntax="no" + local _backend_supported="yes" + if curl -V | grep -q ' OpenSSL/'; then + _openssl_syntax="yes" + elif curl -V | grep -iq ' LibreSSL/'; then + _openssl_syntax="yes" + elif curl -V | grep -iq ' BoringSSL/'; then + _openssl_syntax="yes" + elif curl -V | grep -iq ' GnuTLS/'; then + _gnutls_syntax="yes" + else + _backend_supported="no" + fi + + local _args_supported="no" + if [ "$_backend_supported" = "yes" ]; then + # "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc. + if check_help_for "notspecified" "curl" "--tlsv1.2" "--ciphers" "--proto"; then + _args_supported="yes" + fi + fi + + local _cs="" + if [ "$_args_supported" = "yes" ]; then + if [ "$_openssl_syntax" = "yes" ]; then + _cs=$(get_strong_ciphersuites_for "openssl") + elif [ "$_gnutls_syntax" = "yes" ]; then + _cs=$(get_strong_ciphersuites_for "gnutls") + fi + fi + + RETVAL="$_cs" +} + +# Return cipher suite string specified by user, otherwise return strong TLS 1.2-1.3 cipher suites +# if support by local tools is detected. Detection currently supports these wget backends: +# GnuTLS and OpenSSL (possibly also LibreSSL and BoringSSL). Return value can be empty. +get_ciphersuites_for_wget() { + if [ -n "${RUSTUP_TLS_CIPHERSUITES-}" ]; then + # user specified custom cipher suites, assume they know what they're doing + RETVAL="$RUSTUP_TLS_CIPHERSUITES" + return + fi + + local _cs="" + if wget -V | grep -q '\-DHAVE_LIBSSL'; then + # "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc. + if check_help_for "notspecified" "wget" "TLSv1_2" "--ciphers" "--https-only" "--secure-protocol"; then + _cs=$(get_strong_ciphersuites_for "openssl") + fi + elif wget -V | grep -q '\-DHAVE_LIBGNUTLS'; then + # "unspecified" is for arch, allows for possibility old OS using macports, homebrew, etc. + if check_help_for "notspecified" "wget" "TLSv1_2" "--ciphers" "--https-only" "--secure-protocol"; then + _cs=$(get_strong_ciphersuites_for "gnutls") + fi + fi + + RETVAL="$_cs" +} + +# Return strong TLS 1.2-1.3 cipher suites in OpenSSL or GnuTLS syntax. TLS 1.2 +# excludes non-ECDHE and non-AEAD cipher suites. DHE is excluded due to bad +# DH params often found on servers (see RFC 7919). Sequence matches or is +# similar to Firefox 68 ESR with weak cipher suites disabled via about:config. +# $1 must be openssl or gnutls. +get_strong_ciphersuites_for() { + if [ "$1" = "openssl" ]; then + # OpenSSL is forgiving of unknown values, no problems with TLS 1.3 values on versions that don't support it yet. + echo "TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384" + elif [ "$1" = "gnutls" ]; then + # GnuTLS isn't forgiving of unknown values, so this may require a GnuTLS version that supports TLS 1.3 even if wget doesn't. + # Begin with SECURE128 (and higher) then remove/add to build cipher suites. Produces same 9 cipher suites as OpenSSL but in slightly different order. + echo "SECURE128:-VERS-SSL3.0:-VERS-TLS1.0:-VERS-TLS1.1:-VERS-DTLS-ALL:-CIPHER-ALL:-MAC-ALL:-KX-ALL:+AEAD:+ECDHE-ECDSA:+ECDHE-RSA:+AES-128-GCM:+CHACHA20-POLY1305:+AES-256-GCM" + fi +} + +# This is just for indicating that commands' results are being +# intentionally ignored. Usually, because it's being executed +# as part of error handling. +ignore() { + "$@" +} + +main "$@" || exit 1 diff --git a/resources/config/common.toml b/resources/config/common.toml index 1d4dd024..9b621ade 100644 --- a/resources/config/common.toml +++ b/resources/config/common.toml @@ -48,7 +48,7 @@ shared-map = {shard = 32, capacity = 10} [global.tracing] method = "log" path = "__PATH__/logs" -prefix = "smtp.log" +prefix = "stalwart.log" rotate = "daily" level = "info" diff --git a/resources/config/smtp.toml b/resources/config/smtp.toml index dce1797e..eb28009d 100644 --- a/resources/config/smtp.toml +++ b/resources/config/smtp.toml @@ -72,8 +72,7 @@ wait = "5s" relay = [ { if = "authenticated-as", ne = "", then = true }, { else = false } ] max-recipients = 25 -directory = [ { if = "authenticated-as", ne = "", then = "__SMTP_DIRECTORY__" }, - { else = false } ] +directory = "__SMTP_DIRECTORY__" [session.rcpt.cache] entries = 1000 diff --git a/resources/config/stalwart-config.zip b/resources/config/stalwart-config.zip deleted file mode 100644 index 68603260f3bb22ccbef581c922853e832d4c3f2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10702 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 Label - stalwart.smtp + stalwart.__NAME__ ServiceDescription - Stalwart SMTP Server + Stalwart __TITLE__ Server ProgramArguments - /usr/local/stalwart-smtp/bin/stalwart-smtp - --config=/usr/local/stalwart-smtp/etc/config.toml + __PATH__/bin/stalwart-__NAME__ + --config=__PATH__/etc/config.toml RunAtLoad diff --git a/tests/src/imap/basic.rs b/tests/src/imap/basic.rs index 0e7b02a6..6694f5e5 100644 --- a/tests/src/imap/basic.rs +++ b/tests/src/imap/basic.rs @@ -21,7 +21,10 @@ * for more details. */ +use imap::op::authenticate::decode_challenge_oauth; use imap_proto::ResponseType; +use mail_parser::decoders::base64::base64_decode; +use mail_send::Credentials; use super::{AssertResult, ImapConnection, Type}; @@ -50,3 +53,24 @@ pub async fn test(imap: &mut ImapConnection, _imap_check: &mut ImapConnection) { imap.send_untagged("AGJvYXR5AG1jYm9hdGZhY2U=").await; imap.assert_read(Type::Tagged, ResponseType::No).await; } + +#[test] +fn decode_challenge() { + assert!( + Credentials::OAuthBearer { + token: "vF9dft4qmTc2Nvb3RlckBhbHRhdmlzdGEuY29tCg==".to_string() + } == decode_challenge_oauth( + &base64_decode( + concat!( + "bixhPXVzZXJAZXhhbXBsZS5jb20sAWhv", + "c3Q9c2VydmVyLmV4YW1wbGUuY29tAXBvcnQ9MTQzAWF1dGg9QmVhcmVyI", + "HZGOWRmdDRxbVRjMk52YjNSbGNrQmhiSFJoZG1semRHRXVZMjl0Q2c9PQ", + "EB" + ) + .as_bytes(), + ) + .unwrap(), + ) + .unwrap() + ); +} diff --git a/tests/src/imap/mailbox.rs b/tests/src/imap/mailbox.rs index f2ddcb0e..ceab2931 100644 --- a/tests/src/imap/mailbox.rs +++ b/tests/src/imap/mailbox.rs @@ -21,6 +21,7 @@ * for more details. */ +use imap::op::list::matches_pattern; use imap_proto::ResponseType; use super::{AssertResult, ImapConnection, Type}; @@ -338,3 +339,75 @@ pub async fn test(mut imap: &mut ImapConnection, mut imap_check: &mut ImapConnec imap.send("RENAME \"Recycle Bin\" \"Deleted Items\"").await; imap.assert_read(Type::Tagged, ResponseType::Ok).await; } + +#[test] +fn mailbox_matches_pattern() { + let mailboxes = [ + "imaptest", + "imaptest/test", + "imaptest/test2", + "imaptest/test3", + "imaptest/test3/test4", + "imaptest/test3/test4/test5", + "foobar/test", + "foobar/test/test", + "foobar/test1/test1", + ]; + + for (pattern, expected_match) in [ + ( + "imaptest/%", + vec!["imaptest/test", "imaptest/test2", "imaptest/test3"], + ), + ("imaptest/%/%", vec!["imaptest/test3/test4"]), + ( + "imaptest/*", + vec![ + "imaptest/test", + "imaptest/test2", + "imaptest/test3", + "imaptest/test3/test4", + "imaptest/test3/test4/test5", + ], + ), + ("imaptest/*test4", vec!["imaptest/test3/test4"]), + ( + "imaptest/*test*", + vec![ + "imaptest/test", + "imaptest/test2", + "imaptest/test3", + "imaptest/test3/test4", + "imaptest/test3/test4/test5", + ], + ), + ("imaptest/%3/%", vec!["imaptest/test3/test4"]), + ("imaptest/%3/%4", vec!["imaptest/test3/test4"]), + ("imaptest/%t*4", vec!["imaptest/test3/test4"]), + ("*st/%3/%4/%5", vec!["imaptest/test3/test4/test5"]), + ( + "*%*%*%", + vec![ + "imaptest", + "imaptest/test", + "imaptest/test2", + "imaptest/test3", + "imaptest/test3/test4", + "imaptest/test3/test4/test5", + "foobar/test", + "foobar/test/test", + "foobar/test1/test1", + ], + ), + ("foobar*test", vec!["foobar/test", "foobar/test/test"]), + ] { + let patterns = vec![pattern.to_string()]; + let mut matched_mailboxes = Vec::new(); + for mailbox in mailboxes { + if matches_pattern(&patterns, mailbox) { + matched_mailboxes.push(mailbox); + } + } + assert_eq!(matched_mailboxes, expected_match, "for pattern {}", pattern); + } +} diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index 98a66c5a..307d4212 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -247,13 +247,27 @@ pub async fn jmap_tests() { email_submission::test(params.server.clone(), &mut params.client).await; websocket::test(params.server.clone(), &mut params.client).await; quota::test(params.server.clone(), &mut params.client).await; - stress_test::test(params.server.clone(), params.client).await; if delete { params.temp_dir.delete(); } } +#[tokio::test] +#[ignore] +pub async fn jmap_stress_tests() { + tracing::subscriber::set_global_default( + tracing_subscriber::FmtSubscriber::builder() + .with_max_level(tracing::Level::WARN) + .finish(), + ) + .unwrap(); + + let params = init_jmap_tests(true).await; + stress_test::test(params.server.clone(), params.client).await; + params.temp_dir.delete(); +} + #[allow(dead_code)] struct JMAPTest { server: Arc, diff --git a/tests/src/jmap/push_subscription.rs b/tests/src/jmap/push_subscription.rs index 84f9de30..a929714e 100644 --- a/tests/src/jmap/push_subscription.rs +++ b/tests/src/jmap/push_subscription.rs @@ -39,6 +39,7 @@ use jmap::{ HtmlResponse, StateChangeResponse, }, auth::AccessToken, + push::ece::ece_encrypt, JMAP, }; use jmap_client::{client::Client, mailbox::Role, push_subscription::Keys}; @@ -375,3 +376,24 @@ async fn assert_state(event_rx: &mut mpsc::Receiver, id: &Id, state state.iter().collect::>() ); } + +#[test] +fn ece_roundtrip() { + for len in [1, 2, 5, 16, 256, 1024, 2048, 4096, 1024 * 1024] { + let (keypair, auth_secret) = ece::generate_keypair_and_auth_secret().unwrap(); + + let bytes: Vec = (0..len).map(|_| store::rand::random::()).collect(); + + let encrypted_bytes = + ece_encrypt(&keypair.pub_as_raw().unwrap(), &auth_secret, &bytes).unwrap(); + + let decrypted_bytes = ece::decrypt( + &keypair.raw_components().unwrap(), + &auth_secret, + &encrypted_bytes, + ) + .unwrap(); + + assert_eq!(bytes, decrypted_bytes, "len: {}", len); + } +}