melib 0.8.6

library for e-mail clients and other e-mail applications
Documentation
/*
 * meli - melib
 *
 * Copyright 2019-2020 Manos Pitsidianakis
 *
 * This file is part of meli.
 *
 * meli is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * meli is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with meli. If not, see <http://www.gnu.org/licenses/>.
 */

use std::{borrow::Cow, convert::TryFrom};

pub use query_parser::query;
use Query::*;

use crate::utils::{
    datetime::{formats, UnixTimestamp},
    parsec::*,
};

#[derive(Clone, Debug, PartialEq, Serialize)]
pub enum Query {
    Before(UnixTimestamp),
    After(UnixTimestamp),
    Between(UnixTimestamp, UnixTimestamp),
    On(UnixTimestamp),
    /* * * * */
    From(String),
    To(String),
    Cc(String),
    Bcc(String),
    InReplyTo(String),
    References(String),
    AllAddresses(String),
    /* * * * */
    Body(String),
    Subject(String),
    AllText(String),
    /* * * * */
    Flags(Vec<String>),
    HasAttachment,
    And(Box<Query>, Box<Query>),
    Or(Box<Query>, Box<Query>),
    Not(Box<Query>),
    /// By us.
    Answered,
    /// By an address/name.
    AnsweredBy {
        by: String,
    },
    Larger {
        than: usize,
    },
    Smaller {
        than: usize,
    },
}

pub trait QueryTrait {
    fn is_match(&self, query: &Query) -> bool;
}

impl QueryTrait for crate::Envelope {
    fn is_match(&self, query: &Query) -> bool {
        use Query::*;
        match query {
            Before(timestamp) => self.date() < *timestamp,
            After(timestamp) => self.date() > *timestamp,
            Between(timestamp_a, timestamp_b) => {
                self.date() > *timestamp_a && self.date() < *timestamp_b
            }
            On(timestamp) => {
                self.date() > timestamp.saturating_sub(60 * 60 * 24)
                    && self.date() < *timestamp + 60 * 60 * 24
            }
            From(s) => self.other_headers()["From"].contains(s),
            To(s) => self.other_headers()["To"].contains(s),
            Cc(s) => self.other_headers()["Cc"].contains(s),
            Bcc(s) => self.other_headers()["Bcc"].contains(s),
            AllAddresses(s) => {
                self.is_match(&From(s.clone()))
                    || self.is_match(&To(s.clone()))
                    || self.is_match(&Cc(s.clone()))
                    || self.is_match(&Bcc(s.clone()))
            }
            Flags(v) => v.iter().any(|s| self.flags() == s.as_str()),
            Subject(s) => self.other_headers()["Subject"].contains(s),
            HasAttachment => self.has_attachments(),
            And(q_a, q_b) => self.is_match(q_a) && self.is_match(q_b),
            Or(q_a, q_b) => self.is_match(q_a) || self.is_match(q_b),
            Not(q) => !self.is_match(q),
            InReplyTo(_) => {
                log::warn!("Filtering with InReplyTo is unimplemented.");
                false
            }
            References(_) => {
                log::warn!("Filtering with References is unimplemented.");
                false
            }
            AllText(_) => {
                log::warn!("Filtering with AllText is unimplemented.");
                false
            }
            Body(_) => {
                log::warn!("Filtering with Body is unimplemented.");
                false
            }
            Answered => {
                log::warn!("Filtering with Answered is unimplemented.");
                false
            }
            AnsweredBy { .. } => {
                log::warn!("Filtering with AnsweredBy is unimplemented.");
                false
            }
            Larger { .. } => {
                log::warn!("Filtering with Larger is unimplemented.");
                false
            }
            Smaller { .. } => {
                log::warn!("Filtering with Smaller is unimplemented.");
                false
            }
        }
    }
}

impl TryFrom<&str> for Query {
    type Error = crate::error::Error;
    fn try_from(t: &str) -> crate::error::Result<Self> {
        query()
            .parse_complete(t)
            .map(|(_, q)| q)
            .map_err(|err| err.into())
    }
}

pub mod query_parser {
    use super::*;

    fn date<'a>() -> impl Parser<'a, UnixTimestamp> {
        move |input| {
            literal().parse(input).and_then(|(next_input, result)| {
                if let Ok((_, t)) = crate::utils::datetime::parse_timestamp_from_string(
                    result,
                    formats::RFC3339_DATE,
                ) {
                    Ok((next_input, t))
                } else {
                    Err(next_input)
                }
            })
        }
    }

    fn before<'a>() -> impl Parser<'a, Query> {
        prefix(
            whitespace_wrap(match_literal("before:")),
            whitespace_wrap(date()),
        )
        .map(Query::Before)
    }

    fn after<'a>() -> impl Parser<'a, Query> {
        prefix(
            whitespace_wrap(match_literal("after:")),
            whitespace_wrap(date()),
        )
        .map(Query::After)
    }

    fn between<'a>() -> impl Parser<'a, Query> {
        prefix(
            whitespace_wrap(match_literal("between:")),
            pair(
                suffix(whitespace_wrap(date()), whitespace_wrap(match_literal(","))),
                whitespace_wrap(date()),
            ),
        )
        .map(|(t1, t2)| Query::Between(t1, t2))
    }

    fn on<'a>() -> impl Parser<'a, Query> {
        prefix(
            whitespace_wrap(match_literal("on:")),
            whitespace_wrap(date()),
        )
        .map(Query::After)
    }

    fn smaller<'a>() -> impl Parser<'a, Query> {
        prefix(
            whitespace_wrap(match_literal("smaller:")),
            whitespace_wrap(integer()),
        )
        .map(|than| Query::Smaller { than })
    }

    fn larger<'a>() -> impl Parser<'a, Query> {
        prefix(
            whitespace_wrap(match_literal("larger:")),
            whitespace_wrap(integer()),
        )
        .map(|than| Query::Larger { than })
    }

    fn answered_by<'a>() -> impl Parser<'a, Query> {
        prefix(
            whitespace_wrap(match_literal("answered-by:")),
            whitespace_wrap(literal()),
        )
        .map(|by| Query::AnsweredBy { by })
    }

    fn answered<'a>() -> impl Parser<'a, Query> {
        move |input| {
            whitespace_wrap(match_literal_anycase("answered"))
                .map(|()| Query::Answered)
                .parse(input)
        }
    }

    fn subject<'a>() -> impl Parser<'a, Query> {
        prefix(
            whitespace_wrap(match_literal("subject:")),
            whitespace_wrap(literal()),
        )
        .map(Query::Subject)
    }

    fn from<'a>() -> impl Parser<'a, Query> {
        prefix(
            whitespace_wrap(match_literal("from:")),
            whitespace_wrap(literal()),
        )
        .map(Query::From)
    }

    fn to<'a>() -> impl Parser<'a, Query> {
        prefix(
            whitespace_wrap(match_literal("to:")),
            whitespace_wrap(literal()),
        )
        .map(Query::To)
    }

    fn cc<'a>() -> impl Parser<'a, Query> {
        prefix(
            whitespace_wrap(match_literal("cc:")),
            whitespace_wrap(literal()),
        )
        .map(Query::Cc)
    }

    fn bcc<'a>() -> impl Parser<'a, Query> {
        prefix(
            whitespace_wrap(match_literal("bcc:")),
            whitespace_wrap(literal()),
        )
        .map(Query::Bcc)
    }

    fn all_addresses<'a>() -> impl Parser<'a, Query> {
        prefix(
            whitespace_wrap(match_literal("all-addresses:")),
            whitespace_wrap(literal()),
        )
        .map(Query::AllAddresses)
    }

    fn or<'a>() -> impl Parser<'a, Query> {
        move |input| {
            whitespace_wrap(match_literal_anycase("or"))
                .parse(input)
                .and_then(|(last_input, _)| query().parse(debug!(last_input)))
        }
    }

    fn not<'a>() -> impl Parser<'a, Query> {
        move |input| {
            whitespace_wrap(either(
                match_literal_anycase("not"),
                match_literal_anycase("!"),
            ))
            .parse(input)
            .and_then(|(last_input, _)| query().parse(debug!(last_input)))
        }
    }

    fn and<'a>() -> impl Parser<'a, Query> {
        move |input| {
            whitespace_wrap(match_literal_anycase("and"))
                .parse(input)
                .and_then(|(last_input, _)| query().parse(debug!(last_input)))
        }
    }

    fn has_attachment<'a>() -> impl Parser<'a, Query> {
        move |input| {
            whitespace_wrap(match_literal_anycase("has:attachment"))
                .map(|()| Query::HasAttachment)
                .parse(input)
        }
    }

    fn literal<'a>() -> impl Parser<'a, String> {
        move |input| either(quoted_string(), string()).parse(input)
    }

    fn parentheses_query<'a>() -> impl Parser<'a, Query> {
        move |input| {
            delimited(
                whitespace_wrap(match_literal("(")),
                whitespace_wrap(query()),
                whitespace_wrap(match_literal(")")),
            )
            .parse(input)
        }
    }

    fn flags<'a>() -> impl Parser<'a, Query> {
        move |input| {
            whitespace_wrap(either(
                either(
                    match_literal_anycase("flags:"),
                    match_literal_anycase("tags:"),
                ),
                match_literal_anycase("is:"),
            ))
            .parse(input)
            .and_then(|(rest, _)| {
                map(one_or_more(pred(any_char, |c| *c != ' ')), |chars| {
                    chars.into_iter().collect::<String>()
                })
                .parse(rest)
            })
            .and_then(|(rest, flags_list)| {
                if let Ok(r) = flags_list
                    .split(',')
                    .map(|t| {
                        either(quoted_string(), string())
                            .parse_complete(t)
                            .map(|(_, r)| r)
                    })
                    .collect::<std::result::Result<Vec<String>, &str>>()
                    .map(Flags)
                {
                    Ok((rest, r))
                } else {
                    Err(rest)
                }
            })
        }
    }

    /// Parser from `String` to `Query`.
    ///
    /// # Invocation
    /// ```
    /// use melib::{
    ///     search::{query, Query},
    ///     utils::parsec::Parser,
    /// };
    ///
    /// let input = "test";
    /// let query = query().parse(input);
    /// assert_eq!(Ok(("", Query::AllText("test".to_string()))), query);
    /// ```
    pub fn query<'a>() -> impl Parser<'a, Query> {
        move |input| {
            let (rest, query_a): (&'a str, Query) = if let Ok(q) = parentheses_query()
                .parse(input)
                .or_else(|_| from().parse(input))
                .or_else(|_| to().parse(input))
                .or_else(|_| cc().parse(input))
                .or_else(|_| bcc().parse(input))
                .or_else(|_| all_addresses().parse(input))
                .or_else(|_| subject().parse(input))
                .or_else(|_| before().parse(input))
                .or_else(|_| after().parse(input))
                .or_else(|_| on().parse(input))
                .or_else(|_| between().parse(input))
                .or_else(|_| flags().parse(input))
                .or_else(|_| answered().parse(input))
                .or_else(|_| answered_by().parse(input))
                .or_else(|_| larger().parse(input))
                .or_else(|_| smaller().parse(input))
                .or_else(|_| has_attachment().parse(input))
            {
                Ok(q)
            } else if let Ok((rest, query_a)) = not().parse(input) {
                Ok((rest, Not(Box::new(query_a))))
            } else if let Ok((rest, query_a)) = {
                let result = literal().parse(input);
                if result.is_ok()
                    && result
                        .as_ref()
                        .map(|(_, s)| s != "and" && s != "or" && s != "not")
                        .unwrap_or(false)
                {
                    result.map(|(r, s)| (r, AllText(s)))
                } else {
                    Err("")
                }
            } {
                Ok((rest, query_a))
            } else {
                Err("")
            }?;
            if rest.is_empty() {
                return Ok((rest, query_a));
            }

            if let Ok((rest, query_b)) = and().parse(rest) {
                Ok((rest, And(Box::new(query_a), Box::new(query_b))))
            } else if let Ok((rest, query_b)) = or().parse(rest) {
                Ok((rest, Or(Box::new(query_a), Box::new(query_b))))
            } else if let Ok((rest, query_b)) = query().parse(rest) {
                Ok((rest, And(Box::new(query_a), Box::new(query_b))))
            } else {
                Ok((rest, query_a))
            }
        }
    }
}

#[inline(always)]
pub fn escape_double_quote(w: &str) -> Cow<str> {
    if w.contains('"') {
        Cow::from(w.replace('"', "\"\""))
    } else {
        Cow::from(w)
    }
}

use serde::{de, Deserialize, Deserializer};
impl<'de> Deserialize<'de> for Query {
    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = <String>::deserialize(deserializer)?;
        let ret = query()
            .parse(&s)
            .map(|(_, q)| q)
            .map_err(|err| de::Error::custom(format!("invalid query value: {}", err)));
        ret
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_query_parsing() {
        assert_eq!(
            Err("subject:test and"),
            query().parse_complete("subject:test and")
        );
        assert_eq!(
            Ok((
                "",
                And(
                    Box::new(Subject("test".to_string())),
                    Box::new(AllText("i".to_string()))
                )
            )),
            query().parse_complete("subject:test and i")
        );
        assert_eq!(
            Ok(("", AllText("test".to_string()))),
            query().parse_complete("test")
        );
        assert_eq!(
            Ok(("", Subject("test".to_string()))),
            query().parse_complete("subject:test")
        );
        assert_eq!(
            Ok((
                "",
                And(
                    Box::new(From("Manos".to_string())),
                    Box::new(From("Sia".to_string()))
                )
            )),
            query().parse_complete("from:Manos and from:Sia")
        );
        assert_eq!(
            Ok((
                "",
                Or(
                    Box::new(Subject("wah ah ah".to_string())),
                    Box::new(And(
                        Box::new(From("Manos".to_string())),
                        Box::new(From("Sia".to_string()))
                    ))
                )
            )),
            query().parse_complete("subject:\"wah ah ah\" or (from:Manos and from:Sia)")
        );
        assert_eq!(
            Ok((
                "",
                Or(
                    Box::new(Subject("wah".to_string())),
                    Box::new(And(
                        Box::new(From("Manos".to_string())),
                        Box::new(Or(
                            Box::new(Subject("foo".to_string())),
                            Box::new(Subject("bar".to_string())),
                        ))
                    ))
                )
            )),
            query().parse_complete("subject:wah or (from:Manos and (subject:foo or subject:bar))")
        );
        assert_eq!(
            Ok((
                "",
                And(
                    Box::new(From("Manos".to_string())),
                    Box::new(And(
                        Box::new(Or(
                            Box::new(Subject("foo".to_string())),
                            Box::new(Subject("bar".to_string()))
                        )),
                        Box::new(Or(
                            Box::new(From("woo".to_string())),
                            Box::new(From("my".to_string()))
                        ))
                    ))
                )
            )),
            query().parse_complete(
                "(from:Manos and (subject:foo or subject:bar) and (from:woo or from:my))"
            )
        );
        assert_eq!(
            Ok(("", Flags(vec!["test".to_string(), "testtest".to_string()]))),
            query().parse_complete("flags:test,testtest")
        );
        assert_eq!(
            query().parse_complete("flags:test,testtest"),
            query().parse_complete("tags:test,testtest")
        );
        assert_eq!(
            query().parse_complete("flags:seen"),
            query().parse_complete("tags:seen")
        );
        assert_eq!(
            query().parse_complete("is:unseen"),
            query().parse_complete("tags:unseen")
        );
        assert_eq!(
            Ok(("", Flags(vec!["f".to_string()]))),
            query().parse_complete("tags:f")
        );
    }
}