mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-11-01 07:16:07 +08:00
Install script fixes, moved SMTP management API to JMAP listener.
This commit is contained in:
parent
81a5c2f9bf
commit
c1ae11c84b
15 changed files with 212 additions and 116 deletions
140
.github/workflows/build.yml
vendored
140
.github/workflows/build.yml
vendored
|
|
@ -5,7 +5,7 @@ on:
|
|||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
@ -240,67 +240,113 @@ jobs:
|
|||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
docker-amd:
|
||||
name: Build Docker AMD64 images
|
||||
if: '!cancelled()'
|
||||
build_docker:
|
||||
name: Build Docker image for ${{ matrix.platform }}
|
||||
runs-on: ubuntu-latest
|
||||
if: '!cancelled()'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
steps:
|
||||
- name: Checkout
|
||||
-
|
||||
name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
tags: |
|
||||
type=schedule
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=sha
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
-
|
||||
name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v4
|
||||
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()'
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true
|
||||
#cache-from: type=registry,ref=${{ env.REGISTRY_IMAGE }}:buildcache-${{ env.PLATFORM_PAIR }}
|
||||
#cache-to: type=registry,ref=s${{ env.REGISTRY_IMAGE }}:buildcache-${{ env.PLATFORM_PAIR }},mode=max
|
||||
cache-from: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
|
||||
cache-to: type=gha,scope=build-${{ env.PLATFORM_PAIR }}
|
||||
-
|
||||
name: Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
-
|
||||
name: Upload digest
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: digests
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge_docker:
|
||||
name: Merge and push Docker manifest
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build_docker
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
-
|
||||
name: Download digests
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: digests
|
||||
path: /tmp/digests
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
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
|
||||
-
|
||||
name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||
-
|
||||
name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -15,6 +15,16 @@ try each of these servers individually:
|
|||
* [Stalwart IMAP Server](https://github.com/stalwartlabs/imap-server/)
|
||||
* [Stalwart SMTP Server](https://github.com/stalwartlabs/smtp-server/)
|
||||
|
||||
## Why choose Stalwart?
|
||||
|
||||
Within the field of mail servers, established names like Postfix, Courier and Dovecot have long been the go-to solutions. However, the landscape of internet messaging is evolving, with a need for more efficient, easy to maintain, reliable, and secure systems. Here's why you might consider making the switch to Stalwart Mail Server:
|
||||
|
||||
- Designed with the latest internet messaging protocols in mind - JMAP and IMAP4rev2, along with the conventional SMTP.
|
||||
- Leverages the performance and security benefits of the Rust programming language. This statically typed, compiled language is known for its memory safety and concurrency support, reducing the likelihood of typical security issues like buffer overflows.
|
||||
- Thanks to its native FoundationDB and S3 storage support, it can be scaled across many servers, accommodating millions of users.
|
||||
- Available as a single, integrated package that includes JMAP, IMAP, and SMTP servers. This means that you don't have to install, configure and maintain multiple servers to get a complete solution.
|
||||
- Designed to be easy to install and maintain, with a single configuration file and a simple command-line interface.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the terms of the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) as published by
|
||||
|
|
|
|||
|
|
@ -50,9 +50,9 @@ async fn main() -> std::io::Result<()> {
|
|||
let is_jmap = args.command.is_jmap();
|
||||
let credentials = if let Some(credentials) = args.credentials {
|
||||
parse_credentials(&credentials)
|
||||
} else if is_jmap {
|
||||
} else {
|
||||
let credentials = rpassword::prompt_password(
|
||||
"\nEnter JMAP admin credentials or press [ENTER] to use OAuth: ",
|
||||
"\nEnter administrator credentials or press [ENTER] to use OAuth: ",
|
||||
)
|
||||
.unwrap();
|
||||
if !credentials.is_empty() {
|
||||
|
|
@ -60,8 +60,6 @@ async fn main() -> std::io::Result<()> {
|
|||
} else {
|
||||
oauth(&args.url).await
|
||||
}
|
||||
} else {
|
||||
parse_credentials(&rpassword::prompt_password("\nEnter SMTP admin credentials: ").unwrap())
|
||||
};
|
||||
|
||||
if is_jmap {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ use serde::Deserialize;
|
|||
pub struct Cli {
|
||||
#[clap(subcommand)]
|
||||
pub command: Commands,
|
||||
/// JMAP or SMTP server base URL
|
||||
/// Server base URL
|
||||
#[clap(short, long)]
|
||||
pub url: String,
|
||||
/// Authentication credentials
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ pub async fn cmd_queue(url: &str, credentials: Credentials, command: QueueComman
|
|||
.collect(),
|
||||
));
|
||||
for (message, id) in smtp_manage_request::<Vec<Option<Message>>>(
|
||||
&build_query(url, "/queue/status?ids=", chunk),
|
||||
&build_query(url, "/admin/queue/status?ids=", chunk),
|
||||
&credentials,
|
||||
)
|
||||
.await
|
||||
|
|
@ -172,7 +172,7 @@ pub async fn cmd_queue(url: &str, credentials: Credentials, command: QueueComman
|
|||
}
|
||||
QueueCommands::Status { ids } => {
|
||||
for (message, id) in smtp_manage_request::<Vec<Option<Message>>>(
|
||||
&build_query(url, "/queue/status?ids=", &parse_ids(&ids)),
|
||||
&build_query(url, "/admin/queue/status?ids=", &parse_ids(&ids)),
|
||||
&credentials,
|
||||
)
|
||||
.await
|
||||
|
|
@ -311,7 +311,7 @@ pub async fn cmd_queue(url: &str, credentials: Credentials, command: QueueComman
|
|||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let mut query = form_urlencoded::Serializer::new(format!("{url}/queue/retry?"));
|
||||
let mut query = form_urlencoded::Serializer::new(format!("{url}/admin/queue/retry?"));
|
||||
|
||||
if let Some(filter) = &domain {
|
||||
query.append_pair("filter", filter);
|
||||
|
|
@ -365,7 +365,7 @@ pub async fn cmd_queue(url: &str, credentials: Credentials, command: QueueComman
|
|||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let mut query = form_urlencoded::Serializer::new(format!("{url}/queue/cancel?"));
|
||||
let mut query = form_urlencoded::Serializer::new(format!("{url}/admin/queue/cancel?"));
|
||||
|
||||
if let Some(filter) = &rcpt {
|
||||
query.append_pair("filter", filter);
|
||||
|
|
@ -443,7 +443,7 @@ async fn query_messages(
|
|||
before: &Option<DateTime>,
|
||||
after: &Option<DateTime>,
|
||||
) -> Vec<u64> {
|
||||
let mut query = form_urlencoded::Serializer::new(format!("{url}/queue/list?"));
|
||||
let mut query = form_urlencoded::Serializer::new(format!("{url}/admin/queue/list?"));
|
||||
|
||||
if let Some(sender) = from {
|
||||
query.append_pair("from", sender);
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ pub async fn cmd_report(url: &str, credentials: Credentials, command: ReportComm
|
|||
page_size,
|
||||
} => {
|
||||
let stdout = Term::buffered_stdout();
|
||||
let mut query = form_urlencoded::Serializer::new(format!("{url}/report/list?"));
|
||||
let mut query = form_urlencoded::Serializer::new(format!("{url}/admin/report/list?"));
|
||||
|
||||
if let Some(domain) = &domain {
|
||||
query.append_pair("domain", domain);
|
||||
|
|
@ -73,7 +73,7 @@ pub async fn cmd_report(url: &str, credentials: Credentials, command: ReportComm
|
|||
.collect(),
|
||||
));
|
||||
for (report, id) in smtp_manage_request::<Vec<Option<Report>>>(
|
||||
&format!("{url}/report/status?ids={}", chunk.join(",")),
|
||||
&format!("{url}/admin/report/status?ids={}", chunk.join(",")),
|
||||
&credentials,
|
||||
)
|
||||
.await
|
||||
|
|
@ -110,7 +110,7 @@ pub async fn cmd_report(url: &str, credentials: Credentials, command: ReportComm
|
|||
}
|
||||
ReportCommands::Status { ids } => {
|
||||
for (report, id) in smtp_manage_request::<Vec<Option<Report>>>(
|
||||
&format!("{url}/report/status?ids={}", ids.join(",")),
|
||||
&format!("{url}/admin/report/status?ids={}", ids.join(",")),
|
||||
&credentials,
|
||||
)
|
||||
.await
|
||||
|
|
@ -164,7 +164,7 @@ pub async fn cmd_report(url: &str, credentials: Credentials, command: ReportComm
|
|||
let mut success_count = 0;
|
||||
let mut failed_list = vec![];
|
||||
for (success, id) in smtp_manage_request::<Vec<bool>>(
|
||||
&format!("{url}/report/cancel?ids={}", ids.join(",")),
|
||||
&format!("{url}/admin/report/cancel?ids={}", ids.join(",")),
|
||||
&credentials,
|
||||
)
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -363,15 +363,20 @@ fn main() -> std::io::Result<()> {
|
|||
|
||||
// Obtain TLS certificate path
|
||||
let (cert_path, pk_path) = if !args.docker {
|
||||
#[cfg(not(target_env = "msvc"))]
|
||||
let cert_base_path = format!("/etc/letsencrypt/live/{}/", hostname);
|
||||
#[cfg(target_env = "msvc")]
|
||||
let cert_base_path = format!("C:\\Program Files\\Letsencrypt\\live\\{}\\", hostname);
|
||||
|
||||
(
|
||||
input(
|
||||
&format!("Where is the TLS certificate for '{hostname}' located?"),
|
||||
&format!("/etc/letsencrypt/live/{hostname}/fullchain.pem"),
|
||||
&format!("{cert_base_path}fullchain.pem"),
|
||||
file_exists,
|
||||
)?,
|
||||
input(
|
||||
&format!("Where is the TLS private key for '{hostname}' located?"),
|
||||
&format!("/etc/letsencrypt/live/{hostname}/privkey.pem"),
|
||||
&format!("{cert_base_path}privkey.pem"),
|
||||
file_exists,
|
||||
)?,
|
||||
)
|
||||
|
|
@ -827,12 +832,20 @@ impl SelectItem for Blob {
|
|||
|
||||
impl Component {
|
||||
fn default_base_path(&self) -> &'static str {
|
||||
#[cfg(not(target_env = "msvc"))]
|
||||
match self {
|
||||
Self::AllInOne => "/opt/stalwart-mail",
|
||||
Self::Jmap => "/opt/stalwart-jmap",
|
||||
Self::Imap => "/opt/stalwart-imap",
|
||||
Self::Smtp => "/opt/stalwart-smtp",
|
||||
}
|
||||
#[cfg(target_env = "msvc")]
|
||||
match self {
|
||||
Self::AllInOne => "C:\\Program Files\\Stalwart Mail",
|
||||
Self::Jmap => "C:\\Program Files\\Stalwart JMAP",
|
||||
Self::Imap => "C:\\Program Files\\Stalwart IMAP",
|
||||
Self::Smtp => "C:\\Program Files\\Stalwart SMTP",
|
||||
}
|
||||
}
|
||||
|
||||
fn binary_name(&self) -> &'static str {
|
||||
|
|
|
|||
|
|
@ -339,6 +339,12 @@ pub async fn parse_jmap_request(
|
|||
.into_http_response(),
|
||||
};
|
||||
}
|
||||
(path_1 @ ("queue" | "report"), path_2, &Method::GET) => {
|
||||
return jmap
|
||||
.smtp
|
||||
.handle_manage_request(req.uri(), req.method(), path_1, path_2)
|
||||
.await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ use directory::config::ConfigDirectory;
|
|||
use imap::core::{ImapSessionManager, IMAP};
|
||||
use jmap::{api::JmapSessionManager, services::IPC_CHANNEL_BUFFER, JMAP};
|
||||
use managesieve::core::ManageSieveSessionManager;
|
||||
use smtp::core::{SmtpAdminSessionManager, SmtpSessionManager, SMTP};
|
||||
use smtp::core::{SmtpSessionManager, SMTP};
|
||||
use tokio::sync::mpsc;
|
||||
use utils::{
|
||||
config::{Config, ServerProtocol},
|
||||
|
|
@ -79,7 +79,7 @@ async fn main() -> std::io::Result<()> {
|
|||
server.spawn(SmtpSessionManager::new(smtp.clone()), shutdown_rx)
|
||||
}
|
||||
ServerProtocol::Http => {
|
||||
server.spawn(SmtpAdminSessionManager::new(smtp.clone()), shutdown_rx)
|
||||
tracing::debug!("Ignoring HTTP server listener, using JMAP port instead.");
|
||||
}
|
||||
ServerProtocol::Jmap => {
|
||||
server.spawn(JmapSessionManager::new(jmap.clone()), shutdown_rx)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ use hyper::{
|
|||
header::{self, AUTHORIZATION},
|
||||
server::conn::http1,
|
||||
service::service_fn,
|
||||
Method, StatusCode,
|
||||
Method, StatusCode, Uri,
|
||||
};
|
||||
use mail_parser::{decoders::base64::base64_decode, DateTime};
|
||||
use mail_send::Credentials;
|
||||
|
|
@ -312,15 +312,33 @@ impl SMTP {
|
|||
|
||||
let mut path = req.uri().path().split('/');
|
||||
path.next();
|
||||
let (status, response) = match (req.method(), path.next(), path.next()) {
|
||||
(&Method::GET, Some("queue"), Some("list")) => {
|
||||
path.next(); // Skip the leading /admin
|
||||
Ok(self
|
||||
.handle_manage_request(
|
||||
req.uri(),
|
||||
req.method(),
|
||||
path.next().unwrap_or_default(),
|
||||
path.next().unwrap_or_default(),
|
||||
)
|
||||
.await)
|
||||
}
|
||||
|
||||
pub async fn handle_manage_request(
|
||||
&self,
|
||||
uri: &Uri,
|
||||
method: &Method,
|
||||
path_1: &str,
|
||||
path_2: &str,
|
||||
) -> hyper::Response<BoxBody<Bytes, hyper::Error>> {
|
||||
let (status, response) = match (method, path_1, path_2) {
|
||||
(&Method::GET, "queue", "list") => {
|
||||
let mut from = None;
|
||||
let mut to = None;
|
||||
let mut before = None;
|
||||
let mut after = None;
|
||||
let mut error = None;
|
||||
|
||||
if let Some(query) = req.uri().query() {
|
||||
if let Some(query) = uri.query() {
|
||||
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
|
||||
match key.as_ref() {
|
||||
"from" => {
|
||||
|
|
@ -373,11 +391,11 @@ impl SMTP {
|
|||
Some(error) => error.into_bad_request(),
|
||||
}
|
||||
}
|
||||
(&Method::GET, Some("queue"), Some("status")) => {
|
||||
(&Method::GET, "queue", "status") => {
|
||||
let mut queue_ids = Vec::new();
|
||||
let mut error = None;
|
||||
|
||||
if let Some(query) = req.uri().query() {
|
||||
if let Some(query) = uri.query() {
|
||||
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
|
||||
match key.as_ref() {
|
||||
"id" | "ids" => match value.parse_queue_ids() {
|
||||
|
|
@ -412,13 +430,13 @@ impl SMTP {
|
|||
Some(error) => error.into_bad_request(),
|
||||
}
|
||||
}
|
||||
(&Method::GET, Some("queue"), Some("retry")) => {
|
||||
(&Method::GET, "queue", "retry") => {
|
||||
let mut queue_ids = Vec::new();
|
||||
let mut time = Instant::now();
|
||||
let mut item = None;
|
||||
let mut error = None;
|
||||
|
||||
if let Some(query) = req.uri().query() {
|
||||
if let Some(query) = uri.query() {
|
||||
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
|
||||
match key.as_ref() {
|
||||
"id" | "ids" => match value.parse_queue_ids() {
|
||||
|
|
@ -467,12 +485,12 @@ impl SMTP {
|
|||
Some(error) => error.into_bad_request(),
|
||||
}
|
||||
}
|
||||
(&Method::GET, Some("queue"), Some("cancel")) => {
|
||||
(&Method::GET, "queue", "cancel") => {
|
||||
let mut queue_ids = Vec::new();
|
||||
let mut item = None;
|
||||
let mut error = None;
|
||||
|
||||
if let Some(query) = req.uri().query() {
|
||||
if let Some(query) = uri.query() {
|
||||
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
|
||||
match key.as_ref() {
|
||||
"id" | "ids" => match value.parse_queue_ids() {
|
||||
|
|
@ -511,12 +529,12 @@ impl SMTP {
|
|||
Some(error) => error.into_bad_request(),
|
||||
}
|
||||
}
|
||||
(&Method::GET, Some("report"), Some("list")) => {
|
||||
(&Method::GET, "report", "list") => {
|
||||
let mut domain = None;
|
||||
let mut type_ = None;
|
||||
let mut error = None;
|
||||
|
||||
if let Some(query) = req.uri().query() {
|
||||
if let Some(query) = uri.query() {
|
||||
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
|
||||
match key.as_ref() {
|
||||
"type" => match value.as_ref() {
|
||||
|
|
@ -558,11 +576,11 @@ impl SMTP {
|
|||
Some(error) => error.into_bad_request(),
|
||||
}
|
||||
}
|
||||
(&Method::GET, Some("report"), Some("status")) => {
|
||||
(&Method::GET, "report", "status") => {
|
||||
let mut report_ids = Vec::new();
|
||||
let mut error = None;
|
||||
|
||||
if let Some(query) = req.uri().query() {
|
||||
if let Some(query) = uri.query() {
|
||||
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
|
||||
match key.as_ref() {
|
||||
"id" | "ids" => match value.parse_report_ids() {
|
||||
|
|
@ -597,11 +615,11 @@ impl SMTP {
|
|||
Some(error) => error.into_bad_request(),
|
||||
}
|
||||
}
|
||||
(&Method::GET, Some("report"), Some("cancel")) => {
|
||||
(&Method::GET, "report", "cancel") => {
|
||||
let mut report_ids = Vec::new();
|
||||
let mut error = None;
|
||||
|
||||
if let Some(query) = req.uri().query() {
|
||||
if let Some(query) = uri.query() {
|
||||
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
|
||||
match key.as_ref() {
|
||||
"id" | "ids" => match value.parse_report_ids() {
|
||||
|
|
@ -640,12 +658,12 @@ impl SMTP {
|
|||
StatusCode::NOT_FOUND,
|
||||
format!(
|
||||
"{{\"error\": \"not-found\", \"details\": \"URL {} does not exist.\"}}",
|
||||
req.uri().path()
|
||||
uri.path()
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
Ok(hyper::Response::builder()
|
||||
hyper::Response::builder()
|
||||
.status(status)
|
||||
.header(header::CONTENT_TYPE, "application/json; charset=utf-8")
|
||||
.body(
|
||||
|
|
@ -653,7 +671,7 @@ impl SMTP {
|
|||
.map_err(|never| match never {})
|
||||
.boxed(),
|
||||
)
|
||||
.unwrap())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn send_queue_event<T: Serialize>(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env sh
|
||||
# shellcheck shell=dash
|
||||
|
||||
# Stalwart SMTP install script -- based on the rustup installation script.
|
||||
# Stalwart Mail install script -- based on the rustup installation script.
|
||||
|
||||
set -e
|
||||
set -u
|
||||
|
|
@ -67,14 +67,14 @@ main() {
|
|||
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 RealName "Stalwart Mail 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 RealName "Stalwart Mail 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
|
||||
|
|
|
|||
|
|
@ -64,9 +64,9 @@ connect-timeout = "30s"
|
|||
|
||||
[directory."ldap".filter]
|
||||
name = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(uid=?))"
|
||||
email = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=?)(mailAlias=?)))"
|
||||
email = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=?)(mailAlias=?)(mailList=?)))"
|
||||
verify = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=*?*)(mailAlias=*?*)))"
|
||||
expand = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(sn=?))"
|
||||
expand = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(mailList=?))"
|
||||
domains = "(&(|(objectClass=posixAccount)(objectClass=posixGroup))(|(mail=*@?)(mailAlias=*@?)))"
|
||||
|
||||
[directory."ldap".object-classes]
|
||||
|
|
|
|||
|
|
@ -74,10 +74,6 @@ relay = [ { if = "authenticated-as", ne = "", then = true },
|
|||
max-recipients = 25
|
||||
directory = "__SMTP_DIRECTORY__"
|
||||
|
||||
[session.rcpt.cache]
|
||||
entries = 1000
|
||||
ttl = {positive = 10, negative = 5}
|
||||
|
||||
[session.rcpt.errors]
|
||||
total = 5
|
||||
wait = "5s"
|
||||
|
|
@ -218,10 +214,10 @@ mta-sts = 1024
|
|||
[report]
|
||||
path = "__PATH__/reports"
|
||||
hash = 64
|
||||
#submitter = "mx.domain.org"
|
||||
#submitter = "__HOST__"
|
||||
|
||||
[report.analysis]
|
||||
addresses = ["dmarc@*", "abuse@*"]
|
||||
addresses = ["dmarc@*", "abuse@*", "postmaster@*"]
|
||||
forward = true
|
||||
#store = "__PATH__/incoming"
|
||||
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ async fn manage_queue() {
|
|||
);
|
||||
|
||||
// Fetch and validate messages
|
||||
let ids = send_manage_request::<Vec<QueueId>>("/queue/list")
|
||||
let ids = send_manage_request::<Vec<QueueId>>("/admin/queue/list")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
|
|
@ -264,15 +264,24 @@ async fn manage_queue() {
|
|||
|
||||
// Test list search
|
||||
for (query, expected_ids) in [
|
||||
("/queue/list?from=bill1@foobar.net".to_string(), vec!["a"]),
|
||||
("/queue/list?to=foobar.org".to_string(), vec!["d", "e", "f"]),
|
||||
(
|
||||
"/queue/list?from=bill3@foobar.net&to=rcpt5@example1.com".to_string(),
|
||||
"/admin/queue/list?from=bill1@foobar.net".to_string(),
|
||||
vec!["a"],
|
||||
),
|
||||
(
|
||||
"/admin/queue/list?to=foobar.org".to_string(),
|
||||
vec!["d", "e", "f"],
|
||||
),
|
||||
(
|
||||
"/admin/queue/list?from=bill3@foobar.net&to=rcpt5@example1.com".to_string(),
|
||||
vec!["c"],
|
||||
),
|
||||
(format!("/queue/list?before={test_search}"), vec!["a", "b"]),
|
||||
(
|
||||
format!("/queue/list?after={test_search}"),
|
||||
format!("/admin/queue/list?before={test_search}"),
|
||||
vec!["a", "b"],
|
||||
),
|
||||
(
|
||||
format!("/admin/queue/list?after={test_search}"),
|
||||
vec!["d", "e", "f", "c"],
|
||||
),
|
||||
] {
|
||||
|
|
@ -290,7 +299,7 @@ async fn manage_queue() {
|
|||
// Retry delivery
|
||||
assert_eq!(
|
||||
send_manage_request::<Vec<bool>>(&format!(
|
||||
"/queue/retry?id={},{}",
|
||||
"/admin/queue/retry?id={},{}",
|
||||
id_map.get("e").unwrap(),
|
||||
id_map.get("f").unwrap()
|
||||
))
|
||||
|
|
@ -301,7 +310,7 @@ async fn manage_queue() {
|
|||
);
|
||||
assert_eq!(
|
||||
send_manage_request::<Vec<bool>>(&format!(
|
||||
"/queue/retry?id={}&filter=example1.org&at=2200-01-01T00:00:00Z",
|
||||
"/admin/queue/retry?id={}&filter=example1.org&at=2200-01-01T00:00:00Z",
|
||||
id_map.get("a").unwrap(),
|
||||
))
|
||||
.await
|
||||
|
|
@ -365,7 +374,7 @@ async fn manage_queue() {
|
|||
] {
|
||||
assert_eq!(
|
||||
send_manage_request::<Vec<bool>>(&format!(
|
||||
"/queue/cancel?id={}{}{}",
|
||||
"/admin/queue/cancel?id={}{}{}",
|
||||
id_map.get(id).unwrap(),
|
||||
if !filter.is_empty() { "&filter=" } else { "" },
|
||||
filter
|
||||
|
|
@ -378,7 +387,7 @@ async fn manage_queue() {
|
|||
);
|
||||
}
|
||||
assert_eq!(
|
||||
send_manage_request::<Vec<QueueId>>("/queue/list")
|
||||
send_manage_request::<Vec<QueueId>>("/admin/queue/list")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data()
|
||||
|
|
@ -464,7 +473,7 @@ fn assert_timestamp(timestamp: &DateTime, expected: i64, ctx: &str, message: &Me
|
|||
|
||||
async fn get_messages(ids: &[QueueId]) -> Vec<Option<Message>> {
|
||||
send_manage_request(&format!(
|
||||
"/queue/status?id={}",
|
||||
"/admin/queue/status?id={}",
|
||||
ids.iter()
|
||||
.map(|id| id.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ async fn manage_reports() {
|
|||
.await;
|
||||
|
||||
// List reports
|
||||
let ids = send_manage_request::<Vec<String>>("/report/list")
|
||||
let ids = send_manage_request::<Vec<String>>("/admin/report/list")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
|
|
@ -172,12 +172,12 @@ async fn manage_reports() {
|
|||
|
||||
// Test list search
|
||||
for (query, expected_ids) in [
|
||||
("/report/list?type=dmarc", vec!["a", "b"]),
|
||||
("/report/list?type=tls", vec!["c", "d"]),
|
||||
("/report/list?domain=foobar.org", vec!["a", "c"]),
|
||||
("/report/list?domain=foobar.net", vec!["b", "d"]),
|
||||
("/report/list?domain=foobar.org&type=dmarc", vec!["a"]),
|
||||
("/report/list?domain=foobar.net&type=tls", vec!["d"]),
|
||||
("/admin/report/list?type=dmarc", vec!["a", "b"]),
|
||||
("/admin/report/list?type=tls", vec!["c", "d"]),
|
||||
("/admin/report/list?domain=foobar.org", vec!["a", "c"]),
|
||||
("/admin/report/list?domain=foobar.net", vec!["b", "d"]),
|
||||
("/admin/report/list?domain=foobar.org&type=dmarc", vec!["a"]),
|
||||
("/admin/report/list?domain=foobar.net&type=tls", vec!["d"]),
|
||||
] {
|
||||
let expected_ids = HashSet::from_iter(expected_ids.into_iter().map(|s| s.to_string()));
|
||||
let ids = send_manage_request::<Vec<String>>(query)
|
||||
|
|
@ -194,7 +194,7 @@ async fn manage_reports() {
|
|||
for id in ["a", "b"] {
|
||||
assert_eq!(
|
||||
send_manage_request::<Vec<bool>>(&format!(
|
||||
"/report/cancel?id={}",
|
||||
"/admin/report/cancel?id={}",
|
||||
id_map.get(id).unwrap(),
|
||||
))
|
||||
.await
|
||||
|
|
@ -205,7 +205,7 @@ async fn manage_reports() {
|
|||
);
|
||||
}
|
||||
assert_eq!(
|
||||
send_manage_request::<Vec<String>>("/report/list")
|
||||
send_manage_request::<Vec<String>>("/admin/report/list")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data()
|
||||
|
|
@ -227,7 +227,7 @@ async fn manage_reports() {
|
|||
}
|
||||
|
||||
async fn get_reports(ids: &[String]) -> Vec<Option<Report>> {
|
||||
send_manage_request(&format!("/report/status?id={}", ids.join(",")))
|
||||
send_manage_request(&format!("/admin/report/status?id={}", ids.join(",")))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue