Skip to content

Commit 66272f7

Browse files
committed
v0.3.3
1 parent c2e909a commit 66272f7

26 files changed

+735
-68
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
44

5+
## [0.3.3] - 2023-08-02
6+
7+
### Added
8+
- Encryption at rest with **S/MIME** and **PGP** support.
9+
- Support for referencing context variables from dynamic values.
10+
11+
### Changed
12+
13+
### Fixed
14+
- Support for PKCS8v1 ED25519 keys (#20).
15+
- Automatic retry for import/export blob downloads (#14)
16+
517
## [0.3.2] - 2023-07-28
618

719
### Added

Cargo.lock

Lines changed: 26 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
[![](https://img.shields.io/discord/923615863037390889?label=Chat)](https://discord.gg/jtgtCNj66U)
66
[![](https://img.shields.io/twitter/follow/stalwartlabs)](https://twitter.com/stalwartlabs)
77
[![](https://img.shields.io/mastodon/follow/109929667531941122)](https://mastodon.social/@stalwartlabs)
8-
[![](https://img.shields.io/badge/Follow-%40stalwartlabs-8A2BE2)](https://www.threads.net/@stalwartlabs)
98

109
**Stalwart Mail Server** is an open-source mail server solution with JMAP, IMAP4, and SMTP support and a wide range of modern features. It is written in Rust and designed to be secure, fast, robust and scalable.
1110

@@ -35,6 +34,7 @@ Key features:
3534
- Email aliases, mailing lists, subaddressing and catch-all addresses support.
3635
- Integration with **OpenTelemetry** to enable monitoring, tracing, and performance analysis.
3736
- **Secure**:
37+
- Encryption at rest with **S/MIME** and **PGP** support.
3838
- OAuth 2.0 [authorization code](https://www.rfc-editor.org/rfc/rfc8628) and [device authorization](https://www.rfc-editor.org/rfc/rfc8628) flows.
3939
- Access Control Lists (ACLs).
4040
- Rate limiting.

crates/cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ authors = ["Stalwart Labs Ltd. <[email protected]>"]
55
license = "AGPL-3.0-only"
66
repository = "https://github.com/stalwartlabs/cli"
77
homepage = "https://github.com/stalwartlabs/cli"
8-
version = "0.3.2"
8+
version = "0.3.3"
99
edition = "2021"
1010
readme = "README.md"
1111
resolver = "2"

crates/imap/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "imap"
3-
version = "0.3.2"
3+
version = "0.3.3"
44
edition = "2021"
55
resolver = "2"
66

crates/install/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ authors = ["Stalwart Labs Ltd. <[email protected]>"]
55
license = "AGPL-3.0-only"
66
repository = "https://github.com/stalwartlabs/mail-server"
77
homepage = "https://github.com/stalwartlabs/mail-server"
8-
version = "0.3.2"
8+
version = "0.3.3"
99
edition = "2021"
1010
readme = "README.md"
1111
resolver = "2"

crates/jmap/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "jmap"
3-
version = "0.3.2"
3+
version = "0.3.3"
44
edition = "2021"
55
resolver = "2"
66

crates/jmap/src/auth/oauth/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ impl TokenResponse {
213213
}
214214
}
215215

216+
#[derive(Debug)]
216217
pub struct FormData {
217218
fields: HashMap<String, Vec<u8>>,
218219
}

crates/jmap/src/email/crypto.rs

Lines changed: 64 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
* for more details.
2222
*/
2323

24-
use std::{borrow::Cow, collections::BTreeSet};
24+
use std::{borrow::Cow, collections::BTreeSet, fmt::Display};
2525

2626
use aes::cipher::{block_padding::Pkcs7, BlockEncryptMut, KeyIvInit};
2727
use jmap_proto::types::{collection::Collection, property::Property};
@@ -53,6 +53,7 @@ const CRYPT_HTML_HEADER: &str = include_str!("../../../../resources/htx/crypto_h
5353
const CRYPT_HTML_FOOTER: &str = include_str!("../../../../resources/htx/crypto_footer.htx");
5454
const CRYPT_HTML_FORM: &str = include_str!("../../../../resources/htx/crypto_form.htx");
5555
const CRYPT_HTML_SUCCESS: &str = include_str!("../../../../resources/htx/crypto_success.htx");
56+
const CRYPT_HTML_DISABLED: &str = include_str!("../../../../resources/htx/crypto_disabled.htx");
5657
const CRYPT_HTML_ERROR: &str = include_str!("../../../../resources/htx/crypto_error.htx");
5758

5859
#[derive(Debug)]
@@ -75,9 +76,9 @@ pub enum EncryptionMethod {
7576

7677
#[derive(Debug, serde::Serialize, serde::Deserialize)]
7778
pub struct EncryptionParams {
78-
method: EncryptionMethod,
79-
algo: Algorithm,
80-
certs: Vec<Vec<u8>>,
79+
pub method: EncryptionMethod,
80+
pub algo: Algorithm,
81+
pub certs: Vec<Vec<u8>>,
8182
}
8283

8384
#[async_trait::async_trait]
@@ -496,7 +497,7 @@ fn try_parse_pem(bytes: &[u8]) -> Result<Option<(EncryptionMethod, Vec<Vec<u8>>)
496497
Ok(method.map(|method| (method, certs)))
497498
}
498499

499-
impl Serialize for EncryptionParams {
500+
impl Serialize for &EncryptionParams {
500501
fn serialize(self) -> Vec<u8> {
501502
let len = bincode::serialized_size(&self).unwrap_or_default();
502503
let mut buf = Vec::with_capacity(len as usize + 1);
@@ -529,7 +530,7 @@ impl Deserialize for EncryptionParams {
529530
}
530531
}
531532

532-
impl ToBitmaps for EncryptionParams {
533+
impl ToBitmaps for &EncryptionParams {
533534
fn to_bitmaps(&self, _: &mut Vec<store::write::Operation>, _: u8, _: bool) {
534535
unreachable!()
535536
}
@@ -538,70 +539,65 @@ impl ToBitmaps for EncryptionParams {
538539
impl JMAP {
539540
// Code authorization flow, handles an authorization request
540541
pub async fn handle_crypto_update(&self, req: &mut HttpRequest) -> HttpResponse {
541-
let response = match *req.method() {
542+
let mut response = String::with_capacity(
543+
CRYPT_HTML_HEADER.len() + CRYPT_HTML_FOOTER.len() + CRYPT_HTML_FORM.len(),
544+
);
545+
response.push_str(&CRYPT_HTML_HEADER.replace("@@@", "/crypto"));
546+
547+
match *req.method() {
542548
hyper::Method::POST => {
543549
// Parse form
544550
let form = match FormData::from_request(req, 1024 * 1024).await {
545551
Ok(form) => form,
546552
Err(err) => return err,
547553
};
548554

549-
if let Err(error) = self.validate_form(form).await {
550-
let mut response = String::with_capacity(
551-
CRYPT_HTML_HEADER.len()
552-
+ CRYPT_HTML_FOOTER.len()
553-
+ CRYPT_HTML_ERROR.len()
554-
+ error.len(),
555-
);
556-
557-
response.push_str(&CRYPT_HTML_HEADER.replace("@@@", "/crypto"));
558-
response.push_str(&CRYPT_HTML_ERROR.replace("@@@", &error));
559-
response.push_str(CRYPT_HTML_FOOTER);
560-
561-
response
562-
} else {
563-
let mut response = String::with_capacity(
564-
CRYPT_HTML_HEADER.len()
565-
+ CRYPT_HTML_FOOTER.len()
566-
+ CRYPT_HTML_SUCCESS.len(),
567-
);
568-
569-
response.push_str(&CRYPT_HTML_HEADER.replace("@@@", "/crypto"));
570-
response.push_str(CRYPT_HTML_SUCCESS);
571-
response.push_str(CRYPT_HTML_FOOTER);
572-
573-
response
555+
match self.validate_form(form).await {
556+
Ok(Some(params)) => {
557+
response.push_str(
558+
&CRYPT_HTML_SUCCESS
559+
.replace(
560+
"$$$",
561+
format!("{} ({})", params.method, params.algo).as_str(),
562+
)
563+
.replace("@@@", params.certs.len().to_string().as_str()),
564+
);
565+
}
566+
Ok(None) => {
567+
response.push_str(CRYPT_HTML_DISABLED);
568+
}
569+
Err(error) => {
570+
response.push_str(&CRYPT_HTML_ERROR.replace("@@@", &error));
571+
}
574572
}
575573
}
576574

577575
hyper::Method::GET => {
578-
let mut response = String::with_capacity(
579-
CRYPT_HTML_HEADER.len() + CRYPT_HTML_FOOTER.len() + CRYPT_HTML_FORM.len(),
580-
);
581-
582-
response.push_str(&CRYPT_HTML_HEADER.replace("@@@", "/crypto"));
583576
response.push_str(CRYPT_HTML_FORM);
584-
response.push_str(CRYPT_HTML_FOOTER);
585-
586-
response
587577
}
588578
_ => unreachable!(),
589579
};
590580

581+
response.push_str(CRYPT_HTML_FOOTER);
582+
591583
HtmlResponse::new(response).into_http_response()
592584
}
593585

594-
async fn validate_form(&self, mut form: FormData) -> Result<(), Cow<str>> {
595-
if let (Some(certificate), Some(email), Some(password), Some(encryption)) = (
596-
form.remove_bytes("certificate"),
586+
async fn validate_form(
587+
&self,
588+
mut form: FormData,
589+
) -> Result<Option<EncryptionParams>, Cow<str>> {
590+
let certificate = form.remove_bytes("certificate");
591+
if let (Some(email), Some(password), Some(encryption)) = (
597592
form.get("email"),
598593
form.get("password"),
599594
form.get("encryption"),
600595
) {
601596
// Validate fields
602597
if email.is_empty() || password.is_empty() {
603598
return Err(Cow::from("Please enter your login and password"));
604-
} else if encryption != "disable" && certificate.is_empty() {
599+
} else if encryption != "disable" && certificate.as_ref().map_or(true, |c| c.is_empty())
600+
{
605601
return Err(Cow::from("Please select one or more certificates"));
606602
}
607603

@@ -611,7 +607,8 @@ impl JMAP {
611607
.await
612608
.ok_or_else(|| Cow::from("Invalid login or password"))?;
613609
if encryption != "disable" {
614-
let (method, certs) = try_parse_certs(certificate).map_err(Cow::from)?;
610+
let (method, certs) =
611+
try_parse_certs(certificate.unwrap_or_default()).map_err(Cow::from)?;
615612
let algo = match (encryption, method) {
616613
("pgp-256", EncryptionMethod::PGP) => Algorithm::Aes256,
617614
("pgp-128", EncryptionMethod::PGP) => Algorithm::Aes128,
@@ -645,10 +642,12 @@ impl JMAP {
645642
.with_account_id(token.primary_id())
646643
.with_collection(Collection::Principal)
647644
.update_document(0)
648-
.value(Property::Parameters, params, F_VALUE);
645+
.value(Property::Parameters, &params, F_VALUE);
649646
self.write_batch(batch).await.map_err(|_| {
650647
Cow::from("Failed to save encryption parameters, please try again later")
651648
})?;
649+
650+
Ok(Some(params))
652651
} else {
653652
// Remove encryption params
654653
let mut batch = BatchBuilder::new();
@@ -660,11 +659,28 @@ impl JMAP {
660659
self.write_batch(batch).await.map_err(|_| {
661660
Cow::from("Failed to save encryption parameters, please try again later")
662661
})?;
662+
Ok(None)
663663
}
664-
665-
Ok(())
666664
} else {
667665
Err(Cow::from("Missing form parameters"))
668666
}
669667
}
670668
}
669+
670+
impl Display for EncryptionMethod {
671+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
672+
match self {
673+
EncryptionMethod::PGP => write!(f, "PGP"),
674+
EncryptionMethod::SMIME => write!(f, "S/MIME"),
675+
}
676+
}
677+
}
678+
679+
impl Display for Algorithm {
680+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
681+
match self {
682+
Algorithm::Aes128 => write!(f, "AES-128"),
683+
Algorithm::Aes256 => write!(f, "AES-256"),
684+
}
685+
}
686+
}

crates/main/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ homepage = "https://stalw.art"
77
keywords = ["imap", "jmap", "smtp", "email", "mail", "server"]
88
categories = ["email"]
99
license = "AGPL-3.0-only"
10-
version = "0.3.2"
10+
version = "0.3.3"
1111
edition = "2021"
1212
resolver = "2"
1313

crates/smtp/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ homepage = "https://stalw.art/smtp"
77
keywords = ["smtp", "email", "mail", "server"]
88
categories = ["email"]
99
license = "AGPL-3.0-only"
10-
version = "0.3.2"
10+
version = "0.3.3"
1111
edition = "2021"
1212
resolver = "2"
1313

resources/htx/crypto_disabled.htx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div class="illustration"><i class="icon ion-unlocked"></i></div><p class="auth"><b>Encryption at rest disabled</b><br /><br />Messages will now be stored in plain text on the server..</p>

0 commit comments

Comments
 (0)