diff --git a/Cargo.lock b/Cargo.lock index 33f07c6..f620d61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -390,16 +390,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -595,21 +585,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -627,9 +602,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -637,21 +612,21 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -660,21 +635,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-io", @@ -683,7 +658,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1154,12 +1128,9 @@ dependencies = [ "email-encoding", "email_address", "fastrand", - "futures-util", - "hostname", "httpdate", "idna", "mime", - "native-tls", "nom 8.0.0", "percent-encoding", "quoted_printable", @@ -1275,23 +1246,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "native-tls" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5d26952a508f321b4d3d2e80e78fc2603eaefcdf0c30783867f19586518bdc" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nom" version = "7.1.3" @@ -1356,50 +1310,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "openssl-sys" -version = "0.9.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "option-ext" version = "0.2.0" @@ -1785,38 +1695,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "security-framework" -version = "3.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" version = "1.0.27" diff --git a/Cargo.toml b/Cargo.toml index 8bdc4a7..40d54a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,7 @@ console = "0.15" tokio-tungstenite = { version = "0.24", features = ["rustls-tls-webpki-roots"] } futures-util = { version = "0.3", default-features = false, features = ["sink"] } hostname = "0.4.2" -lettre = { version = "0.11.19", features = ["smtp-transport", "rustls-tls"] } +lettre = { version = "0.11.19", default-features = false, features = ["builder", "smtp-transport", "rustls-tls"] } mail-parser = "0.11.2" rustls-pki-types = "1.14.0" tokio-rustls = "0.26.4" diff --git a/Dockerfile b/Dockerfile index 0975ee8..e9d3497 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # ── Stage 1: Build ──────────────────────────────────────────── -FROM rust:1.83-slim AS builder +FROM rust:1.93-slim-bookworm AS builder WORKDIR /app COPY Cargo.toml Cargo.lock ./ @@ -8,8 +8,8 @@ COPY src/ src/ RUN cargo build --release --locked && \ strip target/release/zeroclaw -# ── Stage 2: Runtime (distroless nonroot — no shell, no OS, tiny, UID 65534) ── -FROM gcr.io/distroless/cc-debian12:nonroot +# ── Stage 2: Runtime (distroless, runs as root for /data write access) ── +FROM gcr.io/distroless/cc-debian12 COPY --from=builder /app/target/release/zeroclaw /usr/local/bin/zeroclaw @@ -32,9 +32,6 @@ ENV ZEROCLAW_WORKSPACE=/data/workspace # Example: # docker run -e API_KEY=sk-... -e PROVIDER=openrouter zeroclaw/zeroclaw -# Explicitly set non-root user (distroless:nonroot defaults to 65534, but be explicit) -USER 65534:65534 - EXPOSE 3000 ENTRYPOINT ["zeroclaw"] diff --git a/src/channels/mod.rs b/src/channels/mod.rs index 061aa22..6b2b876 100644 --- a/src/channels/mod.rs +++ b/src/channels/mod.rs @@ -11,6 +11,7 @@ pub mod whatsapp; pub use cli::CliChannel; pub use discord::DiscordChannel; +pub use email_channel::EmailChannel; pub use imessage::IMessageChannel; pub use irc::IrcChannel; pub use matrix::MatrixChannel; @@ -256,6 +257,7 @@ pub fn handle_command(command: crate::ChannelCommands, config: &Config) -> Resul ("iMessage", config.channels_config.imessage.is_some()), ("Matrix", config.channels_config.matrix.is_some()), ("WhatsApp", config.channels_config.whatsapp.is_some()), + ("Email", config.channels_config.email.is_some()), ("IRC", config.channels_config.irc.is_some()), ] { println!(" {} {name}", if configured { "✅" } else { "❌" }); @@ -363,6 +365,10 @@ pub async fn doctor_channels(config: Config) -> Result<()> { )); } + if let Some(ref email_cfg) = config.channels_config.email { + channels.push(("Email", Arc::new(EmailChannel::new(email_cfg.clone())))); + } + if let Some(ref irc) = config.channels_config.irc { channels.push(( "IRC", @@ -548,6 +554,10 @@ pub async fn start_channels(config: Config) -> Result<()> { ))); } + if let Some(ref email_cfg) = config.channels_config.email { + channels.push(Arc::new(EmailChannel::new(email_cfg.clone()))); + } + if let Some(ref irc) = config.channels_config.irc { channels.push(Arc::new(IrcChannel::new( irc.server.clone(), diff --git a/src/config/schema.rs b/src/config/schema.rs index c6b02d2..e93eda4 100644 --- a/src/config/schema.rs +++ b/src/config/schema.rs @@ -541,6 +541,7 @@ pub struct ChannelsConfig { pub imessage: Option, pub matrix: Option, pub whatsapp: Option, + pub email: Option, pub irc: Option, } @@ -555,6 +556,7 @@ impl Default for ChannelsConfig { imessage: None, matrix: None, whatsapp: None, + email: None, irc: None, } } @@ -889,6 +891,7 @@ mod tests { imessage: None, matrix: None, whatsapp: None, + email: None, irc: None, }, memory: MemoryConfig::default(), @@ -1102,6 +1105,7 @@ default_temperature = 0.7 allowed_users: vec!["@u:m".into()], }), whatsapp: None, + email: None, irc: None, }; let toml_str = toml::to_string_pretty(&c).unwrap(); @@ -1259,6 +1263,7 @@ channel_id = "C123" app_secret: None, allowed_numbers: vec!["+1".into()], }), + email: None, irc: None, }; let toml_str = toml::to_string_pretty(&c).unwrap(); diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index 2845a17..af3b861 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -210,6 +210,8 @@ fn has_supervised_channels(config: &Config) -> bool { || config.channels_config.slack.is_some() || config.channels_config.imessage.is_some() || config.channels_config.matrix.is_some() + || config.channels_config.whatsapp.is_some() + || config.channels_config.email.is_some() } #[cfg(test)] diff --git a/src/onboard/wizard.rs b/src/onboard/wizard.rs index d4e0b04..41831c2 100644 --- a/src/onboard/wizard.rs +++ b/src/onboard/wizard.rs @@ -129,7 +129,8 @@ pub fn run_wizard() -> Result { || config.channels_config.discord.is_some() || config.channels_config.slack.is_some() || config.channels_config.imessage.is_some() - || config.channels_config.matrix.is_some(); + || config.channels_config.matrix.is_some() + || config.channels_config.email.is_some(); if has_channels && config.api_key.is_some() { let launch: bool = Confirm::new() @@ -184,7 +185,8 @@ pub fn run_channels_repair_wizard() -> Result { || config.channels_config.discord.is_some() || config.channels_config.slack.is_some() || config.channels_config.imessage.is_some() - || config.channels_config.matrix.is_some(); + || config.channels_config.matrix.is_some() + || config.channels_config.email.is_some(); if has_channels && config.api_key.is_some() { let launch: bool = Confirm::new() @@ -1114,6 +1116,7 @@ fn setup_channels() -> Result { imessage: None, matrix: None, whatsapp: None, + email: None, irc: None, }; @@ -1891,6 +1894,9 @@ fn setup_channels() -> Result { if config.whatsapp.is_some() { active.push("WhatsApp"); } + if config.email.is_some() { + active.push("Email"); + } if config.irc.is_some() { active.push("IRC"); } @@ -2346,7 +2352,8 @@ fn print_summary(config: &Config) { || config.channels_config.discord.is_some() || config.channels_config.slack.is_some() || config.channels_config.imessage.is_some() - || config.channels_config.matrix.is_some(); + || config.channels_config.matrix.is_some() + || config.channels_config.email.is_some(); println!(); println!( @@ -2408,6 +2415,9 @@ fn print_summary(config: &Config) { if config.channels_config.matrix.is_some() { channels.push("Matrix"); } + if config.channels_config.email.is_some() { + channels.push("Email"); + } if config.channels_config.webhook.is_some() { channels.push("Webhook"); } diff --git a/src/providers/anthropic.rs b/src/providers/anthropic.rs index d9da513..c81bac0 100644 --- a/src/providers/anthropic.rs +++ b/src/providers/anthropic.rs @@ -50,10 +50,7 @@ impl AnthropicProvider { .map(str::trim) .filter(|k| !k.is_empty()) .map(ToString::to_string), -<<<<<<< HEAD -======= base_url, ->>>>>>> origin/main client: Client::builder() .timeout(std::time::Duration::from_secs(120)) .connect_timeout(std::time::Duration::from_secs(10)) @@ -95,11 +92,7 @@ impl Provider for AnthropicProvider { let mut request = self .client -<<<<<<< HEAD - .post("https://api.anthropic.com/v1/messages") -======= .post(format!("{}/v1/messages", self.base_url)) ->>>>>>> origin/main .header("anthropic-version", "2023-06-01") .header("content-type", "application/json") .json(&request); @@ -136,20 +129,14 @@ mod tests { let p = AnthropicProvider::new(Some("sk-ant-test123")); assert!(p.credential.is_some()); assert_eq!(p.credential.as_deref(), Some("sk-ant-test123")); -<<<<<<< HEAD -======= assert_eq!(p.base_url, "https://api.anthropic.com"); ->>>>>>> origin/main } #[test] fn creates_without_key() { let p = AnthropicProvider::new(None); assert!(p.credential.is_none()); -<<<<<<< HEAD -======= assert_eq!(p.base_url, "https://api.anthropic.com"); ->>>>>>> origin/main } #[test] @@ -163,8 +150,6 @@ mod tests { let p = AnthropicProvider::new(Some(" sk-ant-test123 ")); assert!(p.credential.is_some()); assert_eq!(p.credential.as_deref(), Some("sk-ant-test123")); -<<<<<<< HEAD -======= } #[test] @@ -184,7 +169,6 @@ mod tests { fn default_base_url_when_none_provided() { let p = AnthropicProvider::with_base_url(None, None); assert_eq!(p.base_url, "https://api.anthropic.com"); ->>>>>>> origin/main } #[tokio::test]