diff --git a/Cargo.lock b/Cargo.lock index 34a76dd..70740d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1954,6 +1954,7 @@ dependencies = [ "format-bytes", "futures 0.3.13", "gluon", + "hex 0.4.3", "inotify", "log", "notify-rust", @@ -1962,6 +1963,7 @@ dependencies = [ "parking_lot", "quoted_printable", "serde", + "sha2", "sqlx", "structopt", "tokio 1.3.0", diff --git a/Cargo.toml b/Cargo.toml index 7b402a6..92a5c09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,8 @@ xdg = "2.2.0" downcast-rs = "1.2.0" quoted_printable = "0.4.2" sqlx = { version = "0.5.1", features = ["runtime-tokio-rustls", "sqlite"] } +sha2 = "0.9.3" +hex = "0.4.3" [dependencies.panorama-imap] path = "imap" diff --git a/imap/src/parser/mod.rs b/imap/src/parser/mod.rs index bb10518..35f8965 100644 --- a/imap/src/parser/mod.rs +++ b/imap/src/parser/mod.rs @@ -184,7 +184,7 @@ fn build_msg_att_static(pair: Pair) -> AttributeValue { Rule::number => Some(build_number(unwrap1(pairs.next().unwrap()))), _ => None, }; - let data = Some(pairs.next().unwrap().as_str().to_owned()); + let data = build_nstring(pairs.next().unwrap()); AttributeValue::BodySection(BodySection { section, index, diff --git a/migrations/1_initial.sql b/migrations/1_initial.sql index 02cb0cc..13a0d2e 100644 --- a/migrations/1_initial.sql +++ b/migrations/1_initial.sql @@ -8,5 +8,7 @@ CREATE TABLE IF NOT EXISTS "mail" ( "id" INTEGER PRIMARY KEY, "account" TEXT, "folder" TEXT, - "uid" INTEGER + "uidvalidity" INTEGER, + "uid" INTEGER, + "filename" TEXT ); diff --git a/src/mail/client.rs b/src/mail/client.rs index f8e6a7a..6a9869e 100644 --- a/src/mail/client.rs +++ b/src/mail/client.rs @@ -73,14 +73,16 @@ pub async fn sync_main( let select = authed.select(folder).await?; debug!("select response: {:?}", select); - if let Some(exists) = select.exists { + if let (Some(exists), Some(uidvalidity)) = (select.exists, select.uid_validity) { if exists < 10 { let mut fetched = authed .uid_fetch(&(1..=exists).collect::>(), FetchItems::PanoramaAll) .await?; while let Some((uid, attrs)) = fetched.next().await { debug!("- {} : {:?}", uid, attrs); - mail_store.store_email(&acct_name, &folder, uid).await?; + mail_store + .store_email(&acct_name, &folder, uid, uidvalidity, attrs) + .await?; } } } diff --git a/src/mail/store.rs b/src/mail/store.rs index 26d509c..b614db5 100644 --- a/src/mail/store.rs +++ b/src/mail/store.rs @@ -3,9 +3,12 @@ use std::path::PathBuf; use anyhow::Result; +use panorama_imap::response::AttributeValue; +use sha2::{Digest, Sha256, Sha512}; use sqlx::{ migrate::{MigrateDatabase, Migrator}, sqlite::{Sqlite, SqlitePool}, + Error, }; use tokio::fs; @@ -51,19 +54,65 @@ impl MailStore { acct: impl AsRef, folder: impl AsRef, uid: u32, + uidvalidity: u32, + attrs: Vec, ) -> Result<()> { - let id = sqlx::query(STORE_EMAIL_SQL) + let mut body = None; + for attr in attrs { + if let AttributeValue::BodySection(body_attr) = attr { + body = body_attr.data; + } + } + + let body = match body { + Some(v) => v, + None => return Ok(()), + }; + + let mut hasher = Sha256::new(); + hasher.update(body.as_bytes()); + let hash = hasher.finalize(); + let filename = format!("{}.mail", hex::encode(hash)); + let path = self.mail_dir.join(&filename); + fs::write(path, body).await?; + + let existing = sqlx::query( + r#" + SELECT FROM "mail" + WHERE account = ? AND folder = ? + AND uid = ? AND uidvalidity = ? + "#, + ) + .bind(acct.as_ref()) + .bind(folder.as_ref()) + .bind(uid) + .bind(uidvalidity) + .fetch_one(&self.pool) + .await; + + let exists = match existing { + Ok(_) => true, + Err(Error::RowNotFound) => true, + _ => false, + }; + + if !exists { + let id = sqlx::query( + r#" + INSERT INTO "mail" (account, folder, uid, uidvalidity, filename) + VALUES (?, ?, ?, ?, ?) + "#, + ) .bind(acct.as_ref()) .bind(folder.as_ref()) .bind(uid) + .bind(uidvalidity) + .bind(filename) .execute(&self.pool) .await? .last_insert_rowid(); + } + Ok(()) } } - -const STORE_EMAIL_SQL: &str = r#" -INSERT INTO "mail" (account, folder, uid) -VALUES (?, ?, ?) -"#;