melib/search: implement more search criteria in Query type

pull/223/head
Manos Pitsidianakis 2023-06-04 13:54:20 +03:00
parent 23d95973d4
commit 6bf1756de8
9 changed files with 471 additions and 190 deletions

View File

@ -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 {
&timestamp_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")))"#
);
}
}

View File

@ -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()

View File

@ -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!(),
}
}
}

View File

@ -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,
),
);

View File

@ -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!(

View File

@ -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
);
}
}

View File

@ -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;

View File

@ -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);

View File

@ -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
)
);
}
}