email-lib 0.27.0

Cross-platform, asynchronous Rust library to manage emails
Documentation
use std::{fs, path::Path};

use async_trait::async_trait;
use mail_parser::MessageParser;
use tracing::{debug, info, trace, warn};

use super::{Envelopes, ListEnvelopes, ListEnvelopesOptions};
use crate::{
    email::error::Error,
    envelope::Envelope,
    maildir::MaildirContextSync,
    search_query::{filter::SearchEmailsFilterQuery, SearchEmailsQuery},
    AnyResult,
};

#[cfg(test)]
static USER_TZ: &chrono::Utc = &chrono::Utc;
#[cfg(not(test))]
static USER_TZ: &chrono::Local = &chrono::Local;

#[derive(Clone)]
pub struct ListMaildirEnvelopes {
    ctx: MaildirContextSync,
}

impl ListMaildirEnvelopes {
    pub fn new(ctx: &MaildirContextSync) -> Self {
        Self { ctx: ctx.clone() }
    }

    pub fn new_boxed(ctx: &MaildirContextSync) -> Box<dyn ListEnvelopes> {
        Box::new(Self::new(ctx))
    }

    pub fn some_new_boxed(ctx: &MaildirContextSync) -> Option<Box<dyn ListEnvelopes>> {
        Some(Self::new_boxed(ctx))
    }
}

#[async_trait]
impl ListEnvelopes for ListMaildirEnvelopes {
    async fn list_envelopes(
        &self,
        folder: &str,
        opts: ListEnvelopesOptions,
    ) -> AnyResult<Envelopes> {
        info!("listing maildir envelopes from folder {folder}");

        let ctx = self.ctx.lock().await;
        let mdir = ctx.get_maildir_from_folder_alias(folder)?;

        let entries = mdir.read().map_err(Error::ListMaildirEntriesError)?;
        let mut envelopes = Envelopes::from_mdir_entries(entries, opts.query.as_ref());
        debug!("found {} maildir envelopes", envelopes.len());
        trace!("{envelopes:#?}");

        let page_begin = opts.page * opts.page_size;
        debug!("page begin: {}", page_begin);
        if page_begin > envelopes.len() {
            return Err(Error::GetEnvelopesOutOfBoundsMaildirError(
                folder.to_owned(),
                page_begin + 1,
            )
            .into());
        }

        let page_end = envelopes.len().min(if opts.page_size == 0 {
            envelopes.len()
        } else {
            page_begin + opts.page_size
        });
        debug!("page end: {}", page_end);

        opts.sort_envelopes(&mut envelopes);
        *envelopes = envelopes[page_begin..page_end].into();

        Ok(envelopes)
    }
}

impl SearchEmailsQuery {
    pub fn matches_maildir_search_query(&self, envelope: &Envelope, msg_path: &Path) -> bool {
        self.filter
            .as_ref()
            .map(|f| f.matches_maildir_search_query(envelope, msg_path))
            .unwrap_or(true)
    }
}

fn contains_ignore_ascii_case(haystack: &[u8], needle: &[u8]) -> bool {
    for window in haystack.windows(needle.len()) {
        if window.eq_ignore_ascii_case(needle) {
            return true;
        }
    }

    false
}

impl SearchEmailsFilterQuery {
    pub fn matches_maildir_search_query(&self, envelope: &Envelope, msg_path: &Path) -> bool {
        match self {
            SearchEmailsFilterQuery::And(left, right) => {
                let left = left.matches_maildir_search_query(envelope, msg_path);
                let right = right.matches_maildir_search_query(envelope, msg_path);
                left && right
            }
            SearchEmailsFilterQuery::Or(left, right) => {
                let left = left.matches_maildir_search_query(envelope, msg_path);
                let right = right.matches_maildir_search_query(envelope, msg_path);
                left || right
            }
            SearchEmailsFilterQuery::Not(filter) => {
                !filter.matches_maildir_search_query(envelope, msg_path)
            }
            SearchEmailsFilterQuery::Date(date) => {
                &envelope.date.with_timezone(USER_TZ).date_naive() == date
            }
            SearchEmailsFilterQuery::BeforeDate(date) => {
                &envelope.date.with_timezone(USER_TZ).date_naive() < date
            }
            SearchEmailsFilterQuery::AfterDate(date) => {
                &envelope.date.with_timezone(USER_TZ).date_naive() > date
            }
            SearchEmailsFilterQuery::From(pattern) => {
                let pattern = pattern.as_bytes();
                if let Some(name) = &envelope.from.name {
                    if contains_ignore_ascii_case(name.as_bytes(), pattern) {
                        return true;
                    }
                }
                contains_ignore_ascii_case(envelope.from.addr.as_bytes(), pattern)
            }
            SearchEmailsFilterQuery::To(pattern) => {
                let pattern = pattern.as_bytes();
                if let Some(name) = &envelope.to.name {
                    if contains_ignore_ascii_case(name.as_bytes(), pattern) {
                        return true;
                    }
                }
                contains_ignore_ascii_case(envelope.to.addr.as_bytes(), pattern)
            }
            SearchEmailsFilterQuery::Subject(pattern) => {
                contains_ignore_ascii_case(envelope.subject.as_bytes(), pattern.as_bytes())
            }
            SearchEmailsFilterQuery::Body(pattern) => match fs::read(msg_path) {
                Ok(contents) => {
                    if let Some(msg) = MessageParser::new().parse(&contents) {
                        for plain in msg.text_bodies() {
                            if contains_ignore_ascii_case(plain.contents(), pattern.as_bytes()) {
                                return true;
                            }
                        }
                        for html in msg.html_bodies() {
                            if contains_ignore_ascii_case(html.contents(), pattern.as_bytes()) {
                                return true;
                            }
                        }
                    }
                    false
                }
                Err(_err) => {
                    warn!("cannot find message at {msg_path:?}, skipping body filter");
                    trace!("{_err:?}");
                    true
                }
            },
            SearchEmailsFilterQuery::Flag(flag) => envelope.flags.contains(flag),
        }
    }
}