mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-09-20 07:16:18 +08:00
v0.7.2
This commit is contained in:
parent
929d84468f
commit
3cc3b726ea
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -2,6 +2,21 @@
|
|||
|
||||
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
|
||||
## [0.7.2] - 2024-04-17
|
||||
|
||||
To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin version.
|
||||
|
||||
## Added
|
||||
- Support for DNS-01 and HTTP-01 ACME challenges (#226)
|
||||
- Configurable external resources (#355)
|
||||
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
- Startup failure when Elasticsearch is down/starting up (#334)
|
||||
- URL decode path elements in REST API.
|
||||
|
||||
## [0.7.1] - 2024-04-12
|
||||
|
||||
To upgrade replace the `stalwart-mail` binary.
|
||||
|
|
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -6622,7 +6622,7 @@ name = "utils"
|
|||
version = "0.7.2"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"base64 0.21.7",
|
||||
"base64 0.22.0",
|
||||
"blake3",
|
||||
"chrono",
|
||||
"dashmap",
|
||||
|
|
|
@ -91,7 +91,7 @@ Key features:
|
|||
- Self-service portal for password reset and encryption-at-rest key management.
|
||||
- **Secure and robust**:
|
||||
- Encryption at rest with **S/MIME** or **OpenPGP**.
|
||||
- Automatic TLS certificate provisioning with [ACME](https://datatracker.ietf.org/doc/html/rfc8555).
|
||||
- Automatic TLS certificate provisioning with [ACME](https://datatracker.ietf.org/doc/html/rfc8555) using `TLS-ALPN-01`, `DNS-01` or `HTTP-01` challenges.
|
||||
- OAuth 2.0 [authorization code](https://www.rfc-editor.org/rfc/rfc8628) and [device authorization](https://www.rfc-editor.org/rfc/rfc8628) flows.
|
||||
- Automated blocking of hosts that cause multiple authentication errors (aka **fail2ban**).
|
||||
- Access Control Lists (ACLs).
|
||||
|
|
|
@ -203,10 +203,10 @@ fn build_dns_updater(config: &mut Config, acme_id: &str) -> Option<DnsUpdater> {
|
|||
match config.value_require(("acme", acme_id, "provider"))? {
|
||||
"rfc2136-tsig" => {
|
||||
let algorithm: TsigAlgorithm = config
|
||||
.value_require(("acme", acme_id, "algorithm"))?
|
||||
.value_require(("acme", acme_id, "tsig-algorithm"))?
|
||||
.parse()
|
||||
.map_err(|_| {
|
||||
config.new_parse_error(("acme", acme_id, "algorithm"), "Invalid algorithm")
|
||||
config.new_parse_error(("acme", acme_id, "tsig-algorithm"), "Invalid algorithm")
|
||||
})
|
||||
.ok()?;
|
||||
let key = STANDARD
|
||||
|
|
|
@ -46,6 +46,8 @@ use crate::{
|
|||
JMAP,
|
||||
};
|
||||
|
||||
use super::decode_path_element;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Algorithm {
|
||||
Rsa,
|
||||
|
@ -76,7 +78,7 @@ impl JMAP {
|
|||
|
||||
async fn handle_get_public_key(&self, path: Vec<&str>) -> HttpResponse {
|
||||
let signature_id = match path.get(1) {
|
||||
Some(signature_id) => *signature_id,
|
||||
Some(signature_id) => decode_path_element(signature_id),
|
||||
None => {
|
||||
return RequestError::not_found().into_http_response();
|
||||
}
|
||||
|
|
|
@ -39,6 +39,8 @@ use crate::{
|
|||
JMAP,
|
||||
};
|
||||
|
||||
use super::decode_path_element;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct DnsRecord {
|
||||
#[serde(rename = "type")]
|
||||
|
@ -82,7 +84,8 @@ impl JMAP {
|
|||
}
|
||||
(Some(domain), &Method::GET) => {
|
||||
// Obtain DNS records
|
||||
match self.build_dns_records(domain).await {
|
||||
let domain = decode_path_element(domain);
|
||||
match self.build_dns_records(domain.as_ref()).await {
|
||||
Ok(records) => JsonResponse::new(json!({
|
||||
"data": records,
|
||||
}))
|
||||
|
@ -92,7 +95,8 @@ impl JMAP {
|
|||
}
|
||||
(Some(domain), &Method::POST) => {
|
||||
// Create domain
|
||||
match self.core.storage.data.create_domain(domain).await {
|
||||
let domain = decode_path_element(domain);
|
||||
match self.core.storage.data.create_domain(domain.as_ref()).await {
|
||||
Ok(_) => {
|
||||
// Set default domain name if missing
|
||||
if matches!(
|
||||
|
@ -103,7 +107,7 @@ impl JMAP {
|
|||
.core
|
||||
.storage
|
||||
.config
|
||||
.set([("lookup.default.domain", *domain)])
|
||||
.set([("lookup.default.domain", domain.as_ref())])
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to set default domain name: {}", err);
|
||||
|
@ -120,7 +124,8 @@ impl JMAP {
|
|||
}
|
||||
(Some(domain), &Method::DELETE) => {
|
||||
// Delete domain
|
||||
match self.core.storage.data.delete_domain(domain).await {
|
||||
let domain = decode_path_element(domain);
|
||||
match self.core.storage.data.delete_domain(domain.as_ref()).await {
|
||||
Ok(_) => JsonResponse::new(json!({
|
||||
"data": (),
|
||||
}))
|
||||
|
|
|
@ -128,3 +128,12 @@ impl From<String> for ManagementApiError {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode_path_element(item: &str) -> Cow<'_, str> {
|
||||
// Bit hackish but avoids an extra dependency
|
||||
form_urlencoded::parse(item.as_bytes())
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|(k, _)| k)
|
||||
.unwrap_or_else(|| item.into())
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ use crate::{
|
|||
JMAP,
|
||||
};
|
||||
|
||||
use super::ManagementApiError;
|
||||
use super::{decode_path_element, ManagementApiError};
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PrincipalResponse {
|
||||
|
@ -151,7 +151,8 @@ impl JMAP {
|
|||
}
|
||||
(Some(name), method) => {
|
||||
// Fetch, update or delete principal
|
||||
let account_id = match self.core.storage.data.get_account_id(name).await {
|
||||
let name = decode_path_element(name);
|
||||
let account_id = match self.core.storage.data.get_account_id(name.as_ref()).await {
|
||||
Ok(Some(account_id)) => account_id,
|
||||
Ok(None) => {
|
||||
return RequestError::blank(
|
||||
|
|
|
@ -45,6 +45,8 @@ use crate::{
|
|||
JMAP,
|
||||
};
|
||||
|
||||
use super::decode_path_element;
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||
pub struct Message {
|
||||
pub id: QueueId,
|
||||
|
@ -122,7 +124,7 @@ impl JMAP {
|
|||
|
||||
match (
|
||||
path.get(1).copied().unwrap_or_default(),
|
||||
path.get(2).copied(),
|
||||
path.get(2).copied().map(decode_path_element),
|
||||
req.method(),
|
||||
) {
|
||||
("messages", None, &Method::GET) => {
|
||||
|
@ -439,7 +441,7 @@ impl JMAP {
|
|||
}
|
||||
("reports", Some(report_id), &Method::GET) => {
|
||||
let mut result = None;
|
||||
if let Some(report_id) = parse_queued_report_id(report_id) {
|
||||
if let Some(report_id) = parse_queued_report_id(report_id.as_ref()) {
|
||||
match report_id {
|
||||
QueueClass::DmarcReportHeader(event) => {
|
||||
let mut rua = Vec::new();
|
||||
|
@ -475,7 +477,7 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
("reports", Some(report_id), &Method::DELETE) => {
|
||||
if let Some(report_id) = parse_queued_report_id(report_id) {
|
||||
if let Some(report_id) = parse_queued_report_id(report_id.as_ref()) {
|
||||
match report_id {
|
||||
QueueClass::DmarcReportHeader(event) => {
|
||||
self.smtp.delete_dmarc_report(event).await;
|
||||
|
|
|
@ -40,6 +40,8 @@ use crate::{
|
|||
JMAP,
|
||||
};
|
||||
|
||||
use super::decode_path_element;
|
||||
|
||||
enum ReportType {
|
||||
Dmarc,
|
||||
Tls,
|
||||
|
@ -50,7 +52,7 @@ impl JMAP {
|
|||
pub async fn handle_manage_reports(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse {
|
||||
match (
|
||||
path.get(1).copied().unwrap_or_default(),
|
||||
path.get(2).copied(),
|
||||
path.get(2).copied().map(decode_path_element),
|
||||
req.method(),
|
||||
) {
|
||||
(class @ ("dmarc" | "tls" | "arf"), None, &Method::GET) => {
|
||||
|
@ -159,7 +161,7 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
(class @ ("dmarc" | "tls" | "arf"), Some(report_id), &Method::GET) => {
|
||||
if let Some(report_id) = parse_incoming_report_id(class, report_id) {
|
||||
if let Some(report_id) = parse_incoming_report_id(class, report_id.as_ref()) {
|
||||
match &report_id {
|
||||
ReportClass::Tls { .. } => match self
|
||||
.core
|
||||
|
@ -215,7 +217,7 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
(class @ ("dmarc" | "tls" | "arf"), Some(report_id), &Method::DELETE) => {
|
||||
if let Some(report_id) = parse_incoming_report_id(class, report_id) {
|
||||
if let Some(report_id) = parse_incoming_report_id(class, report_id.as_ref()) {
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch.clear(ValueClass::Report(report_id));
|
||||
let result = self.core.storage.data.write(batch.build()).await.is_ok();
|
||||
|
|
|
@ -32,7 +32,7 @@ use crate::{
|
|||
JMAP,
|
||||
};
|
||||
|
||||
use super::ManagementApiError;
|
||||
use super::{decode_path_element, ManagementApiError};
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
|
@ -269,7 +269,9 @@ impl JMAP {
|
|||
}
|
||||
}
|
||||
(Some(prefix), &Method::DELETE) if !prefix.is_empty() => {
|
||||
match self.core.storage.config.clear(prefix).await {
|
||||
let prefix = decode_path_element(prefix);
|
||||
|
||||
match self.core.storage.config.clear(prefix.as_ref()).await {
|
||||
Ok(_) => JsonResponse::new(json!({
|
||||
"data": (),
|
||||
}))
|
||||
|
|
|
@ -21,7 +21,7 @@ chrono = "0.4"
|
|||
rand = "0.8.5"
|
||||
webpki-roots = { version = "0.26"}
|
||||
ring = { version = "0.17" }
|
||||
base64 = "0.21"
|
||||
base64 = "0.22"
|
||||
serde_json = "1.0"
|
||||
rcgen = "0.13"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"]}
|
||||
|
|
|
@ -8,7 +8,7 @@ version: '3'
|
|||
services:
|
||||
pebble:
|
||||
image: letsencrypt/pebble:latest
|
||||
command: pebble -config /test/config/pebble-config.json -strict -dnsserver 8.8.8.8:53 #-dnsserver 10.30.50.3:8053
|
||||
command: pebble -config /test/config/pebble-config.json -strict -dnsserver 10.30.50.3:8053 #-dnsserver 8.8.8.8:53
|
||||
ports:
|
||||
- 14000:14000 # HTTPS ACME API
|
||||
- 15000:15000 # HTTPS Management API
|
||||
|
|
Loading…
Reference in a new issue