use ecr_server::auth::TokenStore;
use ecr_server::state::AppState;
use ecr_server::{app, watcher};
use ecr_store::NotmuchStore;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
pub struct Options {
pub bind: SocketAddr,
pub read_only: bool,
pub no_watch: bool,
pub allowed_origins: Vec<String>,
pub web_dir: Option<PathBuf>,
pub token_path: PathBuf,
}
pub async fn run(options: Options) -> anyhow::Result<()> {
let Options {
bind,
read_only,
no_watch,
allowed_origins,
web_dir,
token_path,
} = options;
let store = Arc::new(NotmuchStore::open()?);
let report = ecr_store::doctor::run_with_paths(store.paths()).await;
if !report.is_healthy() {
eprintln!("{}", ecr_store::doctor::render(&report));
anyhow::bail!("refusing to start: the mail setup is not healthy");
}
let tokens = TokenStore::load(&token_path)?;
if tokens.is_empty() {
tracing::warn!(
"no device tokens exist, so the API is unauthenticated; run `ecr token new <name>`"
);
}
let state = AppState::new(Arc::clone(&store), tokens, read_only);
let _watcher = if no_watch {
None
} else {
match watcher::spawn(state.clone()) {
Ok(watcher) => Some(watcher),
Err(err) => {
tracing::warn!(%err, "could not watch the maildir; new mail will need a manual sync");
None
}
}
};
let listener = tokio::net::TcpListener::bind(bind)
.await
.map_err(|err| match err.kind() {
std::io::ErrorKind::AddrInUse => anyhow::anyhow!(
"{bind} is already in use.\n\
Something else is listening there. Either stop it, or choose \
another address:\n \
ecr serve --bind 127.0.0.1:8384"
),
std::io::ErrorKind::PermissionDenied => {
anyhow::anyhow!("not allowed to bind {bind} (ports below 1024 need root)")
}
std::io::ErrorKind::AddrNotAvailable => anyhow::anyhow!(
"{bind} is not an address on this machine; check the interface is up"
),
_ => anyhow::anyhow!("could not bind {bind}: {err}"),
})?;
let web = ecr_server::web::locate(web_dir);
let accounts = report.accounts.len();
eprintln!();
eprintln!(" ecr is running");
eprintln!(" open http://{bind}");
eprintln!(" accounts {accounts}");
if read_only {
eprintln!(" mode read-only (no tagging, syncing or sending)");
}
match &web {
Some(dir) => eprintln!(" client {}", dir.display()),
None => eprintln!(
" client not built — run `just build-web`, or `just dev` for hot reload"
),
}
eprintln!();
let cors = (!allowed_origins.is_empty()).then_some(allowed_origins);
app::serve(listener, state, cors, web.as_deref()).await
}