Skip to main content

postcrate_core/
config.rs

1//! Static configuration for a `Service`. Resolved once at construction.
2//! Anything the user can change at runtime lives in the `settings` table,
3//! not here.
4
5use std::net::IpAddr;
6use std::path::{Path, PathBuf};
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::{Error, Result};
11use crate::smtp::tls::TlsConfig;
12
13/// Bind selection at startup. Runtime `exposeOnLan` toggles override this
14/// at restart time (the running listeners aren't moved).
15#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
16pub enum BindHost {
17    /// `127.0.0.1` โ€” Postcrate's default and only safe choice.
18    Loopback,
19    /// `0.0.0.0` โ€” opt-in only. Logs a warning at startup.
20    AllInterfaces,
21}
22
23impl BindHost {
24    pub fn as_ip(self) -> IpAddr {
25        match self {
26            BindHost::Loopback => IpAddr::from([127, 0, 0, 1]),
27            BindHost::AllInterfaces => IpAddr::from([0, 0, 0, 0]),
28        }
29    }
30}
31
32/// Configuration for the [`crate::Service`].
33#[derive(Debug, Clone)]
34pub struct CoreConfig {
35    pub data_dir: PathBuf,
36    pub db_path: PathBuf,
37    pub blobs_dir: PathBuf,
38    pub default_smtp_port: u16,
39    pub http_port: u16,
40    pub bind_host: BindHost,
41    pub max_message_bytes: u64,
42    pub ephemeral_port_range: (u16, u16),
43    /// EHLO hostname advertised to clients. Defaults to `postcrate.local`.
44    pub ehlo_hostname: String,
45    /// SMTP receive line length (RFC 5321 ยง4.5.3.1.6 is 1000 incl. CRLF).
46    pub smtp_max_line_bytes: usize,
47    /// Threshold above which DATA streams to a tempfile.
48    pub data_spill_bytes: usize,
49    /// Bounded queue size between SMTP sessions and the ingest worker.
50    pub ingest_channel_capacity: usize,
51    /// STARTTLS configuration. Disabled by default; enabling requires
52    /// the `tls` Cargo feature to be active on `postcrate-core`.
53    pub tls: TlsConfig,
54}
55
56impl CoreConfig {
57    /// Convenience constructor.
58    pub fn for_data_dir(data_dir: impl Into<PathBuf>) -> Result<Self> {
59        let data_dir = data_dir.into();
60        let db_path = data_dir.join("postcrate.sqlite");
61        let blobs_dir = data_dir.join("blobs");
62        Ok(Self {
63            data_dir,
64            db_path,
65            blobs_dir,
66            default_smtp_port: 1025,
67            http_port: 1080,
68            bind_host: BindHost::Loopback,
69            max_message_bytes: 50 * 1024 * 1024,
70            ephemeral_port_range: (1100, 1199),
71            ehlo_hostname: "postcrate.local".to_string(),
72            smtp_max_line_bytes: 1000,
73            data_spill_bytes: 256 * 1024,
74            ingest_channel_capacity: 1024,
75            tls: TlsConfig::default(),
76        })
77    }
78
79    /// Resolve the platform-appropriate default data directory.
80    pub fn default_data_dir() -> Result<PathBuf> {
81        // We deliberately avoid pulling in `directories` to keep the dep
82        // surface minimal โ€” the binaries take an explicit `--data-dir`,
83        // and embedders pass their own.
84        if let Ok(home) = std::env::var("HOME") {
85            #[cfg(target_os = "macos")]
86            {
87                let p = Path::new(&home).join("Library/Application Support/Postcrate");
88                return Ok(p);
89            }
90            #[cfg(target_os = "linux")]
91            {
92                if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
93                    return Ok(Path::new(&xdg).join("postcrate"));
94                }
95                return Ok(Path::new(&home).join(".local/share/postcrate"));
96            }
97            #[cfg(not(any(target_os = "macos", target_os = "linux")))]
98            {
99                return Ok(Path::new(&home).join(".postcrate"));
100            }
101        }
102        if let Ok(appdata) = std::env::var("APPDATA") {
103            return Ok(Path::new(&appdata).join("Postcrate"));
104        }
105        Err(Error::Internal(
106            "could not resolve a default data directory; pass one explicitly".into(),
107        ))
108    }
109
110    pub(crate) fn raw_dir(&self) -> PathBuf {
111        self.blobs_dir.join("raw")
112    }
113
114    pub(crate) fn incoming_dir(&self) -> PathBuf {
115        self.blobs_dir.join("raw").join("incoming")
116    }
117
118    pub(crate) fn att_dir(&self) -> PathBuf {
119        self.blobs_dir.join("att")
120    }
121
122    pub(crate) async fn ensure_dirs(&self) -> Result<()> {
123        for dir in [
124            &self.data_dir,
125            &self.blobs_dir,
126            &self.raw_dir(),
127            &self.incoming_dir(),
128            &self.att_dir(),
129        ] {
130            tokio::fs::create_dir_all(dir).await?;
131        }
132        Ok(())
133    }
134}