carrier/crates/mail-mcp/src/main.rs
Sulkta cd04d2c49c mail-mcp v0.1 — Rust MCP email server
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.
2026-05-21 06:50:25 -07:00

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(())
}