You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

520 lines
15 KiB

  1. /*
  2. * meli - configuration module.
  3. *
  4. * Copyright 2017 Manos Pitsidianakis
  5. *
  6. * This file is part of meli.
  7. *
  8. * meli is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU General Public License as published by
  10. * the Free Software Foundation, either version 3 of the License, or
  11. * (at your option) any later version.
  12. *
  13. * meli is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License
  19. * along with meli. If not, see <http://www.gnu.org/licenses/>.
  20. */
  21. extern crate bincode;
  22. extern crate serde;
  23. extern crate toml;
  24. extern crate xdg;
  25. pub mod composing;
  26. pub mod notifications;
  27. pub mod pager;
  28. pub mod pgp;
  29. pub mod shortcuts;
  30. pub mod accounts;
  31. pub use self::accounts::Account;
  32. pub use self::composing::*;
  33. pub use self::pgp::*;
  34. pub use self::shortcuts::*;
  35. use self::default_vals::*;
  36. use self::notifications::NotificationsSettings;
  37. use crate::pager::PagerSettings;
  38. use melib::backends::SpecialUseMailbox;
  39. use melib::conf::AccountSettings;
  40. use melib::error::*;
  41. use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
  42. use std::collections::HashMap;
  43. use std::env;
  44. use std::fs::{File, OpenOptions};
  45. use std::io::{self, BufRead, Read, Write};
  46. use std::path::PathBuf;
  47. #[macro_export]
  48. macro_rules! split_command {
  49. ($cmd:expr) => {{
  50. $cmd.split_whitespace().collect::<Vec<&str>>()
  51. }};
  52. }
  53. #[derive(Copy, Debug, Clone, PartialEq)]
  54. pub enum ToggleFlag {
  55. Unset,
  56. InternalVal(bool),
  57. False,
  58. True,
  59. }
  60. impl From<bool> for ToggleFlag {
  61. fn from(val: bool) -> Self {
  62. if val {
  63. ToggleFlag::True
  64. } else {
  65. ToggleFlag::False
  66. }
  67. }
  68. }
  69. impl Default for ToggleFlag {
  70. fn default() -> Self {
  71. ToggleFlag::Unset
  72. }
  73. }
  74. impl ToggleFlag {
  75. pub fn is_unset(&self) -> bool {
  76. ToggleFlag::Unset == *self
  77. }
  78. pub fn is_internal(&self) -> bool {
  79. if let ToggleFlag::InternalVal(_) = *self {
  80. true
  81. } else {
  82. false
  83. }
  84. }
  85. pub fn is_false(&self) -> bool {
  86. ToggleFlag::False == *self || ToggleFlag::InternalVal(false) == *self
  87. }
  88. pub fn is_true(&self) -> bool {
  89. ToggleFlag::True == *self || ToggleFlag::InternalVal(true) == *self
  90. }
  91. }
  92. #[derive(Default, Debug, Clone, Serialize, Deserialize)]
  93. pub struct MailUIConf {
  94. pub pager: Option<PagerSettings>,
  95. pub notifications: Option<NotificationsSettings>,
  96. pub shortcuts: Option<Shortcuts>,
  97. pub composing: Option<ComposingSettings>,
  98. pub identity: Option<String>,
  99. pub index_style: Option<IndexStyle>,
  100. }
  101. #[derive(Debug, Clone, Serialize, Deserialize)]
  102. pub struct FolderConf {
  103. pub rename: Option<String>,
  104. #[serde(default = "true_val")]
  105. pub autoload: bool,
  106. #[serde(deserialize_with = "toggleflag_de", default)]
  107. pub subscribe: ToggleFlag,
  108. #[serde(deserialize_with = "toggleflag_de", default)]
  109. pub ignore: ToggleFlag,
  110. #[serde(default = "none")]
  111. pub usage: Option<SpecialUseMailbox>,
  112. #[serde(flatten)]
  113. pub conf_override: MailUIConf,
  114. }
  115. impl Default for FolderConf {
  116. fn default() -> Self {
  117. FolderConf {
  118. rename: None,
  119. autoload: true,
  120. subscribe: ToggleFlag::Unset,
  121. ignore: ToggleFlag::Unset,
  122. usage: None,
  123. conf_override: MailUIConf::default(),
  124. }
  125. }
  126. }
  127. impl FolderConf {
  128. pub fn rename(&self) -> Option<&str> {
  129. self.rename.as_ref().map(String::as_str)
  130. }
  131. }
  132. #[derive(Debug, Clone, Default, Serialize, Deserialize)]
  133. pub struct FileAccount {
  134. root_folder: String,
  135. format: String,
  136. identity: String,
  137. #[serde(flatten)]
  138. pub extra: HashMap<String, String>,
  139. #[serde(default = "none")]
  140. display_name: Option<String>,
  141. index_style: IndexStyle,
  142. /// A command to pipe html output before displaying it in a pager
  143. /// Default: None
  144. #[serde(default = "none", deserialize_with = "non_empty_string")]
  145. html_filter: Option<String>,
  146. #[serde(default = "false_val")]
  147. read_only: bool,
  148. subscribed_folders: Vec<String>,
  149. folders: Option<HashMap<String, FolderConf>>,
  150. }
  151. impl From<FileAccount> for AccountConf {
  152. fn from(x: FileAccount) -> Self {
  153. let format = x.format.to_lowercase();
  154. let root_folder = x.root_folder.clone();
  155. let identity = x.identity.clone();
  156. let display_name = x.display_name.clone();
  157. let mut acc = AccountSettings {
  158. name: String::new(),
  159. root_folder,
  160. format,
  161. identity,
  162. read_only: x.read_only,
  163. display_name,
  164. subscribed_folders: x.subscribed_folders.clone(),
  165. extra: x.extra.clone(),
  166. };
  167. let root_path = PathBuf::from(acc.root_folder.as_str());
  168. let root_tmp = root_path
  169. .components()
  170. .last()
  171. .unwrap()
  172. .as_os_str()
  173. .to_str()
  174. .unwrap()
  175. .to_string();
  176. if !acc.subscribed_folders.contains(&root_tmp) {
  177. acc.subscribed_folders.push(root_tmp);
  178. }
  179. let mut folder_confs = x.folders.clone().unwrap_or_else(Default::default);
  180. for s in &x.subscribed_folders {
  181. if !folder_confs.contains_key(s) {
  182. folder_confs.insert(
  183. s.to_string(),
  184. FolderConf {
  185. subscribe: ToggleFlag::True,
  186. ..FolderConf::default()
  187. },
  188. );
  189. } else {
  190. if !folder_confs[s].subscribe.is_unset() {
  191. eprintln!("Configuration error: folder `{}` cannot both have `subscribe` flag set and be in the `subscribed_folders` array", s);
  192. std::process::exit(1);
  193. }
  194. folder_confs.get_mut(s).unwrap().subscribe = ToggleFlag::True;
  195. }
  196. if folder_confs[s].usage.is_none() {
  197. let name = s
  198. .split(if s.contains('/') { '/' } else { '.' })
  199. .last()
  200. .unwrap_or("");
  201. folder_confs.get_mut(s).unwrap().usage = if name.eq_ignore_ascii_case("inbox") {
  202. Some(SpecialUseMailbox::Inbox)
  203. } else if name.eq_ignore_ascii_case("archive") {
  204. Some(SpecialUseMailbox::Archive)
  205. } else if name.eq_ignore_ascii_case("drafts") {
  206. Some(SpecialUseMailbox::Drafts)
  207. } else if name.eq_ignore_ascii_case("junk") {
  208. Some(SpecialUseMailbox::Junk)
  209. } else if name.eq_ignore_ascii_case("spam") {
  210. Some(SpecialUseMailbox::Junk)
  211. } else if name.eq_ignore_ascii_case("sent") {
  212. Some(SpecialUseMailbox::Sent)
  213. } else if name.eq_ignore_ascii_case("trash") {
  214. Some(SpecialUseMailbox::Trash)
  215. } else {
  216. Some(SpecialUseMailbox::Normal)
  217. };
  218. }
  219. if folder_confs[s].ignore.is_unset() {
  220. use SpecialUseMailbox::*;
  221. if [Junk, Sent, Trash].contains(&folder_confs[s].usage.as_ref().unwrap()) {
  222. folder_confs.get_mut(s).unwrap().ignore = ToggleFlag::InternalVal(true);
  223. }
  224. }
  225. }
  226. AccountConf {
  227. account: acc,
  228. conf: x,
  229. folder_confs,
  230. }
  231. }
  232. }
  233. impl FileAccount {
  234. pub fn folders(&self) -> Option<&HashMap<String, FolderConf>> {
  235. self.folders.as_ref()
  236. }
  237. pub fn folder(&self) -> &str {
  238. &self.root_folder
  239. }
  240. pub fn index_style(&self) -> IndexStyle {
  241. self.index_style
  242. }
  243. pub fn html_filter(&self) -> Option<&str> {
  244. self.html_filter.as_ref().map(String::as_str)
  245. }
  246. }
  247. #[derive(Debug, Clone, Default, Serialize, Deserialize)]
  248. struct FileSettings {
  249. accounts: HashMap<String, FileAccount>,
  250. #[serde(default)]
  251. pager: PagerSettings,
  252. #[serde(default)]
  253. notifications: NotificationsSettings,
  254. #[serde(default)]
  255. shortcuts: Shortcuts,
  256. composing: ComposingSettings,
  257. #[serde(default)]
  258. pgp: PGPSettings,
  259. }
  260. #[derive(Debug, Clone, Default)]
  261. pub struct AccountConf {
  262. pub(crate) account: AccountSettings,
  263. pub(crate) conf: FileAccount,
  264. pub(crate) folder_confs: HashMap<String, FolderConf>,
  265. }
  266. impl AccountConf {
  267. pub fn account(&self) -> &AccountSettings {
  268. &self.account
  269. }
  270. pub fn conf(&self) -> &FileAccount {
  271. &self.conf
  272. }
  273. pub fn conf_mut(&mut self) -> &mut FileAccount {
  274. &mut self.conf
  275. }
  276. }
  277. #[derive(Debug, Clone, Default)]
  278. pub struct Settings {
  279. pub accounts: HashMap<String, AccountConf>,
  280. pub pager: PagerSettings,
  281. pub notifications: NotificationsSettings,
  282. pub shortcuts: Shortcuts,
  283. pub composing: ComposingSettings,
  284. pub pgp: PGPSettings,
  285. }
  286. impl FileSettings {
  287. pub fn new() -> Result<FileSettings> {
  288. let config_path = match env::var("MELI_CONFIG") {
  289. Ok(path) => PathBuf::from(path),
  290. Err(_) => {
  291. let xdg_dirs = xdg::BaseDirectories::with_prefix("meli").unwrap();
  292. xdg_dirs
  293. .place_config_file("config")
  294. .expect("cannot create configuration directory")
  295. }
  296. };
  297. if !config_path.exists() {
  298. println!(
  299. "No configuration found. Would you like to generate one in {}? [Y/n]",
  300. config_path.display()
  301. );
  302. let mut buffer = String::new();
  303. let stdin = io::stdin();
  304. let mut handle = stdin.lock();
  305. loop {
  306. buffer.clear();
  307. handle
  308. .read_line(&mut buffer)
  309. .expect("Could not read from stdin.");
  310. match buffer.trim() {
  311. "Y" | "y" | "yes" | "YES" | "Yes" => {
  312. let mut file = OpenOptions::new()
  313. .write(true)
  314. .create_new(true)
  315. .open(config_path.as_path())
  316. .expect("Could not create config file.");
  317. file.write_all(include_bytes!("../../sample-config"))
  318. .expect("Could not write to config file.");
  319. println!("Written config to {}", config_path.display());
  320. std::process::exit(1);
  321. }
  322. "n" | "N" | "no" | "No" | "NO" => {
  323. std::process::exit(1);
  324. }
  325. _ => {
  326. println!(
  327. "No configuration found. Would you like to generate one in {}? [Y/n]",
  328. config_path.display()
  329. );
  330. }
  331. }
  332. }
  333. }
  334. let mut file = File::open(config_path.to_str().unwrap())?;
  335. let mut contents = String::new();
  336. file.read_to_string(&mut contents)?;
  337. let s = toml::from_str(&contents);
  338. if let Err(e) = s {
  339. eprintln!("Config file contains errors: {}", e.to_string());
  340. std::process::exit(1);
  341. }
  342. Ok(s.unwrap())
  343. }
  344. }
  345. impl Settings {
  346. pub fn new() -> Settings {
  347. let fs = FileSettings::new().unwrap_or_else(|e| {
  348. eprintln!("Configuration error: {}", e);
  349. std::process::exit(1);
  350. });
  351. let mut s: HashMap<String, AccountConf> = HashMap::new();
  352. for (id, x) in fs.accounts {
  353. let mut ac = AccountConf::from(x);
  354. ac.account.set_name(id.clone());
  355. s.insert(id, ac);
  356. }
  357. Settings {
  358. accounts: s,
  359. pager: fs.pager,
  360. notifications: fs.notifications,
  361. shortcuts: fs.shortcuts,
  362. composing: fs.composing,
  363. pgp: fs.pgp,
  364. }
  365. }
  366. }
  367. #[derive(Copy, Debug, Clone, Hash, PartialEq)]
  368. pub enum IndexStyle {
  369. Plain,
  370. Threaded,
  371. Compact,
  372. Conversations,
  373. }
  374. impl Default for IndexStyle {
  375. fn default() -> Self {
  376. IndexStyle::Compact
  377. }
  378. }
  379. fn non_empty_string<'de, D>(deserializer: D) -> std::result::Result<Option<String>, D::Error>
  380. where
  381. D: Deserializer<'de>,
  382. {
  383. let s = <String>::deserialize(deserializer)?;
  384. if s.is_empty() {
  385. Ok(None)
  386. } else {
  387. Ok(Some(s))
  388. }
  389. }
  390. fn toggleflag_de<'de, D>(deserializer: D) -> std::result::Result<ToggleFlag, D::Error>
  391. where
  392. D: Deserializer<'de>,
  393. {
  394. let s = <bool>::deserialize(deserializer);
  395. Ok(match s {
  396. Err(_) => ToggleFlag::Unset,
  397. Ok(true) => ToggleFlag::True,
  398. Ok(false) => ToggleFlag::False,
  399. })
  400. }
  401. impl Serialize for ToggleFlag {
  402. fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
  403. where
  404. S: Serializer,
  405. {
  406. match self {
  407. ToggleFlag::Unset | ToggleFlag::InternalVal(_) => serializer.serialize_none(),
  408. ToggleFlag::False => serializer.serialize_bool(false),
  409. ToggleFlag::True => serializer.serialize_bool(true),
  410. }
  411. }
  412. }
  413. /*
  414. * Deserialize default functions
  415. */
  416. mod default_vals {
  417. pub(in crate::conf) fn false_val() -> bool {
  418. false
  419. }
  420. pub(in crate::conf) fn true_val() -> bool {
  421. true
  422. }
  423. pub(in crate::conf) fn zero_val() -> usize {
  424. 0
  425. }
  426. pub(in crate::conf) fn eighty_percent() -> usize {
  427. 80
  428. }
  429. pub(in crate::conf) fn none<T>() -> Option<T> {
  430. None
  431. }
  432. pub(in crate::conf) fn internal_value_false() -> super::ToggleFlag {
  433. super::ToggleFlag::InternalVal(false)
  434. }
  435. }
  436. impl<'de> Deserialize<'de> for IndexStyle {
  437. fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
  438. where
  439. D: Deserializer<'de>,
  440. {
  441. let s = <String>::deserialize(deserializer)?;
  442. match s.as_str() {
  443. "Plain" | "plain" => Ok(IndexStyle::Plain),
  444. "Threaded" | "threaded" => Ok(IndexStyle::Threaded),
  445. "Compact" | "compact" => Ok(IndexStyle::Compact),
  446. "Conversations" | "conversations" => Ok(IndexStyle::Conversations),
  447. _ => Err(de::Error::custom("invalid `index_style` value")),
  448. }
  449. }
  450. }
  451. impl Serialize for IndexStyle {
  452. fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
  453. where
  454. S: Serializer,
  455. {
  456. match self {
  457. IndexStyle::Plain => serializer.serialize_str("plain"),
  458. IndexStyle::Threaded => serializer.serialize_str("threaded"),
  459. IndexStyle::Compact => serializer.serialize_str("compact"),
  460. IndexStyle::Conversations => serializer.serialize_str("conversations"),
  461. }
  462. }
  463. }