CI/CD tweaks.

This commit is contained in:
mdecimus 2023-07-11 17:16:08 +02:00
parent 843e61139a
commit 9c6c53e21c
23 changed files with 1707 additions and 224 deletions

13
.github/FUNDING.yml vendored Normal file
View 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
View 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
View 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

1
CNAME Normal file
View file

@ -0,0 +1 @@
get.stalw.art

47
Cargo.lock generated
View file

@ -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"

52
Dockerfile Normal file
View 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"]

View file

@ -276,30 +276,3 @@ pub fn decode_challenge_oauth(challenge: &[u8]) -> Result<Credentials<String>, &
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()
);
}
}

View file

@ -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);
}
}
}

View file

@ -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"

6
crates/install/build.rs Normal file
View file

@ -0,0 +1,6 @@
fn main() {
println!(
"cargo:rustc-env=TARGET={}",
std::env::var("TARGET").unwrap()
);
}

View file

@ -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<PathBuf>,
#[clap(long, short = 'c')]
component: Option<Component>,
#[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::<Component>(
"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::<Component>(
"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::<Backend>(
"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::<Backend>(
"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::<Blob>(
"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<S
Ok(instructions)
}
#[cfg(not(target_env = "msvc"))]
/*#[cfg(not(target_env = "msvc"))]
unsafe fn get_uid_gid() -> (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",
}
}
}

View file

@ -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<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);
}
}
}

View file

@ -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"]

641
install.sh Normal file
View 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

View file

@ -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"

View file

@ -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

Binary file not shown.

View file

@ -1,7 +1,7 @@
[Unit]
Description=Stalwart SMTP
Description=Stalwart __TITLE__ Server
Conflicts=postfix.service sendmail.service exim4.service
ConditionPathExists=/usr/local/stalwart-smtp/etc/config.toml
ConditionPathExists=__PATH__/etc/config.toml
After=network-online.target
[Service]
@ -11,11 +11,11 @@ KillMode=process
KillSignal=SIGINT
Restart=on-failure
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
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=stalwart-smtp
SyslogIdentifier=stalwart-__NAME__
[Install]
WantedBy=multi-user.target

View file

@ -4,13 +4,13 @@
<plist version="1.0">
<dict>
<key>Label</key>
<string>stalwart.smtp</string>
<string>stalwart.__NAME__</string>
<key>ServiceDescription</key>
<string>Stalwart SMTP Server</string>
<string>Stalwart __TITLE__ Server</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/stalwart-smtp/bin/stalwart-smtp</string>
<string>--config=/usr/local/stalwart-smtp/etc/config.toml</string>
<string>__PATH__/bin/stalwart-__NAME__</string>
<string>--config=__PATH__/etc/config.toml</string>
</array>
<key>RunAtLoad</key>
<true/>

View file

@ -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()
);
}

View file

@ -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);
}
}

View file

@ -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<JMAP>,

View file

@ -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<PushMessage>, id: &Id, state
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);
}
}