nitidus 0.1.8

A TUI email client
use std::{path::PathBuf, sync::Arc};

use color_eyre::eyre::{bail, eyre, Context, OptionExt};
use email::{
    account::config::DEFAULT_PAGE_SIZE,
    backend::{Backend, BackendBuilder},
    config::Config,
    email::{envelope::Envelopes, message::Messages},
    envelope::{
        list::{ListEnvelopes, ListEnvelopesOptions},
        Id,
    },
    folder::INBOX,
    imap::{ImapContext, ImapContextBuilder},
    message::get::GetMessages,
};
use himalaya::config::TomlConfig;
use pimalaya_tui::terminal::config::TomlConfig as _;
use tracing::{info, instrument};

use crate::config;

pub struct MailClient {
    folder: Option<String>,
    backend: Backend<ImapContext>,
}

impl MailClient {
    #[instrument]
    pub async fn init() -> color_eyre::Result<Self> {
        info!("initializing mail client");
        secret::keyring::set_global_service_name("himalaya-cli");

        let app_config = config::get();
        let himalaya_config = load_config(app_config.himalaya_config.clone()).await?;
        let account_name = app_config.account_name.as_ref().map(String::as_ref);
        let (toml_account_config, account_config) = himalaya_config
            .into_account_configs(account_name, |c: &Config, name| c.account(name).ok())
            .map_err(|err| {
                eyre!(
                    "cannot find account `{}` in config file: {err}",
                    account_name.unwrap_or("default"),
                )
            })?;

        let Some(imap_config) = toml_account_config.imap_config() else {
            bail!("missing backend config")
        };

        info!(?account_config, ?imap_config, "creating imap backend");

        let account_config = Arc::new(account_config.clone());
        let imap_config = Arc::new(imap_config.clone());
        let imap_ctx = ImapContextBuilder::new(account_config.clone(), imap_config);
        let backend = BackendBuilder::new(account_config, imap_ctx.clone())
            .build()
            .await
            .wrap_err("cannot create imap backend")?;

        Ok(Self {
            folder: app_config.folder.clone(),
            backend,
        })
    }

    pub fn folder_or_default(&self) -> &str {
        self.folder.as_ref().map_or(INBOX, String::as_ref)
    }

    #[instrument(skip(self))]
    pub async fn load_folder(&self) -> color_eyre::Result<Envelopes> {
        let folder = self.folder_or_default();
        info!("loading folder {folder}");
        let options = ListEnvelopesOptions {
            page_size: DEFAULT_PAGE_SIZE,
            page: 0,
            query: None,
        };
        let envelopes = self
            .backend
            .list_envelopes(folder, options)
            .await
            .wrap_err("cannot list envelopes")?;
        Ok(envelopes)
    }

    #[instrument(skip(self))]
    pub async fn load_messages(&self, id: &str) -> color_eyre::Result<Messages> {
        let id = Id::single(id);
        let folder = self.folder_or_default();
        info!("loading messages for {id} in folder {folder}");
        let emails = self
            .backend
            .get_messages(folder, &id)
            .await
            .wrap_err("cannot load messages")?;
        Ok(emails)
    }
}

#[instrument]
async fn load_config(path: Option<PathBuf>) -> color_eyre::Result<TomlConfig> {
    info!("loading himalaya config");
    let path = path.ok_or_eyre("config file not found, please run `himalaya` to create one")?;

    let config = TomlConfig::from_paths_or_default(&[path])
        .await
        .wrap_err("cannot load config file. (hint: run `himalaya` to create one)")?;
    Ok(config)
}