mirror of https://github.com/terhechte/postsack
Initial Maildir importer code
parent
65d0ff4d33
commit
33dab3b4b4
@ -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,
|
||||
}))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue