mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-09-20 07:16:18 +08:00
CI/CD tweaks.
This commit is contained in:
parent
843e61139a
commit
9c6c53e21c
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
|
@ -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']
|
306
.github/workflows/build.yml
vendored
Normal file
306
.github/workflows/build.yml
vendored
Normal file
|
@ -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
|
110
.github/workflows/test.yml
vendored
Normal file
110
.github/workflows/test.yml
vendored
Normal file
|
@ -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
|
47
Cargo.lock
generated
47
Cargo.lock
generated
|
@ -1296,6 +1296,18 @@ dependencies = [
|
||||||
"subtle",
|
"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]]
|
[[package]]
|
||||||
name = "fixedbitset"
|
name = "fixedbitset"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
|
@ -4385,7 +4397,9 @@ name = "stalwart-install"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.2",
|
"base64 0.21.2",
|
||||||
|
"clap",
|
||||||
"dialoguer",
|
"dialoguer",
|
||||||
|
"flate2",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"libc",
|
"libc",
|
||||||
"openssl",
|
"openssl",
|
||||||
|
@ -4394,6 +4408,8 @@ dependencies = [
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rpassword",
|
"rpassword",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
"tar",
|
||||||
|
"zip-extract",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -4498,6 +4514,17 @@ dependencies = [
|
||||||
"unicode-xid",
|
"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]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.6.0"
|
version = "3.6.0"
|
||||||
|
@ -5534,6 +5561,15 @@ dependencies = [
|
||||||
"time 0.3.22",
|
"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]]
|
[[package]]
|
||||||
name = "xml-rs"
|
name = "xml-rs"
|
||||||
version = "0.8.15"
|
version = "0.8.15"
|
||||||
|
@ -5572,6 +5608,17 @@ dependencies = [
|
||||||
"zstd",
|
"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]]
|
[[package]]
|
||||||
name = "zstd"
|
name = "zstd"
|
||||||
version = "0.11.2+zstd.1.5.2"
|
version = "0.11.2+zstd.1.5.2"
|
||||||
|
|
52
Dockerfile
Normal file
52
Dockerfile
Normal file
|
@ -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"]
|
|
@ -276,30 +276,3 @@ pub fn decode_challenge_oauth(challenge: &[u8]) -> Result<Credentials<String>, &
|
||||||
|
|
||||||
Err("Failed to find 'auth=Bearer' in challenge.")
|
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()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -303,7 +303,7 @@ impl SessionData {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::while_let_on_iterator)]
|
#[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() {
|
if patterns.is_empty() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -370,79 +370,3 @@ fn matches_pattern(patterns: &[String], mailbox_name: &str) -> bool {
|
||||||
|
|
||||||
false
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ readme = "README.md"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[dependencies]
|
[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"] }
|
rusqlite = { version = "0.29.0", features = ["bundled"] }
|
||||||
rpassword = "7.0"
|
rpassword = "7.0"
|
||||||
indicatif = "0.17.0"
|
indicatif = "0.17.0"
|
||||||
|
@ -20,6 +20,12 @@ openssl = { version = "0.10.55", features = ["vendored"] }
|
||||||
base64 = "0.21.2"
|
base64 = "0.21.2"
|
||||||
pwhash = "1.0.0"
|
pwhash = "1.0.0"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
clap = { version = "4.1.6", features = ["derive"] }
|
||||||
|
|
||||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||||
libc = "0.2.147"
|
libc = "0.2.147"
|
||||||
|
flate2 = "1.0.26"
|
||||||
|
tar = "0.4.38"
|
||||||
|
|
||||||
|
[target.'cfg(target_env = "msvc")'.dependencies]
|
||||||
|
zip-extract = "0.1.2"
|
||||||
|
|
6
crates/install/build.rs
Normal file
6
crates/install/build.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
fn main() {
|
||||||
|
println!(
|
||||||
|
"cargo:rustc-env=TARGET={}",
|
||||||
|
std::env::var("TARGET").unwrap()
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,15 +1,20 @@
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs,
|
||||||
|
io::Cursor,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
process::Command,
|
||||||
time::SystemTime,
|
time::SystemTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
use base64::{engine::general_purpose, Engine};
|
use base64::{engine::general_purpose, Engine};
|
||||||
|
use clap::{Parser, ValueEnum};
|
||||||
use dialoguer::{console::Term, theme::ColorfulTheme, Input, Select};
|
use dialoguer::{console::Term, theme::ColorfulTheme, Input, Select};
|
||||||
|
use flate2::bufread::GzDecoder;
|
||||||
use openssl::rsa::Rsa;
|
use openssl::rsa::Rsa;
|
||||||
use pwhash::sha512_crypt;
|
use pwhash::sha512_crypt;
|
||||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||||
use rusqlite::{Connection, OpenFlags};
|
use rusqlite::{Connection, OpenFlags};
|
||||||
|
use tar::Archive;
|
||||||
|
|
||||||
const CFG_COMMON: &str = include_str!("../../../resources/config/common.toml");
|
const CFG_COMMON: &str = include_str!("../../../resources/config/common.toml");
|
||||||
const CFG_DIRECTORY: &str = include_str!("../../../resources/config/directory.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_IMAP: &str = include_str!("../../../resources/config/imap.toml");
|
||||||
const CFG_SMTP: &str = include_str!("../../../resources/config/smtp.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 {
|
enum Component {
|
||||||
AllInOne,
|
AllInOne,
|
||||||
Jmap,
|
Jmap,
|
||||||
|
@ -36,23 +59,23 @@ enum Blob {
|
||||||
Local,
|
Local,
|
||||||
MinIO,
|
MinIO,
|
||||||
S3,
|
S3,
|
||||||
GCS,
|
Gcs,
|
||||||
Azure,
|
Azure,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum Directory {
|
enum Directory {
|
||||||
SQL,
|
Sql,
|
||||||
LDAP,
|
Ldap,
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum SmtpDirectory {
|
enum SmtpDirectory {
|
||||||
SQL,
|
Sql,
|
||||||
LDAP,
|
Ldap,
|
||||||
LMTP,
|
Lmtp,
|
||||||
IMAP,
|
Imap,
|
||||||
}
|
}
|
||||||
|
|
||||||
const DIRECTORIES: [[&str; 2]; 6] = [
|
const DIRECTORIES: [[&str; 2]; 6] = [
|
||||||
|
@ -64,28 +87,63 @@ const DIRECTORIES: [[&str; 2]; 6] = [
|
||||||
["reports", ""],
|
["reports", ""],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[clap(version, about, long_about = None)]
|
||||||
|
#[clap(name = "stalwart-cli")]
|
||||||
|
pub struct Arguments {
|
||||||
|
#[clap(long, short = 'p')]
|
||||||
|
path: Option<PathBuf>,
|
||||||
|
#[clap(long, short = 'c')]
|
||||||
|
component: Option<Component>,
|
||||||
|
#[clap(long, short = 'd')]
|
||||||
|
docker: bool,
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> std::io::Result<()> {
|
fn main() -> std::io::Result<()> {
|
||||||
let c = "fix";
|
let args = Arguments::parse();
|
||||||
/*#[cfg(not(target_env = "msvc"))]
|
|
||||||
|
#[cfg(not(target_env = "msvc"))]
|
||||||
unsafe {
|
unsafe {
|
||||||
if libc::getuid() != 0 {
|
if libc::getuid() != 0 {
|
||||||
eprintln!("This program must be run as root.");
|
eprintln!("This program must be run as root.");
|
||||||
std::process::exit(1);
|
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::<Component>(
|
// Obtain component to install
|
||||||
"Which components would you like to install?",
|
let (component, skip_download) = if let Some(component) = args.component {
|
||||||
&[
|
(component, true)
|
||||||
"All-in-one mail server (JMAP + IMAP + SMTP)",
|
} else {
|
||||||
"JMAP server",
|
(
|
||||||
"IMAP server",
|
select::<Component>(
|
||||||
"SMTP server",
|
"Which components would you like to install?",
|
||||||
],
|
&[
|
||||||
Component::AllInOne,
|
"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 {
|
let mut cfg_file = match component {
|
||||||
Component::AllInOne | Component::Imap => {
|
Component::AllInOne | Component::Imap => {
|
||||||
[CFG_COMMON, CFG_DIRECTORY, CFG_JMAP, CFG_IMAP, CFG_SMTP].join("\n")
|
[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::Jmap => [CFG_COMMON, CFG_DIRECTORY, CFG_JMAP, CFG_SMTP].join("\n"),
|
||||||
Component::Smtp => [CFG_COMMON, CFG_DIRECTORY, 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 directory = if component != Component::Smtp {
|
||||||
let backend = select::<Backend>(
|
if !skip_download {
|
||||||
"Which database engine would you like to use?",
|
let backend = select::<Backend>(
|
||||||
&[
|
"Which database engine would you like to use?",
|
||||||
"SQLite (single node, replicated with Litestream)",
|
&[
|
||||||
"FoundationDB (distributed and fault-tolerant)",
|
"SQLite (single node, replicated with Litestream)",
|
||||||
],
|
"FoundationDB (distributed and fault-tolerant)",
|
||||||
Backend::SQLite,
|
],
|
||||||
)?;
|
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::<Blob>(
|
let blob = select::<Blob>(
|
||||||
"Where would you like to store e-mails and blobs?",
|
"Where would you like to store e-mails and blobs?",
|
||||||
&[
|
&[
|
||||||
|
@ -135,15 +224,15 @@ fn main() -> std::io::Result<()> {
|
||||||
.replace(
|
.replace(
|
||||||
"__DIRECTORY__",
|
"__DIRECTORY__",
|
||||||
match directory {
|
match directory {
|
||||||
Directory::SQL | Directory::None => "sql",
|
Directory::Sql | Directory::None => "sql",
|
||||||
Directory::LDAP => "ldap",
|
Directory::Ldap => "ldap",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
"__SMTP_DIRECTORY__",
|
"__SMTP_DIRECTORY__",
|
||||||
match directory {
|
match directory {
|
||||||
Directory::SQL | Directory::None => "sql",
|
Directory::Sql | Directory::None => "sql",
|
||||||
Directory::LDAP => "ldap",
|
Directory::Ldap => "ldap",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
|
@ -165,40 +254,100 @@ fn main() -> std::io::Result<()> {
|
||||||
"LMTP server",
|
"LMTP server",
|
||||||
"IMAP server",
|
"IMAP server",
|
||||||
],
|
],
|
||||||
SmtpDirectory::LMTP,
|
SmtpDirectory::Lmtp,
|
||||||
)?;
|
)?;
|
||||||
cfg_file = cfg_file
|
cfg_file = cfg_file
|
||||||
.replace("__NEXT_HOP__", "lmtp")
|
.replace("__NEXT_HOP__", "lmtp")
|
||||||
.replace(
|
.replace(
|
||||||
"__SMTP_DIRECTORY__",
|
"__SMTP_DIRECTORY__",
|
||||||
match smtp_directory {
|
match smtp_directory {
|
||||||
SmtpDirectory::SQL => "sql",
|
SmtpDirectory::Sql => "sql",
|
||||||
SmtpDirectory::LDAP => "ldap",
|
SmtpDirectory::Ldap => "ldap",
|
||||||
SmtpDirectory::LMTP => "lmtp",
|
SmtpDirectory::Lmtp => "lmtp",
|
||||||
SmtpDirectory::IMAP => "imap",
|
SmtpDirectory::Imap => "imap",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
"__DIRECTORY__",
|
"__DIRECTORY__",
|
||||||
match smtp_directory {
|
match smtp_directory {
|
||||||
SmtpDirectory::SQL | SmtpDirectory::LMTP | SmtpDirectory::IMAP => "sql",
|
SmtpDirectory::Sql | SmtpDirectory::Lmtp | SmtpDirectory::Imap => "sql",
|
||||||
SmtpDirectory::LDAP => "ldap",
|
SmtpDirectory::Ldap => "ldap",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.replace("__NEXT_HOP__", "lmtp");
|
.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 {
|
match smtp_directory {
|
||||||
SmtpDirectory::SQL => Directory::SQL,
|
SmtpDirectory::Sql => Directory::Sql,
|
||||||
SmtpDirectory::LDAP => Directory::LDAP,
|
SmtpDirectory::Ldap => Directory::Ldap,
|
||||||
SmtpDirectory::LMTP | SmtpDirectory::IMAP => Directory::None,
|
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(
|
let domain = input(
|
||||||
"What is your main domain name? (you can add others later)",
|
"What is your main domain name? (you can add others later)",
|
||||||
"yourdomain.org",
|
"yourdomain.org",
|
||||||
|
@ -214,18 +363,37 @@ fn main() -> std::io::Result<()> {
|
||||||
.trim()
|
.trim()
|
||||||
.to_lowercase();
|
.to_lowercase();
|
||||||
|
|
||||||
let cert_path = input(
|
// Obtain TLS certificate path
|
||||||
&format!("Where is the TLS certificate for '{hostname}' located?"),
|
let (cert_path, pk_path) = if !args.docker {
|
||||||
&format!("/etc/letsencrypt/live/{hostname}/fullchain.pem"),
|
(
|
||||||
file_exists,
|
input(
|
||||||
)?;
|
&format!("Where is the TLS certificate for '{hostname}' located?"),
|
||||||
let pk_path = input(
|
&format!("/etc/letsencrypt/live/{hostname}/fullchain.pem"),
|
||||||
&format!("Where is the TLS private key for '{hostname}' located?"),
|
file_exists,
|
||||||
&format!("/etc/letsencrypt/live/{hostname}/privkey.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)?;
|
let dkim_instructions = generate_dkim(&base_path, &domain, &hostname)?;
|
||||||
|
|
||||||
|
// Create authentication SQLite database
|
||||||
let admin_password = if matches!(directory, Directory::None) {
|
let admin_password = if matches!(directory, Directory::None) {
|
||||||
create_auth_db(&base_path, &domain)?.into()
|
create_auth_db(&base_path, &domain)?.into()
|
||||||
} else {
|
} else {
|
||||||
|
@ -245,6 +413,13 @@ fn main() -> std::io::Result<()> {
|
||||||
));
|
));
|
||||||
fs::rename(&cfg_path, backup_path)?;
|
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(
|
fs::write(
|
||||||
cfg_path,
|
cfg_path,
|
||||||
cfg_file
|
cfg_file
|
||||||
|
@ -255,6 +430,107 @@ fn main() -> std::io::Result<()> {
|
||||||
.replace("__PK_PATH__", &pk_path),
|
.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");
|
eprintln!("\n🎉 Installation completed!\n\n✅ {dkim_instructions}\n");
|
||||||
|
|
||||||
if let Some(admin_password) = admin_password {
|
if let Some(admin_password) = admin_password {
|
||||||
|
@ -419,11 +695,12 @@ fn generate_dkim(path: &Path, domain: &str, hostname: &str) -> std::io::Result<S
|
||||||
Ok(instructions)
|
Ok(instructions)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_env = "msvc"))]
|
/*#[cfg(not(target_env = "msvc"))]
|
||||||
unsafe fn get_uid_gid() -> (libc::uid_t, libc::gid_t) {
|
unsafe fn get_uid_gid() -> (libc::uid_t, libc::gid_t) {
|
||||||
use std::process::Command;
|
use std::{ffi::CString, process::Command};
|
||||||
let pw = libc::getpwnam("stalwart-mail".as_ptr() as *const i8);
|
let c_str = CString::new("stalwart-mail").unwrap();
|
||||||
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());
|
||||||
|
|
||||||
if pw.is_null() || gr.is_null() {
|
if pw.is_null() || gr.is_null() {
|
||||||
let mut cmd = Command::new("useradd");
|
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);
|
eprintln!("Failed to create stalwart system account: {}", e);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
let pw = libc::getpwnam("stalwart-mail".as_ptr() as *const i8);
|
let pw = libc::getpwnam(c_str.as_ptr());
|
||||||
let gr = libc::getgrnam("stalwart-mail".as_ptr() as *const i8);
|
let gr = libc::getgrnam(c_str.as_ptr());
|
||||||
(pw.as_ref().unwrap().pw_uid, gr.as_ref().unwrap().gr_gid)
|
(pw.as_ref().unwrap().pw_uid, gr.as_ref().unwrap().gr_gid)
|
||||||
} else {
|
} else {
|
||||||
((*pw).pw_uid, ((*gr).gr_gid))
|
((*pw).pw_uid, ((*gr).gr_gid))
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
trait SelectItem {
|
trait SelectItem {
|
||||||
fn from_index(index: usize) -> Self;
|
fn from_index(index: usize) -> Self;
|
||||||
|
@ -490,8 +767,8 @@ impl SelectItem for Backend {
|
||||||
impl SelectItem for Directory {
|
impl SelectItem for Directory {
|
||||||
fn from_index(index: usize) -> Self {
|
fn from_index(index: usize) -> Self {
|
||||||
match index {
|
match index {
|
||||||
0 => Self::SQL,
|
0 => Self::Sql,
|
||||||
1 => Self::LDAP,
|
1 => Self::Ldap,
|
||||||
2 => Self::None,
|
2 => Self::None,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
|
@ -499,8 +776,8 @@ impl SelectItem for Directory {
|
||||||
|
|
||||||
fn to_index(&self) -> usize {
|
fn to_index(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
Self::SQL => 0,
|
Self::Sql => 0,
|
||||||
Self::LDAP => 1,
|
Self::Ldap => 1,
|
||||||
Self::None => 2,
|
Self::None => 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -509,20 +786,20 @@ impl SelectItem for Directory {
|
||||||
impl SelectItem for SmtpDirectory {
|
impl SelectItem for SmtpDirectory {
|
||||||
fn from_index(index: usize) -> Self {
|
fn from_index(index: usize) -> Self {
|
||||||
match index {
|
match index {
|
||||||
0 => Self::SQL,
|
0 => Self::Sql,
|
||||||
1 => Self::LDAP,
|
1 => Self::Ldap,
|
||||||
2 => Self::LMTP,
|
2 => Self::Lmtp,
|
||||||
3 => Self::IMAP,
|
3 => Self::Imap,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_index(&self) -> usize {
|
fn to_index(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
SmtpDirectory::SQL => 0,
|
SmtpDirectory::Sql => 0,
|
||||||
SmtpDirectory::LDAP => 1,
|
SmtpDirectory::Ldap => 1,
|
||||||
SmtpDirectory::LMTP => 2,
|
SmtpDirectory::Lmtp => 2,
|
||||||
SmtpDirectory::IMAP => 3,
|
SmtpDirectory::Imap => 3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -533,7 +810,7 @@ impl SelectItem for Blob {
|
||||||
0 => Blob::Local,
|
0 => Blob::Local,
|
||||||
1 => Blob::MinIO,
|
1 => Blob::MinIO,
|
||||||
2 => Blob::S3,
|
2 => Blob::S3,
|
||||||
3 => Blob::GCS,
|
3 => Blob::Gcs,
|
||||||
4 => Blob::Azure,
|
4 => Blob::Azure,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
|
@ -544,7 +821,7 @@ impl SelectItem for Blob {
|
||||||
Blob::Local => 0,
|
Blob::Local => 0,
|
||||||
Blob::MinIO => 1,
|
Blob::MinIO => 1,
|
||||||
Blob::S3 => 2,
|
Blob::S3 => 2,
|
||||||
Blob::GCS => 3,
|
Blob::Gcs => 3,
|
||||||
Blob::Azure => 4,
|
Blob::Azure => 4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -559,4 +836,22 @@ impl Component {
|
||||||
Self::Smtp => "/opt/stalwart-smtp",
|
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",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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[offset..].copy_from_slice(&(mask ^ (counter as u64)).to_be_bytes());
|
||||||
iv
|
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<u8> = (0..len).map(|_| store::rand::random::<u8>()).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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -34,6 +34,9 @@ num_cpus = { version = "1.15.0", optional = true }
|
||||||
blake3 = "1.3.3"
|
blake3 = "1.3.3"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1.23", features = ["full"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
rocks = ["rocksdb", "rayon", "is_sync", "backend"]
|
rocks = ["rocksdb", "rayon", "is_sync", "backend"]
|
||||||
sqlite = ["rusqlite", "rayon", "r2d2", "num_cpus", "is_sync", "backend"]
|
sqlite = ["rusqlite", "rayon", "r2d2", "num_cpus", "is_sync", "backend"]
|
||||||
|
|
641
install.sh
Normal file
641
install.sh
Normal file
|
@ -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
|
|
@ -48,7 +48,7 @@ shared-map = {shard = 32, capacity = 10}
|
||||||
[global.tracing]
|
[global.tracing]
|
||||||
method = "log"
|
method = "log"
|
||||||
path = "__PATH__/logs"
|
path = "__PATH__/logs"
|
||||||
prefix = "smtp.log"
|
prefix = "stalwart.log"
|
||||||
rotate = "daily"
|
rotate = "daily"
|
||||||
level = "info"
|
level = "info"
|
||||||
|
|
||||||
|
|
|
@ -72,8 +72,7 @@ wait = "5s"
|
||||||
relay = [ { if = "authenticated-as", ne = "", then = true },
|
relay = [ { if = "authenticated-as", ne = "", then = true },
|
||||||
{ else = false } ]
|
{ else = false } ]
|
||||||
max-recipients = 25
|
max-recipients = 25
|
||||||
directory = [ { if = "authenticated-as", ne = "", then = "__SMTP_DIRECTORY__" },
|
directory = "__SMTP_DIRECTORY__"
|
||||||
{ else = false } ]
|
|
||||||
|
|
||||||
[session.rcpt.cache]
|
[session.rcpt.cache]
|
||||||
entries = 1000
|
entries = 1000
|
||||||
|
|
Binary file not shown.
|
@ -1,7 +1,7 @@
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Stalwart SMTP
|
Description=Stalwart __TITLE__ Server
|
||||||
Conflicts=postfix.service sendmail.service exim4.service
|
Conflicts=postfix.service sendmail.service exim4.service
|
||||||
ConditionPathExists=/usr/local/stalwart-smtp/etc/config.toml
|
ConditionPathExists=__PATH__/etc/config.toml
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
@ -11,11 +11,11 @@ KillMode=process
|
||||||
KillSignal=SIGINT
|
KillSignal=SIGINT
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
ExecStart=/usr/local/stalwart-smtp/bin/stalwart-smtp --config=/usr/local/stalwart-smtp/etc/config.toml
|
ExecStart=__PATH__/bin/stalwart-__NAME__ --config=__PATH__/etc/config.toml
|
||||||
PermissionsStartOnly=true
|
PermissionsStartOnly=true
|
||||||
StandardOutput=syslog
|
StandardOutput=syslog
|
||||||
StandardError=syslog
|
StandardError=syslog
|
||||||
SyslogIdentifier=stalwart-smtp
|
SyslogIdentifier=stalwart-__NAME__
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
|
@ -4,13 +4,13 @@
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>Label</key>
|
<key>Label</key>
|
||||||
<string>stalwart.smtp</string>
|
<string>stalwart.__NAME__</string>
|
||||||
<key>ServiceDescription</key>
|
<key>ServiceDescription</key>
|
||||||
<string>Stalwart SMTP Server</string>
|
<string>Stalwart __TITLE__ Server</string>
|
||||||
<key>ProgramArguments</key>
|
<key>ProgramArguments</key>
|
||||||
<array>
|
<array>
|
||||||
<string>/usr/local/stalwart-smtp/bin/stalwart-smtp</string>
|
<string>__PATH__/bin/stalwart-__NAME__</string>
|
||||||
<string>--config=/usr/local/stalwart-smtp/etc/config.toml</string>
|
<string>--config=__PATH__/etc/config.toml</string>
|
||||||
</array>
|
</array>
|
||||||
<key>RunAtLoad</key>
|
<key>RunAtLoad</key>
|
||||||
<true/>
|
<true/>
|
|
@ -21,7 +21,10 @@
|
||||||
* for more details.
|
* for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use imap::op::authenticate::decode_challenge_oauth;
|
||||||
use imap_proto::ResponseType;
|
use imap_proto::ResponseType;
|
||||||
|
use mail_parser::decoders::base64::base64_decode;
|
||||||
|
use mail_send::Credentials;
|
||||||
|
|
||||||
use super::{AssertResult, ImapConnection, Type};
|
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.send_untagged("AGJvYXR5AG1jYm9hdGZhY2U=").await;
|
||||||
imap.assert_read(Type::Tagged, ResponseType::No).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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
* for more details.
|
* for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use imap::op::list::matches_pattern;
|
||||||
use imap_proto::ResponseType;
|
use imap_proto::ResponseType;
|
||||||
|
|
||||||
use super::{AssertResult, ImapConnection, Type};
|
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.send("RENAME \"Recycle Bin\" \"Deleted Items\"").await;
|
||||||
imap.assert_read(Type::Tagged, ResponseType::Ok).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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -247,13 +247,27 @@ pub async fn jmap_tests() {
|
||||||
email_submission::test(params.server.clone(), &mut params.client).await;
|
email_submission::test(params.server.clone(), &mut params.client).await;
|
||||||
websocket::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;
|
quota::test(params.server.clone(), &mut params.client).await;
|
||||||
stress_test::test(params.server.clone(), params.client).await;
|
|
||||||
|
|
||||||
if delete {
|
if delete {
|
||||||
params.temp_dir.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)]
|
#[allow(dead_code)]
|
||||||
struct JMAPTest {
|
struct JMAPTest {
|
||||||
server: Arc<JMAP>,
|
server: Arc<JMAP>,
|
||||||
|
|
|
@ -39,6 +39,7 @@ use jmap::{
|
||||||
HtmlResponse, StateChangeResponse,
|
HtmlResponse, StateChangeResponse,
|
||||||
},
|
},
|
||||||
auth::AccessToken,
|
auth::AccessToken,
|
||||||
|
push::ece::ece_encrypt,
|
||||||
JMAP,
|
JMAP,
|
||||||
};
|
};
|
||||||
use jmap_client::{client::Client, mailbox::Role, push_subscription::Keys};
|
use jmap_client::{client::Client, mailbox::Role, push_subscription::Keys};
|
||||||
|
@ -375,3 +376,24 @@ async fn assert_state(event_rx: &mut mpsc::Receiver<PushMessage>, id: &Id, state
|
||||||
state.iter().collect::<AHashSet<&TypeState>>()
|
state.iter().collect::<AHashSet<&TypeState>>()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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<u8> = (0..len).map(|_| store::rand::random::<u8>()).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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue