Initial Maildir importer code

pull/35/head
Benedikt Terhechte 2 years ago
parent 65d0ff4d33
commit 33dab3b4b4

63
Cargo.lock generated

@ -187,6 +187,16 @@ dependencies = [
"libc",
]
[[package]]
name = "charset"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18e9079d1a12a2cc2bffb5db039c43661836ead4082120d5844f02555aca2d46"
dependencies = [
"base64",
"encoding_rs",
]
[[package]]
name = "chrono"
version = "0.4.19"
@ -659,6 +669,15 @@ dependencies = [
"thiserror",
]
[[package]]
name = "encoding_rs"
version = "0.8.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df"
dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "epaint"
version = "0.15.0"
@ -736,6 +755,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "gethostname"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e692e296bfac1d2533ef168d0b60ff5897b8b70a4009276834014dd8924cc028"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "getrandom"
version = "0.2.3"
@ -1054,6 +1083,27 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "maildir"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6adcf4693a3c725b9c69ccf24f8ed9c6d3e7168c1a45632570d65529adc13b5e"
dependencies = [
"gethostname",
"mailparse",
]
[[package]]
name = "mailparse"
version = "0.13.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d70ae0840b192a2f7d1dc46e75f38720a7e3c52dfdc968ba3202fa270668dc67"
dependencies = [
"base64",
"charset",
"quoted_printable",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
@ -1557,7 +1607,7 @@ dependencies = [
[[package]]
name = "postsack"
version = "1.0.0"
version = "1.0.1"
dependencies = [
"ps-core",
"ps-database",
@ -1669,6 +1719,7 @@ dependencies = [
"email-parser",
"emlx",
"flate2",
"maildir",
"mbox-reader",
"once_cell",
"ps-core",
@ -1697,6 +1748,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "quoted_printable"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fee2dce59f7a43418e3382c766554c614e06a552d53a8f07ef499ea4b332c0f"
[[package]]
name = "rand"
version = "0.8.4"
@ -2109,9 +2166,9 @@ dependencies = [
[[package]]
name = "tinyfiledialogs"
version = "3.8.3"
version = "3.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9545b2375cbcb7a7d70cca5e92fbaa096fd89bebd2fbc54a3da7f37d15a54e6b"
checksum = "adc577626a3c26e4e1d470dbe5fe33d6fabc14e57114cb377acdb4da1a17dde9"
dependencies = [
"cc",
"libc",

@ -16,6 +16,7 @@ pub enum FormatType {
AppleMail,
GmailVault,
Mbox,
Maildir,
}
impl FormatType {
@ -28,6 +29,7 @@ impl FormatType {
FormatType::AppleMail => "Apple Mail",
FormatType::GmailVault => "Gmail Vault Download",
FormatType::Mbox => "Mbox",
FormatType::Maildir => "Maildir",
}
}
}
@ -40,7 +42,7 @@ impl Default for FormatType {
return FormatType::AppleMail;
#[cfg(not(target_os = "macos"))]
return FormatType::Mbox;
return FormatType::Maildir;
}
}
@ -56,6 +58,7 @@ impl From<&str> for FormatType {
"apple" => FormatType::AppleMail,
"gmailvault" => FormatType::GmailVault,
"mbox" => FormatType::Mbox,
"maildir" => FormatType::Maildir,
_ => panic!("Unknown format: {}", &format),
}
}
@ -67,6 +70,7 @@ impl From<FormatType> for String {
FormatType::AppleMail => "apple".to_owned(),
FormatType::GmailVault => "gmailvault".to_owned(),
FormatType::Mbox => "mbox".to_owned(),
FormatType::Maildir => "maildir".to_owned(),
}
}
}

@ -90,6 +90,10 @@ impl ImporterUI {
let importer = ps_importer::mbox_importer(config);
adapter.process(database, importer)?
}
FormatType::Maildir => {
let importer = ps_importer::maildir_importer(config);
adapter.process(database, importer)?
}
};
#[cfg(target_arch = "wasm32")]

@ -16,4 +16,5 @@ mbox-reader = "0.2.0"
shellexpand = "2.1.0"
serde_json = "1.0.70"
serde = { version = "1.0.131", features = ["derive"]}
ps-core = { path = "../ps-core" }
ps-core = { path = "../ps-core" }
maildir = "0.5.0"

@ -0,0 +1,168 @@
use ps_core::eyre::eyre;
use ps_core::tracing;
use rayon::prelude::*;
use walkdir::{DirEntry, WalkDir};
use super::{Config, ImporterFormat, Message, MessageSender, Result};
use super::shared::parse::ParseableEmail;
use maildir;
use ps_core::EmailMeta;
use std::borrow::Cow;
use std::path::{Path, PathBuf};
pub struct Mail {
path: PathBuf,
data: Vec<u8>,
is_seen: bool,
}
#[derive(Default)]
pub struct Maildir;
/// The folder finding code:
/// Find all folders that contain a `cur` or `new` folder
fn inner_folders(config: &Config, sender: MessageSender) -> Result<Vec<PathBuf>> {
// Configure a walkdir entry to:
// - go recursively
// - follow directories starting with a `.`
// - follow the root directory
// - ignore everything else
fn take_entry(entry: &DirEntry, root_path: &Path) -> bool {
let is_dir = entry.path().is_dir();
let contains_dot = entry
.file_name()
.to_str()
.map(|s| s.starts_with('.'))
.unwrap_or(false);
if is_dir && contains_dot {
return true;
}
return entry.path() == root_path;
}
let root_path = config.emails_folder_path.as_path();
let iter = WalkDir::new(&config.emails_folder_path)
.into_iter()
.filter_entry(|e| take_entry(e, root_path));
let folders: Vec<PathBuf> = iter
.filter_map(|e| match e {
Ok(n) if n.path().is_dir() => {
// Check if this folder contains a cur or new
let contents = std::fs::read_dir(n.path()).ok()?;
for entry in contents {
if let Ok(dir_entry) = entry {
if let Some(Some(name)) = dir_entry.path().file_name().map(|e| e.to_str()) {
if name == "cur" || name == "new" {
tracing::trace!("Found folder {}", n.path().display());
return Some(n.path().to_path_buf());
}
}
}
}
None
}
Err(e) => {
tracing::info!("Could not read folder: {}", e);
if let Err(e) = sender.send(Message::Error(eyre!("Could not read folder: {:?}", e)))
{
tracing::error!("Error sending error {}", e);
}
None
}
_ => None,
})
.collect();
Ok(folders)
}
/// The inner email parsing code
fn inner_emails(path: &PathBuf, sender: MessageSender) -> Result<Vec<Mail>> {
let maildir = maildir::Maildir::from(path.clone());
let new_mails = maildir.list_new();
let cur_mails = maildir.list_cur();
tracing::info!("Finding maildirs in {}", path.display());
let parsed_mails = new_mails
.chain(cur_mails)
.filter_map(|m| {
let mail_entry = match m {
Ok(n) => n,
Err(e) => {
//tracing::error!("Could not parse mail: {}", e);
if let Err(e) = sender.send(Message::Error(eyre!("Could parse mail: {:?}", e)))
{
tracing::error!("Error sending error {}", e);
}
return None;
}
};
Some((mail_entry.path().clone(), mail_entry.is_seen()))
})
.par_bridge()
.filter_map(|(path, seen)| {
let data = match std::fs::read(&path) {
Ok(n) => n,
Err(e) => {
tracing::error!("Could not read mail {}: {}", path.display(), e);
if let Err(e) = sender.send(Message::Error(eyre!(
"Could not read mail {}: {}",
path.display(),
e
))) {
tracing::error!("Error sending error {}", e);
}
return None;
}
};
Some(Mail {
path: path.clone(),
is_seen: seen,
data,
})
})
.collect();
Ok(parsed_mails)
}
impl ImporterFormat for Maildir {
type Item = Mail;
fn default_path() -> Option<std::path::PathBuf> {
None
}
fn emails(&self, config: &Config, sender: MessageSender) -> Result<Vec<Self::Item>> {
// First get all the folders containing maildirs
let folders = inner_folders(config, sender.clone())?;
let mails = folders
.par_iter()
.filter_map(|folder| inner_emails(folder, sender.clone()).ok())
.flatten()
.collect();
Ok(mails)
}
}
impl ParseableEmail for Mail {
fn prepare(&mut self) -> Result<()> {
Ok(())
}
fn message(&self) -> Result<Cow<'_, [u8]>> {
Ok(Cow::Borrowed(self.data.as_slice()))
}
fn path(&self) -> &Path {
self.path.as_path()
}
fn meta(&self) -> Result<Option<EmailMeta>> {
Ok(Some(EmailMeta {
tags: Vec::new(),
is_seen: self.is_seen,
}))
}
}

@ -4,11 +4,13 @@ pub use ps_core::eyre::Result;
mod apple_mail;
mod gmailbackup;
mod maildir_mail;
mod mbox;
pub mod shared;
pub use apple_mail::AppleMail;
pub use gmailbackup::Gmail;
pub use maildir_mail::Maildir;
pub use mbox::Mbox;
use shared::parse::ParseableEmail;

@ -88,10 +88,15 @@ pub fn mbox_importer(config: Config) -> Importer<formats::Mbox> {
Importer::new(config, formats::Mbox::default())
}
pub fn maildir_importer(config: Config) -> Importer<formats::Maildir> {
Importer::new(config, formats::Maildir::default())
}
pub fn default_path(format: &FormatType) -> Option<PathBuf> {
match format {
FormatType::AppleMail => formats::AppleMail::default_path(),
FormatType::GmailVault => formats::Gmail::default_path(),
FormatType::Mbox => formats::Mbox::default_path(),
FormatType::Maildir => formats::Maildir::default_path(),
}
}

Loading…
Cancel
Save