melib/search: implement more search criteria in Query type
parent
23d95973d4
commit
6bf1756de8
|
@ -24,7 +24,7 @@
|
|||
use std::collections::VecDeque;
|
||||
|
||||
use crate::{
|
||||
datetime::{timestamp_to_string, IMAP_DATE},
|
||||
datetime::{formats::IMAP_DATE, timestamp_to_string},
|
||||
search::*,
|
||||
};
|
||||
|
||||
|
@ -41,8 +41,11 @@ impl private::Sealed for Query {}
|
|||
|
||||
macro_rules! space_pad {
|
||||
($s:ident) => {{
|
||||
if !$s.is_empty() {
|
||||
if !$s.is_empty() && !$s.ends_with('(') && !$s.ends_with(' ') {
|
||||
$s.push(' ');
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
@ -65,36 +68,43 @@ impl ToImapSearch for Query {
|
|||
s.push(lit);
|
||||
}
|
||||
Q(Subject(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("SUBJECT \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Q(From(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("FROM \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Q(To(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("TO \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Q(Cc(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("CC \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Q(Bcc(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("BCC \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Q(AllText(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("TEXT \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Q(Flags(v)) => {
|
||||
space_pad!(s);
|
||||
for f in v {
|
||||
match f.as_str() {
|
||||
"draft" => {
|
||||
|
@ -130,21 +140,26 @@ impl ToImapSearch for Query {
|
|||
}
|
||||
}
|
||||
Q(And(q1, q2)) => {
|
||||
space_pad!(s);
|
||||
let is_empty = space_pad!(s);
|
||||
if !is_empty {
|
||||
stack.push_front(Lit(')'));
|
||||
}
|
||||
stack.push_front(Q(q2));
|
||||
stack.push_front(Lit(' '));
|
||||
stack.push_front(Q(q1));
|
||||
if !is_empty {
|
||||
stack.push_front(Lit('('));
|
||||
}
|
||||
}
|
||||
Q(Or(q1, q2)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("OR ");
|
||||
s.push_str("OR");
|
||||
stack.push_front(Q(q2));
|
||||
stack.push_front(Lit(' '));
|
||||
stack.push_front(Q(q1));
|
||||
}
|
||||
Q(Not(q)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("NOT ");
|
||||
s.push_str("NOT (");
|
||||
stack.push_front(Lit(')'));
|
||||
stack.push_front(Q(q));
|
||||
}
|
||||
Q(Before(t)) => {
|
||||
|
@ -182,8 +197,23 @@ impl ToImapSearch for Query {
|
|||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Q(AllAddresses(_)) => {
|
||||
// From OR To OR Cc OR Bcc
|
||||
Q(AllAddresses(t)) => {
|
||||
let is_empty = space_pad!(s);
|
||||
if !is_empty {
|
||||
s.push('(');
|
||||
}
|
||||
s.push_str("OR FROM \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push_str("\" (OR TO \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push_str("\" (OR CC \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push_str("\" BCC \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push_str(r#""))"#);
|
||||
if !is_empty {
|
||||
s.push(')');
|
||||
}
|
||||
}
|
||||
Q(Body(t)) => {
|
||||
space_pad!(s);
|
||||
|
@ -192,10 +222,33 @@ impl ToImapSearch for Query {
|
|||
s.push('"');
|
||||
}
|
||||
Q(HasAttachment) => {
|
||||
// ???
|
||||
log::warn!("HasAttachment in IMAP is unimplemented.");
|
||||
}
|
||||
Q(Answered) => {
|
||||
space_pad!(s);
|
||||
s.push_str(r#"ANSWERED ""#);
|
||||
}
|
||||
Q(AnsweredBy { by }) => {
|
||||
space_pad!(s);
|
||||
s.push_str(r#"HEADER "From" ""#);
|
||||
s.extend(escape_double_quote(by).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Q(Larger { than }) => {
|
||||
space_pad!(s);
|
||||
s.push_str("LARGER ");
|
||||
s.push_str(&than.to_string());
|
||||
}
|
||||
Q(Smaller { than }) => {
|
||||
space_pad!(s);
|
||||
s.push_str("SMALLER ");
|
||||
s.push_str(&than.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
while s.ends_with(' ') {
|
||||
s.pop();
|
||||
}
|
||||
s
|
||||
}
|
||||
}
|
||||
|
@ -231,5 +284,28 @@ mod tests {
|
|||
×tamp_to_string(1685739600, Some(IMAP_DATE), true),
|
||||
"03-Jun-2023"
|
||||
);
|
||||
|
||||
let (_, q) = query()
|
||||
.parse_complete("before:2023-06-04 from:user@example.org")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
&q.to_imap_search(),
|
||||
r#"BEFORE 04-Jun-2023 FROM "user@example.org""#
|
||||
);
|
||||
let (_, q) = query()
|
||||
.parse_complete(r#"subject:"wah ah ah" or (from:Manos and from:Sia)"#)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
&q.to_imap_search(),
|
||||
r#"OR SUBJECT "wah ah ah" (FROM "Manos" FROM "Sia")"#
|
||||
);
|
||||
|
||||
let (_, q) = query()
|
||||
.parse_complete(r#"subject:wo or (all-addresses:Manos)"#)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
&q.to_imap_search(),
|
||||
r#"OR SUBJECT "wo" (OR FROM "Manos" (OR TO "Manos" (OR CC "Manos" BCC "Manos")))"#
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
*/
|
||||
|
||||
use super::*;
|
||||
use crate::datetime;
|
||||
|
||||
impl MboxFormat {
|
||||
pub fn append(
|
||||
|
@ -49,9 +50,9 @@ impl MboxFormat {
|
|||
}
|
||||
writer.write_all(&b" "[..])?;
|
||||
writer.write_all(
|
||||
crate::datetime::timestamp_to_string(
|
||||
delivery_date.unwrap_or_else(crate::datetime::now),
|
||||
Some(crate::datetime::ASCTIME_FMT),
|
||||
datetime::timestamp_to_string(
|
||||
delivery_date.unwrap_or_else(datetime::now),
|
||||
Some(datetime::formats::ASCTIME_FMT),
|
||||
true,
|
||||
)
|
||||
.trim()
|
||||
|
|
|
@ -1258,6 +1258,10 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
|
|||
q.query_to_string(ret);
|
||||
ret.push_str("))");
|
||||
}
|
||||
Answered => todo!(),
|
||||
AnsweredBy { .. } => todo!(),
|
||||
Larger { .. } => todo!(),
|
||||
Smaller { .. } => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ use xdg_utils::query_mime_info;
|
|||
|
||||
use super::*;
|
||||
use crate::{
|
||||
datetime,
|
||||
email::{
|
||||
attachment_types::{Charset, ContentTransferEncoding, ContentType, MultipartType},
|
||||
attachments::AttachmentBuilder,
|
||||
|
@ -61,9 +62,9 @@ impl Default for Draft {
|
|||
let mut headers = HeaderMap::default();
|
||||
headers.insert(
|
||||
HeaderName::DATE,
|
||||
crate::datetime::timestamp_to_string(
|
||||
crate::datetime::now(),
|
||||
Some(crate::datetime::RFC822_DATE),
|
||||
datetime::timestamp_to_string(
|
||||
datetime::now(),
|
||||
Some(datetime::formats::RFC822_DATE),
|
||||
true,
|
||||
),
|
||||
);
|
||||
|
|
|
@ -24,7 +24,10 @@ use std::{borrow::Cow, convert::TryFrom};
|
|||
pub use query_parser::query;
|
||||
use Query::*;
|
||||
|
||||
use crate::{parsec::*, UnixTimestamp};
|
||||
use crate::{
|
||||
datetime::{formats, UnixTimestamp},
|
||||
parsec::*,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize)]
|
||||
pub enum Query {
|
||||
|
@ -50,6 +53,18 @@ pub enum Query {
|
|||
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 {
|
||||
|
@ -85,7 +100,38 @@ impl QueryTrait for crate::Envelope {
|
|||
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),
|
||||
_ => false,
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,6 +149,87 @@ impl TryFrom<&str> for Query {
|
|||
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::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:")),
|
||||
|
@ -143,6 +270,14 @@ pub mod query_parser {
|
|||
.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"))
|
||||
|
@ -249,8 +384,17 @@ pub mod query_parser {
|
|||
.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)
|
||||
|
@ -282,7 +426,7 @@ pub mod query_parser {
|
|||
} 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, Or(Box::new(query_a), Box::new(query_b))))
|
||||
Ok((rest, And(Box::new(query_a), Box::new(query_b))))
|
||||
} else {
|
||||
Ok((rest, query_a))
|
||||
}
|
||||
|
@ -321,8 +465,8 @@ mod tests {
|
|||
#[test]
|
||||
fn test_query_parsing() {
|
||||
assert_eq!(
|
||||
Err("subject: test and"),
|
||||
query().parse_complete("subject: test and")
|
||||
Err("subject:test and"),
|
||||
query().parse_complete("subject:test and")
|
||||
);
|
||||
assert_eq!(
|
||||
Ok((
|
||||
|
@ -332,7 +476,7 @@ mod tests {
|
|||
Box::new(AllText("i".to_string()))
|
||||
)
|
||||
)),
|
||||
query().parse_complete("subject: test and i")
|
||||
query().parse_complete("subject:test and i")
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(("", AllText("test".to_string()))),
|
||||
|
@ -340,7 +484,17 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
Ok(("", Subject("test".to_string()))),
|
||||
query().parse_complete("subject: test")
|
||||
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((
|
||||
|
@ -353,7 +507,7 @@ mod tests {
|
|||
))
|
||||
)
|
||||
)),
|
||||
query().parse_complete("subject: \"wah ah ah\" or (from: Manos and from: Sia)")
|
||||
query().parse_complete("subject:\"wah ah ah\" or (from:Manos and from:Sia)")
|
||||
);
|
||||
assert_eq!(
|
||||
Ok((
|
||||
|
@ -369,8 +523,7 @@ mod tests {
|
|||
))
|
||||
)
|
||||
)),
|
||||
query()
|
||||
.parse_complete("subject: wah or (from: Manos and (subject:foo or subject: bar))")
|
||||
query().parse_complete("subject:wah or (from:Manos and (subject:foo or subject:bar))")
|
||||
);
|
||||
assert_eq!(
|
||||
Ok((
|
||||
|
@ -390,7 +543,7 @@ mod tests {
|
|||
)
|
||||
)),
|
||||
query().parse_complete(
|
||||
"(from: Manos and (subject:foo or subject: bar) and (from:woo or from:my))"
|
||||
"(from:Manos and (subject:foo or subject:bar) and (from:woo or from:my))"
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
|
|
|
@ -47,17 +47,27 @@ use std::{
|
|||
use crate::error::{Result, ResultIntoError};
|
||||
|
||||
pub type UnixTimestamp = u64;
|
||||
pub const RFC3339_FMT_WITH_TIME: &str = "%Y-%m-%dT%H:%M:%S\0";
|
||||
pub const RFC3339_FMT: &str = "%Y-%m-%d\0";
|
||||
pub const RFC822_DATE: &str = "%a, %d %b %Y %H:%M:%S %z\0";
|
||||
pub const RFC822_FMT_WITH_TIME: &str = "%a, %e %h %Y %H:%M:%S \0";
|
||||
pub const RFC822_FMT: &str = "%e %h %Y %H:%M:%S \0";
|
||||
pub const DEFAULT_FMT: &str = "%a, %d %b %Y %R\0";
|
||||
//"Tue May 21 13:46:22 1991\n"
|
||||
//"Wed Sep 9 00:27:54 2020\n"
|
||||
pub const ASCTIME_FMT: &str = "%a %b %d %H:%M:%S %Y\n\0";
|
||||
/// Source: RFC3501 Section 9. Formal syntax, item `date-text`
|
||||
pub const IMAP_DATE: &str = "%d-%b-%Y\0";
|
||||
|
||||
pub mod formats {
|
||||
/// <date>T<time>
|
||||
pub const RFC3339_DATETIME: &str = "%Y-%m-%dT%H:%M:%S\0";
|
||||
/// <date>T<time>
|
||||
pub const RFC3339_DATETIME_AND_SPACE: &str = "%Y-%m-%d %H:%M:%S\0";
|
||||
|
||||
pub const RFC3339_DATE: &str = "%Y-%m-%d\0";
|
||||
|
||||
pub const RFC822_DATE: &str = "%a, %d %b %Y %H:%M:%S %z\0";
|
||||
pub const RFC822_FMT_WITH_TIME: &str = "%a, %e %h %Y %H:%M:%S \0";
|
||||
pub const RFC822_FMT: &str = "%e %h %Y %H:%M:%S \0";
|
||||
pub const DEFAULT_FMT: &str = "%a, %d %b %Y %R\0";
|
||||
//"Tue May 21 13:46:22 1991\n"
|
||||
//"Wed Sep 9 00:27:54 2020\n"
|
||||
pub const ASCTIME_FMT: &str = "%a %b %d %H:%M:%S %Y\n\0";
|
||||
/// Source: RFC3501 Section 9. Formal syntax, item `date-text`
|
||||
pub const IMAP_DATE: &str = "%d-%b-%Y\0";
|
||||
}
|
||||
|
||||
use formats as fmt;
|
||||
|
||||
extern "C" {
|
||||
fn strptime(
|
||||
|
@ -195,7 +205,7 @@ pub fn timestamp_to_string(timestamp: UnixTimestamp, fmt: Option<&str>, posix: b
|
|||
{
|
||||
Cow::from(cstring)
|
||||
} else {
|
||||
unsafe { CStr::from_bytes_with_nul_unchecked(DEFAULT_FMT.as_bytes()).into() }
|
||||
unsafe { CStr::from_bytes_with_nul_unchecked(fmt::DEFAULT_FMT.as_bytes()).into() }
|
||||
};
|
||||
|
||||
let mut vec: [u8; 256] = [0; 256];
|
||||
|
@ -338,7 +348,7 @@ where
|
|||
{
|
||||
let s = CString::new(s)?;
|
||||
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
|
||||
for fmt in &[RFC822_FMT_WITH_TIME, RFC822_FMT, ASCTIME_FMT] {
|
||||
for fmt in &[fmt::RFC822_FMT_WITH_TIME, fmt::RFC822_FMT, fmt::ASCTIME_FMT] {
|
||||
let fmt = unsafe { CStr::from_bytes_with_nul_unchecked(fmt.as_bytes()) };
|
||||
let ret = {
|
||||
let _with_locale = Locale::new(
|
||||
|
@ -400,7 +410,7 @@ where
|
|||
{
|
||||
let s = CString::new(s)?;
|
||||
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
|
||||
for fmt in &[RFC3339_FMT_WITH_TIME, RFC3339_FMT] {
|
||||
for fmt in &[fmt::RFC3339_DATETIME, fmt::RFC3339_DATE] {
|
||||
let fmt = unsafe { CStr::from_bytes_with_nul_unchecked(fmt.as_bytes()) };
|
||||
let ret = {
|
||||
let _with_locale = Locale::new(
|
||||
|
@ -456,8 +466,14 @@ where
|
|||
Ok(0)
|
||||
}
|
||||
|
||||
// FIXME: Handle non-local timezone?
|
||||
pub fn timestamp_from_string<T>(s: T, fmt: &str) -> Result<Option<UnixTimestamp>>
|
||||
where
|
||||
T: Into<Vec<u8>>,
|
||||
{
|
||||
Ok(Some(parse_timestamp_from_string(s, fmt)?.1))
|
||||
}
|
||||
|
||||
pub fn parse_timestamp_from_string<T>(s: T, fmt: &str) -> Result<(usize, UnixTimestamp)>
|
||||
where
|
||||
T: Into<Vec<u8>>,
|
||||
{
|
||||
|
@ -468,15 +484,13 @@ where
|
|||
Cow::from(CString::new(fmt.as_bytes())?)
|
||||
};
|
||||
unsafe {
|
||||
let ret = strptime(
|
||||
CString::new(s)?.as_ptr(),
|
||||
fmt.as_ptr(),
|
||||
&mut new_tm as *mut _,
|
||||
);
|
||||
let val = CString::new(s)?;
|
||||
let ret = strptime(val.as_ptr(), fmt.as_ptr(), &mut new_tm as *mut _);
|
||||
if ret.is_null() {
|
||||
return Ok(None);
|
||||
return Err("Could not parse time with strptime.".into());
|
||||
}
|
||||
Ok(Some(mktime(&new_tm as *const _) as u64))
|
||||
let rest: isize = val.as_ptr().offset_from(ret);
|
||||
Ok((rest.unsigned_abs(), mktime(&new_tm as *const _) as u64))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -493,124 +507,6 @@ pub fn now() -> UnixTimestamp {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datetime_timestamp() {
|
||||
timestamp_to_string(0, None, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datetime_rfcs() {
|
||||
if unsafe { libc::setlocale(libc::LC_ALL, b"\0".as_ptr() as _) }.is_null() {
|
||||
println!("Unable to set locale.");
|
||||
}
|
||||
/* Some tests were lazily stolen from https://rachelbythebay.com/w/2013/06/11/time/ */
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Wed, 8 Jan 2020 10:44:03 -0800").unwrap(),
|
||||
1578509043
|
||||
);
|
||||
|
||||
/*
|
||||
macro_rules! mkt {
|
||||
($year:literal, $month:literal, $day:literal, $hour:literal, $minute:literal, $second:literal) => {
|
||||
libc::tm {
|
||||
tm_sec: $second,
|
||||
tm_min: $minute,
|
||||
tm_hour: $hour,
|
||||
tm_mday: $day,
|
||||
tm_mon: $month - 1,
|
||||
tm_year: $year - 1900,
|
||||
tm_wday: 0,
|
||||
tm_yday: 0,
|
||||
tm_isdst: 0,
|
||||
tm_gmtoff: 0,
|
||||
tm_zone: std::ptr::null(),
|
||||
}
|
||||
};
|
||||
}
|
||||
*/
|
||||
//unsafe { __tm_to_secs(&mkt!(2009, 02, 13, 23, 31, 30) as *const _) },
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Fri, 13 Feb 2009 15:31:30 -0800").unwrap(),
|
||||
1234567890
|
||||
);
|
||||
|
||||
//unsafe { __tm_to_secs(&mkt!(2931, 05, 05, 00, 33, 09) as *const _) },
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Sat, 05 May 2931 00:33:09 +0000").unwrap(),
|
||||
30336942789
|
||||
);
|
||||
//2214-11-06 20:05:12 = 7726651512 [OK]
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Sun, 06 Nov 2214 17:05:12 -0300").unwrap(), //2214-11-06 20:05:12
|
||||
7726651512
|
||||
);
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Sun, 06 Nov 2214 17:05:12 -0300").unwrap(), //2214-11-06 20:05:12
|
||||
rfc822_to_timestamp("Sun, 06 Nov 2214 17:05:12 (ADT)").unwrap(), //2214-11-06 20:05:12
|
||||
);
|
||||
//2661-11-06 06:38:02 = 21832612682 [OK]
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Wed, 06 Nov 2661 06:38:02 +0000").unwrap(), //2661-11-06 06:38:02
|
||||
21832612682
|
||||
);
|
||||
//2508-12-09 04:27:08 = 17007251228 [OK]
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Sun, 09 Dec 2508 04:27:08 +0000").unwrap(), //2508-12-09 04:27:08
|
||||
17007251228
|
||||
);
|
||||
//2375-11-07 05:08:24 = 12807349704 [OK]
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Fri, 07 Nov 2375 05:08:24 +0000").unwrap(), //2375-11-07 05:08:24
|
||||
12807349704
|
||||
);
|
||||
//2832-09-03 02:46:10 = 27223353970 [OK]
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Fri, 03 Sep 2832 02:46:10 +0000").unwrap(), //2832-09-03 02:46:10
|
||||
27223353970
|
||||
);
|
||||
//2983-02-25 12:47:17 = 31972020437 [OK]
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Tue, 25 Feb 2983 15:47:17 +0300").unwrap(), //2983-02-25 12:47:17
|
||||
31972020437
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Thu, 30 Mar 2017 17:32:06 +0300 (EEST)").unwrap(),
|
||||
1490884326
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 17:36:34 +0530").unwrap(),
|
||||
1493035594
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 17:36:34 +0530").unwrap(),
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 12:06:34 +0000").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 17:36:34 +0530").unwrap(),
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 17:36:34 (SLST)").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 17:36:34 +0530").unwrap(),
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 17:36:34 SLST").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("27 Dec 2019 14:42:46 +0100").unwrap(),
|
||||
1577454166
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Mon, 16 Mar 2020 10:23:01 +0200").unwrap(),
|
||||
1584346981
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::zero_prefixed_literal)]
|
||||
const TIMEZONE_ABBR: &[(&[u8], (i8, i8))] = &[
|
||||
(b"ACDT", (10, 30)),
|
||||
|
@ -803,3 +699,126 @@ const TIMEZONE_ABBR: &[(&[u8], (i8, i8))] = &[
|
|||
(b"YAKT", (09, 0)),
|
||||
(b"YEKT", (05, 0)),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_datetime_timestamp() {
|
||||
timestamp_to_string(0, None, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datetime_rfcs() {
|
||||
if unsafe { libc::setlocale(libc::LC_ALL, b"\0".as_ptr() as _) }.is_null() {
|
||||
println!("Unable to set locale.");
|
||||
}
|
||||
/* Some tests were lazily stolen from https://rachelbythebay.com/w/2013/06/11/time/ */
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Wed, 8 Jan 2020 10:44:03 -0800").unwrap(),
|
||||
1578509043
|
||||
);
|
||||
|
||||
/*
|
||||
macro_rules! mkt {
|
||||
($year:literal, $month:literal, $day:literal, $hour:literal, $minute:literal, $second:literal) => {
|
||||
libc::tm {
|
||||
tm_sec: $second,
|
||||
tm_min: $minute,
|
||||
tm_hour: $hour,
|
||||
tm_mday: $day,
|
||||
tm_mon: $month - 1,
|
||||
tm_year: $year - 1900,
|
||||
tm_wday: 0,
|
||||
tm_yday: 0,
|
||||
tm_isdst: 0,
|
||||
tm_gmtoff: 0,
|
||||
tm_zone: std::ptr::null(),
|
||||
}
|
||||
};
|
||||
}
|
||||
*/
|
||||
//unsafe { __tm_to_secs(&mkt!(2009, 02, 13, 23, 31, 30) as *const _) },
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Fri, 13 Feb 2009 15:31:30 -0800").unwrap(),
|
||||
1234567890
|
||||
);
|
||||
|
||||
//unsafe { __tm_to_secs(&mkt!(2931, 05, 05, 00, 33, 09) as *const _) },
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Sat, 05 May 2931 00:33:09 +0000").unwrap(),
|
||||
30336942789
|
||||
);
|
||||
//2214-11-06 20:05:12 = 7726651512 [OK]
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Sun, 06 Nov 2214 17:05:12 -0300").unwrap(), //2214-11-06 20:05:12
|
||||
7726651512
|
||||
);
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Sun, 06 Nov 2214 17:05:12 -0300").unwrap(), //2214-11-06 20:05:12
|
||||
rfc822_to_timestamp("Sun, 06 Nov 2214 17:05:12 (ADT)").unwrap(), //2214-11-06 20:05:12
|
||||
);
|
||||
//2661-11-06 06:38:02 = 21832612682 [OK]
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Wed, 06 Nov 2661 06:38:02 +0000").unwrap(), //2661-11-06 06:38:02
|
||||
21832612682
|
||||
);
|
||||
//2508-12-09 04:27:08 = 17007251228 [OK]
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Sun, 09 Dec 2508 04:27:08 +0000").unwrap(), //2508-12-09 04:27:08
|
||||
17007251228
|
||||
);
|
||||
//2375-11-07 05:08:24 = 12807349704 [OK]
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Fri, 07 Nov 2375 05:08:24 +0000").unwrap(), //2375-11-07 05:08:24
|
||||
12807349704
|
||||
);
|
||||
//2832-09-03 02:46:10 = 27223353970 [OK]
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Fri, 03 Sep 2832 02:46:10 +0000").unwrap(), //2832-09-03 02:46:10
|
||||
27223353970
|
||||
);
|
||||
//2983-02-25 12:47:17 = 31972020437 [OK]
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Tue, 25 Feb 2983 15:47:17 +0300").unwrap(), //2983-02-25 12:47:17
|
||||
31972020437
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Thu, 30 Mar 2017 17:32:06 +0300 (EEST)").unwrap(),
|
||||
1490884326
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 17:36:34 +0530").unwrap(),
|
||||
1493035594
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 17:36:34 +0530").unwrap(),
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 12:06:34 +0000").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 17:36:34 +0530").unwrap(),
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 17:36:34 (SLST)").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 17:36:34 +0530").unwrap(),
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 17:36:34 SLST").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("27 Dec 2019 14:42:46 +0100").unwrap(),
|
||||
1577454166
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Mon, 16 Mar 2020 10:23:01 +0200").unwrap(),
|
||||
1584346981
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,10 @@
|
|||
|
||||
//! Parser combinators.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::datetime::{parse_timestamp_from_string, UnixTimestamp};
|
||||
|
||||
pub type Result<'a, Output> = std::result::Result<(&'a str, Output), &'a str>;
|
||||
|
||||
pub trait Parser<'a, Output> {
|
||||
|
@ -390,7 +394,10 @@ pub fn any_char(input: &str) -> Result<char> {
|
|||
}
|
||||
|
||||
pub fn string<'a>() -> impl Parser<'a, String> {
|
||||
one_or_more(pred(any_char, |c| c.is_alphanumeric())).map(|r| r.into_iter().collect())
|
||||
one_or_more(pred(any_char, |c| {
|
||||
c.is_alphanumeric() || (c.is_ascii_graphic() && !['"', '(', ')', ' '].contains(c))
|
||||
}))
|
||||
.map(|r| r.into_iter().collect())
|
||||
}
|
||||
|
||||
pub fn space1<'a>() -> impl Parser<'a, Vec<char>> {
|
||||
|
@ -599,6 +606,19 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub fn date<'a, T: Into<Cow<'static, str>>>(fmt: T) -> impl Parser<'a, UnixTimestamp> {
|
||||
let fmt = fmt.into();
|
||||
move |input: &'a str| match parse_timestamp_from_string(input, &fmt) {
|
||||
Ok((idx, t)) => Ok((&input[idx..], t)),
|
||||
Err(_) => Err(input),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn integer<'a>() -> impl Parser<'a, usize> {
|
||||
use std::str::FromStr;
|
||||
map_res(is_a(b"0123456789"), |s| usize::from_str(s))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashMap;
|
||||
|
|
|
@ -28,7 +28,9 @@ use std::{
|
|||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
use melib::{email::attachment_types::ContentType, list_management, parser::BytesExt, HeaderName};
|
||||
use melib::{
|
||||
datetime, email::attachment_types::ContentType, list_management, parser::BytesExt, HeaderName,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::*;
|
||||
|
@ -1276,9 +1278,9 @@ impl Component for MailView {
|
|||
)
|
||||
.is_true()
|
||||
{
|
||||
let local_date = melib::datetime::timestamp_to_string(
|
||||
let local_date = datetime::timestamp_to_string(
|
||||
envelope.timestamp,
|
||||
Some(melib::datetime::RFC822_DATE),
|
||||
Some(datetime::formats::RFC822_DATE),
|
||||
false,
|
||||
);
|
||||
let orig_offset = find_offset(orig_date);
|
||||
|
|
|
@ -515,23 +515,28 @@ pub fn query_to_sql(q: &Query) -> String {
|
|||
ret
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_to_sql() {
|
||||
use melib::{parsec::Parser, search::query};
|
||||
assert_eq!(
|
||||
"(subject LIKE \"%test%\" ) AND (body_text LIKE \"%i%\" ) ",
|
||||
&query_to_sql(&query().parse_complete("subject: test and i").unwrap().1)
|
||||
);
|
||||
assert_eq!(
|
||||
"(subject LIKE \"%github%\" ) OR ((_from LIKE \"%epilys%\" ) AND ((subject LIKE \"%lib%\" \
|
||||
) OR (subject LIKE \"%meli%\" ) ) ) ",
|
||||
&query_to_sql(
|
||||
&query()
|
||||
.parse_complete(
|
||||
"subject: github or (from: epilys and (subject:lib or subject: meli))"
|
||||
)
|
||||
.unwrap()
|
||||
.1
|
||||
)
|
||||
);
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_query_to_sql() {
|
||||
use melib::{parsec::Parser, search::query};
|
||||
assert_eq!(
|
||||
"(subject LIKE \"%test%\" ) AND (body_text LIKE \"%i%\" ) ",
|
||||
&query_to_sql(&query().parse_complete("subject:test and i").unwrap().1)
|
||||
);
|
||||
assert_eq!(
|
||||
"(subject LIKE \"%github%\" ) OR ((_from LIKE \"%epilys%\" ) AND ((subject LIKE \
|
||||
\"%lib%\" ) OR (subject LIKE \"%meli%\" ) ) ) ",
|
||||
&query_to_sql(
|
||||
&query()
|
||||
.parse_complete(
|
||||
"subject:github or (from:epilys and (subject:lib or subject:meli))"
|
||||
)
|
||||
.unwrap()
|
||||
.1
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue