diff --git a/.gitignore b/.gitignore index ab94559..f1886a5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ /output.log /config.toml /public +/hellosu +/hellosu.db* diff --git a/imap/src/client/mod.rs b/imap/src/client/mod.rs index afc400c..3825e81 100644 --- a/imap/src/client/mod.rs +++ b/imap/src/client/mod.rs @@ -56,7 +56,8 @@ use tokio_rustls::{ use crate::command::{Command, FetchItems, SearchCriteria}; use crate::response::{ - AttributeValue, Envelope, MailboxData, Response, ResponseData, ResponseDone, + AttributeValue, Envelope, MailboxData, MailboxFlag, Response, ResponseCode, ResponseData, + ResponseDone, Status, }; pub use self::inner::{Client, ResponseStream}; @@ -67,7 +68,7 @@ pub use self::inner::{Client, ResponseStream}; /// the connection to the server. /// /// [1]: self::ClientConfigBuilder::build -/// [2]: self::ClientConfig::connect +/// [2]: self::ClientConfig::open pub type ClientBuilder = ClientConfigBuilder; /// An IMAP client that hasn't been connected yet. @@ -194,20 +195,36 @@ impl ClientAuthenticated { } /// Runs the SELECT command - pub async fn select(&mut self, mailbox: impl AsRef) -> Result<()> { + pub async fn select(&mut self, mailbox: impl AsRef) -> Result { let cmd = Command::Select { mailbox: mailbox.as_ref().to_owned(), }; + let stream = self.execute(cmd).await?; let (_, data) = stream.wait().await?; + + let mut select = SelectResponse::default(); for resp in data { debug!("execute called returned: {:?}", resp); + match resp { + Response::MailboxData(MailboxData::Flags(flags)) => select.flags = flags, + Response::MailboxData(MailboxData::Exists(exists)) => select.exists = Some(exists), + Response::MailboxData(MailboxData::Recent(recent)) => select.recent = Some(recent), + Response::Data(ResponseData { + status: Status::Ok, + code: Some(code), + .. + }) => match code { + ResponseCode::Unseen(value) => select.unseen = Some(value), + ResponseCode::UidNext(value) => select.uid_next = Some(value), + ResponseCode::UidValidity(value) => select.uid_validity = Some(value), + _ => {} + }, + _ => {} + } } - // nuke the capabilities cache - // self.nuke_capabilities(); - - Ok(()) + Ok(select) } /// Runs the SEARCH command @@ -274,6 +291,16 @@ impl ClientAuthenticated { } } +#[derive(Debug, Default)] +pub struct SelectResponse { + flags: Vec, + exists: Option, + recent: Option, + uid_next: Option, + uid_validity: Option, + unseen: Option, +} + /// A token that represents an idling connection. /// /// Dropping this token indicates that the idling should be completed, and the DONE command will be @@ -289,7 +316,8 @@ pub struct IdleToken { #[cfg_attr(docsrs, doc(cfg(feature = "rfc2177-idle")))] impl Drop for IdleToken { fn drop(&mut self) { - self.sender.send(format!("DONE\r\n")); + // TODO: should ignore this? + self.sender.send(format!("DONE\r\n")).unwrap(); } } diff --git a/migrations/1_initial.sql b/migrations/1_initial.sql new file mode 100644 index 0000000..aa20c24 --- /dev/null +++ b/migrations/1_initial.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS "accounts" ( + "id" INTEGER PRIMARY KEY, + -- hash of the account details, used to check if accounts have changed + "checksum" TEXT, + "name" TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS "mail" ( + "id" INTEGER PRIMARY KEY, + "account_id" INTEGER, + "folder" TEXT, + "uid" INTEGER, + + FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") +); diff --git a/src/mail/client.rs b/src/mail/client.rs index e8b707e..dd3491f 100644 --- a/src/mail/client.rs +++ b/src/mail/client.rs @@ -19,13 +19,14 @@ use tokio::{ use crate::config::{Config, ConfigWatcher, ImapAuth, MailAccountConfig, TlsMethod}; -use super::{MailCommand, MailEvent}; +use super::{MailCommand, MailEvent, MailStore}; -/// The main sequence of steps for the IMAP thread to follow -pub async fn imap_main( +/// The main function for the IMAP syncing thread +pub async fn sync_main( acct_name: impl AsRef, acct: MailAccountConfig, mail2ui_tx: UnboundedSender, + mail_store: MailStore, ) -> Result<()> { let acct_name = acct_name.as_ref().to_owned(); @@ -66,7 +67,8 @@ pub async fn imap_main( // let's just select INBOX for now, maybe have a config for default mailbox later? debug!("selecting the INBOX mailbox"); - authed.select("INBOX").await?; + let select = authed.select("INBOX").await?; + debug!("select result: {:?}", select); loop { let folder_list = authed.list().await?; diff --git a/src/mail/mod.rs b/src/mail/mod.rs index c01bbf3..1dbd8e8 100644 --- a/src/mail/mod.rs +++ b/src/mail/mod.rs @@ -29,6 +29,7 @@ use crate::config::{Config, ConfigWatcher, ImapAuth, MailAccountConfig, TlsMetho pub use self::event::MailEvent; pub use self::metadata::EmailMetadata; +pub use self::store::MailStore; /// Command sent to the mail thread by something else (i.e. UI) #[derive(Debug)] @@ -66,14 +67,23 @@ pub async fn run_mail( conn.abort(); } + let mail_store = MailStore::new().await?; for (acct_name, acct) in config.mail_accounts.into_iter() { let mail2ui_tx = mail2ui_tx.clone(); + let mail_store = mail_store.clone(); let handle = tokio::spawn(async move { // debug!("opening imap connection for {:?}", acct); // this loop is to make sure accounts are restarted on error loop { - match client::imap_main(&acct_name, acct.clone(), mail2ui_tx.clone()).await { + match client::sync_main( + &acct_name, + acct.clone(), + mail2ui_tx.clone(), + mail_store.clone(), + ) + .await + { Ok(_) => {} Err(err) => { error!("IMAP Error: {}", err); diff --git a/src/mail/store.rs b/src/mail/store.rs index a88edaf..ec74489 100644 --- a/src/mail/store.rs +++ b/src/mail/store.rs @@ -1,23 +1,49 @@ //! Package for managing the offline storage of emails +use std::path::PathBuf; + use anyhow::Result; -use sqlx::sqlite::SqlitePool; +use sqlx::{ + migrate::{MigrateDatabase, Migrator}, + sqlite::{Sqlite, SqlitePool}, +}; +use tokio::fs; + +static MIGRATOR: Migrator = sqlx::migrate!(); /// SQLite email manager +/// +/// This struct is clone-safe: cloning it will just return a reference to the same data structure #[derive(Clone)] pub struct MailStore { + mail_dir: PathBuf, pool: SqlitePool, } impl MailStore { + /// Creates a new MailStore pub async fn new() -> Result { - let pool = SqlitePool::connect("hellosu.db").await?; + let db_path = "sqlite:hellosu.db"; - let run = tokio::spawn(listen_loop(pool.clone())); + // create the database file if it doesn't already exist -_ - + if !Sqlite::database_exists(db_path).await? { + Sqlite::create_database(db_path).await?; + } + + let pool = SqlitePool::connect(db_path).await?; + MIGRATOR.run(&pool).await?; + debug!("run migrations : {:?}", MIGRATOR); + + let mail_dir = PathBuf::from("hellosu/"); + if !mail_dir.exists() { + fs::create_dir_all(&mail_dir).await?; + } + + Ok(MailStore { mail_dir, pool }) + } + + /// Gets the list of all the UIDs in the given folder that need to be updated + pub fn get_new_uids(&self, exists: u32) { - Ok(MailStore { pool }) } } - -async fn listen_loop(pool: SqlitePool) { -} diff --git a/src/main.rs b/src/main.rs index 1d5fa9f..dff22a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -127,7 +127,7 @@ fn setup_logger(log_file: Option>) -> Result<()> { .format(move |out, message, record| { out.finish(format_args!( "{}[{}][{}] {}", - now, + chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), record.target(), colors.color(record.level()), message