diff --git a/Cargo.toml b/Cargo.toml
index 6438e4ed..0221b0b5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,8 +16,9 @@
[workspace]
members = [
- "crates/protocol",
+ "crates/jmap",
+ "crates/jmap-proto",
"crates/store",
- "crates/core",
+ "crates/utils",
"tests",
]
diff --git a/crates/protocol/Cargo.lock b/crates/jmap-proto/Cargo.lock
similarity index 100%
rename from crates/protocol/Cargo.lock
rename to crates/jmap-proto/Cargo.lock
diff --git a/crates/protocol/Cargo.toml b/crates/jmap-proto/Cargo.toml
similarity index 86%
rename from crates/protocol/Cargo.toml
rename to crates/jmap-proto/Cargo.toml
index 87065c46..43dc29d2 100644
--- a/crates/protocol/Cargo.toml
+++ b/crates/jmap-proto/Cargo.toml
@@ -1,11 +1,11 @@
[package]
-name = "protocol"
+name = "jmap_proto"
version = "0.1.0"
edition = "2021"
[dependencies]
store = { path = "../store" }
-utils = { path = "/home/vagrant/code/utils" }
+utils = { path = "../utils" }
mail-parser = { git = "https://github.com/stalwartlabs/mail-parser", features = ["full_encoding", "serde_support", "ludicrous_mode"] }
fast-float = "0.2.0"
serde = { version = "1.0", features = ["derive"]}
diff --git a/crates/protocol/src/error/method.rs b/crates/jmap-proto/src/error/method.rs
similarity index 100%
rename from crates/protocol/src/error/method.rs
rename to crates/jmap-proto/src/error/method.rs
diff --git a/crates/protocol/src/error/mod.rs b/crates/jmap-proto/src/error/mod.rs
similarity index 100%
rename from crates/protocol/src/error/mod.rs
rename to crates/jmap-proto/src/error/mod.rs
diff --git a/crates/protocol/src/error/request.rs b/crates/jmap-proto/src/error/request.rs
similarity index 100%
rename from crates/protocol/src/error/request.rs
rename to crates/jmap-proto/src/error/request.rs
diff --git a/crates/protocol/src/error/set.rs b/crates/jmap-proto/src/error/set.rs
similarity index 100%
rename from crates/protocol/src/error/set.rs
rename to crates/jmap-proto/src/error/set.rs
diff --git a/crates/protocol/src/lib.rs b/crates/jmap-proto/src/lib.rs
similarity index 100%
rename from crates/protocol/src/lib.rs
rename to crates/jmap-proto/src/lib.rs
diff --git a/crates/protocol/src/method/changes.rs b/crates/jmap-proto/src/method/changes.rs
similarity index 100%
rename from crates/protocol/src/method/changes.rs
rename to crates/jmap-proto/src/method/changes.rs
diff --git a/crates/protocol/src/method/copy.rs b/crates/jmap-proto/src/method/copy.rs
similarity index 100%
rename from crates/protocol/src/method/copy.rs
rename to crates/jmap-proto/src/method/copy.rs
diff --git a/crates/protocol/src/method/get.rs b/crates/jmap-proto/src/method/get.rs
similarity index 100%
rename from crates/protocol/src/method/get.rs
rename to crates/jmap-proto/src/method/get.rs
diff --git a/crates/protocol/src/method/import.rs b/crates/jmap-proto/src/method/import.rs
similarity index 100%
rename from crates/protocol/src/method/import.rs
rename to crates/jmap-proto/src/method/import.rs
diff --git a/crates/protocol/src/method/mod.rs b/crates/jmap-proto/src/method/mod.rs
similarity index 100%
rename from crates/protocol/src/method/mod.rs
rename to crates/jmap-proto/src/method/mod.rs
diff --git a/crates/protocol/src/method/parse.rs b/crates/jmap-proto/src/method/parse.rs
similarity index 100%
rename from crates/protocol/src/method/parse.rs
rename to crates/jmap-proto/src/method/parse.rs
diff --git a/crates/protocol/src/method/query.rs b/crates/jmap-proto/src/method/query.rs
similarity index 100%
rename from crates/protocol/src/method/query.rs
rename to crates/jmap-proto/src/method/query.rs
diff --git a/crates/protocol/src/method/query_changes.rs b/crates/jmap-proto/src/method/query_changes.rs
similarity index 100%
rename from crates/protocol/src/method/query_changes.rs
rename to crates/jmap-proto/src/method/query_changes.rs
diff --git a/crates/protocol/src/method/search_snippet.rs b/crates/jmap-proto/src/method/search_snippet.rs
similarity index 100%
rename from crates/protocol/src/method/search_snippet.rs
rename to crates/jmap-proto/src/method/search_snippet.rs
diff --git a/crates/protocol/src/method/set.rs b/crates/jmap-proto/src/method/set.rs
similarity index 100%
rename from crates/protocol/src/method/set.rs
rename to crates/jmap-proto/src/method/set.rs
diff --git a/crates/protocol/src/method/validate.rs b/crates/jmap-proto/src/method/validate.rs
similarity index 100%
rename from crates/protocol/src/method/validate.rs
rename to crates/jmap-proto/src/method/validate.rs
diff --git a/crates/protocol/src/object/email.rs b/crates/jmap-proto/src/object/email.rs
similarity index 100%
rename from crates/protocol/src/object/email.rs
rename to crates/jmap-proto/src/object/email.rs
diff --git a/crates/protocol/src/object/email_submission.rs b/crates/jmap-proto/src/object/email_submission.rs
similarity index 100%
rename from crates/protocol/src/object/email_submission.rs
rename to crates/jmap-proto/src/object/email_submission.rs
diff --git a/crates/protocol/src/object/mailbox.rs b/crates/jmap-proto/src/object/mailbox.rs
similarity index 100%
rename from crates/protocol/src/object/mailbox.rs
rename to crates/jmap-proto/src/object/mailbox.rs
diff --git a/crates/protocol/src/object/mod.rs b/crates/jmap-proto/src/object/mod.rs
similarity index 100%
rename from crates/protocol/src/object/mod.rs
rename to crates/jmap-proto/src/object/mod.rs
diff --git a/crates/protocol/src/object/sieve.rs b/crates/jmap-proto/src/object/sieve.rs
similarity index 100%
rename from crates/protocol/src/object/sieve.rs
rename to crates/jmap-proto/src/object/sieve.rs
diff --git a/crates/protocol/src/parser/base32.rs b/crates/jmap-proto/src/parser/base32.rs
similarity index 100%
rename from crates/protocol/src/parser/base32.rs
rename to crates/jmap-proto/src/parser/base32.rs
diff --git a/crates/protocol/src/parser/impls.rs b/crates/jmap-proto/src/parser/impls.rs
similarity index 100%
rename from crates/protocol/src/parser/impls.rs
rename to crates/jmap-proto/src/parser/impls.rs
diff --git a/crates/protocol/src/parser/json.rs b/crates/jmap-proto/src/parser/json.rs
similarity index 100%
rename from crates/protocol/src/parser/json.rs
rename to crates/jmap-proto/src/parser/json.rs
diff --git a/crates/protocol/src/parser/mod.rs b/crates/jmap-proto/src/parser/mod.rs
similarity index 100%
rename from crates/protocol/src/parser/mod.rs
rename to crates/jmap-proto/src/parser/mod.rs
diff --git a/crates/protocol/src/request/capability.rs b/crates/jmap-proto/src/request/capability.rs
similarity index 100%
rename from crates/protocol/src/request/capability.rs
rename to crates/jmap-proto/src/request/capability.rs
diff --git a/crates/protocol/src/request/echo.rs b/crates/jmap-proto/src/request/echo.rs
similarity index 100%
rename from crates/protocol/src/request/echo.rs
rename to crates/jmap-proto/src/request/echo.rs
diff --git a/crates/protocol/src/request/method.rs b/crates/jmap-proto/src/request/method.rs
similarity index 100%
rename from crates/protocol/src/request/method.rs
rename to crates/jmap-proto/src/request/method.rs
diff --git a/crates/protocol/src/request/mod.rs b/crates/jmap-proto/src/request/mod.rs
similarity index 100%
rename from crates/protocol/src/request/mod.rs
rename to crates/jmap-proto/src/request/mod.rs
diff --git a/crates/protocol/src/request/parser.rs b/crates/jmap-proto/src/request/parser.rs
similarity index 100%
rename from crates/protocol/src/request/parser.rs
rename to crates/jmap-proto/src/request/parser.rs
diff --git a/crates/protocol/src/request/reference.rs b/crates/jmap-proto/src/request/reference.rs
similarity index 100%
rename from crates/protocol/src/request/reference.rs
rename to crates/jmap-proto/src/request/reference.rs
diff --git a/crates/protocol/src/response/mod.rs b/crates/jmap-proto/src/response/mod.rs
similarity index 100%
rename from crates/protocol/src/response/mod.rs
rename to crates/jmap-proto/src/response/mod.rs
diff --git a/crates/protocol/src/response/references.rs b/crates/jmap-proto/src/response/references.rs
similarity index 100%
rename from crates/protocol/src/response/references.rs
rename to crates/jmap-proto/src/response/references.rs
diff --git a/crates/protocol/src/types/acl.rs b/crates/jmap-proto/src/types/acl.rs
similarity index 100%
rename from crates/protocol/src/types/acl.rs
rename to crates/jmap-proto/src/types/acl.rs
diff --git a/crates/protocol/src/types/blob.rs b/crates/jmap-proto/src/types/blob.rs
similarity index 100%
rename from crates/protocol/src/types/blob.rs
rename to crates/jmap-proto/src/types/blob.rs
diff --git a/crates/protocol/src/types/collection.rs b/crates/jmap-proto/src/types/collection.rs
similarity index 100%
rename from crates/protocol/src/types/collection.rs
rename to crates/jmap-proto/src/types/collection.rs
diff --git a/crates/protocol/src/types/date.rs b/crates/jmap-proto/src/types/date.rs
similarity index 100%
rename from crates/protocol/src/types/date.rs
rename to crates/jmap-proto/src/types/date.rs
diff --git a/crates/protocol/src/types/id.rs b/crates/jmap-proto/src/types/id.rs
similarity index 100%
rename from crates/protocol/src/types/id.rs
rename to crates/jmap-proto/src/types/id.rs
diff --git a/crates/protocol/src/types/keyword.rs b/crates/jmap-proto/src/types/keyword.rs
similarity index 100%
rename from crates/protocol/src/types/keyword.rs
rename to crates/jmap-proto/src/types/keyword.rs
diff --git a/crates/protocol/src/types/mod.rs b/crates/jmap-proto/src/types/mod.rs
similarity index 100%
rename from crates/protocol/src/types/mod.rs
rename to crates/jmap-proto/src/types/mod.rs
diff --git a/crates/protocol/src/types/pointer.rs b/crates/jmap-proto/src/types/pointer.rs
similarity index 100%
rename from crates/protocol/src/types/pointer.rs
rename to crates/jmap-proto/src/types/pointer.rs
diff --git a/crates/protocol/src/types/property.rs b/crates/jmap-proto/src/types/property.rs
similarity index 100%
rename from crates/protocol/src/types/property.rs
rename to crates/jmap-proto/src/types/property.rs
diff --git a/crates/protocol/src/types/state.rs b/crates/jmap-proto/src/types/state.rs
similarity index 100%
rename from crates/protocol/src/types/state.rs
rename to crates/jmap-proto/src/types/state.rs
diff --git a/crates/protocol/src/types/type_state.rs b/crates/jmap-proto/src/types/type_state.rs
similarity index 100%
rename from crates/protocol/src/types/type_state.rs
rename to crates/jmap-proto/src/types/type_state.rs
diff --git a/crates/protocol/src/types/value.rs b/crates/jmap-proto/src/types/value.rs
similarity index 100%
rename from crates/protocol/src/types/value.rs
rename to crates/jmap-proto/src/types/value.rs
diff --git a/crates/core/Cargo.toml b/crates/jmap/Cargo.toml
similarity index 82%
rename from crates/core/Cargo.toml
rename to crates/jmap/Cargo.toml
index fbd9fb85..d7d363c1 100644
--- a/crates/core/Cargo.toml
+++ b/crates/jmap/Cargo.toml
@@ -1,12 +1,12 @@
[package]
-name = "core"
+name = "jmap"
version = "0.1.0"
edition = "2021"
[dependencies]
store = { path = "../store" }
-protocol = { path = "../protocol" }
-utils = { path = "/home/vagrant/code/utils" }
+jmap_proto = { path = "../jmap-proto" }
+utils = { path = "../utils" }
mail-parser = { git = "https://github.com/stalwartlabs/mail-parser", features = ["full_encoding", "serde_support", "ludicrous_mode"] }
mail-builder = { git = "https://github.com/stalwartlabs/mail-builder", features = ["ludicrous_mode"] }
mail-send = { git = "https://github.com/stalwartlabs/mail-send" }
diff --git a/crates/core/src/api/mod.rs b/crates/jmap/src/api/mod.rs
similarity index 100%
rename from crates/core/src/api/mod.rs
rename to crates/jmap/src/api/mod.rs
diff --git a/crates/core/src/api/request.rs b/crates/jmap/src/api/request.rs
similarity index 99%
rename from crates/core/src/api/request.rs
rename to crates/jmap/src/api/request.rs
index dbe420b4..8e28d8fe 100644
--- a/crates/core/src/api/request.rs
+++ b/crates/jmap/src/api/request.rs
@@ -1,4 +1,4 @@
-use protocol::{
+use jmap_proto::{
error::request::RequestError,
method::{get, query},
request::{Request, RequestMethod},
diff --git a/crates/core/src/blob/get.rs b/crates/jmap/src/blob/get.rs
similarity index 97%
rename from crates/core/src/blob/get.rs
rename to crates/jmap/src/blob/get.rs
index be05b74f..eb072839 100644
--- a/crates/core/src/blob/get.rs
+++ b/crates/jmap/src/blob/get.rs
@@ -1,8 +1,8 @@
+use jmap_proto::types::blob::BlobId;
use mail_parser::{
decoders::{base64::base64_decode, quoted_printable::quoted_printable_decode},
Encoding,
};
-use protocol::types::blob::BlobId;
use crate::JMAP;
diff --git a/crates/core/src/blob/mod.rs b/crates/jmap/src/blob/mod.rs
similarity index 100%
rename from crates/core/src/blob/mod.rs
rename to crates/jmap/src/blob/mod.rs
diff --git a/crates/core/src/email/body.rs b/crates/jmap/src/email/body.rs
similarity index 99%
rename from crates/core/src/email/body.rs
rename to crates/jmap/src/email/body.rs
index 05797ae7..e096d07e 100644
--- a/crates/core/src/email/body.rs
+++ b/crates/jmap/src/email/body.rs
@@ -1,8 +1,8 @@
-use mail_parser::{HeaderValue, MessagePart, MimeHeaders, PartType};
-use protocol::{
+use jmap_proto::{
object::Object,
types::{blob::BlobId, property::Property, value::Value},
};
+use mail_parser::{HeaderValue, MessagePart, MimeHeaders, PartType};
use super::headers::HeaderToValue;
diff --git a/crates/core/src/email/get.rs b/crates/jmap/src/email/get.rs
similarity index 99%
rename from crates/core/src/email/get.rs
rename to crates/jmap/src/email/get.rs
index 4e50eddf..ac192d62 100644
--- a/crates/core/src/email/get.rs
+++ b/crates/jmap/src/email/get.rs
@@ -1,10 +1,10 @@
-use mail_parser::Message;
-use protocol::{
+use jmap_proto::{
error::method::MethodError,
method::get::{GetRequest, GetResponse},
object::{email::GetArguments, Object},
types::{blob::BlobId, collection::Collection, property::Property, value::Value},
};
+use mail_parser::Message;
use store::ValueKey;
use crate::{email::headers::HeaderToValue, JMAP};
diff --git a/crates/core/src/email/headers.rs b/crates/jmap/src/email/headers.rs
similarity index 99%
rename from crates/core/src/email/headers.rs
rename to crates/jmap/src/email/headers.rs
index 19997680..03334d4f 100644
--- a/crates/core/src/email/headers.rs
+++ b/crates/jmap/src/email/headers.rs
@@ -1,11 +1,11 @@
-use mail_parser::{parsers::MessageStream, Addr, HeaderName, HeaderValue, MessagePart, RfcHeader};
-use protocol::{
+use jmap_proto::{
object::Object,
types::{
property::{HeaderForm, Property},
value::Value,
},
};
+use mail_parser::{parsers::MessageStream, Addr, HeaderName, HeaderValue, MessagePart, RfcHeader};
pub trait IntoForm {
fn into_form(self, form: &HeaderForm) -> Value;
diff --git a/crates/core/src/email/import.rs b/crates/jmap/src/email/import.rs
similarity index 99%
rename from crates/core/src/email/import.rs
rename to crates/jmap/src/email/import.rs
index 4076137e..2a6e1e34 100644
--- a/crates/core/src/email/import.rs
+++ b/crates/jmap/src/email/import.rs
@@ -1,4 +1,4 @@
-use protocol::{
+use jmap_proto::{
error::{
method::MethodError,
set::{SetError, SetErrorType},
diff --git a/crates/core/src/email/index.rs b/crates/jmap/src/email/index.rs
similarity index 99%
rename from crates/core/src/email/index.rs
rename to crates/jmap/src/email/index.rs
index d8ccfa32..53e87687 100644
--- a/crates/core/src/email/index.rs
+++ b/crates/jmap/src/email/index.rs
@@ -1,11 +1,6 @@
use std::borrow::Cow;
-use mail_parser::{
- decoders::html::html_to_text,
- parsers::{fields::thread::thread_name, preview::preview_text},
- Addr, GetHeader, Group, HeaderName, HeaderValue, Message, MessagePart, PartType, RfcHeader,
-};
-use protocol::{
+use jmap_proto::{
object::Object,
types::{
blob::{BlobId, BlobSection},
@@ -15,6 +10,11 @@ use protocol::{
value::Value,
},
};
+use mail_parser::{
+ decoders::html::html_to_text,
+ parsers::{fields::thread::thread_name, preview::preview_text},
+ Addr, GetHeader, Group, HeaderName, HeaderValue, Message, MessagePart, PartType, RfcHeader,
+};
use store::{
fts::{builder::FtsIndexBuilder, Language},
write::{BatchBuilder, F_BITMAP, F_INDEX, F_VALUE},
diff --git a/crates/core/src/email/ingest.rs b/crates/jmap/src/email/ingest.rs
similarity index 99%
rename from crates/core/src/email/ingest.rs
rename to crates/jmap/src/email/ingest.rs
index c2f20ba7..0dec48a8 100644
--- a/crates/core/src/email/ingest.rs
+++ b/crates/jmap/src/email/ingest.rs
@@ -1,13 +1,13 @@
-use mail_parser::{
- parsers::fields::thread::thread_name, HeaderName, HeaderValue, Message, RfcHeader,
-};
-use protocol::{
+use jmap_proto::{
object::Object,
types::{
blob::BlobId, collection::Collection, id::Id, keyword::Keyword, property::Property,
value::Value,
},
};
+use mail_parser::{
+ parsers::fields::thread::thread_name, HeaderName, HeaderValue, Message, RfcHeader,
+};
use store::{
query::Filter,
write::{log::ChangeLogBuilder, now, BatchBuilder, F_BITMAP, F_CLEAR, F_VALUE},
diff --git a/crates/core/src/email/mod.rs b/crates/jmap/src/email/mod.rs
similarity index 100%
rename from crates/core/src/email/mod.rs
rename to crates/jmap/src/email/mod.rs
diff --git a/crates/core/src/email/query.rs b/crates/jmap/src/email/query.rs
similarity index 99%
rename from crates/core/src/email/query.rs
rename to crates/jmap/src/email/query.rs
index 9524006a..4c0a879b 100644
--- a/crates/core/src/email/query.rs
+++ b/crates/jmap/src/email/query.rs
@@ -1,4 +1,4 @@
-use protocol::{
+use jmap_proto::{
error::method::MethodError,
method::query::{Comparator, Filter, QueryRequest, QueryResponse, SortProperty},
object::email::QueryArguments,
diff --git a/crates/core/src/lib.rs b/crates/jmap/src/lib.rs
similarity index 96%
rename from crates/core/src/lib.rs
rename to crates/jmap/src/lib.rs
index 47ee198a..b01e3b62 100644
--- a/crates/core/src/lib.rs
+++ b/crates/jmap/src/lib.rs
@@ -1,4 +1,4 @@
-use protocol::error::method::MethodError;
+use jmap_proto::error::method::MethodError;
use store::{fts::Language, Store};
pub mod api;
diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml
index 4fc015b0..77c889b3 100644
--- a/crates/store/Cargo.toml
+++ b/crates/store/Cargo.toml
@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
-utils = { path = "/home/vagrant/code/utils" }
+utils = { path = "../utils" }
rocksdb = { version = "0.20.1", optional = true }
foundationdb = { version = "0.7.0", optional = true }
rusqlite = { version = "0.29.0", features = ["bundled"], optional = true }
diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml
new file mode 100644
index 00000000..0518bddd
--- /dev/null
+++ b/crates/utils/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "utils"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+serde = { version = "1.0", features = ["derive"]}
diff --git a/crates/utils/src/codec/base32_custom.rs b/crates/utils/src/codec/base32_custom.rs
new file mode 100644
index 00000000..cbf4fa90
--- /dev/null
+++ b/crates/utils/src/codec/base32_custom.rs
@@ -0,0 +1,215 @@
+/*
+ * Copyright (c) 2020-2022, Stalwart Labs Ltd.
+ *
+ * This file is part of the Stalwart JMAP Server.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * in the LICENSE file at the top-level directory of this distribution.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * You can be released from the requirements of the AGPLv3 license by
+ * purchasing a commercial license. Please contact licensing@stalw.art
+ * for more details.
+*/
+
+use std::slice::Iter;
+
+use super::leb128::{Leb128Iterator, Leb128Writer};
+
+pub static BASE32_ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz792013";
+pub static BASE32_INVERSE: [u8; 256] = [
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 29, 30, 28, 31, 255, 255, 255, 26, 255, 27,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 10, 11,
+ 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255, 255, 0, 1, 2,
+ 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+];
+
+pub struct Base32Writer {
+ last_byte: u8,
+ pos: usize,
+ result: String,
+}
+
+impl Base32Writer {
+ pub fn with_capacity(capacity: usize) -> Self {
+ Base32Writer {
+ result: String::with_capacity((capacity + 3) / 4 * 5),
+ last_byte: 0,
+ pos: 0,
+ }
+ }
+
+ pub fn push_char(&mut self, ch: char) {
+ self.result.push(ch);
+ }
+
+ fn push_byte(&mut self, byte: u8, is_remainder: bool) {
+ let (ch1, ch2) = match self.pos % 5 {
+ 0 => ((byte & 0xF8) >> 3, u8::MAX),
+ 1 => (
+ (((self.last_byte & 0x07) << 2) | ((byte & 0xC0) >> 6)),
+ ((byte & 0x3E) >> 1),
+ ),
+ 2 => (
+ (((self.last_byte & 0x01) << 4) | ((byte & 0xF0) >> 4)),
+ u8::MAX,
+ ),
+ 3 => (
+ (((self.last_byte & 0x0F) << 1) | (byte >> 7)),
+ ((byte & 0x7C) >> 2),
+ ),
+ 4 => (
+ (((self.last_byte & 0x03) << 3) | ((byte & 0xE0) >> 5)),
+ (byte & 0x1F),
+ ),
+ _ => unreachable!(),
+ };
+
+ self.result.push(char::from(BASE32_ALPHABET[ch1 as usize]));
+ if !is_remainder {
+ if ch2 != u8::MAX {
+ self.result.push(char::from(BASE32_ALPHABET[ch2 as usize]));
+ }
+ self.last_byte = byte;
+ self.pos += 1;
+ }
+ }
+
+ pub fn finalize(mut self) -> String {
+ if self.pos % 5 != 0 {
+ self.push_byte(0, true);
+ }
+
+ self.result
+ }
+}
+
+impl std::io::Write for Base32Writer {
+ fn write(&mut self, bytes: &[u8]) -> std::io::Result {
+ let start_pos = self.pos;
+
+ for &byte in bytes {
+ self.push_byte(byte, false);
+ }
+
+ Ok(self.pos - start_pos)
+ }
+
+ fn flush(&mut self) -> std::io::Result<()> {
+ Ok(())
+ }
+}
+
+#[derive(Debug)]
+pub struct Base32Reader<'x> {
+ bytes: Iter<'x, u8>,
+ last_byte: u8,
+ pos: usize,
+}
+
+impl<'x> Base32Reader<'x> {
+ pub fn new(bytes: &'x [u8]) -> Self {
+ Base32Reader {
+ bytes: bytes.iter(),
+ pos: 0,
+ last_byte: 0,
+ }
+ }
+
+ #[inline(always)]
+ fn map_byte(&mut self) -> Option {
+ match self.bytes.next() {
+ Some(&byte) => match BASE32_INVERSE[byte as usize] {
+ byte if byte != u8::MAX => {
+ self.last_byte = byte;
+ Some(byte)
+ }
+ _ => None,
+ },
+ _ => None,
+ }
+ }
+}
+
+impl Iterator for Base32Reader<'_> {
+ type Item = u8;
+ fn next(&mut self) -> Option {
+ let pos = self.pos % 5;
+ let last_byte = self.last_byte;
+ let byte = self.map_byte()?;
+ self.pos += 1;
+
+ match pos {
+ 0 => ((byte << 3) | (self.map_byte().unwrap_or(0) >> 2)).into(),
+ 1 => ((last_byte << 6) | (byte << 1) | (self.map_byte().unwrap_or(0) >> 4)).into(),
+ 2 => ((last_byte << 4) | (byte >> 1)).into(),
+ 3 => ((last_byte << 7) | (byte << 2) | (self.map_byte().unwrap_or(0) >> 3)).into(),
+ 4 => ((last_byte << 5) | byte).into(),
+ _ => None,
+ }
+ }
+}
+
+impl Leb128Iterator for Base32Reader<'_> {}
+impl Leb128Writer for Base32Writer {}
+
+#[cfg(test)]
+mod tests {
+ use std::io::Write;
+
+ use crate::codec::base32_custom::{Base32Reader, Base32Writer};
+
+ #[test]
+ fn base32_roundtrip() {
+ let mut bytes = Vec::with_capacity(100);
+ for byte in 0..100 {
+ bytes.push((100 - byte) as u8);
+ let mut writer = Base32Writer::with_capacity(10);
+ writer.write_all(&bytes).unwrap();
+ let result = writer.finalize();
+
+ let mut bytes_result = Vec::new();
+ for byte in Base32Reader::new(result.as_bytes()) {
+ bytes_result.push(byte);
+ }
+
+ assert_eq!(bytes, bytes_result);
+ }
+
+ for bytes in [
+ vec![0],
+ vec![32, 43, 55, 99, 43, 55],
+ vec![84, 4, 43, 77, 62, 55, 92],
+ vec![84, 4, 43, 77, 62, 55, 92],
+ ] {
+ let mut writer = Base32Writer::with_capacity(10);
+ writer.write_all(&bytes).unwrap();
+ let result = writer.finalize();
+
+ let mut bytes_result = Vec::new();
+ for byte in Base32Reader::new(result.as_bytes()) {
+ bytes_result.push(byte);
+ }
+
+ assert_eq!(bytes, bytes_result);
+ }
+ }
+}
diff --git a/crates/utils/src/codec/leb128.rs b/crates/utils/src/codec/leb128.rs
new file mode 100644
index 00000000..9d5b2604
--- /dev/null
+++ b/crates/utils/src/codec/leb128.rs
@@ -0,0 +1,180 @@
+/*
+ * Copyright (c) 2020-2022, Stalwart Labs Ltd.
+ *
+ * This file is part of the Stalwart JMAP Server.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * in the LICENSE file at the top-level directory of this distribution.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * You can be released from the requirements of the AGPLv3 license by
+ * purchasing a commercial license. Please contact licensing@stalw.art
+ * for more details.
+*/
+
+#![allow(dead_code)]
+
+use std::{borrow::Borrow, io::Write};
+
+pub trait Leb128_ {
+ fn to_leb128_writer(self, out: &mut impl Write) -> std::io::Result;
+ fn to_leb128_bytes(self, out: &mut Vec);
+ fn from_leb128_bytes(slice: &[u8]) -> Option<(Self, usize)>
+ where
+ Self: std::marker::Sized;
+ fn from_leb128_it(it: T) -> Option
+ where
+ Self: std::marker::Sized,
+ T: Iterator- ,
+ I: Borrow;
+}
+
+pub trait Leb128Vec {
+ fn push_leb128(&mut self, value: T);
+}
+
+pub trait Leb128Writer: Write + Sized {
+ #[inline(always)]
+ fn write_leb128(&mut self, value: T) -> std::io::Result {
+ T::to_leb128_writer(value, self)
+ }
+}
+
+impl Leb128Vec for Vec {
+ #[inline(always)]
+ fn push_leb128(&mut self, value: T) {
+ T::to_leb128_bytes(value, self);
+ }
+}
+
+pub trait Leb128Iterator: Iterator
-
+where
+ I: Borrow,
+{
+ #[inline(always)]
+ fn next_leb128(&mut self) -> Option {
+ T::from_leb128_it(self)
+ }
+
+ #[inline(always)]
+ fn skip_leb128(&mut self) -> Option<()> {
+ for byte in self {
+ if (byte.borrow() & 0x80) == 0 {
+ return Some(());
+ }
+ }
+ None
+ }
+}
+
+pub trait Leb128Reader: AsRef<[u8]> {
+ #[inline(always)]
+ fn read_leb128(&self) -> Option<(T, usize)> {
+ T::from_leb128_bytes(self.as_ref())
+ }
+
+ #[inline(always)]
+ fn skip_leb128(&self) -> Option {
+ for (pos, byte) in self.as_ref().iter().enumerate() {
+ if (byte & 0x80) == 0 {
+ return (pos + 1).into();
+ }
+ }
+ None
+ }
+}
+
+impl Leb128Reader for &[u8] {}
+impl Leb128Reader for Vec {}
+impl Leb128Reader for Box<[u8]> {}
+impl<'x> Leb128Iterator<&'x u8> for std::slice::Iter<'x, u8> {}
+
+// Based on leb128.rs from rustc
+macro_rules! impl_unsigned_leb128 {
+ ($int_ty:ident, $shifts:expr) => {
+ impl Leb128_ for $int_ty {
+ #[inline(always)]
+ fn to_leb128_writer(self, out: &mut impl Write) -> std::io::Result {
+ let mut value = self;
+ let mut bytes_written = 0;
+ loop {
+ if value < 0x80 {
+ bytes_written += out.write(&[value as u8])?;
+ break;
+ } else {
+ bytes_written += out.write(&[((value & 0x7f) | 0x80) as u8])?;
+ value >>= 7;
+ }
+ }
+ Ok(bytes_written)
+ }
+
+ #[inline(always)]
+ fn to_leb128_bytes(self, out: &mut Vec) {
+ let mut value = self;
+ loop {
+ if value < 0x80 {
+ out.push(value as u8);
+ break;
+ } else {
+ out.push(((value & 0x7f) | 0x80) as u8);
+ value >>= 7;
+ }
+ }
+ }
+
+ #[inline(always)]
+ fn from_leb128_bytes(slice: &[u8]) -> Option<($int_ty, usize)> {
+ let mut result = 0;
+
+ for (shift, (pos, &byte)) in $shifts.into_iter().zip(slice.iter().enumerate()) {
+ if (byte & 0x80) == 0 {
+ result |= (byte as $int_ty) << shift;
+ return Some((result, pos + 1));
+ } else {
+ result |= ((byte & 0x7F) as $int_ty) << shift;
+ }
+ }
+
+ None
+ }
+
+ #[inline(always)]
+ fn from_leb128_it(it: T) -> Option<$int_ty>
+ where
+ T: Iterator
- ,
+ I: Borrow,
+ {
+ let mut result = 0;
+
+ for (shift, byte_) in $shifts.into_iter().zip(it) {
+ let byte = byte_.borrow();
+
+ if (byte & 0x80) == 0 {
+ result |= (*byte as $int_ty) << shift;
+ return Some(result);
+ } else {
+ result |= ((byte & 0x7F) as $int_ty) << shift;
+ }
+ }
+
+ None
+ }
+ }
+ };
+}
+
+impl_unsigned_leb128!(u8, [0]);
+impl_unsigned_leb128!(u16, [0, 7, 14]);
+impl_unsigned_leb128!(u32, [0, 7, 14, 21, 28]);
+impl_unsigned_leb128!(u64, [0, 7, 14, 21, 28, 35, 42, 49, 56, 63]);
+impl_unsigned_leb128!(usize, [0, 7, 14, 21, 28, 35, 42, 49, 56, 63]);
diff --git a/crates/utils/src/codec/mod.rs b/crates/utils/src/codec/mod.rs
new file mode 100644
index 00000000..095dfa5d
--- /dev/null
+++ b/crates/utils/src/codec/mod.rs
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2023 Stalwart Labs Ltd.
+ *
+ * This file is part of the Stalwart SMTP Server.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * in the LICENSE file at the top-level directory of this distribution.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * You can be released from the requirements of the AGPLv3 license by
+ * purchasing a commercial license. Please contact licensing@stalw.art
+ * for more details.
+*/
+
+pub mod base32_custom;
+pub mod leb128;
diff --git a/crates/utils/src/config/mod.rs b/crates/utils/src/config/mod.rs
new file mode 100644
index 00000000..b4af37ba
--- /dev/null
+++ b/crates/utils/src/config/mod.rs
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2023 Stalwart Labs Ltd.
+ *
+ * This file is part of the Stalwart SMTP Server.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * in the LICENSE file at the top-level directory of this distribution.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * You can be released from the requirements of the AGPLv3 license by
+ * purchasing a commercial license. Please contact licensing@stalw.art
+ * for more details.
+*/
+
+pub mod parser;
+pub mod utils;
+
+use std::collections::BTreeMap;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Config {
+ keys: BTreeMap,
+}
+
+pub type Result = std::result::Result;
diff --git a/crates/utils/src/config/parser.rs b/crates/utils/src/config/parser.rs
new file mode 100644
index 00000000..cfc53bcb
--- /dev/null
+++ b/crates/utils/src/config/parser.rs
@@ -0,0 +1,545 @@
+/*
+ * Copyright (c) 2023 Stalwart Labs Ltd.
+ *
+ * This file is part of the Stalwart SMTP Server.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * in the LICENSE file at the top-level directory of this distribution.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * You can be released from the requirements of the AGPLv3 license by
+ * purchasing a commercial license. Please contact licensing@stalw.art
+ * for more details.
+*/
+
+use std::{
+ collections::{btree_map::Entry, BTreeMap},
+ iter::Peekable,
+ str::Chars,
+};
+
+use super::{Config, Result};
+use std::fmt::Write;
+
+const MAX_NEST_LEVEL: usize = 10;
+
+// Simple TOML parser for Stalwart Mail Server configuration files.
+
+impl Config {
+ pub fn parse(toml: &str) -> Result {
+ let mut parser = TomlParser::new(toml);
+ let mut table_name = String::new();
+ let mut last_array_name = String::new();
+ let mut last_array_pos = 0;
+
+ while parser.seek_next_char() {
+ match parser.peek_char()? {
+ '[' => {
+ parser.next_char(true, false)?;
+ table_name.clear();
+ let mut is_array = match parser.next_char(true, false)? {
+ '[' => true,
+ ch => {
+ table_name.push(ch);
+ false
+ }
+ };
+ let mut in_quote = false;
+ let mut last_ch = char::from(0);
+ loop {
+ let ch = parser.next_char(!in_quote, false)?;
+ match ch {
+ '\"' if !in_quote || last_ch != '\\' => {
+ in_quote = !in_quote;
+ }
+ '\\' if in_quote => (),
+ ']' if !in_quote => {
+ if table_name.is_empty() {
+ return Err(format!(
+ "Empty table name at line {}.",
+ parser.line
+ ));
+ }
+ if is_array {
+ if table_name == last_array_name {
+ last_array_pos += 1;
+ } else {
+ last_array_pos = 0;
+ last_array_name = table_name.to_string();
+ }
+ is_array = false;
+ write!(table_name, ".{last_array_pos}").ok();
+ } else {
+ break;
+ }
+ }
+ _ => {
+ if !in_quote {
+ if ch.is_alphanumeric() || ['.', '-', '_'].contains(&ch) {
+ table_name.push(ch.to_ascii_lowercase());
+ } else {
+ return Err(format!(
+ "Unexpected character {:?} at line {}.",
+ ch, parser.line
+ ));
+ }
+ } else {
+ table_name.push(ch);
+ }
+ }
+ }
+ last_ch = ch;
+ }
+ parser.skip_line();
+ }
+ 'a'..='z' | 'A'..='Z' | '0'..='9' | '\"' => {
+ let key = parser.key(if !table_name.is_empty() {
+ format!("{table_name}.")
+ } else {
+ String::with_capacity(10)
+ })?;
+ parser.value(key, &['\n'], 0)?;
+ }
+ '#' => {
+ parser.skip_line();
+ }
+ ch => {
+ let ch = *ch;
+ return Err(format!(
+ "Unexpected character {:?} at line {}.",
+ ch, parser.line
+ ));
+ }
+ }
+ }
+
+ Ok(Self { keys: parser.keys })
+ }
+}
+
+struct TomlParser<'x> {
+ keys: BTreeMap,
+ iter: Peekable>,
+ line: usize,
+}
+
+impl<'x> TomlParser<'x> {
+ fn new(toml: &'x str) -> Self {
+ Self {
+ keys: BTreeMap::new(),
+ iter: toml.chars().peekable(),
+ line: 1,
+ }
+ }
+
+ fn seek_next_char(&mut self) -> bool {
+ while let Some(ch) = self.iter.peek() {
+ match ch {
+ '\n' => {
+ self.iter.next();
+ self.line += 1;
+ }
+ '\r' | ' ' | '\t' => {
+ self.iter.next();
+ }
+ '#' => {
+ self.skip_line();
+ }
+ _ => {
+ return true;
+ }
+ }
+ }
+
+ false
+ }
+
+ fn peek_char(&mut self) -> Result<&char> {
+ self.iter.peek().ok_or_else(|| "".to_string())
+ }
+
+ fn next_char(&mut self, skip_wsp: bool, allow_lf: bool) -> Result {
+ for ch in &mut self.iter {
+ match ch {
+ '\r' => (),
+ ' ' | '\t' if skip_wsp => (),
+ '\n' => {
+ return if allow_lf {
+ self.line += 1;
+ Ok(ch)
+ } else {
+ Err(format!("Unexpected end of line at line: {}", self.line))
+ };
+ }
+ _ => {
+ return Ok(ch);
+ }
+ }
+ }
+ Err(format!("Unexpected EOF at line: {}", self.line))
+ }
+
+ fn skip_line(&mut self) {
+ for ch in &mut self.iter {
+ if ch == '\n' {
+ self.line += 1;
+ break;
+ }
+ }
+ }
+
+ #[allow(clippy::while_let_on_iterator)]
+ fn key(&mut self, mut key: String) -> Result {
+ while let Some(ch) = self.iter.next() {
+ match ch {
+ '=' => {
+ if !key.is_empty() {
+ return Ok(key);
+ } else {
+ return Err(format!("Empty key at line: {}", self.line));
+ }
+ }
+ 'a'..='z' | '.' | 'A'..='Z' | '0'..='9' | '_' | '-' => {
+ key.push(ch);
+ }
+ '\"' => {
+ let mut last_ch = char::from(0);
+ while let Some(ch) = self.iter.next() {
+ match ch {
+ '\\' => (),
+ '\"' if last_ch != '\\' => {
+ break;
+ }
+ '\n' => {
+ return Err(format!(
+ "Unexpected end of line at line: {}",
+ self.line
+ ));
+ }
+ _ => {
+ key.push(ch);
+ }
+ }
+ last_ch = ch;
+ }
+ }
+ ' ' | '\t' | '\r' => (),
+ '\n' => {
+ return Err(format!("Unexpected end of line at line: {}", self.line));
+ }
+ _ => {
+ return Err(format!(
+ "Unexpected character {:?} found in key at line {}.",
+ ch, self.line
+ ));
+ }
+ }
+ }
+ Err(format!("Unexpected EOF at line: {}", self.line))
+ }
+
+ fn value(&mut self, key: String, stop_chars: &[char], nest_level: usize) -> Result {
+ if nest_level == MAX_NEST_LEVEL {
+ return Err(format!("Too many nested structures at line {}.", self.line));
+ }
+ match self.next_char(true, false)? {
+ '[' => {
+ let mut array_pos = 0;
+ self.seek_next_char();
+ loop {
+ match self.value(format!("{key}.{array_pos}"), &[',', ']'], nest_level + 1)? {
+ ',' => {
+ self.seek_next_char();
+ array_pos += 1;
+ }
+ ']' => break,
+ ch => {
+ return Err(format!(
+ "Unexpected character {:?} found in array for property {:?} at line {}.",
+ ch, key, self.line
+ ));
+ }
+ }
+ }
+ }
+ '{' => loop {
+ let sub_key = self.key(format!("{key}."))?;
+ self.seek_next_char();
+
+ match self.value(sub_key, &[',', '}'], nest_level + 1)? {
+ ',' => {
+ self.seek_next_char();
+ }
+ '}' => break,
+ ch => {
+ return Err(format!(
+ "Unexpected character {:?} found in inline table for property {:?} at line {}.",
+ ch, key, self.line
+ ));
+ }
+ }
+ },
+ qch @ ('\'' | '\"') => {
+ let mut value = String::new();
+ if matches!(self.iter.peek(), Some(ch) if ch == &qch) {
+ self.iter.next();
+ if matches!(self.iter.peek(), Some(ch) if ch == &qch) {
+ self.iter.next();
+ if matches!(self.iter.peek(), Some(ch) if ch == &'\n') {
+ self.iter.next();
+ self.line += 1;
+ }
+
+ let mut last_ch = char::from(0);
+ let mut prev_last_ch = char::from(0);
+ loop {
+ let ch = self.next_char(false, true)?;
+ if !(ch == qch && last_ch == qch && prev_last_ch == qch) {
+ value.push(ch);
+ prev_last_ch = last_ch;
+ last_ch = ch;
+ } else {
+ value.truncate(value.len() - 2);
+ break;
+ }
+ }
+ }
+ } else {
+ let mut last_ch = char::from(0);
+
+ loop {
+ let ch = self.next_char(false, true)?;
+ match ch {
+ '\\' if last_ch != '\\' => (),
+ 't' if last_ch == '\\' => {
+ value.push('\t');
+ }
+ 'r' if last_ch == '\\' => {
+ value.push('\r');
+ }
+ 'n' if last_ch == '\\' => {
+ value.push('\n');
+ }
+ ch => {
+ if ch != qch || last_ch == '\\' {
+ value.push(ch);
+ } else {
+ break;
+ }
+ }
+ }
+ last_ch = ch;
+ }
+ }
+ match self.keys.entry(key) {
+ Entry::Vacant(e) => {
+ value.shrink_to_fit();
+ e.insert(value);
+ }
+ Entry::Occupied(e) => {
+ return Err(format!(
+ "Duplicate key {:?} at line {}.",
+ e.key(),
+ self.line
+ ));
+ }
+ }
+ }
+ ch if ch.is_alphanumeric() || ['.', '+', '-'].contains(&ch) => {
+ let mut value = String::with_capacity(4);
+ value.push(ch);
+ while let Some(ch) = self.iter.peek() {
+ if ch.is_alphanumeric() || ['.', '+', '-'].contains(ch) {
+ value.push(self.next_char(true, false)?);
+ } else {
+ break;
+ }
+ }
+ match self.keys.entry(key) {
+ Entry::Vacant(e) => {
+ value.shrink_to_fit();
+ e.insert(value);
+ }
+ Entry::Occupied(e) => {
+ return Err(format!(
+ "Duplicate key {:?} at line {}.",
+ e.key(),
+ self.line
+ ));
+ }
+ }
+ }
+ ch => {
+ return if stop_chars.contains(&ch) {
+ Ok(ch)
+ } else {
+ Err(format!(
+ "Expected {:?} but found {:?} in value at line {}.",
+ stop_chars, ch, self.line
+ ))
+ }
+ }
+ }
+
+ loop {
+ match self.next_char(true, true)? {
+ '#' => {
+ self.skip_line();
+ if stop_chars.contains(&'\n') {
+ return Ok('\n');
+ }
+ }
+ ch if stop_chars.contains(&ch) => {
+ return Ok(ch);
+ }
+ '\n' if !stop_chars.contains(&'\n') => (),
+ ch => {
+ return Err(format!(
+ "Expected {:?} but found {:?} in value at line {}.",
+ stop_chars, ch, self.line
+ ));
+ }
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::{collections::BTreeMap, fs, path::PathBuf};
+
+ use crate::config::Config;
+
+ #[test]
+ fn toml_parse() {
+ let mut file = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ file.push("resources");
+ file.push("tests");
+ file.push("config");
+ file.push("toml-parser.toml");
+
+ let config = Config::parse(&fs::read_to_string(file).unwrap()).unwrap();
+ assert_eq!(
+ config.keys,
+ BTreeMap::from_iter([
+ ("arrays.colors.0".to_string(), "red".to_string()),
+ ("arrays.colors.1".to_string(), "yellow".to_string()),
+ ("arrays.colors.2".to_string(), "green".to_string()),
+ (
+ "arrays.contributors.0".to_string(),
+ "Foo Bar ".to_string()
+ ),
+ (
+ "arrays.contributors.1.email".to_string(),
+ "bazqux@example.com".to_string()
+ ),
+ (
+ "arrays.contributors.1.name".to_string(),
+ "Baz Qux".to_string()
+ ),
+ (
+ "arrays.contributors.1.url".to_string(),
+ "https://example.com/bazqux".to_string()
+ ),
+ ("arrays.integers.0".to_string(), "1".to_string()),
+ ("arrays.integers.1".to_string(), "2".to_string()),
+ ("arrays.integers.2".to_string(), "3".to_string()),
+ ("arrays.integers2.0".to_string(), "1".to_string()),
+ ("arrays.integers2.1".to_string(), "2".to_string()),
+ ("arrays.integers2.2".to_string(), "3".to_string()),
+ ("arrays.integers3.0".to_string(), "4".to_string()),
+ ("arrays.integers3.1".to_string(), "5".to_string()),
+ (
+ "arrays.nested_arrays_of_ints.0.0".to_string(),
+ "1".to_string()
+ ),
+ (
+ "arrays.nested_arrays_of_ints.0.1".to_string(),
+ "2".to_string()
+ ),
+ (
+ "arrays.nested_arrays_of_ints.1.0".to_string(),
+ "3".to_string()
+ ),
+ (
+ "arrays.nested_arrays_of_ints.1.1".to_string(),
+ "4".to_string()
+ ),
+ (
+ "arrays.nested_arrays_of_ints.1.2".to_string(),
+ "5".to_string()
+ ),
+ ("arrays.nested_mixed_array.0.0".to_string(), "1".to_string()),
+ ("arrays.nested_mixed_array.0.1".to_string(), "2".to_string()),
+ ("arrays.nested_mixed_array.1.0".to_string(), "a".to_string()),
+ ("arrays.nested_mixed_array.1.1".to_string(), "b".to_string()),
+ ("arrays.nested_mixed_array.1.2".to_string(), "c".to_string()),
+ ("arrays.numbers.0".to_string(), "0.1".to_string()),
+ ("arrays.numbers.1".to_string(), "0.2".to_string()),
+ ("arrays.numbers.2".to_string(), "0.5".to_string()),
+ ("arrays.numbers.3".to_string(), "1".to_string()),
+ ("arrays.numbers.4".to_string(), "2".to_string()),
+ ("arrays.numbers.5".to_string(), "5".to_string()),
+ ("arrays.string_array.0".to_string(), "all".to_string()),
+ ("arrays.string_array.1".to_string(), "strings".to_string()),
+ (
+ "arrays.string_array.2".to_string(),
+ "are the same".to_string()
+ ),
+ ("arrays.string_array.3".to_string(), "type".to_string()),
+ ("database.data.0.0".to_string(), "delta".to_string()),
+ ("database.data.0.1".to_string(), "phi".to_string()),
+ ("database.data.1.0".to_string(), "3.14".to_string()),
+ ("database.enabled".to_string(), "true".to_string()),
+ ("database.ports.0".to_string(), "8000".to_string()),
+ ("database.ports.1".to_string(), "8001".to_string()),
+ ("database.ports.2".to_string(), "8002".to_string()),
+ ("database.temp_targets.case".to_string(), "72.0".to_string()),
+ ("database.temp_targets.cpu".to_string(), "79.5".to_string()),
+ ("products.0.name".to_string(), "Hammer".to_string()),
+ ("products.0.sku".to_string(), "738594937".to_string()),
+ ("products.2.color".to_string(), "gray".to_string()),
+ ("products.2.name".to_string(), "Nail".to_string()),
+ ("products.2.sku".to_string(), "284758393".to_string()),
+ ("servers.127.0.0.1".to_string(), "value".to_string()),
+ ("servers.alpha.ip".to_string(), "10.0.0.1".to_string()),
+ ("servers.alpha.role".to_string(), "frontend".to_string()),
+ ("servers.beta.ip".to_string(), "10.0.0.2".to_string()),
+ ("servers.beta.role".to_string(), "backend".to_string()),
+ (
+ "servers.character encoding".to_string(),
+ "value".to_string()
+ ),
+ (
+ "strings.my \"string\" test.lines".to_string(),
+ concat!(
+ "The first newline is\ntrimmed in raw strings.\n",
+ "All other whitespace\nis preserved.\n"
+ )
+ .to_string()
+ ),
+ (
+ "strings.my \"string\" test.str1".to_string(),
+ "I'm a string.".to_string()
+ ),
+ (
+ "strings.my \"string\" test.str2".to_string(),
+ "You can \"quote\" me.".to_string()
+ ),
+ (
+ "strings.my \"string\" test.str3".to_string(),
+ "Name\tTabs\nNew Line.".to_string()
+ ),
+ ])
+ );
+ }
+}
diff --git a/crates/utils/src/config/utils.rs b/crates/utils/src/config/utils.rs
new file mode 100644
index 00000000..4f662804
--- /dev/null
+++ b/crates/utils/src/config/utils.rs
@@ -0,0 +1,499 @@
+/*
+ * Copyright (c) 2023 Stalwart Labs Ltd.
+ *
+ * This file is part of the Stalwart SMTP Server.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ * in the LICENSE file at the top-level directory of this distribution.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * You can be released from the requirements of the AGPLv3 license by
+ * purchasing a commercial license. Please contact licensing@stalw.art
+ * for more details.
+*/
+
+use std::{net::IpAddr, time::Duration};
+
+use super::Config;
+
+impl Config {
+ pub fn property(&self, key: impl AsKey) -> super::Result