Phase A: mail_send + mail_inbox_list + mail_inbox_read. Replaces a legacy mail CLI script with a typed MCP server. Outbound guarantees Date, Message-ID (own-domain), User-Agent, MIME-Version, multipart/alternative for HTML+text, multipart/mixed for attachments, In-Reply-To + References for threading. Single account in v0.1 (default_account from config). Phase B adds multi-account + threading + search; Phase C adds mark + attachments + reply helper. Stack: rmcp 0.1 (matches aldabra), lettre 0.11 + tokio-rustls, async-imap 0.10, mail-parser 0.9. Stderr-only logging (stdout is the MCP transport). Smoke verified 2026-05-21: send -> land -> read user@example.com round trip, DKIM-Signature + Authentication-Results pass at the rspamd relay.
66 lines
1.8 KiB
Rust
66 lines
1.8 KiB
Rust
//! mail-mcp — MCP server entry point.
|
|
//!
|
|
//! Speaks MCP over stdio. Any MCP client launches this as a
|
|
//! subprocess
|
|
//! and gets `mail_send` + `mail_inbox_list` + `mail_inbox_read`.
|
|
//!
|
|
//! Logging is stderr-only — stdout belongs to the JSON-RPC transport.
|
|
|
|
mod config;
|
|
mod imap;
|
|
mod smtp;
|
|
mod tools;
|
|
|
|
use std::process::ExitCode;
|
|
|
|
use anyhow::Result;
|
|
use rmcp::{transport::stdio, ServiceExt};
|
|
use tracing_subscriber::EnvFilter;
|
|
|
|
use crate::config::Config;
|
|
use crate::tools::MailService;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> ExitCode {
|
|
tracing_subscriber::fmt()
|
|
.with_writer(std::io::stderr)
|
|
.with_env_filter(
|
|
EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
|
|
)
|
|
.init();
|
|
|
|
// rustls 0.23 requires the process-wide default CryptoProvider to be set
|
|
// before any TLS handshake. lettre carries its own internal provider for
|
|
// SMTP; our IMAP path goes through tokio-rustls + raw `ClientConfig`,
|
|
// which needs this. Install once at startup; ignore the Err which only
|
|
// surfaces if a provider is already installed (idempotent).
|
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
|
|
|
match run().await {
|
|
Ok(()) => ExitCode::SUCCESS,
|
|
Err(e) => {
|
|
tracing::error!("{e:#}");
|
|
ExitCode::FAILURE
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn run() -> Result<()> {
|
|
let cfg = Config::load()?;
|
|
tracing::info!(
|
|
accounts = cfg.accounts.len(),
|
|
default_account = %cfg.default_account,
|
|
"mail-mcp starting"
|
|
);
|
|
|
|
let service = MailService::new(cfg);
|
|
let server = service
|
|
.serve(stdio())
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("rmcp serve failed: {e}"))?;
|
|
server
|
|
.waiting()
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("rmcp wait failed: {e}"))?;
|
|
Ok(())
|
|
}
|