1use std::env;
4
5use std::fs::{self, File};
6use std::io::{self, Write};
7use std::path::PathBuf;
8
9use directories_next::BaseDirs;
10use serde::{Deserialize, Serialize};
11use thiserror::Error;
12use toml::{de::Error as DerTomlError, ser::Error as SerTomlError};
13use zeroize::{Zeroize, ZeroizeOnDrop};
14
15#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
22pub struct Config {
23 pub username: String,
25 pub password: String,
27 pub smtp_server: String,
29 pub smtp_port: u16,
31 pub message: String,
35 pub message_warning: String,
37 pub subject: String,
41 pub subject_warning: String,
43 pub to: String,
45 pub from: String,
47 pub attachment: Option<String>,
49 pub timer_warning: u64,
51 pub timer_dead_man: u64,
53 pub web_password: String,
55 pub cookie_exp_days: u64,
57 pub log_level: Option<String>,
59}
60
61impl Default for Config {
62 fn default() -> Self {
63 let web_password = env::var("WEB_PASSWORD")
64 .ok()
65 .unwrap_or("password".to_string());
66 Self {
67 username: "me@example.com".to_string(),
68 password: "".to_string(),
69 smtp_server: "smtp.example.com".to_string(),
70 smtp_port: 587,
71 message: "I'm probably dead, go to Central Park NY under bench #137 you'll find an age-encrypted drive. Password is our favorite music in Pascal case.".to_string(),
72 message_warning: "Hey, you haven't checked in for a while. Are you okay?".to_string(),
73 subject: "[URGENT] Something Happened to Me!".to_string(),
74 subject_warning: "[URGENT] You need to check in!".to_string(),
75 to: "someone@example.com".to_string(),
76 from: "me@example.com".to_string(),
77 attachment: None,
78 timer_warning: 60 * 60 * 24 * 14, timer_dead_man: 60 * 60 * 24 * 7, web_password,
81 cookie_exp_days: 7,
82 log_level: None,
83 }
84 }
85}
86
87#[derive(Error, Debug)]
89pub enum ConfigError {
90 #[error(transparent)]
92 Io(#[from] std::io::Error),
93
94 #[error(transparent)]
96 TomlSerialization(#[from] SerTomlError),
97
98 #[error(transparent)]
100 TomlDeserialization(#[from] DerTomlError),
101
102 #[error("Attachment not found")]
104 AttachmentNotFound,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum Email {
110 Warning,
112 DeadMan,
114}
115
116pub fn config_path() -> Result<PathBuf, ConfigError> {
130 let base_dir = if cfg!(test) {
131 std::env::temp_dir()
133 } else {
134 BaseDirs::new()
135 .ok_or_else(|| {
136 ConfigError::Io(io::Error::new(
137 io::ErrorKind::NotFound,
138 "Failed to find home directory",
139 ))
140 })?
141 .config_dir()
142 .to_path_buf()
143 };
144
145 let config_dir = base_dir.join(if cfg!(test) {
146 "deadman_test"
147 } else {
148 "deadman"
149 });
150
151 fs::create_dir_all(&config_dir)?;
152 Ok(config_dir.join("config.toml"))
153}
154
155pub fn save_config(config: &Config) -> Result<(), ConfigError> {
165 let config_path = config_path()?;
166 let mut file = File::create(config_path)?;
167 let config = toml::to_string(config)?;
168
169 file.write_all(config.as_bytes())?;
170
171 Ok(())
172}
173
174pub fn load_or_initialize_config() -> Result<Config, ConfigError> {
191 let config_path = config_path()?;
192 if !config_path.exists() {
193 let config = Config::default();
194 save_config(&config)?;
195
196 Ok(config)
197 } else {
198 let config = fs::read_to_string(&config_path)?;
199 let config: Config = toml::from_str(&config)?;
200
201 Ok(config)
202 }
203}
204
205pub fn attachment_path(config: &Config) -> Result<PathBuf, ConfigError> {
212 let attachment_path = config
213 .attachment
214 .as_ref()
215 .ok_or(ConfigError::AttachmentNotFound)?;
216 Ok(PathBuf::from(attachment_path))
217}
218
219#[cfg(test)]
220mod test {
221 use super::*;
222 use std::{
223 env, file, process, thread,
224 time::{SystemTime, UNIX_EPOCH},
225 };
226
227 fn get_isolated_test_dir() -> PathBuf {
229 let thread_id = format!("{:?}", thread::current().id());
230 let timestamp = SystemTime::now()
231 .duration_since(UNIX_EPOCH)
232 .unwrap()
233 .as_nanos();
234 let pid = process::id();
235
236 let test_dir = env::temp_dir().join("deadman_test_isolated").join(format!(
237 "{}_{}_{}_{}",
238 file!().replace(['/', '\\'], "_"),
239 pid,
240 thread_id.replace([':', '(', ')'], "_"),
241 timestamp
242 ));
243
244 fs::create_dir_all(&test_dir).expect("Failed to create test directory");
245 test_dir
246 }
247
248 fn save_config_with_path(config: &Config, path: &PathBuf) -> Result<(), ConfigError> {
250 if let Some(parent) = path.parent() {
252 fs::create_dir_all(parent)?;
253 }
254 let mut file = File::create(path)?;
255 let config_str = toml::to_string(config)?;
256 file.write_all(config_str.as_bytes())?;
257 file.sync_all()?; Ok(())
259 }
260
261 fn load_config_from_path(path: &PathBuf) -> Result<Config, ConfigError> {
262 let config_str = fs::read_to_string(path)?;
263 let config: Config = toml::from_str(&config_str)?;
264 Ok(config)
265 }
266
267 #[test]
268 fn save_config() {
269 let test_dir = get_isolated_test_dir();
270 let test_path = test_dir.join("config.toml");
271 let config = Config::default();
272
273 save_config_with_path(&config, &test_path).unwrap();
275
276 let loaded_config = load_config_from_path(&test_path).unwrap();
277 assert_eq!(loaded_config, Config::default());
278
279 let _ = fs::remove_dir_all(&test_dir);
281 }
282
283 #[test]
284 fn load_or_initialize_config_with_existing_file() {
285 let test_dir = get_isolated_test_dir();
286 let test_path = test_dir.join("config.toml");
287 let config = Config::default();
288
289 save_config_with_path(&config, &test_path).unwrap();
291
292 let loaded_config = load_config_from_path(&test_path).unwrap();
294 assert_eq!(loaded_config, Config::default());
295
296 let _ = fs::remove_dir_all(&test_dir);
298 }
299
300 #[test]
301 fn config_path_in_test_mode() {
302 let path = config_path().unwrap();
304 assert!(path.to_string_lossy().contains("deadman_test"));
305
306 if let Some(parent) = path.parent() {
308 let _ = fs::remove_dir_all(parent);
309 }
310 }
311
312 #[test]
313 fn example_config_is_valid() {
314 let example_config = fs::read_to_string("../../config.example.toml").unwrap();
315 let config: Result<Config, toml::de::Error> = toml::from_str(&example_config);
316 assert!(config.is_ok());
317 }
318}