Compare commits
5 Commits
master
...
duesee/exp
Author | SHA1 | Date |
---|---|---|
Damian Poddebniak | 456c21cc03 | |
Damian Poddebniak | 6c576b2606 | |
Damian Poddebniak | 9c1cdc5696 | |
Damian Poddebniak | af8fe0b6d8 | |
Damian Poddebniak | 613de70a93 |
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
|
@ -25,13 +25,13 @@ path = "src/lib.rs"
|
|||
[[bin]]
|
||||
name = "managesieve-client"
|
||||
path = "src/managesieve.rs"
|
||||
required-features = ["melib/imap_backend"]
|
||||
|
||||
[dependencies]
|
||||
async-task = "^4.2.0"
|
||||
bincode = { version = "^1.3.0", default-features = false }
|
||||
bitflags = "1.0"
|
||||
crossbeam = { version = "^0.8" }
|
||||
flate2 = { version = "1", optional = true }
|
||||
flate2 = { version = "1.0.16", optional = true }
|
||||
futures = "0.3.5"
|
||||
indexmap = { version = "^1.6", features = ["serde-1", ] }
|
||||
libc = { version = "0.2.125", default-features = false, features = ["extra_traits",] }
|
||||
|
@ -49,7 +49,7 @@ signal-hook = { version = "^0.3", default-features = false }
|
|||
signal-hook-registry = { version = "1.2.0", default-features = false }
|
||||
smallvec = { version = "^1.5.0", features = ["serde", ] }
|
||||
structopt = { version = "0.3.14", default-features = false }
|
||||
svg_crate = { version = "^0.13", optional = true, package = "svg" }
|
||||
svg_crate = { version = "^0.10", optional = true, package = "svg" }
|
||||
termion = { version = "1.5.1", default-features = false }
|
||||
toml = { version = "0.5.6", default-features = false, features = ["preserve_order", ] }
|
||||
unicode-segmentation = "1.2.1" # >:c
|
||||
|
@ -59,14 +59,12 @@ xdg = "2.1.0"
|
|||
notify-rust = { version = "^4", default-features = false, features = ["dbus", ], optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
flate2 = { version = "1", optional = true }
|
||||
flate2 = { version = "1.0.16", optional = true }
|
||||
proc-macro2 = "1.0.37"
|
||||
quote = "^1.0"
|
||||
regex = "1"
|
||||
syn = { version = "1", features = [] }
|
||||
syn = { version = "1.0.92", features = [] }
|
||||
|
||||
[dev-dependencies]
|
||||
flate2 = { version = "1" }
|
||||
regex = "1"
|
||||
tempfile = "3.3"
|
||||
|
||||
|
|
20
Makefile
20
Makefile
|
@ -18,12 +18,6 @@
|
|||
# along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
.POSIX:
|
||||
.SUFFIXES:
|
||||
CARGO_TARGET_DIR ?= target
|
||||
MIN_RUSTC ?= 1.65.0
|
||||
CARGO_BIN ?= cargo
|
||||
CARGO_ARGS ?=
|
||||
CARGO_SORT_BIN = cargo-sort
|
||||
PRINTF = /usr/bin/printf
|
||||
|
||||
# Options
|
||||
PREFIX ?= /usr/local
|
||||
|
@ -31,6 +25,11 @@ EXPANDED_PREFIX := `cd ${PREFIX} && pwd -P`
|
|||
BINDIR ?= ${EXPANDED_PREFIX}/bin
|
||||
MANDIR ?= ${EXPANDED_PREFIX}/share/man
|
||||
|
||||
CARGO_TARGET_DIR ?= target
|
||||
MIN_RUSTC ?= 1.39.0
|
||||
CARGO_BIN ?= cargo
|
||||
CARGO_ARGS ?=
|
||||
|
||||
# Installation parameters
|
||||
DOCS_SUBDIR ?= docs/
|
||||
MANPAGES ?= meli.1 meli.conf.5 meli-themes.5
|
||||
|
@ -97,15 +96,6 @@ help:
|
|||
check:
|
||||
@${CARGO_BIN} check ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --tests --examples --benches --bins
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
@$(CARGO_BIN) +nightly fmt --all || $(CARGO_BIN) fmt --all
|
||||
@OUT=$$($(CARGO_SORT_BIN) -w 2>&1) || $(PRINTF) "WARN: %s cargo-sort failed or binary not found in PATH.\n" "$$OUT"
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@$(CARGO_BIN) clippy --no-deps --all-features --all --tests --examples --benches --bins
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all --tests --examples --benches --bins
|
||||
|
|
1
build.rs
1
build.rs
|
@ -26,7 +26,6 @@ mod config_macros;
|
|||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=src/conf/.rebuild.overrides.rs");
|
||||
config_macros::override_derive(&[
|
||||
("src/conf/pager.rs", "PagerSettings"),
|
||||
("src/conf/listing.rs", "ListingSettings"),
|
||||
|
|
|
@ -26,7 +26,6 @@ use std::{
|
|||
};
|
||||
|
||||
use quote::{format_ident, quote};
|
||||
use regex::Regex;
|
||||
|
||||
// Write ConfigStructOverride to overrides.rs
|
||||
pub fn override_derive(filenames: &[(&str, &str)]) {
|
||||
|
@ -57,17 +56,11 @@ pub fn override_derive(filenames: &[(&str, &str)]) {
|
|||
#![allow(clippy::derivable_impls)]
|
||||
|
||||
//! This module is automatically generated by config_macros.rs.
|
||||
|
||||
use super::*;
|
||||
use melib::HeaderName;
|
||||
|
||||
"##
|
||||
.to_string();
|
||||
|
||||
let cfg_attr_default_attr_regex = Regex::new(r"\s*default\s*[,]").unwrap();
|
||||
let cfg_attr_default_val_attr_regex = Regex::new(r#"\s*default\s*=\s*"[^"]*"\s*,\s*"#).unwrap();
|
||||
let cfg_attr_feature_regex = Regex::new(r"[(](?:not[(]\s*)?feature").unwrap();
|
||||
|
||||
'file_loop: for (filename, ident) in filenames {
|
||||
println!("cargo:rerun-if-changed={}", filename);
|
||||
let mut file = File::open(filename)
|
||||
|
@ -116,23 +109,10 @@ use melib::HeaderName;
|
|||
.iter()
|
||||
.filter_map(|f| {
|
||||
let mut new_attr = f.clone();
|
||||
if let proc_macro2::TokenTree::Group(g) =
|
||||
if let quote::__private::TokenTree::Group(g) =
|
||||
f.tokens.clone().into_iter().next().unwrap()
|
||||
{
|
||||
let mut attr_inner_value = f.tokens.to_string();
|
||||
if cfg_attr_feature_regex.is_match(&attr_inner_value) {
|
||||
attr_inner_value = cfg_attr_default_val_attr_regex
|
||||
.replace_all(&attr_inner_value, "")
|
||||
.to_string();
|
||||
if attr_inner_value.contains("default") {
|
||||
attr_inner_value = cfg_attr_default_attr_regex
|
||||
.replace_all(&attr_inner_value, "")
|
||||
.to_string();
|
||||
}
|
||||
let new_toks: proc_macro2::TokenStream =
|
||||
attr_inner_value.parse().unwrap();
|
||||
new_attr.tokens = quote! { #new_toks };
|
||||
}
|
||||
let attr_inner_value = f.tokens.to_string();
|
||||
if !attr_inner_value.starts_with("( default")
|
||||
&& !attr_inner_value.starts_with("( default =")
|
||||
&& !attr_inner_value.starts_with("(default")
|
||||
|
@ -173,9 +153,7 @@ use melib::HeaderName;
|
|||
#[serde(default)]
|
||||
pub #ident : Option<#ty>
|
||||
};
|
||||
if !field_idents.contains(&ident) {
|
||||
field_idents.push(ident);
|
||||
}
|
||||
field_idents.push(ident);
|
||||
field_tokentrees.push(t);
|
||||
}
|
||||
//let fields = &s.fields;
|
||||
|
|
|
@ -173,6 +173,8 @@ theme_default
|
|||
.It
|
||||
error_message
|
||||
.It
|
||||
email_header
|
||||
.It
|
||||
highlight
|
||||
.It
|
||||
status.bar
|
||||
|
|
|
@ -292,7 +292,7 @@ mode and
|
|||
key to exit.
|
||||
.It
|
||||
At any time you may press
|
||||
.Shortcut e composing edit Ns
|
||||
.Shortcut e composing edit_mail Ns
|
||||
to launch your editor (see
|
||||
.Xr meli.conf 5 COMPOSING Ns
|
||||
, setting
|
||||
|
@ -320,7 +320,7 @@ To stop your editor and return to
|
|||
press
|
||||
.Aq Ctrl-z
|
||||
and to resume editing press the
|
||||
.Ic edit
|
||||
.Ic edit_mail
|
||||
command again.
|
||||
.El
|
||||
.Ss Attachments
|
||||
|
@ -353,7 +353,7 @@ To save your draft without sending it, issue
|
|||
and select 'save as draft'.
|
||||
.sp
|
||||
To open a draft for further editing, select your draft in the mail listing and press
|
||||
.Ic edit Ns
|
||||
.Ic edit_mail Ns
|
||||
\&.
|
||||
.Sh CONTACTS
|
||||
.Nm
|
||||
|
|
|
@ -605,7 +605,7 @@ Reply to all.
|
|||
.El
|
||||
.sp
|
||||
To launch your editor, press
|
||||
.ShortcutPeriod e composing edit
|
||||
.ShortcutPeriod e composing edit_mail
|
||||
\&.
|
||||
To send your draft, press
|
||||
.ShortcutPeriod s composing send_mail
|
||||
|
@ -619,7 +619,7 @@ and select
|
|||
You can return to the draft by going to your
|
||||
.Qq Drafts
|
||||
mailbox and selecting
|
||||
.ShortcutPeriod e envelope_view edit
|
||||
.ShortcutPeriod e envelope_view edit_mail
|
||||
\&.
|
||||
.Bd -literal -offset center
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
|
@ -648,7 +648,7 @@ mailbox and selecting
|
|||
.Ed
|
||||
.sp
|
||||
If you enable the embed terminal option, you can launch your terminal editor of choice when you press
|
||||
.Ic edit Ns
|
||||
.Ic edit_mail Ns
|
||||
\&.
|
||||
.Bd -literal -offset center
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
|
|
|
@ -114,7 +114,7 @@ editor_command = 'vim +/^$'
|
|||
|
||||
[shortcuts]
|
||||
[shortcuts.composing]
|
||||
edit = 'e'
|
||||
edit_mail = 'e'
|
||||
|
||||
[shortcuts.listing]
|
||||
new_mail = 'm'
|
||||
|
@ -263,11 +263,6 @@ The port to connect to
|
|||
If port is 993 and use_starttls is unspecified, it becomes false by default.
|
||||
.\" default value
|
||||
.Pq Em true
|
||||
.It Ic use_tls Ar boolean
|
||||
.Pq Em optional
|
||||
Connect with TLS (or upgrade from plain connection to TLS if STARTTLS is set).
|
||||
.\" default value
|
||||
.Pq Em true
|
||||
.It Ic danger_accept_invalid_certs Ar boolean
|
||||
.Pq Em optional
|
||||
Do not validate TLS certificates.
|
||||
|
@ -604,7 +599,7 @@ Forward emails as attachment? (Alternative is inline).
|
|||
.Pq Em optional
|
||||
Alternative lists of reply prefixes (etc. ["Re:", "RE:", ...]) to strip.
|
||||
.\" default value
|
||||
.Dl Em [Re:, RE:, Fwd:, Fw:, 回复:, 回覆:, SV:, Sv:, VS:, Antw:, Doorst:, VS:, VL:, REF:, TR:, TR:, AW:, WG:, ΑΠ:, Απ:, απ:, ΠΡΘ:, Πρθ:, πρθ:, ΣΧΕΤ:, Σχετ:, σχετ:, ΠΡΘ:, Πρθ:, πρθ:, Vá:, Továbbítás:, R:, I:, RIF:, FS:, BLS:, TRS:, VS:, VB:, RV:, RES:, Res, ENC:, Odp:, PD:, YNT:, İLT:, ATB:, YML:]
|
||||
.Pq Em ["Re:", "RE:", "Fwd:", "Fw:", "回复:", "回覆:", "SV:", "Sv:", "VS:", "Antw:", "Doorst:", "VS:", "VL:", "REF:", "TR:", "TR:", "AW:", "WG:", "ΑΠ:", "Απ:", "απ:", "ΠΡΘ:", "Πρθ:", "πρθ:", "ΣΧΕΤ:", "Σχετ:", "σχετ:", "ΠΡΘ:", "Πρθ:", "πρθ:", "Vá:", "Továbbítás:", "R:", "I:", "RIF:", "FS:", "BLS:", "TRS:", "VS:", "VB:", "RV:", "RES:", "Res", "ENC:", "Odp:", "PD:", "YNT:", "İLT:", "ATB:", "YML:"]
|
||||
.It Ic reply_prefix Ar String
|
||||
.Pq Em optional
|
||||
The prefix to use in reply subjects.
|
||||
|
@ -870,19 +865,11 @@ Select thread entry.
|
|||
.It Ic increase_sidebar
|
||||
Increase sidebar width.
|
||||
.\" default value
|
||||
.Pq Em C-f
|
||||
.Pq Em C-p
|
||||
.It Ic decrease_sidebar
|
||||
Decrease sidebar width.
|
||||
.\" default value
|
||||
.Pq Em C-d
|
||||
.It Ic next_entry
|
||||
When reading a mail item, change focus on next entry according to the current sorting.
|
||||
.\" default value
|
||||
.Pq Em C-n
|
||||
.It Ic previous_entry
|
||||
When reading a mail item, change focus on previous entry according to the current sorting.
|
||||
.\" default value
|
||||
.Pq Em C-p
|
||||
.Pq Em C-o
|
||||
.It Ic toggle_menu_visibility
|
||||
Toggle visibility of side menu in mail list.
|
||||
.\" default value
|
||||
|
@ -964,7 +951,7 @@ Toggle visibility of side menu in mail list.
|
|||
.sp
|
||||
.Em composing
|
||||
.Bl -tag -width 36n
|
||||
.It Ic edit
|
||||
.It Ic edit_mail
|
||||
Edit mail.
|
||||
.\" default value
|
||||
.Pq Em e
|
||||
|
@ -1128,7 +1115,7 @@ Play sound file in notifications if possible.
|
|||
.Sh PAGER
|
||||
Default values are shown in parentheses.
|
||||
.Bl -tag -width 36n
|
||||
.It Ic sticky_headers Ar boolean
|
||||
.It Ic headers_sticky Ar boolean
|
||||
.Pq Em optional
|
||||
Always show headers when scrolling.
|
||||
.\" default value
|
||||
|
@ -1179,26 +1166,6 @@ A command to launch URLs with.
|
|||
The URL will be given as the first argument of the command.
|
||||
.\" default value
|
||||
.Pq Em xdg-open
|
||||
.It Ic show_extra_headers Ar [String]
|
||||
.Pq Em optional
|
||||
Extra headers to display, if present, in the default header preamble of the pager.
|
||||
This setting is useful especially when used per-folder or per-account.
|
||||
For example, if you use
|
||||
.Ql rss2email
|
||||
.Pq See Xr r2e 1
|
||||
the e-mail you will receive will have the
|
||||
.Ql X-RSS-Feed
|
||||
header by default.
|
||||
You can show them only in the folder where you keep your feed items:
|
||||
.Pp
|
||||
.Bd -literal -compact
|
||||
[accounts."personal".mailboxes]
|
||||
INBOX = {}
|
||||
"INBOX/Sent" = { sort_order=0 }
|
||||
"INBOX/Feeds" = { pager.show_extra_headers = ["X-RSS-Feed"] }
|
||||
.Ed
|
||||
.\" default value
|
||||
.Pq Em empty
|
||||
.El
|
||||
.Sh LISTING
|
||||
Default values are shown in parentheses.
|
||||
|
@ -1275,11 +1242,6 @@ Flag to show if thread entry contains attachments.
|
|||
Should threads with differentiating Subjects show a list of those subjects on the entry title?
|
||||
.\" default value
|
||||
.Pq Em "true"
|
||||
.It Ic threaded_repeat_identical_from_values Ar bool
|
||||
In threaded listing style, repeat identical From column values within a thread.
|
||||
Not repeating adds empty space in the From column which might result in less visual clutter.
|
||||
.\" default value
|
||||
.Pq Em "false"
|
||||
.El
|
||||
.Ss Examples of sidebar mailbox tree customization
|
||||
The default values
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
#[pager]
|
||||
#filter = "COLUMNS=72 /usr/local/bin/pygmentize -l email"
|
||||
#pager_context = 0 # default, optional
|
||||
#sticky_headers = true # default, optional
|
||||
#headers_sticky = true # default, optional
|
||||
#
|
||||
#[notifications]
|
||||
#script = "notify-send"
|
||||
|
@ -101,7 +101,7 @@
|
|||
#
|
||||
###shortcuts
|
||||
#[shortcuts.composing]
|
||||
#edit = 'e'
|
||||
#edit_mail = 'e'
|
||||
#
|
||||
#[shortcuts.contact-list]
|
||||
#create_contact = 'c'
|
||||
|
|
|
@ -20,8 +20,10 @@ name = "melib"
|
|||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
log = { version = "0.4", features = ["std"]}
|
||||
async-stream = "^0.3"
|
||||
base64 = { version = "^0.13", optional = true }
|
||||
bincode = { version = "^1.3.0", default-features = false }
|
||||
bitflags = "1.0"
|
||||
data-encoding = { version = "2.1.1" }
|
||||
encoding = { version = "0.2.33", default-features = false }
|
||||
|
@ -34,7 +36,6 @@ isahc = { version = "^1.7.2", optional = true, default-features = false, feature
|
|||
libc = { version = "0.2.125", features = ["extra_traits",] }
|
||||
|
||||
libloading = "^0.7"
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
native-tls = { version = "0.2.3", default-features = false, optional = true }
|
||||
nix = "^0.24"
|
||||
nom = { version = "7" }
|
||||
|
@ -43,28 +44,17 @@ regex = { version = "1" }
|
|||
rusqlite = { version = "^0.28", default-features = false, optional = true }
|
||||
serde = { version = "1.0.71", features = ["rc", ] }
|
||||
serde_derive = "1.0.71"
|
||||
serde_json = { version = "1.0", features = ["raw_value",] }
|
||||
serde_json = { version = "1.0", optional = true, features = ["raw_value",] }
|
||||
smallvec = { version = "^1.5.0", features = ["serde", ] }
|
||||
smol = "1.0.0"
|
||||
# TODO(duesee): Use latest version from crates.io.
|
||||
imap-codec = { path = "../../imap-codec", optional = true, features = ["ext_enable", "ext_literal", "ext_sasl_ir", "ext_unselect", "ext_condstore_qresync"] }
|
||||
|
||||
unicode-segmentation = { version = "1.2.1", default-features = false, optional = true }
|
||||
uuid = { version = "^1", features = ["serde", "v4", "v5"] }
|
||||
xdg = "2.1.0"
|
||||
xdg-utils = "^0.4.0"
|
||||
|
||||
[dependencies.imap-codec]
|
||||
version = "0.10.0"
|
||||
features = [
|
||||
"ext_condstore_qresync",
|
||||
"ext_enable",
|
||||
"ext_idle",
|
||||
"ext_literal",
|
||||
"ext_move",
|
||||
"ext_sasl_ir",
|
||||
"ext_unselect"
|
||||
]
|
||||
optional = true
|
||||
|
||||
[dev-dependencies]
|
||||
mailin-embedded = { version = "0.7", features = ["rtls"] }
|
||||
stderrlog = "^0.5"
|
||||
|
@ -77,8 +67,8 @@ deflate_compression = ["flate2", "imap-codec/ext_compress"]
|
|||
gpgme = []
|
||||
http = ["isahc"]
|
||||
http-static = ["isahc", "isahc/static-curl"]
|
||||
imap_backend = ["imap-codec", "tls"]
|
||||
jmap_backend = ["http"]
|
||||
imap_backend = ["tls", "imap-codec"]
|
||||
jmap_backend = ["http", "serde_json"]
|
||||
maildir_backend = ["notify"]
|
||||
mbox_backend = ["notify"]
|
||||
notmuch_backend = []
|
||||
|
|
|
@ -19,8 +19,6 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#![allow(clippy::needless_range_loop)]
|
||||
|
||||
#[cfg(feature = "unicode_algorithms")]
|
||||
include!("src/text_processing/types.rs");
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ use std::{collections::HashMap, ops::Deref};
|
|||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::utils::{
|
||||
use crate::{
|
||||
datetime::{self, UnixTimestamp},
|
||||
parsec::Parser,
|
||||
};
|
||||
|
@ -41,37 +41,22 @@ pub enum CardId {
|
|||
Hash(u64),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CardId {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
impl Into<String> for CardId {
|
||||
fn into(self) -> String {
|
||||
match self {
|
||||
Self::Uuid(u) => u.as_hyphenated().fmt(fmt),
|
||||
Self::Hash(u) => u.fmt(fmt),
|
||||
CardId::Uuid(u) => u.to_string(),
|
||||
CardId::Hash(u) => u.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CardId> for String {
|
||||
fn from(val: CardId) -> Self {
|
||||
val.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for CardId {
|
||||
fn from(s: String) -> Self {
|
||||
use std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
if let Ok(u) = Uuid::parse_str(s.as_str()) {
|
||||
Self::Uuid(u)
|
||||
} else if let Ok(num) = u64::from_str(s.trim()) {
|
||||
Self::Hash(num)
|
||||
fn from(s: String) -> CardId {
|
||||
if let Ok(u) = uuid::Uuid::parse_str(s.as_str()) {
|
||||
CardId::Uuid(u)
|
||||
} else {
|
||||
let mut hasher = DefaultHasher::default();
|
||||
s.hash(&mut hasher);
|
||||
Self::Hash(hasher.finish())
|
||||
use std::str::FromStr;
|
||||
CardId::Hash(u64::from_str(&s).unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -108,8 +93,8 @@ pub struct Card {
|
|||
}
|
||||
|
||||
impl AddressBook {
|
||||
pub fn new(display_name: String) -> Self {
|
||||
Self {
|
||||
pub fn new(display_name: String) -> AddressBook {
|
||||
AddressBook {
|
||||
display_name,
|
||||
created: datetime::now(),
|
||||
last_edited: datetime::now(),
|
||||
|
@ -117,8 +102,8 @@ impl AddressBook {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn with_account(s: &crate::conf::AccountSettings) -> Self {
|
||||
let mut ret = Self::new(s.name.clone());
|
||||
pub fn with_account(s: &crate::conf::AccountSettings) -> AddressBook {
|
||||
let mut ret = AddressBook::new(s.name.clone());
|
||||
if let Some(mutt_alias_file) = s.extra.get("mutt_alias_file").map(String::as_str) {
|
||||
match std::fs::read_to_string(std::path::Path::new(mutt_alias_file))
|
||||
.map_err(|err| err.to_string())
|
||||
|
@ -186,8 +171,8 @@ impl Deref for AddressBook {
|
|||
}
|
||||
|
||||
impl Card {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
pub fn new() -> Card {
|
||||
Card {
|
||||
id: CardId::Uuid(Uuid::new_v4()),
|
||||
title: String::new(),
|
||||
name: String::new(),
|
||||
|
@ -308,8 +293,8 @@ impl Card {
|
|||
}
|
||||
|
||||
impl From<HashMap<String, String>> for Card {
|
||||
fn from(mut map: HashMap<String, String>) -> Self {
|
||||
let mut card = Self::new();
|
||||
fn from(mut map: HashMap<String, String>) -> Card {
|
||||
let mut card = Card::new();
|
||||
if let Some(val) = map.remove("TITLE") {
|
||||
card.title = val;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
use std::collections::VecDeque;
|
||||
|
||||
use super::*;
|
||||
use crate::utils::parsec::{is_not, map_res, match_literal_anycase, prefix, Parser};
|
||||
use crate::parsec::{is_not, map_res, match_literal_anycase, prefix, Parser};
|
||||
|
||||
//alias <nickname> [ <long name> ] <address>
|
||||
// From mutt doc:
|
||||
|
|
|
@ -32,7 +32,7 @@ use std::{collections::HashMap, convert::TryInto};
|
|||
use super::*;
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
utils::parsec::{match_literal_anycase, one_or_more, peek, prefix, take_until, Parser},
|
||||
parsec::{match_literal_anycase, one_or_more, peek, prefix, take_until, Parser},
|
||||
};
|
||||
|
||||
/* Supported vcard versions */
|
||||
|
@ -84,7 +84,7 @@ pub struct ContentLine {
|
|||
}
|
||||
|
||||
impl CardDeserializer {
|
||||
pub fn try_from_str(mut input: &str) -> Result<VCard<impl VCardVersion>> {
|
||||
pub fn from_str(mut input: &str) -> Result<VCard<impl VCardVersion>> {
|
||||
input = if (!input.starts_with(HEADER_CRLF) || !input.ends_with(FOOTER_CRLF))
|
||||
&& (!input.starts_with(HEADER_LF) || !input.ends_with(FOOTER_LF))
|
||||
{
|
||||
|
@ -222,9 +222,8 @@ impl<V: VCardVersion> TryInto<Card> for VCard<V> {
|
|||
T102200Z
|
||||
T102200-0800
|
||||
*/
|
||||
card.birthday =
|
||||
crate::utils::datetime::timestamp_from_string(val.value.as_str(), "%Y%m%d\0")
|
||||
.unwrap_or_default();
|
||||
card.birthday = crate::datetime::timestamp_from_string(val.value.as_str(), "%Y%m%d\0")
|
||||
.unwrap_or_default();
|
||||
}
|
||||
if let Some(val) = self.0.remove("EMAIL") {
|
||||
card.set_email(val.value);
|
||||
|
@ -270,7 +269,7 @@ fn test_load_cards() {
|
|||
for s in parse_card().parse(contents.as_str()).unwrap().1 {
|
||||
println!("");
|
||||
println!("{}", s);
|
||||
println!("{:?}", CardDeserializer::try_from_str(s));
|
||||
println!("{:?}", CardDeserializer::from_str(s));
|
||||
println!("");
|
||||
}
|
||||
*/
|
||||
|
@ -295,7 +294,7 @@ pub fn load_cards(p: &std::path::Path) -> Result<Vec<Card>> {
|
|||
Ok((_, c)) => {
|
||||
for s in c {
|
||||
ret.push(
|
||||
CardDeserializer::try_from_str(s)
|
||||
CardDeserializer::from_str(s)
|
||||
.and_then(TryInto::try_into)
|
||||
.map(|mut card| {
|
||||
Card::set_external_resource(&mut card, true);
|
||||
|
@ -326,13 +325,7 @@ pub fn load_cards(p: &std::path::Path) -> Result<Vec<Card>> {
|
|||
#[test]
|
||||
fn test_card() {
|
||||
let j = "BEGIN:VCARD\r\nVERSION:4.0\r\nN:Gump;Forrest;;Mr.;\r\nFN:Forrest Gump\r\nORG:Bubba Gump Shrimp Co.\r\nTITLE:Shrimp Man\r\nPHOTO;MEDIATYPE=image/gif:http://www.example.com/dir_photos/my_photo.gif\r\nTEL;TYPE=work,voice;VALUE=uri:tel:+1-111-555-1212\r\nTEL;TYPE=home,voice;VALUE=uri:tel:+1-404-555-1212\r\nADR;TYPE=WORK;PREF=1;LABEL=\"100 Waters Edge\\nBaytown\\, LA 30314\\nUnited States of America\":;;100 Waters Edge;Baytown;LA;30314;United States of America\r\nADR;TYPE=HOME;LABEL=\"42 Plantation St.\\nBaytown\\, LA 30314\\nUnited States of America\":;;42 Plantation St.;Baytown;LA;30314;United States of America\r\nEMAIL:forrestgump@example.com\r\nREV:20080424T195243Z\r\nx-qq:21588891\r\nEND:VCARD\r\n";
|
||||
println!(
|
||||
"results = {:#?}",
|
||||
CardDeserializer::try_from_str(j).unwrap()
|
||||
);
|
||||
println!("results = {:#?}", CardDeserializer::from_str(j).unwrap());
|
||||
let j = "BEGIN:VCARD\nVERSION:4.0\nN:Gump;Forrest;;Mr.;\nFN:Forrest Gump\nORG:Bubba Gump Shrimp Co.\nTITLE:Shrimp Man\nPHOTO;MEDIATYPE=image/gif:http://www.example.com/dir_photos/my_photo.gif\nTEL;TYPE=work,voice;VALUE=uri:tel:+1-111-555-1212\nTEL;TYPE=home,voice;VALUE=uri:tel:+1-404-555-1212\nADR;TYPE=WORK;PREF=1;LABEL=\"100 Waters Edge\\nBaytown\\, LA 30314\\nUnited States of America\":;;100 Waters Edge;Baytown;LA;30314;United States of America\nADR;TYPE=HOME;LABEL=\"42 Plantation St.\\nBaytown\\, LA 30314\\nUnited States of America\":;;42 Plantation St.;Baytown;LA;30314;United States of America\nEMAIL:forrestgump@example.com\nREV:20080424T195243Z\nx-qq:21588891\nEND:VCARD\n";
|
||||
println!(
|
||||
"results = {:#?}",
|
||||
CardDeserializer::try_from_str(j).unwrap()
|
||||
);
|
||||
println!("results = {:#?}", CardDeserializer::from_str(j).unwrap());
|
||||
}
|
||||
|
|
|
@ -86,8 +86,6 @@ pub type BackendCreator = Box<
|
|||
) -> Result<Box<dyn MailBackend>>,
|
||||
>;
|
||||
|
||||
pub type BackendValidateConfigFn = Box<dyn Fn(&mut AccountSettings) -> Result<()>>;
|
||||
|
||||
/// A hashmap containing all available mail backends.
|
||||
/// An abstraction over any available backends.
|
||||
pub struct Backends {
|
||||
|
@ -96,12 +94,12 @@ pub struct Backends {
|
|||
|
||||
pub struct Backend {
|
||||
pub create_fn: Box<dyn Fn() -> BackendCreator>,
|
||||
pub validate_conf_fn: BackendValidateConfigFn,
|
||||
pub validate_conf_fn: Box<dyn Fn(&mut AccountSettings) -> Result<()>>,
|
||||
}
|
||||
|
||||
impl Default for Backends {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
Backends::new()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,7 +149,7 @@ pub const NOTMUCH_ERROR_DETAILS: &str = r#"If notmuch is installed but the libra
|
|||
|
||||
impl Backends {
|
||||
pub fn new() -> Self {
|
||||
let mut b = Self {
|
||||
let mut b = Backends {
|
||||
map: HashMap::with_capacity_and_hasher(1, Default::default()),
|
||||
};
|
||||
#[cfg(feature = "maildir_backend")]
|
||||
|
@ -274,8 +272,8 @@ pub enum BackendEvent {
|
|||
}
|
||||
|
||||
impl From<Error> for BackendEvent {
|
||||
fn from(val: Error) -> Self {
|
||||
Self::Notice {
|
||||
fn from(val: Error) -> BackendEvent {
|
||||
BackendEvent::Notice {
|
||||
description: val.summary.to_string(),
|
||||
content: Some(val.to_string()),
|
||||
level: LogLevel::ERROR,
|
||||
|
@ -312,10 +310,9 @@ pub struct RefreshEvent {
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct BackendEventConsumer(Arc<dyn Fn(AccountHash, BackendEvent) + Send + Sync>);
|
||||
|
||||
impl BackendEventConsumer {
|
||||
pub fn new(b: Arc<dyn Fn(AccountHash, BackendEvent) + Send + Sync>) -> Self {
|
||||
Self(b)
|
||||
BackendEventConsumer(b)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -358,7 +355,6 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
|
|||
Ok(Box::pin(async { Ok(()) }))
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn fetch(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
|
@ -497,9 +493,8 @@ pub struct ReadOnlyOp {
|
|||
}
|
||||
|
||||
impl ReadOnlyOp {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(op: Box<dyn BackendOp>) -> Box<dyn BackendOp> {
|
||||
Box::new(Self { op })
|
||||
Box::new(ReadOnlyOp { op })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -512,9 +507,8 @@ impl BackendOp for ReadOnlyOp {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Copy, Hash, Eq, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Copy, Hash, Eq, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum SpecialUsageMailbox {
|
||||
#[default]
|
||||
Normal,
|
||||
Inbox,
|
||||
Archive,
|
||||
|
@ -545,22 +539,28 @@ impl std::fmt::Display for SpecialUsageMailbox {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for SpecialUsageMailbox {
|
||||
fn default() -> Self {
|
||||
SpecialUsageMailbox::Normal
|
||||
}
|
||||
}
|
||||
|
||||
impl SpecialUsageMailbox {
|
||||
pub fn detect_usage(name: &str) -> Option<Self> {
|
||||
pub fn detect_usage(name: &str) -> Option<SpecialUsageMailbox> {
|
||||
if name.eq_ignore_ascii_case("inbox") {
|
||||
Some(Self::Inbox)
|
||||
Some(SpecialUsageMailbox::Inbox)
|
||||
} else if name.eq_ignore_ascii_case("archive") {
|
||||
Some(Self::Archive)
|
||||
Some(SpecialUsageMailbox::Archive)
|
||||
} else if name.eq_ignore_ascii_case("drafts") {
|
||||
Some(Self::Drafts)
|
||||
Some(SpecialUsageMailbox::Drafts)
|
||||
} else if name.eq_ignore_ascii_case("junk") || name.eq_ignore_ascii_case("spam") {
|
||||
Some(Self::Junk)
|
||||
Some(SpecialUsageMailbox::Junk)
|
||||
} else if name.eq_ignore_ascii_case("sent") {
|
||||
Some(Self::Sent)
|
||||
Some(SpecialUsageMailbox::Sent)
|
||||
} else if name.eq_ignore_ascii_case("trash") {
|
||||
Some(Self::Trash)
|
||||
Some(SpecialUsageMailbox::Trash)
|
||||
} else {
|
||||
Some(Self::Normal)
|
||||
Some(SpecialUsageMailbox::Normal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -608,7 +608,7 @@ pub struct MailboxPermissions {
|
|||
|
||||
impl Default for MailboxPermissions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
MailboxPermissions {
|
||||
create_messages: false,
|
||||
remove_messages: false,
|
||||
set_flags: false,
|
||||
|
@ -635,7 +635,7 @@ pub struct EnvelopeHashBatch {
|
|||
|
||||
impl From<EnvelopeHash> for EnvelopeHashBatch {
|
||||
fn from(value: EnvelopeHash) -> Self {
|
||||
Self {
|
||||
EnvelopeHashBatch {
|
||||
first: value,
|
||||
rest: SmallVec::new(),
|
||||
}
|
||||
|
@ -649,16 +649,16 @@ impl std::convert::TryFrom<&[EnvelopeHash]> for EnvelopeHashBatch {
|
|||
if value.is_empty() {
|
||||
return Err(());
|
||||
}
|
||||
Ok(Self {
|
||||
Ok(EnvelopeHashBatch {
|
||||
first: value[0],
|
||||
rest: value[1..].iter().cloned().collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&EnvelopeHashBatch> for BTreeSet<EnvelopeHash> {
|
||||
fn from(val: &EnvelopeHashBatch) -> Self {
|
||||
val.iter().collect()
|
||||
impl Into<BTreeSet<EnvelopeHash>> for &EnvelopeHashBatch {
|
||||
fn into(self) -> BTreeSet<EnvelopeHash> {
|
||||
self.iter().collect::<BTreeSet<EnvelopeHash>>()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -667,10 +667,6 @@ impl EnvelopeHashBatch {
|
|||
std::iter::once(self.first).chain(self.rest.iter().cloned())
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
1 + self.rest.len()
|
||||
}
|
||||
|
@ -719,10 +715,6 @@ impl LazyCountSet {
|
|||
self.not_yet_seen = self.not_yet_seen.saturating_sub(self.set.len() - old_len);
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.set.len() + self.not_yet_seen
|
||||
|
|
|
@ -27,16 +27,12 @@ mod mailbox;
|
|||
pub use mailbox::*;
|
||||
mod operations;
|
||||
pub use operations::*;
|
||||
#[macro_use]
|
||||
mod connection;
|
||||
pub use connection::*;
|
||||
mod watch;
|
||||
pub use watch::*;
|
||||
mod search;
|
||||
pub use search::*;
|
||||
mod cache;
|
||||
use cache::{ImapCacheReset, ModSequence};
|
||||
pub mod error;
|
||||
pub mod managesieve;
|
||||
mod untagged;
|
||||
|
||||
|
@ -44,6 +40,7 @@ use std::{
|
|||
collections::{hash_map::DefaultHasher, BTreeSet, HashMap, HashSet},
|
||||
convert::TryFrom,
|
||||
hash::Hasher,
|
||||
num::NonZeroU32,
|
||||
pin::Pin,
|
||||
str::FromStr,
|
||||
sync::{Arc, Mutex},
|
||||
|
@ -52,10 +49,9 @@ use std::{
|
|||
|
||||
use futures::{lock::Mutex as FutureMutex, stream::Stream};
|
||||
use imap_codec::{
|
||||
command::CommandBody,
|
||||
core::Literal,
|
||||
flag::{Flag as ImapCodecFlag, StoreResponse, StoreType},
|
||||
sequence::{SequenceSet, ONE},
|
||||
command::{fetch::FetchAttribute, CommandBody, ListMailbox as ImapCodecListMailbox},
|
||||
core::{AString, NonEmptyVec},
|
||||
message::{Mailbox as ImapCodecMailbox, Section},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
|
@ -65,9 +61,9 @@ use crate::{
|
|||
},
|
||||
collection::Collection,
|
||||
conf::AccountSettings,
|
||||
connections::timeout,
|
||||
email::{parser::BytesExt, *},
|
||||
error::{Error, Result, ResultIntoError},
|
||||
utils::futures::timeout,
|
||||
};
|
||||
|
||||
pub type ImapNum = usize;
|
||||
|
@ -146,7 +142,7 @@ macro_rules! get_conf_val {
|
|||
#[derive(Debug)]
|
||||
pub struct UIDStore {
|
||||
account_hash: AccountHash,
|
||||
account_name: Arc<str>,
|
||||
account_name: Arc<String>,
|
||||
keep_offline_cache: bool,
|
||||
capabilities: Arc<Mutex<Capabilities>>,
|
||||
hash_index: Arc<Mutex<HashMap<EnvelopeHash, (UID, MailboxHash)>>>,
|
||||
|
@ -171,11 +167,11 @@ pub struct UIDStore {
|
|||
impl UIDStore {
|
||||
fn new(
|
||||
account_hash: AccountHash,
|
||||
account_name: Arc<str>,
|
||||
account_name: Arc<String>,
|
||||
event_consumer: BackendEventConsumer,
|
||||
timeout: Option<Duration>,
|
||||
) -> Self {
|
||||
Self {
|
||||
UIDStore {
|
||||
account_hash,
|
||||
account_name,
|
||||
keep_offline_cache: false,
|
||||
|
@ -369,8 +365,6 @@ impl MailBackend for ImapType {
|
|||
}
|
||||
};
|
||||
Ok(Box::pin(async_stream::try_stream! {
|
||||
#[cfg(debug_assertions)]
|
||||
let id = state.connection.lock().await.id.clone();
|
||||
{
|
||||
let f = &state.uid_store.mailboxes.lock().await[&mailbox_hash];
|
||||
prepare_cl(f);
|
||||
|
@ -381,10 +375,8 @@ impl MailBackend for ImapType {
|
|||
};
|
||||
loop {
|
||||
let res = fetch_hlpr(&mut state).await.map_err(|err| {
|
||||
#[cfg(debug_assertions)]
|
||||
log::trace!("{} fetch_hlpr err {:?}", id, &err);
|
||||
err
|
||||
})?;
|
||||
debug!("fetch_hlpr err {:?}", &err);
|
||||
err})?;
|
||||
yield res;
|
||||
if state.stage == FetchStage::Finished {
|
||||
return;
|
||||
|
@ -422,7 +414,7 @@ impl MailBackend for ImapType {
|
|||
.collect());
|
||||
}
|
||||
}
|
||||
let new_mailboxes = Self::imap_mailboxes(&connection).await?;
|
||||
let new_mailboxes = ImapType::imap_mailboxes(&connection).await?;
|
||||
let mut mailboxes = uid_store.mailboxes.lock().await;
|
||||
*mailboxes = new_mailboxes;
|
||||
/*
|
||||
|
@ -464,7 +456,7 @@ impl MailBackend for ImapType {
|
|||
Ok(Box::pin(async move {
|
||||
match timeout(timeout_dur, connection.lock()).await {
|
||||
Ok(mut conn) => {
|
||||
imap_trace!(conn, "is_online");
|
||||
debug!("is_online");
|
||||
match timeout(timeout_dur, conn.connect()).await {
|
||||
Ok(Ok(())) => Ok(()),
|
||||
Err(err) | Ok(Err(err)) => {
|
||||
|
@ -498,24 +490,14 @@ impl MailBackend for ImapType {
|
|||
};
|
||||
while let Err(err) = if has_idle {
|
||||
idle(ImapWatchKit {
|
||||
conn: ImapConnection::new_connection(
|
||||
&server_conf,
|
||||
#[cfg(debug_assertions)]
|
||||
"watch()::idle".into(),
|
||||
uid_store.clone(),
|
||||
),
|
||||
conn: ImapConnection::new_connection(&server_conf, uid_store.clone()),
|
||||
main_conn: main_conn.clone(),
|
||||
uid_store: uid_store.clone(),
|
||||
})
|
||||
.await
|
||||
} else {
|
||||
poll_with_examine(ImapWatchKit {
|
||||
conn: ImapConnection::new_connection(
|
||||
&server_conf,
|
||||
#[cfg(debug_assertions)]
|
||||
"watch()::poll_with_examine".into(),
|
||||
uid_store.clone(),
|
||||
),
|
||||
conn: ImapConnection::new_connection(&server_conf, uid_store.clone()),
|
||||
main_conn: main_conn.clone(),
|
||||
uid_store: uid_store.clone(),
|
||||
})
|
||||
|
@ -527,27 +509,16 @@ impl MailBackend for ImapType {
|
|||
} else {
|
||||
return Err(err);
|
||||
}
|
||||
log::trace!(
|
||||
"{} Watch failure: {}",
|
||||
uid_store.account_name,
|
||||
err.to_string()
|
||||
);
|
||||
debug!("Watch failure: {}", err.to_string());
|
||||
match timeout(uid_store.timeout, main_conn_lck.connect())
|
||||
.await
|
||||
.and_then(|res| res)
|
||||
{
|
||||
Err(err2) => {
|
||||
log::trace!(
|
||||
"{} Watch reconnect attempt failed: {}",
|
||||
uid_store.account_name,
|
||||
err2.to_string()
|
||||
);
|
||||
debug!("Watch reconnect attempt failed: {}", err2.to_string());
|
||||
}
|
||||
Ok(()) => {
|
||||
log::trace!(
|
||||
"{} Watch reconnect attempt succesful",
|
||||
uid_store.account_name
|
||||
);
|
||||
debug!("Watch reconnect attempt succesful");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
@ -559,7 +530,7 @@ impl MailBackend for ImapType {
|
|||
});
|
||||
return Err(err);
|
||||
}
|
||||
log::trace!("{} watch future returning", uid_store.account_name);
|
||||
debug!("watch future returning");
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
@ -617,18 +588,32 @@ impl MailBackend for ImapType {
|
|||
.unwrap()
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"LITERAL+"));
|
||||
let data = if has_literal_plus {
|
||||
Literal::try_from(bytes)?.into_non_sync()
|
||||
if has_literal_plus {
|
||||
conn.send_command(
|
||||
format!(
|
||||
"APPEND \"{}\" ({}) {{{}+}}",
|
||||
&path,
|
||||
flags_to_imap_list!(flags),
|
||||
bytes.len()
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
Literal::try_from(bytes)?
|
||||
};
|
||||
conn.send_command(CommandBody::append(
|
||||
path,
|
||||
flags.derive_imap_codec_flags(),
|
||||
None,
|
||||
data,
|
||||
)?)
|
||||
.await?;
|
||||
conn.send_command(
|
||||
format!(
|
||||
"APPEND \"{}\" ({}) {{{}}}",
|
||||
&path,
|
||||
flags_to_imap_list!(flags),
|
||||
bytes.len()
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
// wait for "+ Ready for literal data" reply
|
||||
conn.wait_for_continuation_request().await?;
|
||||
}
|
||||
conn.send_literal(&bytes).await?;
|
||||
conn.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
Ok(())
|
||||
|
@ -677,24 +662,36 @@ impl MailBackend for ImapType {
|
|||
conn.select_mailbox(source_mailbox_hash, &mut response, false)
|
||||
.await?;
|
||||
if has_move {
|
||||
conn.send_command(CommandBody::r#move(uids.as_slice(), dest_path, true)?)
|
||||
.await?;
|
||||
let command = {
|
||||
let mut cmd = format!("UID MOVE {}", uids[0]);
|
||||
for uid in uids.iter().skip(1) {
|
||||
cmd = format!("{},{}", cmd, uid);
|
||||
}
|
||||
format!("{} \"{}\"", cmd, dest_path)
|
||||
};
|
||||
conn.send_command(command.as_bytes()).await?;
|
||||
conn.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
} else {
|
||||
conn.send_command(CommandBody::copy(uids.as_slice(), dest_path, true)?)
|
||||
.await?;
|
||||
let command = {
|
||||
let mut cmd = format!("UID COPY {}", uids[0]);
|
||||
for uid in uids.iter().skip(1) {
|
||||
cmd = format!("{},{}", cmd, uid);
|
||||
}
|
||||
format!("{} \"{}\"", cmd, dest_path)
|
||||
};
|
||||
conn.send_command(command.as_bytes()).await?;
|
||||
conn.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
if move_ {
|
||||
conn.send_command(CommandBody::store(
|
||||
uids.as_slice(),
|
||||
StoreType::Add,
|
||||
StoreResponse::Answer,
|
||||
vec![ImapCodecFlag::Deleted],
|
||||
true,
|
||||
)?)
|
||||
.await?;
|
||||
let command = {
|
||||
let mut cmd = format!("UID STORE {}", uids[0]);
|
||||
for uid in uids.iter().skip(1) {
|
||||
cmd = format!("{},{}", cmd, uid);
|
||||
}
|
||||
format!("{} +FLAGS (\\Deleted)", cmd)
|
||||
};
|
||||
conn.send_command(command.as_bytes()).await?;
|
||||
conn.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
}
|
||||
|
@ -776,7 +773,9 @@ impl MailBackend for ImapType {
|
|||
}
|
||||
Err(tag) => {
|
||||
let hash = TagHash::from_bytes(tag.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| tag.to_string());
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, tag.to_string());
|
||||
}
|
||||
cmd.push_str(tag);
|
||||
cmd.push(' ');
|
||||
}
|
||||
|
@ -787,17 +786,16 @@ impl MailBackend for ImapType {
|
|||
cmd.push(')');
|
||||
cmd
|
||||
};
|
||||
conn.send_command_raw(command.as_bytes()).await?;
|
||||
conn.send_command(command.as_bytes()).await?;
|
||||
conn.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
if set_seen {
|
||||
for f in uid_store.mailboxes.lock().await.values() {
|
||||
if let Ok(mut unseen) = f.unseen.lock() {
|
||||
for env_hash in env_hashes.iter() {
|
||||
unseen.remove(env_hash);
|
||||
}
|
||||
};
|
||||
}
|
||||
let f = &uid_store.mailboxes.lock().await[&mailbox_hash];
|
||||
if let Ok(mut unseen) = f.unseen.lock() {
|
||||
for env_hash in env_hashes.iter() {
|
||||
unseen.remove(env_hash);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
if flags.iter().any(|(_, b)| !*b) {
|
||||
|
@ -853,17 +851,16 @@ impl MailBackend for ImapType {
|
|||
cmd.push(')');
|
||||
cmd
|
||||
};
|
||||
conn.send_command_raw(command.as_bytes()).await?;
|
||||
conn.send_command(command.as_bytes()).await?;
|
||||
conn.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
if set_unseen {
|
||||
for f in uid_store.mailboxes.lock().await.values() {
|
||||
if let Ok(mut unseen) = f.unseen.lock() {
|
||||
for env_hash in env_hashes.iter() {
|
||||
unseen.insert_new(env_hash);
|
||||
}
|
||||
};
|
||||
}
|
||||
let f = &uid_store.mailboxes.lock().await[&mailbox_hash];
|
||||
if let Ok(mut unseen) = f.unseen.lock() {
|
||||
for env_hash in env_hashes.iter() {
|
||||
unseen.insert_new(env_hash);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -885,14 +882,10 @@ impl MailBackend for ImapType {
|
|||
flag_future.await?;
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
let mut conn = connection.lock().await;
|
||||
conn.send_command(CommandBody::Expunge).await?;
|
||||
conn.send_command_imap_codec(CommandBody::Expunge).await?;
|
||||
conn.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
imap_trace!(
|
||||
conn,
|
||||
"EXPUNGE response: {}",
|
||||
&String::from_utf8_lossy(&response)
|
||||
);
|
||||
debug!("EXPUNGE response: {}", &String::from_utf8_lossy(&response));
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
@ -943,11 +936,7 @@ impl MailBackend for ImapType {
|
|||
}
|
||||
for root_mailbox in mailboxes.values().filter(|f| f.parent.is_none()) {
|
||||
if path.starts_with(&root_mailbox.name) {
|
||||
log::trace!(
|
||||
"{} path starts with {:?}",
|
||||
uid_store.account_name,
|
||||
&root_mailbox
|
||||
);
|
||||
debug!("path starts with {:?}", &root_mailbox);
|
||||
path = path.replace(
|
||||
'/',
|
||||
(root_mailbox.separator as char).encode_utf8(&mut [0; 4]),
|
||||
|
@ -966,13 +955,13 @@ impl MailBackend for ImapType {
|
|||
conn_lck.unselect().await?;
|
||||
|
||||
conn_lck
|
||||
.send_command(CommandBody::create(path.as_str())?)
|
||||
.send_command(format!("CREATE \"{}\"", path,).as_bytes())
|
||||
.await?;
|
||||
conn_lck
|
||||
.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
conn_lck
|
||||
.send_command(CommandBody::subscribe(path.as_str())?)
|
||||
.send_command(format!("SUBSCRIBE \"{}\"", path,).as_bytes())
|
||||
.await?;
|
||||
conn_lck
|
||||
.read_response(&mut response, RequiredResponses::empty())
|
||||
|
@ -1020,6 +1009,9 @@ impl MailBackend for ImapType {
|
|||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let mailbox = ImapCodecMailbox::try_from(imap_path.as_str()).unwrap();
|
||||
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
{
|
||||
let mut conn_lck = connection.lock().await;
|
||||
|
@ -1028,7 +1020,9 @@ impl MailBackend for ImapType {
|
|||
conn_lck.unselect().await?;
|
||||
if is_subscribed {
|
||||
conn_lck
|
||||
.send_command(CommandBody::unsubscribe(imap_path.as_str())?)
|
||||
.send_command_imap_codec(CommandBody::Unsubscribe {
|
||||
mailbox: mailbox.clone(),
|
||||
})
|
||||
.await?;
|
||||
conn_lck
|
||||
.read_response(&mut response, RequiredResponses::empty())
|
||||
|
@ -1036,7 +1030,7 @@ impl MailBackend for ImapType {
|
|||
}
|
||||
|
||||
conn_lck
|
||||
.send_command(CommandBody::delete(imap_path.as_str())?)
|
||||
.send_command_imap_codec(debug!(CommandBody::Delete { mailbox }))
|
||||
.await?;
|
||||
conn_lck
|
||||
.read_response(&mut response, RequiredResponses::empty())
|
||||
|
@ -1065,24 +1059,24 @@ impl MailBackend for ImapType {
|
|||
let uid_store = self.uid_store.clone();
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
let imap_path = {
|
||||
let mailboxes = uid_store.mailboxes.lock().await;
|
||||
if mailboxes[&mailbox_hash].is_subscribed() == new_val {
|
||||
return Ok(());
|
||||
}
|
||||
mailboxes[&mailbox_hash].imap_path().to_string()
|
||||
};
|
||||
let mailboxes = uid_store.mailboxes.lock().await;
|
||||
if mailboxes[&mailbox_hash].is_subscribed() == new_val {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
{
|
||||
let mailbox =
|
||||
ImapCodecMailbox::try_from(mailboxes[&mailbox_hash].imap_path()).unwrap();
|
||||
|
||||
let mut conn_lck = connection.lock().await;
|
||||
if new_val {
|
||||
conn_lck
|
||||
.send_command(CommandBody::subscribe(imap_path.as_str())?)
|
||||
.send_command_imap_codec(CommandBody::Subscribe { mailbox })
|
||||
.await?;
|
||||
} else {
|
||||
conn_lck
|
||||
.send_command(CommandBody::unsubscribe(imap_path.as_str())?)
|
||||
.send_command_imap_codec(CommandBody::Unsubscribe { mailbox })
|
||||
.await?;
|
||||
}
|
||||
conn_lck
|
||||
|
@ -1141,7 +1135,7 @@ impl MailBackend for ImapType {
|
|||
}
|
||||
{
|
||||
let mut conn_lck = connection.lock().await;
|
||||
conn_lck.send_command_raw(command.as_bytes()).await?;
|
||||
conn_lck.send_command(debug!(command).as_bytes()).await?;
|
||||
conn_lck
|
||||
.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
|
@ -1198,7 +1192,94 @@ impl MailBackend for ImapType {
|
|||
));
|
||||
}
|
||||
let mailbox_hash = mailbox_hash.unwrap();
|
||||
let query_str = query.to_imap_search();
|
||||
fn rec(q: &crate::search::Query, s: &mut String) {
|
||||
use crate::search::{escape_double_quote, Query::*};
|
||||
match q {
|
||||
Subject(t) => {
|
||||
s.push_str(" SUBJECT \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
From(t) => {
|
||||
s.push_str(" FROM \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
To(t) => {
|
||||
s.push_str(" TO \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Cc(t) => {
|
||||
s.push_str(" CC \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Bcc(t) => {
|
||||
s.push_str(" BCC \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
AllText(t) => {
|
||||
s.push_str(" TEXT \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Flags(v) => {
|
||||
for f in v {
|
||||
match f.as_str() {
|
||||
"draft" => {
|
||||
s.push_str(" DRAFT ");
|
||||
}
|
||||
"deleted" => {
|
||||
s.push_str(" DELETED ");
|
||||
}
|
||||
"flagged" => {
|
||||
s.push_str(" FLAGGED ");
|
||||
}
|
||||
"recent" => {
|
||||
s.push_str(" RECENT ");
|
||||
}
|
||||
"seen" | "read" => {
|
||||
s.push_str(" SEEN ");
|
||||
}
|
||||
"unseen" | "unread" => {
|
||||
s.push_str(" UNSEEN ");
|
||||
}
|
||||
"answered" => {
|
||||
s.push_str(" ANSWERED ");
|
||||
}
|
||||
"unanswered" => {
|
||||
s.push_str(" UNANSWERED ");
|
||||
}
|
||||
keyword => {
|
||||
s.push_str(" KEYWORD ");
|
||||
s.push_str(keyword);
|
||||
s.push(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
And(q1, q2) => {
|
||||
rec(q1, s);
|
||||
s.push(' ');
|
||||
rec(q2, s);
|
||||
}
|
||||
Or(q1, q2) => {
|
||||
s.push_str(" OR ");
|
||||
rec(q1, s);
|
||||
s.push(' ');
|
||||
rec(q2, s);
|
||||
}
|
||||
Not(q) => {
|
||||
s.push_str(" NOT ");
|
||||
rec(q, s);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut query_str = String::new();
|
||||
rec(&query, &mut query_str);
|
||||
let connection = self.connection.clone();
|
||||
let uid_store = self.uid_store.clone();
|
||||
|
||||
|
@ -1207,14 +1288,11 @@ impl MailBackend for ImapType {
|
|||
let mut conn = connection.lock().await;
|
||||
conn.examine_mailbox(mailbox_hash, &mut response, false)
|
||||
.await?;
|
||||
conn.send_command_raw(
|
||||
format!("UID SEARCH CHARSET UTF-8 {}", query_str.trim()).as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
conn.send_command(format!("UID SEARCH CHARSET UTF-8 {}", query_str.trim()).as_bytes())
|
||||
.await?;
|
||||
conn.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await?;
|
||||
imap_trace!(
|
||||
conn,
|
||||
debug!(
|
||||
"searching for {} returned: {}",
|
||||
query_str,
|
||||
String::from_utf8_lossy(&response)
|
||||
|
@ -1240,7 +1318,6 @@ impl MailBackend for ImapType {
|
|||
}
|
||||
|
||||
impl ImapType {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(
|
||||
s: &AccountSettings,
|
||||
is_subscribed: Box<dyn Fn(&str) -> bool + Send + Sync>,
|
||||
|
@ -1301,7 +1378,7 @@ impl ImapType {
|
|||
timeout,
|
||||
};
|
||||
let account_hash = AccountHash::from_bytes(s.name.as_bytes());
|
||||
let account_name = s.name.to_string().into();
|
||||
let account_name = Arc::new(s.name.to_string());
|
||||
let uid_store: Arc<UIDStore> = Arc::new(UIDStore {
|
||||
keep_offline_cache,
|
||||
..UIDStore::new(
|
||||
|
@ -1311,14 +1388,9 @@ impl ImapType {
|
|||
server_conf.timeout,
|
||||
)
|
||||
});
|
||||
let connection = ImapConnection::new_connection(
|
||||
&server_conf,
|
||||
#[cfg(debug_assertions)]
|
||||
"ImapType::new".into(),
|
||||
uid_store.clone(),
|
||||
);
|
||||
let connection = ImapConnection::new_connection(&server_conf, uid_store.clone());
|
||||
|
||||
Ok(Box::new(Self {
|
||||
Ok(Box::new(ImapType {
|
||||
server_conf,
|
||||
_is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
|
||||
connection: Arc::new(FutureMutex::new(connection)),
|
||||
|
@ -1327,12 +1399,7 @@ impl ImapType {
|
|||
}
|
||||
|
||||
pub fn shell(&mut self) {
|
||||
let mut conn = ImapConnection::new_connection(
|
||||
&self.server_conf,
|
||||
#[cfg(debug_assertions)]
|
||||
"ImapType::shell".into(),
|
||||
self.uid_store.clone(),
|
||||
);
|
||||
let mut conn = ImapConnection::new_connection(&self.server_conf, self.uid_store.clone());
|
||||
|
||||
futures::executor::block_on(timeout(self.server_conf.timeout, conn.connect()))
|
||||
.unwrap()
|
||||
|
@ -1340,7 +1407,7 @@ impl ImapType {
|
|||
let mut res = Vec::with_capacity(8 * 1024);
|
||||
futures::executor::block_on(timeout(
|
||||
self.server_conf.timeout,
|
||||
conn.send_command(CommandBody::Noop),
|
||||
conn.send_command_imap_codec(CommandBody::Noop),
|
||||
))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
@ -1360,7 +1427,7 @@ impl ImapType {
|
|||
Ok(_) => {
|
||||
futures::executor::block_on(timeout(
|
||||
self.server_conf.timeout,
|
||||
conn.send_command_raw(input.as_bytes()),
|
||||
conn.send_command(input.as_bytes()),
|
||||
))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
@ -1377,7 +1444,7 @@ impl ImapType {
|
|||
if input.trim() == "IDLE" {
|
||||
let mut iter = ImapBlockingConnection::from(conn);
|
||||
while let Some(line) = iter.next() {
|
||||
imap_trace!("out: {}", unsafe { std::str::from_utf8_unchecked(&line) });
|
||||
debug!("out: {}", unsafe { std::str::from_utf8_unchecked(&line) });
|
||||
}
|
||||
conn = iter.into_conn();
|
||||
}
|
||||
|
@ -1403,8 +1470,8 @@ impl ImapType {
|
|||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"LIST-STATUS"));
|
||||
if has_list_status {
|
||||
// TODO(#222): imap-codec does not support "LIST Command Extensions" currently.
|
||||
conn.send_command_raw(b"LIST \"\" \"*\" RETURN (STATUS (MESSAGES UNSEEN))")
|
||||
// TODO(imap-codec): LIST-STATUS not supported.
|
||||
conn.send_command(b"LIST \"\" \"*\" RETURN (STATUS (MESSAGES UNSEEN))")
|
||||
.await?;
|
||||
conn.read_response(
|
||||
&mut res,
|
||||
|
@ -1412,38 +1479,44 @@ impl ImapType {
|
|||
)
|
||||
.await?;
|
||||
} else {
|
||||
conn.send_command(CommandBody::list("", "*")?).await?;
|
||||
conn.send_command_imap_codec(CommandBody::List {
|
||||
reference: ImapCodecMailbox::try_from("").unwrap(),
|
||||
mailbox_wildcard: ImapCodecListMailbox::try_from("*").unwrap(),
|
||||
})
|
||||
.await?;
|
||||
conn.read_response(&mut res, RequiredResponses::LIST_REQUIRED)
|
||||
.await?;
|
||||
}
|
||||
imap_trace!(conn, "LIST reply: {}", String::from_utf8_lossy(&res));
|
||||
debug!("LIST reply: {}", String::from_utf8_lossy(&res));
|
||||
for l in res.split_rn() {
|
||||
if !l.starts_with(b"*") {
|
||||
continue;
|
||||
}
|
||||
if let Ok(mut mailbox) = protocol_parser::list_mailbox_result(l).map(|(_, v)| v) {
|
||||
if let Some(parent) = mailbox.parent {
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = mailboxes.entry(parent) {
|
||||
/* Insert dummy parent entry, populating only the children field. Later
|
||||
* when we encounter the parent entry we will swap its children with
|
||||
* dummy's */
|
||||
e.insert(ImapMailbox {
|
||||
children: vec![mailbox.hash],
|
||||
..ImapMailbox::default()
|
||||
});
|
||||
} else {
|
||||
if mailboxes.contains_key(&parent) {
|
||||
mailboxes
|
||||
.entry(parent)
|
||||
.and_modify(|e| e.children.push(mailbox.hash));
|
||||
} else {
|
||||
/* Insert dummy parent entry, populating only the children field. Later
|
||||
* when we encounter the parent entry we will swap its children with
|
||||
* dummy's */
|
||||
mailboxes.insert(
|
||||
parent,
|
||||
ImapMailbox {
|
||||
children: vec![mailbox.hash],
|
||||
..ImapMailbox::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = mailboxes.entry(mailbox.hash)
|
||||
{
|
||||
e.insert(mailbox);
|
||||
} else {
|
||||
if mailboxes.contains_key(&mailbox.hash) {
|
||||
let entry = mailboxes.entry(mailbox.hash).or_default();
|
||||
std::mem::swap(&mut entry.children, &mut mailbox.children);
|
||||
*entry = mailbox;
|
||||
} else {
|
||||
mailboxes.insert(mailbox.hash, mailbox);
|
||||
}
|
||||
} else if let Ok(status) = protocol_parser::status_response(l).map(|(_, v)| v) {
|
||||
if let Some(mailbox_hash) = status.mailbox {
|
||||
|
@ -1458,14 +1531,14 @@ impl ImapType {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
imap_trace!(conn, "parse error for {:?}", l);
|
||||
debug!("parse error for {:?}", l);
|
||||
}
|
||||
}
|
||||
mailboxes.retain(|_, v| !v.hash.is_null());
|
||||
conn.send_command(CommandBody::lsub("", "*")?).await?;
|
||||
conn.send_command(b"LSUB \"\" \"*\"").await?;
|
||||
conn.read_response(&mut res, RequiredResponses::LSUB_REQUIRED)
|
||||
.await?;
|
||||
imap_trace!(conn, "LSUB reply: {}", String::from_utf8_lossy(&res));
|
||||
debug!("LSUB reply: {}", String::from_utf8_lossy(&res));
|
||||
for l in res.split_rn() {
|
||||
if !l.starts_with(b"*") {
|
||||
continue;
|
||||
|
@ -1480,7 +1553,7 @@ impl ImapType {
|
|||
f.is_subscribed = true;
|
||||
}
|
||||
} else {
|
||||
imap_trace!(conn, "parse error for {:?}", l);
|
||||
debug!("parse error for {:?}", l);
|
||||
}
|
||||
}
|
||||
Ok(mailboxes)
|
||||
|
@ -1621,12 +1694,7 @@ struct FetchState {
|
|||
}
|
||||
|
||||
async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
||||
imap_trace!(
|
||||
state.connection.lock().await,
|
||||
"fetch_hlpr mailbox: {:?} stage: {:?}",
|
||||
state.mailbox_hash,
|
||||
&state.stage
|
||||
);
|
||||
debug!((state.mailbox_hash, &state.stage));
|
||||
loop {
|
||||
match state.stage {
|
||||
FetchStage::InitialFresh => {
|
||||
|
@ -1683,8 +1751,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
}
|
||||
Ok(Some(cached_payload)) => {
|
||||
state.stage = FetchStage::ResyncCache;
|
||||
imap_trace!(
|
||||
state.connection.lock().await,
|
||||
debug!(
|
||||
"fetch_hlpr fetch_cached_envs payload {} len for mailbox_hash {}",
|
||||
cached_payload.len(),
|
||||
state.mailbox_hash
|
||||
|
@ -1755,21 +1822,41 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
conn.examine_mailbox(mailbox_hash, &mut response, false)
|
||||
.await?;
|
||||
if max_uid_left > 0 {
|
||||
imap_trace!(conn, "{} max_uid_left= {}", mailbox_hash, max_uid_left);
|
||||
let sequence_set = if max_uid_left == 1 {
|
||||
SequenceSet::from(ONE)
|
||||
} else {
|
||||
let min = std::cmp::max(max_uid_left.saturating_sub(chunk_size), 1);
|
||||
let max = max_uid_left;
|
||||
debug!("{} max_uid_left= {}", mailbox_hash, max_uid_left);
|
||||
|
||||
SequenceSet::try_from(min..=max)?
|
||||
let attributes = vec![
|
||||
FetchAttribute::Uid,
|
||||
FetchAttribute::Flags,
|
||||
FetchAttribute::Envelope,
|
||||
FetchAttribute::BodyExt {
|
||||
section: Some(Section::HeaderFields(
|
||||
None,
|
||||
NonEmptyVec::from(AString::try_from("REFERENCES").unwrap()),
|
||||
)),
|
||||
partial: None,
|
||||
peek: true,
|
||||
},
|
||||
FetchAttribute::BodyStructure,
|
||||
];
|
||||
|
||||
let command = if max_uid_left == 1 {
|
||||
CommandBody::fetch(1, attributes, true).unwrap()
|
||||
} else {
|
||||
let from = NonZeroU32::try_from(
|
||||
u32::try_from(std::cmp::max(
|
||||
max_uid_left.saturating_sub(chunk_size),
|
||||
1,
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let to =
|
||||
NonZeroU32::try_from(u32::try_from(max_uid_left).unwrap()).unwrap();
|
||||
|
||||
CommandBody::fetch(format!("{}:{}", from, to), attributes, true).unwrap()
|
||||
};
|
||||
conn.send_command(CommandBody::Fetch {
|
||||
sequence_set,
|
||||
macro_or_item_names: common_attributes(),
|
||||
uid: true,
|
||||
})
|
||||
.await?;
|
||||
debug!("sending {:?}", &command);
|
||||
conn.send_command_imap_codec(command).await?;
|
||||
conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await
|
||||
.chain_err_summary(|| {
|
||||
|
@ -1779,8 +1866,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
)
|
||||
})?;
|
||||
let (_, mut v, _) = protocol_parser::fetch_responses(&response)?;
|
||||
imap_trace!(
|
||||
conn,
|
||||
debug!(
|
||||
"fetch response is {} bytes and {} lines and has {} parsed Envelopes",
|
||||
response.len(),
|
||||
String::from_utf8_lossy(&response).lines().count(),
|
||||
|
@ -1796,20 +1882,12 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
} in v.iter_mut()
|
||||
{
|
||||
if uid.is_none() || envelope.is_none() || flags.is_none() {
|
||||
imap_trace!(
|
||||
conn,
|
||||
"BUG? something in fetch is none. UID: {:?}, envelope: {:?} \
|
||||
flags: {:?}",
|
||||
uid,
|
||||
envelope,
|
||||
flags
|
||||
);
|
||||
imap_trace!(
|
||||
conn,
|
||||
"response was: {}",
|
||||
String::from_utf8_lossy(&response)
|
||||
);
|
||||
conn.process_untagged(raw_fetch_value).await?;
|
||||
debug!("BUG? in fetch is none");
|
||||
debug!(uid);
|
||||
debug!(envelope);
|
||||
debug!(flags);
|
||||
debug!("response was: {}", String::from_utf8_lossy(&response));
|
||||
debug!(conn.process_untagged(raw_fetch_value).await)?;
|
||||
continue;
|
||||
}
|
||||
let uid = uid.unwrap();
|
||||
|
@ -1826,7 +1904,9 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
}
|
||||
for f in keywords {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f.to_string());
|
||||
}
|
||||
env.tags_mut().push(hash);
|
||||
}
|
||||
}
|
||||
|
@ -1858,7 +1938,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
let uid = uid.unwrap();
|
||||
let env = envelope.unwrap();
|
||||
/*
|
||||
imap_trace!(
|
||||
debug!(
|
||||
"env hash {} {} UID = {} MSN = {}",
|
||||
env.hash(),
|
||||
env.subject(),
|
||||
|
|
|
@ -34,9 +34,9 @@ pub struct ModSequence(pub std::num::NonZeroU64);
|
|||
|
||||
impl TryFrom<i64> for ModSequence {
|
||||
type Error = ();
|
||||
fn try_from(val: i64) -> std::result::Result<Self, ()> {
|
||||
fn try_from(val: i64) -> std::result::Result<ModSequence, ()> {
|
||||
std::num::NonZeroU64::new(val as u64)
|
||||
.map(|u| Ok(Self(u)))
|
||||
.map(|u| Ok(ModSequence(u)))
|
||||
.unwrap_or(Err(()))
|
||||
}
|
||||
}
|
||||
|
@ -106,17 +106,17 @@ pub use sqlite3_m::*;
|
|||
#[cfg(feature = "sqlite3")]
|
||||
mod sqlite3_m {
|
||||
use super::*;
|
||||
use crate::utils::sqlite3::{
|
||||
use crate::sqlite3::{
|
||||
self,
|
||||
rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput},
|
||||
Connection, DatabaseDescription,
|
||||
DatabaseDescription,
|
||||
};
|
||||
|
||||
type Sqlite3UID = i32;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Sqlite3Cache {
|
||||
connection: Connection,
|
||||
connection: crate::sqlite3::Connection,
|
||||
loaded_mailboxes: BTreeSet<MailboxHash>,
|
||||
uid_store: Arc<UIDStore>,
|
||||
}
|
||||
|
@ -148,7 +148,7 @@ mod sqlite3_m {
|
|||
CREATE INDEX IF NOT EXISTS envelope_idx ON envelopes(hash);
|
||||
CREATE INDEX IF NOT EXISTS mailbox_idx ON mailbox(mailbox_hash);",
|
||||
),
|
||||
version: 3,
|
||||
version: 2,
|
||||
};
|
||||
|
||||
impl ToSql for ModSequence {
|
||||
|
@ -163,7 +163,7 @@ mod sqlite3_m {
|
|||
if i == 0 {
|
||||
return Err(FromSqlError::OutOfRange(0));
|
||||
}
|
||||
Ok(Self::try_from(i).unwrap())
|
||||
Ok(ModSequence::try_from(i).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -172,7 +172,7 @@ mod sqlite3_m {
|
|||
Ok(Box::new(Self {
|
||||
connection: sqlite3::open_or_create_db(
|
||||
&DB_DESCRIPTION,
|
||||
Some(&uid_store.account_name),
|
||||
Some(uid_store.account_name.as_str()),
|
||||
)?,
|
||||
loaded_mailboxes: BTreeSet::default(),
|
||||
uid_store,
|
||||
|
@ -195,13 +195,13 @@ mod sqlite3_m {
|
|||
|
||||
impl ImapCacheReset for Sqlite3Cache {
|
||||
fn reset_db(uid_store: &UIDStore) -> Result<()> {
|
||||
sqlite3::reset_db(&DB_DESCRIPTION, Some(&uid_store.account_name))
|
||||
sqlite3::reset_db(&DB_DESCRIPTION, Some(uid_store.account_name.as_str()))
|
||||
}
|
||||
}
|
||||
|
||||
impl ImapCache for Sqlite3Cache {
|
||||
fn reset(&mut self) -> Result<()> {
|
||||
Self::reset_db(&self.uid_store)
|
||||
Sqlite3Cache::reset_db(&self.uid_store)
|
||||
}
|
||||
|
||||
fn mailbox_state(&mut self, mailbox_hash: MailboxHash) -> Result<Option<()>> {
|
||||
|
@ -256,7 +256,10 @@ mod sqlite3_m {
|
|||
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
|
||||
for f in to_str!(&flags).split('\0') {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
//debug!("hash {} flag {}", hash, &f);
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f.to_string());
|
||||
}
|
||||
}
|
||||
self.loaded_mailboxes.insert(mailbox_hash);
|
||||
Ok(Some(()))
|
||||
|
@ -402,32 +405,19 @@ mod sqlite3_m {
|
|||
return Ok(None);
|
||||
}
|
||||
|
||||
let ret: Vec<(UID, Envelope, Option<ModSequence>)> = match {
|
||||
let mut stmt = self.connection.prepare(
|
||||
"SELECT uid, envelope, modsequence FROM envelopes WHERE mailbox_hash = ?1;",
|
||||
)?;
|
||||
let mut stmt = self.connection.prepare(
|
||||
"SELECT uid, envelope, modsequence FROM envelopes WHERE mailbox_hash = ?1;",
|
||||
)?;
|
||||
|
||||
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
|
||||
// for the temporary to live long enough
|
||||
let x = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash], |row| {
|
||||
Ok((
|
||||
row.get(0).map(|i: Sqlite3UID| i as UID)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
))
|
||||
})?
|
||||
.collect::<std::result::Result<_, _>>();
|
||||
x
|
||||
} {
|
||||
Err(err) if matches!(&err, rusqlite::Error::FromSqlConversionFailure(_, _, _)) => {
|
||||
drop(err);
|
||||
self.reset()?;
|
||||
return Ok(None);
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
Ok(v) => v,
|
||||
};
|
||||
let ret: Vec<(UID, Envelope, Option<ModSequence>)> = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash], |row| {
|
||||
Ok((
|
||||
row.get(0).map(|i: Sqlite3UID| i as UID)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
))
|
||||
})?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
let mut max_uid = 0;
|
||||
let mut env_lck = self.uid_store.envelopes.lock().unwrap();
|
||||
let mut hash_index_lck = self.uid_store.hash_index.lock().unwrap();
|
||||
|
@ -618,8 +608,6 @@ mod sqlite3_m {
|
|||
AND uid = ?2;",
|
||||
)?;
|
||||
|
||||
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
|
||||
// for the temporary to live long enough
|
||||
let x = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash, uid as Sqlite3UID], |row| {
|
||||
Ok((
|
||||
|
@ -637,8 +625,6 @@ mod sqlite3_m {
|
|||
AND hash = ?2;",
|
||||
)?;
|
||||
|
||||
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
|
||||
// for the temporary to live long enough
|
||||
let x = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash, env_hash], |row| {
|
||||
Ok((
|
||||
|
@ -673,8 +659,6 @@ mod sqlite3_m {
|
|||
let mut stmt = self.connection.prepare(
|
||||
"SELECT rfc822 FROM envelopes WHERE mailbox_hash = ?1 AND uid = ?2;",
|
||||
)?;
|
||||
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
|
||||
// for the temporary to live long enough
|
||||
let x = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash, uid as Sqlite3UID], |row| {
|
||||
row.get(0)
|
||||
|
@ -686,8 +670,6 @@ mod sqlite3_m {
|
|||
let mut stmt = self.connection.prepare(
|
||||
"SELECT rfc822 FROM envelopes WHERE mailbox_hash = ?1 AND hash = ?2;",
|
||||
)?;
|
||||
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
|
||||
// for the temporary to live long enough
|
||||
let x = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash, env_hash], |row| row.get(0))?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
|
|
|
@ -19,20 +19,13 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use imap_codec::{
|
||||
fetch::{MacroOrMessageDataItemNames, MessageDataItemName},
|
||||
search::SearchKey,
|
||||
sequence::SequenceSet,
|
||||
status::StatusDataItemName,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl ImapConnection {
|
||||
pub async fn resync(&mut self, mailbox_hash: MailboxHash) -> Result<Option<Vec<Envelope>>> {
|
||||
debug!("resync mailbox_hash {}", mailbox_hash);
|
||||
debug!(&self.sync_policy);
|
||||
if matches!(self.sync_policy, SyncPolicy::None) {
|
||||
if let SyncPolicy::None = self.sync_policy {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
|
@ -107,13 +100,13 @@ impl ImapConnection {
|
|||
.unwrap()
|
||||
.get(&mailbox_hash)
|
||||
.cloned();
|
||||
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
|
||||
//if cached_uidvalidity.is_none() || cached_max_uid.is_none() {
|
||||
// return Ok(None);
|
||||
//}
|
||||
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
|
||||
if cached_uidvalidity.is_none() || cached_max_uid.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let current_uidvalidity: UID = cached_uidvalidity.unwrap_or(1);
|
||||
let max_uid: UID = cached_max_uid.unwrap_or(1);
|
||||
let current_uidvalidity: UID = cached_uidvalidity.unwrap();
|
||||
let max_uid: UID = cached_max_uid.unwrap();
|
||||
let (mailbox_path, mailbox_exists, unseen) = {
|
||||
let f = &self.uid_store.mailboxes.lock().await[&mailbox_hash];
|
||||
(
|
||||
|
@ -134,12 +127,15 @@ impl ImapConnection {
|
|||
}
|
||||
cache_handle.update_mailbox(mailbox_hash, &select_response)?;
|
||||
|
||||
// 2. tag1 UID FETCH <lastseenuid+1>:* <descriptors>
|
||||
self.send_command(CommandBody::fetch(
|
||||
max_uid + 1..,
|
||||
common_attributes(),
|
||||
true,
|
||||
)?)
|
||||
// 2. tag1 UID FETCH <lastseenuid+1>:* <descriptors>
|
||||
self.send_command(
|
||||
format!(
|
||||
"UID FETCH {}:* (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] \
|
||||
BODYSTRUCTURE)",
|
||||
max_uid + 1
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
|
@ -172,7 +168,9 @@ impl ImapConnection {
|
|||
}
|
||||
for f in keywords {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f.to_string());
|
||||
}
|
||||
env.tags_mut().push(hash);
|
||||
}
|
||||
}
|
||||
|
@ -230,25 +228,18 @@ impl ImapConnection {
|
|||
unseen_lck.insert_set(new_unseen);
|
||||
}
|
||||
mailbox_exists.lock().unwrap().insert_set(payload_hash_set);
|
||||
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
|
||||
let sequence_set = if max_uid == 0 {
|
||||
SequenceSet::from(..)
|
||||
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
|
||||
if max_uid == 0 {
|
||||
self.send_command("UID FETCH 1:* FLAGS".as_bytes()).await?;
|
||||
} else {
|
||||
SequenceSet::try_from(..=max_uid)?
|
||||
};
|
||||
self.send_command(CommandBody::Fetch {
|
||||
sequence_set,
|
||||
macro_or_item_names: MacroOrMessageDataItemNames::MessageDataItemNames(vec![
|
||||
MessageDataItemName::Flags,
|
||||
]),
|
||||
uid: true,
|
||||
})
|
||||
.await?;
|
||||
self.send_command(format!("UID FETCH 1:{} FLAGS", max_uid).as_bytes())
|
||||
.await?;
|
||||
}
|
||||
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
// 1) update cached flags for old messages;
|
||||
// 2) find out which old messages got expunged; and
|
||||
// 3) build a mapping between message numbers and UIDs (for old messages).
|
||||
//1) update cached flags for old messages;
|
||||
//2) find out which old messages got expunged; and
|
||||
//3) build a mapping between message numbers and UIDs (for old messages).
|
||||
let mut valid_envs = BTreeSet::default();
|
||||
let mut env_lck = self.uid_store.envelopes.lock().unwrap();
|
||||
let (_, v, _) = protocol_parser::fetch_responses(&response)?;
|
||||
|
@ -422,9 +413,8 @@ impl ImapConnection {
|
|||
// "FETCH 1:* (FLAGS) (CHANGEDSINCE <cached-value>)" or
|
||||
// "SEARCH MODSEQ <cached-value>".
|
||||
|
||||
// 2. tag1 UID FETCH <lastseenuid+1>:* <descriptors>
|
||||
// TODO(#222): imap-codec does not support "CONDSTORE/QRESYNC" currently.
|
||||
self.send_command_raw(
|
||||
// 2. tag1 UID FETCH <lastseenuid+1>:* <descriptors>
|
||||
self.send_command(
|
||||
format!(
|
||||
"UID FETCH {}:* (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] \
|
||||
BODYSTRUCTURE) (CHANGEDSINCE {})",
|
||||
|
@ -465,7 +455,9 @@ impl ImapConnection {
|
|||
}
|
||||
for f in keywords {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f.to_string());
|
||||
}
|
||||
env.tags_mut().push(hash);
|
||||
}
|
||||
}
|
||||
|
@ -517,10 +509,9 @@ impl ImapConnection {
|
|||
unseen_lck.insert_set(new_unseen);
|
||||
}
|
||||
mailbox_exists.lock().unwrap().insert_set(payload_hash_set);
|
||||
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
|
||||
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
|
||||
if cached_max_uid == 0 {
|
||||
// TODO(#222): imap-codec does not support "CONDSTORE/QRESYNC" currently.
|
||||
self.send_command_raw(
|
||||
self.send_command(
|
||||
format!(
|
||||
"UID FETCH 1:* FLAGS (CHANGEDSINCE {})",
|
||||
cached_highestmodseq
|
||||
|
@ -529,8 +520,7 @@ impl ImapConnection {
|
|||
)
|
||||
.await?;
|
||||
} else {
|
||||
// TODO(#222): imap-codec does not support "CONDSTORE/QRESYNC" currently.
|
||||
self.send_command_raw(
|
||||
self.send_command(
|
||||
format!(
|
||||
"UID FETCH 1:{} FLAGS (CHANGEDSINCE {})",
|
||||
cached_max_uid, cached_highestmodseq
|
||||
|
@ -541,7 +531,7 @@ impl ImapConnection {
|
|||
}
|
||||
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
// 1) update cached flags for old messages;
|
||||
//1) update cached flags for old messages;
|
||||
let mut env_lck = self.uid_store.envelopes.lock().unwrap();
|
||||
let (_, v, _) = protocol_parser::fetch_responses(&response)?;
|
||||
for FetchResponse { uid, flags, .. } in v {
|
||||
|
@ -585,18 +575,17 @@ impl ImapConnection {
|
|||
let mut valid_envs = BTreeSet::default();
|
||||
// This should be UID SEARCH 1:<maxuid> but it's difficult to compare to cached
|
||||
// UIDs at the point of calling this function
|
||||
self.send_command(CommandBody::search(None, SearchKey::All, true))
|
||||
.await?;
|
||||
self.send_command(b"UID SEARCH ALL").await?;
|
||||
self.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await?;
|
||||
// 1) update cached flags for old messages;
|
||||
//1) update cached flags for old messages;
|
||||
let (_, v) = protocol_parser::search_results(response.as_slice())?;
|
||||
for uid in v {
|
||||
valid_envs.insert(generate_envelope_hash(&mailbox_path, &uid));
|
||||
}
|
||||
{
|
||||
let mut env_lck = self.uid_store.envelopes.lock().unwrap();
|
||||
let olds = env_lck
|
||||
for env_hash in env_lck
|
||||
.iter()
|
||||
.filter_map(|(h, cenv)| {
|
||||
if cenv.mailbox_hash == mailbox_hash {
|
||||
|
@ -605,8 +594,9 @@ impl ImapConnection {
|
|||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<EnvelopeHash>>();
|
||||
for env_hash in olds.difference(&valid_envs) {
|
||||
.collect::<BTreeSet<EnvelopeHash>>()
|
||||
.difference(&valid_envs)
|
||||
{
|
||||
refresh_events.push((
|
||||
env_lck[env_hash].uid,
|
||||
RefreshEvent {
|
||||
|
@ -693,11 +683,8 @@ impl ImapConnection {
|
|||
.await?;
|
||||
if select_response.uidnext == 0 {
|
||||
/* UIDNEXT shouldn't be 0, since exists != 0 at this point */
|
||||
self.send_command(CommandBody::status(
|
||||
mailbox_path,
|
||||
[StatusDataItemName::UidNext].as_slice(),
|
||||
)?)
|
||||
.await?;
|
||||
self.send_command(format!("STATUS \"{}\" (UIDNEXT)", mailbox_path).as_bytes())
|
||||
.await?;
|
||||
self.read_response(&mut response, RequiredResponses::STATUS)
|
||||
.await?;
|
||||
let (_, status) = protocol_parser::status_response(response.as_slice())?;
|
||||
|
|
|
@ -21,67 +21,49 @@
|
|||
|
||||
use super::protocol_parser::{ImapLineSplit, ImapResponse, RequiredResponses, SelectResponse};
|
||||
use crate::{
|
||||
backends::{BackendEvent, MailboxHash, RefreshEvent},
|
||||
backends::{MailboxHash, RefreshEvent},
|
||||
connections::{lookup_ipv4, timeout, Connection},
|
||||
email::parser::BytesExt,
|
||||
error::*,
|
||||
utils::{
|
||||
connections::{lookup_ipv4, Connection},
|
||||
futures::timeout,
|
||||
},
|
||||
LogLevel,
|
||||
};
|
||||
extern crate native_tls;
|
||||
#[cfg(debug_assertions)]
|
||||
use std::borrow::Cow;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
convert::TryFrom,
|
||||
future::Future,
|
||||
iter::FromIterator,
|
||||
num::NonZeroU32,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant, SystemTime},
|
||||
};
|
||||
|
||||
use futures::io::{AsyncReadExt, AsyncWriteExt};
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
use imap_codec::extensions::compress::CompressionAlgorithm;
|
||||
use imap_codec::{
|
||||
auth::AuthMechanism,
|
||||
codec::{Encode, Fragment},
|
||||
command::{Command, CommandBody},
|
||||
core::{AString, LiteralMode, NonEmptyVec, Tag},
|
||||
extensions::enable::CapabilityEnable,
|
||||
mailbox::Mailbox,
|
||||
search::SearchKey,
|
||||
codec::{Action, Encode},
|
||||
command::{
|
||||
search::SearchKey, status::StatusAttribute, Command, CommandBody, SeqOrUid, Sequence,
|
||||
SequenceSet,
|
||||
},
|
||||
core::{AString, NonEmptyVec},
|
||||
extensions::{compress::CompressionAlgorithm, enable::CapabilityEnable},
|
||||
message::{AuthMechanism, Mailbox, Tag},
|
||||
secret::Secret,
|
||||
sequence::SequenceSet,
|
||||
status::StatusDataItemName,
|
||||
};
|
||||
use native_tls::TlsConnector;
|
||||
pub use smol::Async as AsyncWrapper;
|
||||
|
||||
const IMAP_PROTOCOL_TIMEOUT: Duration = Duration::from_secs(60 * 28);
|
||||
|
||||
macro_rules! imap_trace {
|
||||
($conn:expr, $fmt:literal, $($t:tt)*) => {
|
||||
#[cfg(debug_assertions)]
|
||||
log::trace!(std::concat!("{} ", $fmt), $conn.id, $($t)*);
|
||||
};
|
||||
($conn:expr, $fmt:literal) => {
|
||||
#[cfg(debug_assertions)]
|
||||
log::trace!(std::concat!("{} ", $fmt), $conn.id);
|
||||
};
|
||||
}
|
||||
|
||||
use super::{protocol_parser, Capabilities, ImapServerConf, UIDStore};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum SyncPolicy {
|
||||
None,
|
||||
/// RFC4549 `Synch Ops for Disconnected IMAP4 Clients` <https://tools.ietf.org/html/rfc4549>
|
||||
///rfc4549 `Synch Ops for Disconnected IMAP4 Clients` <https://tools.ietf.org/html/rfc4549>
|
||||
Basic,
|
||||
/// RFC7162 `IMAP Extensions: Quick Flag Changes Resynchronization
|
||||
///rfc7162 `IMAP Extensions: Quick Flag Changes Resynchronization
|
||||
/// (CONDSTORE) and Quick Mailbox Resynchronization (QRESYNC)`
|
||||
Condstore,
|
||||
CondstoreQresync,
|
||||
|
@ -117,8 +99,6 @@ impl Default for ImapExtensionUse {
|
|||
#[derive(Debug)]
|
||||
pub struct ImapStream {
|
||||
pub cmd_id: usize,
|
||||
#[cfg(debug_assertions)]
|
||||
pub id: Cow<'static, str>,
|
||||
pub stream: AsyncWrapper<Connection>,
|
||||
pub protocol: ImapProtocol,
|
||||
pub current_mailbox: MailboxSelection,
|
||||
|
@ -134,7 +114,7 @@ pub enum MailboxSelection {
|
|||
|
||||
impl MailboxSelection {
|
||||
pub fn take(&mut self) -> Self {
|
||||
std::mem::replace(self, Self::None)
|
||||
std::mem::replace(self, MailboxSelection::None)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,8 +124,6 @@ async fn try_await(cl: impl Future<Output = Result<()>> + Send) -> Result<()> {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct ImapConnection {
|
||||
#[cfg(debug_assertions)]
|
||||
pub id: Cow<'static, str>,
|
||||
pub stream: Result<ImapStream>,
|
||||
pub server_conf: ImapServerConf,
|
||||
pub sync_policy: SyncPolicy,
|
||||
|
@ -155,9 +133,8 @@ pub struct ImapConnection {
|
|||
impl ImapStream {
|
||||
pub async fn new_connection(
|
||||
server_conf: &ImapServerConf,
|
||||
#[cfg(debug_assertions)] id: Cow<'static, str>,
|
||||
uid_store: &UIDStore,
|
||||
) -> Result<(Capabilities, Self)> {
|
||||
) -> Result<(Capabilities, ImapStream)> {
|
||||
use std::net::TcpStream;
|
||||
let path = &server_conf.server_hostname;
|
||||
|
||||
|
@ -165,7 +142,7 @@ impl ImapStream {
|
|||
let stream = if server_conf.use_tls {
|
||||
(uid_store.event_consumer)(
|
||||
uid_store.account_hash,
|
||||
BackendEvent::AccountStateChange {
|
||||
crate::backends::BackendEvent::AccountStateChange {
|
||||
message: "Establishing TLS connection.".into(),
|
||||
},
|
||||
);
|
||||
|
@ -175,7 +152,9 @@ impl ImapStream {
|
|||
}
|
||||
let connector = connector
|
||||
.build()
|
||||
.chain_err_kind(ErrorKind::Network(NetworkErrorKind::InvalidTLSConnection))?;
|
||||
.chain_err_kind(crate::error::ErrorKind::Network(
|
||||
crate::error::NetworkErrorKind::InvalidTLSConnection,
|
||||
))?;
|
||||
|
||||
let addr = lookup_ipv4(path, server_conf.server_port)?;
|
||||
|
||||
|
@ -264,8 +243,8 @@ impl ImapStream {
|
|||
midhandshake_stream = Some(stream);
|
||||
}
|
||||
p => {
|
||||
p.chain_err_kind(ErrorKind::Network(
|
||||
NetworkErrorKind::InvalidTLSConnection,
|
||||
p.chain_err_kind(crate::error::ErrorKind::Network(
|
||||
crate::error::NetworkErrorKind::InvalidTLSConnection,
|
||||
))?;
|
||||
}
|
||||
}
|
||||
|
@ -277,7 +256,11 @@ impl ImapStream {
|
|||
.chain_err_summary(|| format!("Could not initiate TLS negotiation to {}.", path))?
|
||||
}
|
||||
} else {
|
||||
let addr = lookup_ipv4(path, server_conf.server_port)?;
|
||||
let addr = if let Ok(a) = lookup_ipv4(path, server_conf.server_port) {
|
||||
a
|
||||
} else {
|
||||
return Err(Error::new(format!("Could not lookup address {}", &path)));
|
||||
};
|
||||
AsyncWrapper::new(Connection::Tcp(
|
||||
if let Some(timeout) = server_conf.timeout {
|
||||
TcpStream::connect_timeout(&addr, timeout)?
|
||||
|
@ -293,37 +276,37 @@ impl ImapStream {
|
|||
log::warn!("Could not set TCP keepalive in IMAP connection: {}", err);
|
||||
}
|
||||
let mut res = Vec::with_capacity(8 * 1024);
|
||||
let mut ret = Self {
|
||||
let mut ret = ImapStream {
|
||||
cmd_id,
|
||||
#[cfg(debug_assertions)]
|
||||
id,
|
||||
stream,
|
||||
protocol: server_conf.protocol,
|
||||
current_mailbox: MailboxSelection::None,
|
||||
timeout: server_conf.timeout,
|
||||
};
|
||||
if matches!(server_conf.protocol, ImapProtocol::ManageSieve) {
|
||||
if let ImapProtocol::ManageSieve = server_conf.protocol {
|
||||
ret.read_response(&mut res).await?;
|
||||
|
||||
let credentials = format!(
|
||||
"\0{}\0{}",
|
||||
&server_conf.server_username, &server_conf.server_password
|
||||
);
|
||||
ret.send_command(CommandBody::authenticate_with_ir(
|
||||
AuthMechanism::PLAIN,
|
||||
credentials.as_bytes(),
|
||||
|
||||
ret.send_command_imap_codec(CommandBody::authenticate(
|
||||
AuthMechanism::Plain,
|
||||
Some(credentials.as_bytes()),
|
||||
))
|
||||
.await?;
|
||||
ret.read_response(&mut res).await?;
|
||||
|
||||
return Ok((Default::default(), ret));
|
||||
}
|
||||
|
||||
(uid_store.event_consumer)(
|
||||
uid_store.account_hash,
|
||||
BackendEvent::AccountStateChange {
|
||||
crate::backends::BackendEvent::AccountStateChange {
|
||||
message: "Negotiating server capabilities.".into(),
|
||||
},
|
||||
);
|
||||
ret.send_command(CommandBody::Capability).await?;
|
||||
ret.send_command_imap_codec(CommandBody::Capability).await?;
|
||||
ret.read_response(&mut res).await?;
|
||||
let capabilities: std::result::Result<Vec<&[u8]>, _> = res
|
||||
.split_rn()
|
||||
|
@ -341,7 +324,7 @@ impl ImapStream {
|
|||
&server_conf.server_hostname,
|
||||
String::from_utf8_lossy(&res)
|
||||
))
|
||||
.set_kind(ErrorKind::ProtocolError));
|
||||
.set_kind(ErrorKind::Bug));
|
||||
}
|
||||
|
||||
let capabilities = capabilities.unwrap();
|
||||
|
@ -352,8 +335,7 @@ impl ImapStream {
|
|||
return Err(Error::new(format!(
|
||||
"Could not connect to {}: server is not IMAP4rev1 compliant",
|
||||
&server_conf.server_hostname
|
||||
))
|
||||
.set_kind(ErrorKind::ProtocolNotSupported));
|
||||
)));
|
||||
} else if capabilities
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"LOGINDISABLED"))
|
||||
|
@ -362,12 +344,12 @@ impl ImapStream {
|
|||
"Could not connect to {}: server does not accept logins [LOGINDISABLED]",
|
||||
&server_conf.server_hostname
|
||||
))
|
||||
.set_err_kind(ErrorKind::Authentication));
|
||||
.set_err_kind(crate::error::ErrorKind::Authentication));
|
||||
}
|
||||
|
||||
(uid_store.event_consumer)(
|
||||
uid_store.account_hash,
|
||||
BackendEvent::AccountStateChange {
|
||||
crate::backends::BackendEvent::AccountStateChange {
|
||||
message: "Attempting authentication.".into(),
|
||||
},
|
||||
);
|
||||
|
@ -388,27 +370,18 @@ impl ImapStream {
|
|||
.map(|capability| String::from_utf8_lossy(capability).to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ")
|
||||
))
|
||||
.set_err_kind(ErrorKind::Authentication));
|
||||
)));
|
||||
}
|
||||
let xoauth2 = base64::decode(&server_conf.server_password)
|
||||
.chain_err_summary(|| {
|
||||
"Could not decode `server_password` from base64. Is the value correct?"
|
||||
})
|
||||
.chain_err_kind(ErrorKind::Configuration)?;
|
||||
ret.send_command(CommandBody::authenticate_with_ir(
|
||||
AuthMechanism::XOAUTH2,
|
||||
&xoauth2,
|
||||
))
|
||||
ret.send_command(
|
||||
format!("AUTHENTICATE XOAUTH2 {}", &server_conf.server_password).as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
_ => {
|
||||
let username = AString::try_from(server_conf.server_username.as_str())
|
||||
.chain_err_kind(ErrorKind::Bug)?;
|
||||
let password = AString::try_from(server_conf.server_password.as_str())
|
||||
.chain_err_kind(ErrorKind::Bug)?;
|
||||
let username = AString::try_from(server_conf.server_username.as_str())?;
|
||||
let password = AString::try_from(server_conf.server_password.as_str())?;
|
||||
|
||||
ret.send_command(CommandBody::Login {
|
||||
ret.send_command_imap_codec(CommandBody::Login {
|
||||
username,
|
||||
password: Secret::new(password),
|
||||
})
|
||||
|
@ -436,7 +409,7 @@ impl ImapStream {
|
|||
"Could not connect. Server replied with '{}'",
|
||||
String::from_utf8_lossy(l[tag_start.len()..].trim())
|
||||
))
|
||||
.set_err_kind(ErrorKind::Authentication));
|
||||
.set_err_kind(crate::error::ErrorKind::Authentication));
|
||||
}
|
||||
should_break = true;
|
||||
}
|
||||
|
@ -449,7 +422,8 @@ impl ImapStream {
|
|||
if capabilities.is_none() {
|
||||
/* sending CAPABILITY after LOGIN automatically is an RFC recommendation, so
|
||||
* check for lazy servers */
|
||||
ret.send_command(CommandBody::Capability).await?;
|
||||
drop(capabilities);
|
||||
ret.send_command_imap_codec(CommandBody::Capability).await?;
|
||||
ret.read_response(&mut res).await.unwrap();
|
||||
let capabilities = protocol_parser::capabilities(&res)?.1;
|
||||
let capabilities = HashSet::from_iter(capabilities.into_iter().map(|s| s.to_vec()));
|
||||
|
@ -513,7 +487,7 @@ impl ImapStream {
|
|||
}
|
||||
}
|
||||
}
|
||||
//imap_trace!(self, "returning IMAP response:\n{:?}", &ret);
|
||||
//debug!("returning IMAP response:\n{:?}", &ret);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -524,50 +498,56 @@ impl ImapStream {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_command(&mut self, body: CommandBody<'_>) -> Result<()> {
|
||||
timeout(self.timeout, async {
|
||||
let command = {
|
||||
let tag = Tag::unvalidated(format!("M{}", self.cmd_id));
|
||||
// TODO(duesee): Rename after `send_command` was removed.
|
||||
pub async fn send_command_imap_codec(&mut self, command_body: CommandBody<'_>) -> Result<()> {
|
||||
let command = Command {
|
||||
// We know that this tag is valid.
|
||||
tag: Tag::unchecked(format!("M{}", self.cmd_id.to_string())),
|
||||
body: command_body,
|
||||
};
|
||||
|
||||
Command { tag, body }
|
||||
};
|
||||
match self.protocol {
|
||||
ImapProtocol::IMAP { .. } => {
|
||||
if matches!(command.body, CommandBody::Login { .. }) {
|
||||
imap_trace!(self, "sent: M{} LOGIN ..", self.cmd_id - 1);
|
||||
} else {
|
||||
imap_trace!(self, "sent: M{} {:?}", self.cmd_id - 1, command.body);
|
||||
_ = timeout(
|
||||
self.timeout,
|
||||
try_await(async move {
|
||||
match self.protocol {
|
||||
ImapProtocol::IMAP { .. } => {
|
||||
self.cmd_id += 1;
|
||||
}
|
||||
ImapProtocol::ManageSieve => {}
|
||||
}
|
||||
ImapProtocol::ManageSieve => {}
|
||||
}
|
||||
|
||||
for action in command.encode() {
|
||||
match action {
|
||||
Fragment::Line { data } => {
|
||||
self.stream.write_all(&data).await?;
|
||||
}
|
||||
Fragment::Literal { data, mode } => {
|
||||
// We only need to wait for a continuation request when we are about to
|
||||
// send a synchronizing literal, i.e., when not using LITERAL+.
|
||||
if mode == LiteralMode::Sync {
|
||||
for action in command.encode() {
|
||||
match action {
|
||||
Action::Send { data } => {
|
||||
self.stream.write_all(&data).await?;
|
||||
}
|
||||
Action::RecvContinuationRequest => {
|
||||
self.wait_for_continuation_request().await?;
|
||||
}
|
||||
self.stream.write_all(&data).await?;
|
||||
Action::Unknown => {
|
||||
return Err("Unexpected message flow".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Note: This is required for compression to work...
|
||||
self.stream.flush().await?;
|
||||
}
|
||||
|
||||
self.cmd_id += 1;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await?
|
||||
match self.protocol {
|
||||
ImapProtocol::IMAP { .. } => {
|
||||
// We do not need to worry about logging sensitive values here because
|
||||
// imap-codec redacts it. The only way to obtain sensitive data (without
|
||||
// calling `expose_secret`) is through `Encode::encode{,_detached}`.
|
||||
debug!("sent: {:?}", command);
|
||||
}
|
||||
ImapProtocol::ManageSieve => {}
|
||||
}
|
||||
Ok(())
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_command_raw(&mut self, command: &[u8]) -> Result<()> {
|
||||
// TODO(duesee): Replace this with `send_command_imap_codec`.
|
||||
pub async fn send_command(&mut self, command: &[u8]) -> Result<()> {
|
||||
_ = timeout(
|
||||
self.timeout,
|
||||
try_await(async move {
|
||||
|
@ -590,11 +570,11 @@ impl ImapStream {
|
|||
match self.protocol {
|
||||
ImapProtocol::IMAP { .. } => {
|
||||
if !command.starts_with(b"LOGIN") {
|
||||
imap_trace!(self, "sent: M{} {}", self.cmd_id - 1, unsafe {
|
||||
debug!("sent: M{} {}", self.cmd_id - 1, unsafe {
|
||||
std::str::from_utf8_unchecked(command)
|
||||
});
|
||||
} else {
|
||||
imap_trace!(self, "sent: M{} LOGIN ..", self.cmd_id - 1);
|
||||
debug!("sent: M{} LOGIN ..", self.cmd_id - 1);
|
||||
}
|
||||
}
|
||||
ImapProtocol::ManageSieve => {}
|
||||
|
@ -624,13 +604,10 @@ impl ImapStream {
|
|||
impl ImapConnection {
|
||||
pub fn new_connection(
|
||||
server_conf: &ImapServerConf,
|
||||
#[cfg(debug_assertions)] id: Cow<'static, str>,
|
||||
uid_store: Arc<UIDStore>,
|
||||
) -> Self {
|
||||
Self {
|
||||
) -> ImapConnection {
|
||||
ImapConnection {
|
||||
stream: Err(Error::new("Offline".to_string())),
|
||||
#[cfg(debug_assertions)]
|
||||
id,
|
||||
server_conf: server_conf.clone(),
|
||||
sync_policy: if uid_store.keep_offline_cache {
|
||||
SyncPolicy::Basic
|
||||
|
@ -658,30 +635,23 @@ impl ImapConnection {
|
|||
}
|
||||
if self.stream.is_ok() {
|
||||
let mut ret = Vec::new();
|
||||
if let Err(_err) = try_await(async {
|
||||
self.send_command(CommandBody::Noop).await?;
|
||||
if let Err(err) = try_await(async {
|
||||
self.send_command_imap_codec(CommandBody::Noop).await?;
|
||||
self.read_response(&mut ret, RequiredResponses::empty())
|
||||
.await
|
||||
})
|
||||
.await
|
||||
{
|
||||
imap_trace!(self, "connect(): connection is probably dead: {:?}", &_err);
|
||||
debug!("connect(): connection is probably dead: {:?}", &err);
|
||||
} else {
|
||||
imap_trace!(
|
||||
self,
|
||||
debug!(
|
||||
"connect(): connection is probably alive, NOOP returned {:?}",
|
||||
&String::from_utf8_lossy(&ret)
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let new_stream = ImapStream::new_connection(
|
||||
&self.server_conf,
|
||||
#[cfg(debug_assertions)]
|
||||
self.id.clone(),
|
||||
&self.uid_store,
|
||||
)
|
||||
.await;
|
||||
let new_stream = ImapStream::new_connection(&self.server_conf, &self.uid_store).await;
|
||||
if let Err(err) = new_stream.as_ref() {
|
||||
self.uid_store.is_online.lock().unwrap().1 = Err(err.clone());
|
||||
} else {
|
||||
|
@ -707,7 +677,7 @@ impl ImapConnection {
|
|||
/* Upgrade to Condstore */
|
||||
let mut ret = Vec::new();
|
||||
if capabilities.contains(&b"ENABLE"[..]) {
|
||||
self.send_command(CommandBody::Enable {
|
||||
self.send_command_imap_codec(CommandBody::Enable {
|
||||
capabilities: NonEmptyVec::from(
|
||||
CapabilityEnable::CondStore,
|
||||
),
|
||||
|
@ -716,16 +686,15 @@ impl ImapConnection {
|
|||
self.read_response(&mut ret, RequiredResponses::empty())
|
||||
.await?;
|
||||
} else {
|
||||
self.send_command(CommandBody::Status {
|
||||
self.send_command_imap_codec(CommandBody::Status {
|
||||
mailbox: Mailbox::Inbox,
|
||||
item_names: vec![
|
||||
StatusDataItemName::UidNext,
|
||||
StatusDataItemName::UidValidity,
|
||||
StatusDataItemName::Unseen,
|
||||
StatusDataItemName::Messages,
|
||||
StatusDataItemName::HighestModSeq,
|
||||
]
|
||||
.into(),
|
||||
attributes: vec![
|
||||
StatusAttribute::UidNext,
|
||||
StatusAttribute::UidValidity,
|
||||
StatusAttribute::Unseen,
|
||||
StatusAttribute::Messages,
|
||||
StatusAttribute::HighestModSeq,
|
||||
],
|
||||
})
|
||||
.await?;
|
||||
self.read_response(&mut ret, RequiredResponses::empty())
|
||||
|
@ -738,8 +707,10 @@ impl ImapConnection {
|
|||
#[cfg(feature = "deflate_compression")]
|
||||
if capabilities.contains(&b"COMPRESS=DEFLATE"[..]) && deflate {
|
||||
let mut ret = Vec::new();
|
||||
self.send_command(CommandBody::compress(CompressionAlgorithm::Deflate))
|
||||
.await?;
|
||||
self.send_command_imap_codec(CommandBody::compress(
|
||||
CompressionAlgorithm::Deflate,
|
||||
))
|
||||
.await?;
|
||||
self.read_response(&mut ret, RequiredResponses::empty())
|
||||
.await?;
|
||||
match ImapResponse::try_from(ret.as_slice())? {
|
||||
|
@ -757,8 +728,6 @@ impl ImapConnection {
|
|||
ImapResponse::Ok(_) => {
|
||||
let ImapStream {
|
||||
cmd_id,
|
||||
#[cfg(debug_assertions)]
|
||||
id,
|
||||
stream,
|
||||
protocol,
|
||||
current_mailbox,
|
||||
|
@ -767,8 +736,6 @@ impl ImapConnection {
|
|||
let stream = stream.into_inner()?;
|
||||
self.stream = Ok(ImapStream {
|
||||
cmd_id,
|
||||
#[cfg(debug_assertions)]
|
||||
id,
|
||||
stream: AsyncWrapper::new(stream.deflate())?,
|
||||
protocol,
|
||||
current_mailbox,
|
||||
|
@ -808,26 +775,24 @@ impl ImapConnection {
|
|||
ret.extend_from_slice(&response);
|
||||
return r.into();
|
||||
}
|
||||
ImapResponse::No(ref _response_code)
|
||||
ImapResponse::No(ref response_code)
|
||||
if required_responses.intersects(RequiredResponses::NO_REQUIRED) =>
|
||||
{
|
||||
imap_trace!(
|
||||
self,
|
||||
debug!(
|
||||
"Received expected NO response: {:?} {:?}",
|
||||
_response_code,
|
||||
response_code,
|
||||
String::from_utf8_lossy(&response)
|
||||
);
|
||||
}
|
||||
ImapResponse::No(ref response_code) => {
|
||||
imap_trace!(
|
||||
self,
|
||||
debug!(
|
||||
"Received NO response: {:?} {:?}",
|
||||
response_code,
|
||||
String::from_utf8_lossy(&response)
|
||||
);
|
||||
(self.uid_store.event_consumer)(
|
||||
self.uid_store.account_hash,
|
||||
BackendEvent::Notice {
|
||||
crate::backends::BackendEvent::Notice {
|
||||
description: response_code.to_string(),
|
||||
content: None,
|
||||
level: LogLevel::ERROR,
|
||||
|
@ -837,15 +802,14 @@ impl ImapConnection {
|
|||
return r.into();
|
||||
}
|
||||
ImapResponse::Bad(ref response_code) => {
|
||||
imap_trace!(
|
||||
self,
|
||||
debug!(
|
||||
"Received BAD response: {:?} {:?}",
|
||||
response_code,
|
||||
String::from_utf8_lossy(&response)
|
||||
);
|
||||
(self.uid_store.event_consumer)(
|
||||
self.uid_store.account_hash,
|
||||
BackendEvent::Notice {
|
||||
crate::backends::BackendEvent::Notice {
|
||||
description: response_code.to_string(),
|
||||
content: None,
|
||||
level: LogLevel::ERROR,
|
||||
|
@ -856,12 +820,12 @@ impl ImapConnection {
|
|||
}
|
||||
_ => {}
|
||||
}
|
||||
/* imap_trace!(self,
|
||||
/*debug!(
|
||||
"check every line for required_responses: {:#?}",
|
||||
&required_responses
|
||||
);*/
|
||||
for l in response.split_rn() {
|
||||
/* imap_trace!(self, "check line: {}", &l); */
|
||||
/* debug!("check line: {}", &l); */
|
||||
if required_responses.check(l) || !self.process_untagged(l).await? {
|
||||
ret.extend_from_slice(l);
|
||||
}
|
||||
|
@ -896,9 +860,9 @@ impl ImapConnection {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_command(&mut self, command: CommandBody<'_>) -> Result<()> {
|
||||
pub async fn send_command_imap_codec(&mut self, command: CommandBody<'_>) -> Result<()> {
|
||||
if let Err(err) =
|
||||
try_await(async { self.stream.as_mut()?.send_command(command).await }).await
|
||||
try_await(async { self.stream.as_mut()?.send_command_imap_codec(command).await }).await
|
||||
{
|
||||
self.stream = Err(err.clone());
|
||||
if err.kind.is_network() {
|
||||
|
@ -911,9 +875,9 @@ impl ImapConnection {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn send_command_raw(&mut self, command: &[u8]) -> Result<()> {
|
||||
pub async fn send_command(&mut self, command: &[u8]) -> Result<()> {
|
||||
if let Err(err) =
|
||||
try_await(async { self.stream.as_mut()?.send_command_raw(command).await }).await
|
||||
try_await(async { self.stream.as_mut()?.send_command(command).await }).await
|
||||
{
|
||||
self.stream = Err(err.clone());
|
||||
if err.kind.is_network() {
|
||||
|
@ -974,15 +938,14 @@ impl ImapConnection {
|
|||
"Trying to select a \\NoSelect mailbox: {}",
|
||||
&imap_path
|
||||
))
|
||||
.set_kind(ErrorKind::Bug));
|
||||
.set_kind(crate::error::ErrorKind::Bug));
|
||||
}
|
||||
self.send_command(CommandBody::select(imap_path.as_str())?)
|
||||
self.send_command_imap_codec(CommandBody::select(imap_path.as_str()).unwrap())
|
||||
.await?;
|
||||
self.read_response(ret, RequiredResponses::SELECT_REQUIRED)
|
||||
.await?;
|
||||
imap_trace!(
|
||||
self,
|
||||
"{} SELECT response {}",
|
||||
debug!(
|
||||
"{} select response {}",
|
||||
imap_path,
|
||||
String::from_utf8_lossy(ret)
|
||||
);
|
||||
|
@ -1004,7 +967,7 @@ impl ImapConnection {
|
|||
}) {
|
||||
(self.uid_store.event_consumer)(
|
||||
self.uid_store.account_hash,
|
||||
BackendEvent::from(err),
|
||||
crate::backends::BackendEvent::from(err),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1061,14 +1024,13 @@ impl ImapConnection {
|
|||
"Trying to examine a \\NoSelect mailbox: {}",
|
||||
&imap_path
|
||||
))
|
||||
.set_kind(ErrorKind::Bug));
|
||||
.set_kind(crate::error::ErrorKind::Bug));
|
||||
}
|
||||
self.send_command(CommandBody::examine(imap_path.as_str())?)
|
||||
self.send_command_imap_codec(CommandBody::examine(imap_path.as_str()).unwrap())
|
||||
.await?;
|
||||
self.read_response(ret, RequiredResponses::EXAMINE_REQUIRED)
|
||||
.await?;
|
||||
|
||||
imap_trace!(self, "EXAMINE response {}", String::from_utf8_lossy(ret));
|
||||
debug!("examine response {}", String::from_utf8_lossy(ret));
|
||||
let select_response = protocol_parser::select_response(ret).chain_err_summary(|| {
|
||||
format!("Could not parse select response for mailbox {}", imap_path)
|
||||
})?;
|
||||
|
@ -1100,7 +1062,7 @@ impl ImapConnection {
|
|||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"UNSELECT"))
|
||||
{
|
||||
self.send_command(CommandBody::Unselect).await?;
|
||||
self.send_command_imap_codec(CommandBody::Unselect).await?;
|
||||
self.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
} else {
|
||||
|
@ -1115,7 +1077,8 @@ impl ImapConnection {
|
|||
nonexistent.push('p');
|
||||
}
|
||||
}
|
||||
self.send_command(CommandBody::select(nonexistent)?).await?;
|
||||
self.send_command_imap_codec(CommandBody::select(nonexistent).unwrap())
|
||||
.await?;
|
||||
self.read_response(&mut response, RequiredResponses::NO_REQUIRED)
|
||||
.await?;
|
||||
}
|
||||
|
@ -1126,7 +1089,10 @@ impl ImapConnection {
|
|||
}
|
||||
|
||||
pub fn add_refresh_event(&mut self, ev: RefreshEvent) {
|
||||
(self.uid_store.event_consumer)(self.uid_store.account_hash, BackendEvent::Refresh(ev));
|
||||
(self.uid_store.event_consumer)(
|
||||
self.uid_store.account_hash,
|
||||
crate::backends::BackendEvent::Refresh(ev),
|
||||
);
|
||||
}
|
||||
|
||||
async fn create_uid_msn_cache(
|
||||
|
@ -1136,14 +1102,16 @@ impl ImapConnection {
|
|||
_select_response: &SelectResponse,
|
||||
) -> Result<()> {
|
||||
debug_assert!(low > 0);
|
||||
self.send_command(CommandBody::search(
|
||||
let mut response = Vec::new();
|
||||
self.send_command_imap_codec(CommandBody::search(
|
||||
None,
|
||||
SearchKey::SequenceSet(SequenceSet::try_from(low..)?),
|
||||
SearchKey::Uid(SequenceSet::from(Sequence::Range(
|
||||
SeqOrUid::Value(NonZeroU32::try_from(u32::try_from(low).unwrap()).unwrap()),
|
||||
SeqOrUid::Asterisk,
|
||||
))),
|
||||
true,
|
||||
))
|
||||
.await?;
|
||||
|
||||
let mut response = Vec::new();
|
||||
self.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await?;
|
||||
let mut msn_index_lck = self.uid_store.msn_index.lock().unwrap();
|
||||
|
@ -1164,7 +1132,7 @@ pub struct ImapBlockingConnection {
|
|||
|
||||
impl From<ImapConnection> for ImapBlockingConnection {
|
||||
fn from(conn: ImapConnection) -> Self {
|
||||
Self {
|
||||
ImapBlockingConnection {
|
||||
buf: vec![0; Connection::IO_BUF_SIZE],
|
||||
conn,
|
||||
prev_res_length: 0,
|
||||
|
@ -1183,7 +1151,7 @@ impl ImapBlockingConnection {
|
|||
self.err.take()
|
||||
}
|
||||
|
||||
pub fn as_stream(&mut self) -> impl Future<Output = Option<Vec<u8>>> + '_ {
|
||||
pub fn as_stream<'a>(&'a mut self) -> impl Future<Output = Option<Vec<u8>>> + 'a {
|
||||
self.result.drain(0..self.prev_res_length);
|
||||
self.prev_res_length = 0;
|
||||
let mut break_flag = false;
|
||||
|
|
|
@ -1,115 +0,0 @@
|
|||
/*
|
||||
* meli - imap module.
|
||||
*
|
||||
* Copyright 2023 Damian Poddebniak <poddebniak@mailbox.org>
|
||||
*
|
||||
* 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::{fmt, sync::Arc};
|
||||
|
||||
use imap_codec::{
|
||||
command::{AppendError, CopyError, ListError},
|
||||
core::LiteralError,
|
||||
extensions::r#move::MoveError,
|
||||
sequence::SequenceSetError,
|
||||
};
|
||||
|
||||
use crate::error::{Error, ErrorKind};
|
||||
|
||||
impl From<LiteralError> for Error {
|
||||
#[inline]
|
||||
fn from(error: LiteralError) -> Self {
|
||||
Self {
|
||||
summary: error.to_string().into(),
|
||||
details: None,
|
||||
source: Some(Arc::new(error)),
|
||||
kind: ErrorKind::Configuration,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SequenceSetError> for Error {
|
||||
#[inline]
|
||||
fn from(error: SequenceSetError) -> Self {
|
||||
Self {
|
||||
summary: error.to_string().into(),
|
||||
details: None,
|
||||
source: Some(Arc::new(error)),
|
||||
kind: ErrorKind::Bug,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, L> From<AppendError<S, L>> for Error
|
||||
where
|
||||
AppendError<S, L>: fmt::Debug + fmt::Display + Sync + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn from(error: AppendError<S, L>) -> Self {
|
||||
Self {
|
||||
summary: error.to_string().into(),
|
||||
details: None,
|
||||
source: Some(Arc::new(error)),
|
||||
kind: ErrorKind::Bug,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, L> From<CopyError<S, L>> for Error
|
||||
where
|
||||
CopyError<S, L>: fmt::Debug + fmt::Display + Sync + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn from(error: CopyError<S, L>) -> Self {
|
||||
Self {
|
||||
summary: error.to_string().into(),
|
||||
details: None,
|
||||
source: Some(Arc::new(error)),
|
||||
kind: ErrorKind::Bug,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, M> From<MoveError<S, M>> for Error
|
||||
where
|
||||
MoveError<S, M>: fmt::Debug + fmt::Display + Sync + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn from(error: MoveError<S, M>) -> Self {
|
||||
Self {
|
||||
summary: error.to_string().into(),
|
||||
details: None,
|
||||
source: Some(Arc::new(error)),
|
||||
kind: ErrorKind::Bug,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<L1, L2> From<ListError<L1, L2>> for Error
|
||||
where
|
||||
ListError<L1, L2>: fmt::Debug + fmt::Display + Sync + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn from(error: ListError<L1, L2>) -> Self {
|
||||
Self {
|
||||
summary: error.to_string().into(),
|
||||
details: None,
|
||||
source: Some(Arc::new(error)),
|
||||
kind: ErrorKind::Bug,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -328,18 +328,13 @@ impl ManageSieveConnection {
|
|||
))),
|
||||
..UIDStore::new(
|
||||
account_hash,
|
||||
account_name.into(),
|
||||
Arc::new(account_name),
|
||||
event_consumer,
|
||||
server_conf.timeout,
|
||||
)
|
||||
});
|
||||
Ok(Self {
|
||||
inner: ImapConnection::new_connection(
|
||||
&server_conf,
|
||||
#[cfg(debug_assertions)]
|
||||
"ManageSieveConnection::new()".into(),
|
||||
uid_store,
|
||||
),
|
||||
inner: ImapConnection::new_connection(&server_conf, uid_store),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -374,7 +369,7 @@ impl ManageSieveConnection {
|
|||
|
||||
pub async fn listscripts(&mut self) -> Result<Vec<(Vec<u8>, bool)>> {
|
||||
let mut ret = Vec::new();
|
||||
self.inner.send_command_raw(b"Listscripts").await?;
|
||||
self.inner.send_command(b"Listscripts").await?;
|
||||
self.inner
|
||||
.read_response(&mut ret, RequiredResponses::empty())
|
||||
.await?;
|
||||
|
|
|
@ -21,8 +21,6 @@
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
use imap_codec::fetch::MessageDataItemName;
|
||||
|
||||
use super::*;
|
||||
use crate::{backends::*, email::*, error::Error};
|
||||
|
||||
|
@ -42,7 +40,7 @@ impl ImapOp {
|
|||
connection: Arc<FutureMutex<ImapConnection>>,
|
||||
uid_store: Arc<UIDStore>,
|
||||
) -> Self {
|
||||
Self {
|
||||
ImapOp {
|
||||
uid,
|
||||
connection,
|
||||
mailbox_hash,
|
||||
|
@ -70,12 +68,8 @@ impl BackendOp for ImapOp {
|
|||
conn.connect().await?;
|
||||
conn.examine_mailbox(mailbox_hash, &mut response, false)
|
||||
.await?;
|
||||
conn.send_command(CommandBody::fetch(
|
||||
uid,
|
||||
vec![MessageDataItemName::Flags, MessageDataItemName::Rfc822],
|
||||
true,
|
||||
)?)
|
||||
.await?;
|
||||
conn.send_command(format!("UID FETCH {} (FLAGS RFC822)", uid).as_bytes())
|
||||
.await?;
|
||||
conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
}
|
||||
|
@ -133,12 +127,8 @@ impl BackendOp for ImapOp {
|
|||
conn.connect().await?;
|
||||
conn.examine_mailbox(mailbox_hash, &mut response, false)
|
||||
.await?;
|
||||
conn.send_command(CommandBody::fetch(
|
||||
uid,
|
||||
vec![MessageDataItemName::Flags],
|
||||
true,
|
||||
)?)
|
||||
.await?;
|
||||
conn.send_command(format!("UID FETCH {} FLAGS", uid).as_bytes())
|
||||
.await?;
|
||||
conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
debug!(
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,311 +0,0 @@
|
|||
/*
|
||||
* meli - imap module.
|
||||
*
|
||||
* Copyright 2017 - 2023 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/>.
|
||||
*/
|
||||
|
||||
//! Convert [`crate::search::Query`] into IMAP search criteria.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::{
|
||||
search::*,
|
||||
utils::datetime::{formats::IMAP_DATE, timestamp_to_string},
|
||||
};
|
||||
|
||||
mod private {
|
||||
pub trait Sealed {}
|
||||
}
|
||||
|
||||
pub trait ToImapSearch: private::Sealed {
|
||||
/// Convert [`crate::search::Query`] into IMAP search criteria.
|
||||
fn to_imap_search(&self) -> String;
|
||||
}
|
||||
|
||||
impl private::Sealed for Query {}
|
||||
|
||||
macro_rules! space_pad {
|
||||
($s:ident) => {{
|
||||
if !$s.is_empty() && !$s.ends_with('(') && !$s.ends_with(' ') {
|
||||
$s.push(' ');
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
impl ToImapSearch for Query {
|
||||
fn to_imap_search(&self) -> String {
|
||||
enum Step<'a> {
|
||||
Q(&'a Query),
|
||||
Lit(char),
|
||||
}
|
||||
use Step::*;
|
||||
|
||||
let mut stack = VecDeque::new();
|
||||
stack.push_front(Q(self));
|
||||
let mut s = String::new();
|
||||
while let Some(q) = stack.pop_front() {
|
||||
use Query::*;
|
||||
match q {
|
||||
Lit(lit) => {
|
||||
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" => {
|
||||
s.push_str("DRAFT ");
|
||||
}
|
||||
"deleted" => {
|
||||
s.push_str("DELETED ");
|
||||
}
|
||||
"flagged" => {
|
||||
s.push_str("FLAGGED ");
|
||||
}
|
||||
"recent" => {
|
||||
s.push_str("RECENT ");
|
||||
}
|
||||
"seen" | "read" => {
|
||||
s.push_str("SEEN ");
|
||||
}
|
||||
"unseen" | "unread" => {
|
||||
s.push_str("UNSEEN ");
|
||||
}
|
||||
"answered" => {
|
||||
s.push_str("ANSWERED ");
|
||||
}
|
||||
"unanswered" => {
|
||||
s.push_str("UNANSWERED ");
|
||||
}
|
||||
keyword => {
|
||||
s.push_str("KEYWORD ");
|
||||
s.push_str(keyword);
|
||||
s.push(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Q(And(q1, q2)) => {
|
||||
let is_empty = space_pad!(s);
|
||||
if !is_empty {
|
||||
stack.push_front(Lit(')'));
|
||||
}
|
||||
stack.push_front(Q(q2));
|
||||
stack.push_front(Q(q1));
|
||||
if !is_empty {
|
||||
stack.push_front(Lit('('));
|
||||
}
|
||||
}
|
||||
Q(Or(q1, q2)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("OR");
|
||||
stack.push_front(Q(q2));
|
||||
stack.push_front(Q(q1));
|
||||
}
|
||||
Q(Not(q)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("NOT (");
|
||||
stack.push_front(Lit(')'));
|
||||
stack.push_front(Q(q));
|
||||
}
|
||||
Q(Before(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("BEFORE ");
|
||||
s.push_str(×tamp_to_string(*t, Some(IMAP_DATE), true));
|
||||
}
|
||||
Q(After(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("SINCE ");
|
||||
s.push_str(×tamp_to_string(*t, Some(IMAP_DATE), true));
|
||||
}
|
||||
Q(Between(t1, t2)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("(SINCE ");
|
||||
s.push_str(×tamp_to_string(*t1, Some(IMAP_DATE), true));
|
||||
s.push_str(" BEFORE ");
|
||||
s.push_str(×tamp_to_string(*t2, Some(IMAP_DATE), true));
|
||||
s.push(')');
|
||||
}
|
||||
Q(On(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("ON ");
|
||||
s.push_str(×tamp_to_string(*t, Some(IMAP_DATE), true));
|
||||
}
|
||||
Q(InReplyTo(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str(r#"HEADER "In-Reply-To" ""#);
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Q(References(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str(r#"HEADER "References" ""#);
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
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);
|
||||
s.push_str(r#"BODY ""#);
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::utils::parsec::Parser;
|
||||
|
||||
#[test]
|
||||
fn test_imap_query_search() {
|
||||
let (_, q) = query().parse_complete("subject: test and i").unwrap();
|
||||
assert_eq!(&q.to_imap_search(), r#"SUBJECT "test" TEXT "i""#);
|
||||
|
||||
let (_, q) = query().parse_complete("is:unseen").unwrap();
|
||||
assert_eq!(&q.to_imap_search(), r#"UNSEEN"#);
|
||||
|
||||
let (_, q) = query().parse_complete("from:user@example.org").unwrap();
|
||||
assert_eq!(&q.to_imap_search(), r#"FROM "user@example.org""#);
|
||||
|
||||
let (_, q) = query()
|
||||
.parse_complete(
|
||||
"from:user@example.org and subject:
|
||||
\"foobar space\"",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
&q.to_imap_search(),
|
||||
r#"FROM "user@example.org" SUBJECT "foobar space""#
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
×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")))"#
|
||||
);
|
||||
}
|
||||
}
|
|
@ -19,9 +19,7 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
use imap_codec::{command::CommandBody, search::SearchKey, sequence::SequenceSet};
|
||||
use std::convert::TryInto;
|
||||
|
||||
use super::{ImapConnection, MailboxSelection, UID};
|
||||
use crate::{
|
||||
|
@ -34,7 +32,6 @@ use crate::{
|
|||
RefreshEventKind::{self, *},
|
||||
TagHash,
|
||||
},
|
||||
email::common_attributes,
|
||||
error::*,
|
||||
};
|
||||
|
||||
|
@ -92,12 +89,7 @@ impl ImapConnection {
|
|||
n,
|
||||
self.uid_store.msn_index.lock().unwrap().get(&mailbox_hash)
|
||||
);
|
||||
self.send_command(CommandBody::search(
|
||||
None,
|
||||
SearchKey::SequenceSet(SequenceSet::from(..)),
|
||||
true,
|
||||
))
|
||||
.await?;
|
||||
self.send_command("UID SEARCH 1:*".as_bytes()).await?;
|
||||
self.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await?;
|
||||
let results = super::protocol_parser::search_results(&response)?
|
||||
|
@ -115,7 +107,7 @@ impl ImapConnection {
|
|||
);
|
||||
}
|
||||
let mut events = vec![];
|
||||
let deleteds = self
|
||||
for (deleted_uid, deleted_hash) in self
|
||||
.uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
|
@ -125,8 +117,8 @@ impl ImapConnection {
|
|||
*mailbox_hash_ == mailbox_hash && !results.contains(u)
|
||||
})
|
||||
.map(|((_, uid), hash)| (*uid, *hash))
|
||||
.collect::<Vec<(UID, crate::email::EnvelopeHash)>>();
|
||||
for (deleted_uid, deleted_hash) in deleteds {
|
||||
.collect::<Vec<(UID, crate::email::EnvelopeHash)>>()
|
||||
{
|
||||
mailbox.exists.lock().unwrap().remove(deleted_hash);
|
||||
mailbox.unseen.lock().unwrap().remove(deleted_hash);
|
||||
self.uid_store
|
||||
|
@ -207,7 +199,7 @@ impl ImapConnection {
|
|||
debug!("exists {}", n);
|
||||
try_fail!(
|
||||
mailbox_hash,
|
||||
self.send_command(CommandBody::fetch(n, common_attributes(), false)?).await
|
||||
self.send_command(format!("FETCH {} (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)", n).as_bytes()).await
|
||||
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED).await
|
||||
);
|
||||
let mut v = match super::protocol_parser::fetch_responses(&response) {
|
||||
|
@ -246,7 +238,9 @@ impl ImapConnection {
|
|||
}
|
||||
for f in keywords {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f.to_string());
|
||||
}
|
||||
env.tags_mut().push(hash);
|
||||
}
|
||||
}
|
||||
|
@ -313,7 +307,7 @@ impl ImapConnection {
|
|||
UntaggedResponse::Recent(_) => {
|
||||
try_fail!(
|
||||
mailbox_hash,
|
||||
self.send_command(CommandBody::search(None, SearchKey::Recent, true)).await
|
||||
self.send_command(b"UID SEARCH RECENT").await
|
||||
self.read_response(&mut response, RequiredResponses::SEARCH).await
|
||||
);
|
||||
match super::protocol_parser::search_results_raw(&response)
|
||||
|
@ -340,7 +334,7 @@ impl ImapConnection {
|
|||
};
|
||||
try_fail!(
|
||||
mailbox_hash,
|
||||
self.send_command_raw(command.as_bytes()).await
|
||||
self.send_command(command.as_bytes()).await
|
||||
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED).await
|
||||
);
|
||||
let mut v = match super::protocol_parser::fetch_responses(&response) {
|
||||
|
@ -379,7 +373,9 @@ impl ImapConnection {
|
|||
}
|
||||
for f in keywords {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f.to_string());
|
||||
}
|
||||
env.tags_mut().push(hash);
|
||||
}
|
||||
}
|
||||
|
@ -469,12 +465,8 @@ impl ImapConnection {
|
|||
} else {
|
||||
try_fail!(
|
||||
mailbox_hash,
|
||||
self.send_command(CommandBody::search(
|
||||
None,
|
||||
SearchKey::SequenceSet(SequenceSet::try_from(msg_seq)?),
|
||||
true
|
||||
))
|
||||
.await,
|
||||
self.send_command(format!("UID SEARCH {}", msg_seq).as_bytes())
|
||||
.await,
|
||||
self.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await,
|
||||
);
|
||||
|
|
|
@ -20,8 +20,6 @@
|
|||
*/
|
||||
use std::sync::Arc;
|
||||
|
||||
use imap_codec::search::SearchKey;
|
||||
|
||||
use super::*;
|
||||
use crate::backends::SpecialUsageMailbox;
|
||||
|
||||
|
@ -122,7 +120,7 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> {
|
|||
}
|
||||
examine_updates(mailbox, &mut conn, &uid_store).await?;
|
||||
}
|
||||
conn.send_command(CommandBody::Idle).await?;
|
||||
conn.send_command(b"IDLE").await?;
|
||||
let mut blockn = ImapBlockingConnection::from(conn);
|
||||
let mut watch = std::time::Instant::now();
|
||||
/* duration interval to send heartbeat */
|
||||
|
@ -146,7 +144,7 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> {
|
|||
.conn
|
||||
.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
blockn.conn.send_command(CommandBody::Idle).await?;
|
||||
blockn.conn.send_command(b"IDLE").await?;
|
||||
let mut main_conn_lck = timeout(uid_store.timeout, main_conn.lock()).await?;
|
||||
main_conn_lck.connect().await?;
|
||||
continue;
|
||||
|
@ -194,7 +192,7 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> {
|
|||
}
|
||||
blockn.conn.process_untagged(l).await?;
|
||||
}
|
||||
blockn.conn.send_command(CommandBody::Idle).await?;
|
||||
blockn.conn.send_command(b"IDLE").await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -261,8 +259,7 @@ pub async fn examine_updates(
|
|||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"LIST-STATUS"));
|
||||
if has_list_status {
|
||||
// TODO(#222): imap-codec does not support "LIST Command Extensions" currently.
|
||||
conn.send_command_raw(
|
||||
conn.send_command(
|
||||
format!(
|
||||
"LIST \"{}\" \"\" RETURN (STATUS (MESSAGES UNSEEN))",
|
||||
mailbox.imap_path()
|
||||
|
@ -302,8 +299,7 @@ pub async fn examine_updates(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
conn.send_command(CommandBody::search(None, SearchKey::Unseen, false))
|
||||
.await?;
|
||||
conn.send_command(b"SEARCH UNSEEN").await?;
|
||||
conn.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await?;
|
||||
let unseen_count = protocol_parser::search_results(&response)?.1.len();
|
||||
|
@ -322,8 +318,7 @@ pub async fn examine_updates(
|
|||
|
||||
if select_response.recent > 0 {
|
||||
/* UID SEARCH RECENT */
|
||||
conn.send_command(CommandBody::search(None, SearchKey::Recent, true))
|
||||
.await?;
|
||||
conn.send_command(b"UID SEARCH RECENT").await?;
|
||||
conn.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await?;
|
||||
let v = protocol_parser::search_results(response.as_slice()).map(|(_, v)| v)?;
|
||||
|
@ -334,15 +329,30 @@ pub async fn examine_updates(
|
|||
);
|
||||
return Ok(());
|
||||
}
|
||||
conn.send_command(CommandBody::fetch(v.as_slice(), common_attributes(), true)?)
|
||||
.await?;
|
||||
let mut cmd = "UID FETCH ".to_string();
|
||||
cmd.push_str(&v[0].to_string());
|
||||
if v.len() != 1 {
|
||||
for n in v.into_iter().skip(1) {
|
||||
cmd.push(',');
|
||||
cmd.push_str(&n.to_string());
|
||||
}
|
||||
}
|
||||
cmd.push_str(
|
||||
" (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)",
|
||||
);
|
||||
conn.send_command(cmd.as_bytes()).await?;
|
||||
conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
} else if select_response.exists > mailbox.exists.lock().unwrap().len() {
|
||||
let min = std::cmp::max(mailbox.exists.lock().unwrap().len(), 1);
|
||||
|
||||
conn.send_command(CommandBody::fetch(min.., common_attributes(), false)?)
|
||||
.await?;
|
||||
conn.send_command(
|
||||
format!(
|
||||
"FETCH {}:* (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] \
|
||||
BODYSTRUCTURE)",
|
||||
std::cmp::max(mailbox.exists.lock().unwrap().len(), 1)
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
} else {
|
||||
|
@ -378,7 +388,9 @@ pub async fn examine_updates(
|
|||
mailbox.exists.lock().unwrap().insert_new(env.hash());
|
||||
for f in keywords {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f.to_string());
|
||||
}
|
||||
env.tags_mut().push(hash);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,9 +34,9 @@ use serde_json::Value;
|
|||
use crate::{
|
||||
backends::*,
|
||||
conf::AccountSettings,
|
||||
connections::timeout,
|
||||
email::*,
|
||||
error::{Error, Result},
|
||||
utils::futures::{sleep, timeout},
|
||||
Collection,
|
||||
};
|
||||
|
||||
|
@ -142,7 +142,7 @@ impl JmapServerConf {
|
|||
s.name,
|
||||
)));
|
||||
}
|
||||
Ok(Self {
|
||||
Ok(JmapServerConf {
|
||||
server_url: get_conf_val!(s["server_url"])?.to_string(),
|
||||
server_username: get_conf_val!(s["server_username"])?.to_string(),
|
||||
server_password: s.server_password()?,
|
||||
|
@ -199,7 +199,9 @@ impl Store {
|
|||
"$junk" | "$notjunk" => { /* ignore */ }
|
||||
_ => {
|
||||
let tag_hash = TagHash::from_bytes(t.as_bytes());
|
||||
tag_lck.entry(tag_hash).or_insert_with(|| t.to_string());
|
||||
if !tag_lck.contains_key(&tag_hash) {
|
||||
tag_lck.insert(tag_hash, t.to_string());
|
||||
}
|
||||
labels.push(tag_hash);
|
||||
}
|
||||
}
|
||||
|
@ -242,12 +244,9 @@ impl Store {
|
|||
self.blob_id_store.lock().unwrap().remove(&env_hash);
|
||||
self.byte_cache.lock().unwrap().remove(&env_hash);
|
||||
let mut mailbox_hashes = SmallVec::new();
|
||||
{
|
||||
let mut mailboxes_lck = self.mailboxes_index.write().unwrap();
|
||||
for (k, set) in mailboxes_lck.iter_mut() {
|
||||
if set.remove(&env_hash) {
|
||||
mailbox_hashes.push(*k);
|
||||
}
|
||||
for (k, set) in self.mailboxes_index.write().unwrap().iter_mut() {
|
||||
if set.remove(&env_hash) {
|
||||
mailbox_hashes.push(*k);
|
||||
}
|
||||
}
|
||||
Some((env_hash, mailbox_hashes))
|
||||
|
@ -345,7 +344,7 @@ impl MailBackend for JmapType {
|
|||
conn.email_changes(mailbox_hash).await?;
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_secs(60)).await;
|
||||
crate::connections::sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
@ -449,7 +448,7 @@ impl MailBackend for JmapType {
|
|||
);
|
||||
|
||||
let import_call: ImportCall = ImportCall::new()
|
||||
.account_id(conn.mail_account_id())
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.emails(email_imports);
|
||||
|
||||
req.add_call(&import_call);
|
||||
|
@ -473,13 +472,13 @@ impl MailBackend for JmapType {
|
|||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
let m = ImportResponse::try_from(v.method_responses.remove(0)).map_err(|err| {
|
||||
let m = ImportResponse::try_from(v.method_responses.remove(0)).or_else(|err| {
|
||||
let ierr: Result<ImportError> =
|
||||
serde_json::from_str(&res_text).map_err(|err| err.into());
|
||||
if let Ok(err) = ierr {
|
||||
Error::new(format!("Could not save message: {:?}", err))
|
||||
Err(Error::new(format!("Could not save message: {:?}", err)))
|
||||
} else {
|
||||
err
|
||||
Err(err.into())
|
||||
}
|
||||
})?;
|
||||
|
||||
|
@ -530,7 +529,7 @@ impl MailBackend for JmapType {
|
|||
conn.connect().await?;
|
||||
let email_call: EmailQuery = EmailQuery::new(
|
||||
Query::new()
|
||||
.account_id(conn.mail_account_id())
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.filter(Some(filter))
|
||||
.position(0),
|
||||
)
|
||||
|
@ -647,6 +646,8 @@ impl MailBackend for JmapType {
|
|||
)
|
||||
};
|
||||
let mut update_map: HashMap<Id<EmailObject>, Value> = HashMap::default();
|
||||
let mut ids: Vec<Id<EmailObject>> = Vec::with_capacity(env_hashes.rest.len() + 1);
|
||||
let mut id_map: HashMap<Id<EmailObject>, EnvelopeHash> = HashMap::default();
|
||||
let mut update_keywords: HashMap<String, Value> = HashMap::default();
|
||||
update_keywords.insert(
|
||||
format!("mailboxIds/{}", &destination_mailbox_id),
|
||||
|
@ -661,8 +662,8 @@ impl MailBackend for JmapType {
|
|||
{
|
||||
for env_hash in env_hashes.iter() {
|
||||
if let Some(id) = store.id_store.lock().unwrap().get(&env_hash) {
|
||||
// ids.push(id.clone());
|
||||
// id_map.insert(id.clone(), env_hash);
|
||||
ids.push(id.clone());
|
||||
id_map.insert(id.clone(), env_hash);
|
||||
update_map.insert(id.clone(), serde_json::json!(update_keywords.clone()));
|
||||
}
|
||||
}
|
||||
|
@ -672,7 +673,7 @@ impl MailBackend for JmapType {
|
|||
|
||||
let email_set_call: EmailSet = EmailSet::new(
|
||||
Set::<EmailObject>::new()
|
||||
.account_id(conn.mail_account_id())
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.update(Some(update_map)),
|
||||
);
|
||||
|
||||
|
@ -778,7 +779,7 @@ impl MailBackend for JmapType {
|
|||
|
||||
let email_set_call: EmailSet = EmailSet::new(
|
||||
Set::<EmailObject>::new()
|
||||
.account_id(conn.mail_account_id())
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.update(Some(update_map)),
|
||||
);
|
||||
|
||||
|
@ -787,7 +788,7 @@ impl MailBackend for JmapType {
|
|||
let email_call: EmailGet = EmailGet::new(
|
||||
Get::new()
|
||||
.ids(Some(JmapArgument::Value(ids)))
|
||||
.account_id(conn.mail_account_id())
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.properties(Some(vec!["keywords".to_string()])),
|
||||
);
|
||||
|
||||
|
@ -901,7 +902,6 @@ impl MailBackend for JmapType {
|
|||
}
|
||||
|
||||
impl JmapType {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(
|
||||
s: &AccountSettings,
|
||||
is_subscribed: Box<dyn Fn(&str) -> bool + Send + Sync>,
|
||||
|
@ -932,7 +932,7 @@ impl JmapType {
|
|||
mailbox_state: Default::default(),
|
||||
});
|
||||
|
||||
Ok(Box::new(Self {
|
||||
Ok(Box::new(JmapType {
|
||||
connection: Arc::new(FutureMutex::new(JmapConnection::new(
|
||||
&server_conf,
|
||||
store.clone(),
|
||||
|
@ -975,8 +975,7 @@ impl JmapType {
|
|||
|
||||
get_conf_val!(s["use_token"], false)?;
|
||||
// either of these two needed
|
||||
get_conf_val!(s["server_password"])
|
||||
.or_else(|_| get_conf_val!(s["server_password_command"]))?;
|
||||
get_conf_val!(s["server_password"]).or(get_conf_val!(s["server_password_command"]))?;
|
||||
|
||||
get_conf_val!(s["danger_accept_invalid_certs"], false)?;
|
||||
Ok(())
|
||||
|
|
|
@ -59,7 +59,7 @@ impl JmapConnection {
|
|||
};
|
||||
let client = client.build()?;
|
||||
let server_conf = server_conf.clone();
|
||||
Ok(Self {
|
||||
Ok(JmapConnection {
|
||||
session: Arc::new(Mutex::new(Default::default())),
|
||||
request_no: Arc::new(Mutex::new(0)),
|
||||
client: Arc::new(client),
|
||||
|
@ -80,15 +80,16 @@ impl JmapConnection {
|
|||
.get_async(&jmap_session_resource_url)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
//*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
Error::new(format!(
|
||||
let err = Error::new(format!(
|
||||
"Could not connect to JMAP server endpoint for {}. Is your server url setting \
|
||||
correct? (i.e. \"jmap.mailserver.org\") (Note: only session resource \
|
||||
discovery via /.well-known/jmap is supported. DNS SRV records are not \
|
||||
suppported.)\nError connecting to server: {}",
|
||||
&self.server_conf.server_url, &err
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_source(Some(Arc::new(err)));
|
||||
//*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
err
|
||||
})?;
|
||||
|
||||
if !req.status().is_success() {
|
||||
|
|
|
@ -76,7 +76,7 @@ impl BackendMailbox for JmapMailbox {
|
|||
}
|
||||
|
||||
fn special_usage(&self) -> SpecialUsageMailbox {
|
||||
match self.role.as_deref() {
|
||||
match self.role.as_ref().map(String::as_str) {
|
||||
Some("inbox") => SpecialUsageMailbox::Inbox,
|
||||
Some("archive") => SpecialUsageMailbox::Archive,
|
||||
Some("junk") => SpecialUsageMailbox::Junk,
|
||||
|
|
|
@ -29,7 +29,6 @@ use super::*;
|
|||
use crate::{
|
||||
backends::jmap::rfc8620::bool_false,
|
||||
email::address::{Address, MailboxAddress},
|
||||
utils::datetime,
|
||||
};
|
||||
|
||||
mod import;
|
||||
|
@ -211,7 +210,7 @@ where
|
|||
T: Deserialize<'de> + Default,
|
||||
{
|
||||
let v = Option::<T>::deserialize(deserializer)?;
|
||||
Ok(v.unwrap_or_default())
|
||||
Ok(v.unwrap_or(T::default()))
|
||||
}
|
||||
|
||||
impl EmailObject {
|
||||
|
@ -242,9 +241,9 @@ pub struct EmailAddress {
|
|||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
impl From<EmailAddress> for crate::email::Address {
|
||||
fn from(val: EmailAddress) -> Self {
|
||||
let EmailAddress { email, mut name } = val;
|
||||
impl Into<crate::email::Address> for EmailAddress {
|
||||
fn into(self) -> crate::email::Address {
|
||||
let Self { email, mut name } = self;
|
||||
crate::make_address!((name.take().unwrap_or_default()), email)
|
||||
}
|
||||
}
|
||||
|
@ -260,15 +259,16 @@ impl std::fmt::Display for EmailAddress {
|
|||
}
|
||||
|
||||
impl std::convert::From<EmailObject> for crate::Envelope {
|
||||
fn from(mut t: EmailObject) -> Self {
|
||||
let mut env = Self::new(t.id.into_hash());
|
||||
fn from(mut t: EmailObject) -> crate::Envelope {
|
||||
let mut env = crate::Envelope::new(t.id.into_hash());
|
||||
if let Ok(d) = crate::email::parser::dates::rfc5322_date(env.date_as_str().as_bytes()) {
|
||||
env.set_datetime(d);
|
||||
}
|
||||
if let Some(ref mut sent_at) = t.sent_at {
|
||||
let unix = datetime::rfc3339_to_timestamp(sent_at.as_bytes().to_vec()).unwrap_or(0);
|
||||
let unix =
|
||||
crate::datetime::rfc3339_to_timestamp(sent_at.as_bytes().to_vec()).unwrap_or(0);
|
||||
env.set_datetime(unix);
|
||||
env.set_date(std::mem::take(sent_at).as_bytes());
|
||||
env.set_date(std::mem::replace(sent_at, String::new()).as_bytes());
|
||||
}
|
||||
|
||||
if let Some(v) = t.message_id.get(0) {
|
||||
|
@ -293,12 +293,12 @@ impl std::convert::From<EmailObject> for crate::Envelope {
|
|||
}
|
||||
env.set_has_attachments(t.has_attachment);
|
||||
if let Some(ref mut subject) = t.subject {
|
||||
env.set_subject(std::mem::take(subject).into_bytes());
|
||||
env.set_subject(std::mem::replace(subject, String::new()).into_bytes());
|
||||
}
|
||||
|
||||
if let Some(ref mut from) = t.from {
|
||||
env.set_from(
|
||||
std::mem::take(from)
|
||||
std::mem::replace(from, SmallVec::new())
|
||||
.into_iter()
|
||||
.map(|addr| addr.into())
|
||||
.collect::<SmallVec<[crate::email::Address; 1]>>(),
|
||||
|
@ -306,7 +306,7 @@ impl std::convert::From<EmailObject> for crate::Envelope {
|
|||
}
|
||||
if let Some(ref mut to) = t.to {
|
||||
env.set_to(
|
||||
std::mem::take(to)
|
||||
std::mem::replace(to, SmallVec::new())
|
||||
.into_iter()
|
||||
.map(|addr| addr.into())
|
||||
.collect::<SmallVec<[crate::email::Address; 1]>>(),
|
||||
|
@ -315,7 +315,7 @@ impl std::convert::From<EmailObject> for crate::Envelope {
|
|||
|
||||
if let Some(ref mut cc) = t.cc {
|
||||
env.set_cc(
|
||||
std::mem::take(cc)
|
||||
std::mem::replace(cc, SmallVec::new())
|
||||
.into_iter()
|
||||
.map(|addr| addr.into())
|
||||
.collect::<SmallVec<[crate::email::Address; 1]>>(),
|
||||
|
@ -324,15 +324,17 @@ impl std::convert::From<EmailObject> for crate::Envelope {
|
|||
|
||||
if let Some(ref mut bcc) = t.bcc {
|
||||
env.set_bcc(
|
||||
std::mem::take(bcc)
|
||||
std::mem::replace(bcc, Vec::new())
|
||||
.into_iter()
|
||||
.map(|addr| addr.into())
|
||||
.collect::<Vec<crate::email::Address>>(),
|
||||
);
|
||||
}
|
||||
|
||||
if let (Some(ref mut r), message_id) = (&mut env.references, &env.message_id) {
|
||||
r.refs.retain(|r| r != message_id);
|
||||
if let Some(ref r) = env.references {
|
||||
if let Some(pos) = r.refs.iter().position(|r| r == env.message_id()) {
|
||||
env.references.as_mut().unwrap().refs.remove(pos);
|
||||
}
|
||||
}
|
||||
|
||||
env
|
||||
|
@ -411,13 +413,14 @@ impl Method<EmailObject> for EmailQuery {
|
|||
}
|
||||
|
||||
impl EmailQuery {
|
||||
pub const RESULT_FIELD_IDS: ResultField<Self, EmailObject> = ResultField::<Self, EmailObject> {
|
||||
field: "/ids",
|
||||
_ph: PhantomData,
|
||||
};
|
||||
pub const RESULT_FIELD_IDS: ResultField<EmailQuery, EmailObject> =
|
||||
ResultField::<EmailQuery, EmailObject> {
|
||||
field: "/ids",
|
||||
_ph: PhantomData,
|
||||
};
|
||||
|
||||
pub fn new(query_call: Query<Filter<EmailFilterCondition, EmailObject>, EmailObject>) -> Self {
|
||||
Self {
|
||||
EmailQuery {
|
||||
query_call,
|
||||
collapse_threads: false,
|
||||
}
|
||||
|
@ -451,7 +454,7 @@ impl Method<EmailObject> for EmailGet {
|
|||
|
||||
impl EmailGet {
|
||||
pub fn new(get_call: Get<EmailObject>) -> Self {
|
||||
Self {
|
||||
EmailGet {
|
||||
get_call,
|
||||
body_properties: Vec::new(),
|
||||
fetch_text_body_values: false,
|
||||
|
@ -545,8 +548,8 @@ impl EmailFilterCondition {
|
|||
impl FilterTrait<EmailObject> for EmailFilterCondition {}
|
||||
|
||||
impl From<EmailFilterCondition> for FilterCondition<EmailFilterCondition, EmailObject> {
|
||||
fn from(val: EmailFilterCondition) -> Self {
|
||||
Self {
|
||||
fn from(val: EmailFilterCondition) -> FilterCondition<EmailFilterCondition, EmailObject> {
|
||||
FilterCondition {
|
||||
cond: val,
|
||||
_ph: PhantomData,
|
||||
}
|
||||
|
@ -583,12 +586,12 @@ pub enum MessageProperty {
|
|||
|
||||
impl From<crate::search::Query> for Filter<EmailFilterCondition, EmailObject> {
|
||||
fn from(val: crate::search::Query) -> Self {
|
||||
let mut ret = Self::Condition(EmailFilterCondition::new().into());
|
||||
let mut ret = Filter::Condition(EmailFilterCondition::new().into());
|
||||
fn rec(q: &crate::search::Query, f: &mut Filter<EmailFilterCondition, EmailObject>) {
|
||||
use datetime::{formats::RFC3339_DATE, timestamp_to_string};
|
||||
|
||||
use crate::search::Query::*;
|
||||
|
||||
use crate::{
|
||||
datetime::{timestamp_to_string, RFC3339_FMT},
|
||||
search::Query::*,
|
||||
};
|
||||
match q {
|
||||
Subject(t) => {
|
||||
*f = Filter::Condition(EmailFilterCondition::new().subject(t.clone()).into());
|
||||
|
@ -614,26 +617,26 @@ impl From<crate::search::Query> for Filter<EmailFilterCondition, EmailObject> {
|
|||
Before(t) => {
|
||||
*f = Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.before(timestamp_to_string(*t, Some(RFC3339_DATE), true))
|
||||
.before(timestamp_to_string(*t, Some(RFC3339_FMT), true))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
After(t) => {
|
||||
*f = Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.after(timestamp_to_string(*t, Some(RFC3339_DATE), true))
|
||||
.after(timestamp_to_string(*t, Some(RFC3339_FMT), true))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
Between(a, b) => {
|
||||
*f = Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.after(timestamp_to_string(*a, Some(RFC3339_DATE), true))
|
||||
.after(timestamp_to_string(*a, Some(RFC3339_FMT), true))
|
||||
.into(),
|
||||
);
|
||||
*f &= Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.before(timestamp_to_string(*b, Some(RFC3339_DATE), true))
|
||||
.before(timestamp_to_string(*b, Some(RFC3339_FMT), true))
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
@ -735,18 +738,6 @@ impl From<crate::search::Query> for Filter<EmailFilterCondition, EmailObject> {
|
|||
rec(q, &mut qhs);
|
||||
*f = !qhs;
|
||||
}
|
||||
Answered => {
|
||||
// TODO
|
||||
}
|
||||
AnsweredBy { .. } => {
|
||||
// TODO
|
||||
}
|
||||
Larger { .. } => {
|
||||
// TODO
|
||||
}
|
||||
Smaller { .. } => {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
rec(&val, &mut ret);
|
||||
|
@ -810,7 +801,7 @@ impl Method<EmailObject> for EmailSet {
|
|||
|
||||
impl EmailSet {
|
||||
pub fn new(set_call: Set<EmailObject>) -> Self {
|
||||
Self { set_call }
|
||||
EmailSet { set_call }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -827,7 +818,7 @@ impl Method<EmailObject> for EmailChanges {
|
|||
|
||||
impl EmailChanges {
|
||||
pub fn new(changes_call: Changes<EmailObject>) -> Self {
|
||||
Self { changes_call }
|
||||
EmailChanges { changes_call }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -846,7 +837,7 @@ impl EmailQueryChanges {
|
|||
pub fn new(
|
||||
query_changes_call: QueryChanges<Filter<EmailFilterCondition, EmailObject>, EmailObject>,
|
||||
) -> Self {
|
||||
Self { query_changes_call }
|
||||
EmailQueryChanges { query_changes_call }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -861,16 +852,17 @@ pub struct EmailQueryChangesResponse {
|
|||
|
||||
impl std::convert::TryFrom<&RawValue> for EmailQueryChangesResponse {
|
||||
type Error = crate::error::Error;
|
||||
fn try_from(t: &RawValue) -> Result<Self> {
|
||||
let res: (String, Self, String) = serde_json::from_str(t.get()).map_err(|err| {
|
||||
crate::error::Error::new(format!(
|
||||
"BUG: Could not deserialize server JSON response properly, please report \
|
||||
this!\nReply from server: {}",
|
||||
&t
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(ErrorKind::Bug)
|
||||
})?;
|
||||
fn try_from(t: &RawValue) -> Result<EmailQueryChangesResponse> {
|
||||
let res: (String, EmailQueryChangesResponse, String) = serde_json::from_str(t.get())
|
||||
.map_err(|err| {
|
||||
crate::error::Error::new(format!(
|
||||
"BUG: Could not deserialize server JSON response properly, please report \
|
||||
this!\nReply from server: {}",
|
||||
&t
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(ErrorKind::Bug)
|
||||
})?;
|
||||
assert_eq!(&res.0, "Email/queryChanges");
|
||||
Ok(res.1)
|
||||
}
|
||||
|
|
|
@ -90,12 +90,6 @@ impl ImportCall {
|
|||
_impl!(emails: HashMap<Id<EmailObject>, EmailImport>);
|
||||
}
|
||||
|
||||
impl Default for ImportCall {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Method<EmailObject> for ImportCall {
|
||||
const NAME: &'static str = "Email/import";
|
||||
}
|
||||
|
@ -116,12 +110,6 @@ impl EmailImport {
|
|||
_impl!(received_at: Option<String>);
|
||||
}
|
||||
|
||||
impl Default for EmailImport {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(tag = "type")]
|
||||
|
@ -196,16 +184,17 @@ pub struct ImportResponse {
|
|||
|
||||
impl std::convert::TryFrom<&RawValue> for ImportResponse {
|
||||
type Error = crate::error::Error;
|
||||
fn try_from(t: &RawValue) -> Result<Self> {
|
||||
let res: (String, Self, String) = serde_json::from_str(t.get()).map_err(|err| {
|
||||
crate::error::Error::new(format!(
|
||||
"BUG: Could not deserialize server JSON response properly, please report \
|
||||
this!\nReply from server: {}",
|
||||
&t
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(ErrorKind::Bug)
|
||||
})?;
|
||||
fn try_from(t: &RawValue) -> Result<ImportResponse> {
|
||||
let res: (String, ImportResponse, String) =
|
||||
serde_json::from_str(t.get()).map_err(|err| {
|
||||
crate::error::Error::new(format!(
|
||||
"BUG: Could not deserialize server JSON response properly, please report \
|
||||
this!\nReply from server: {}",
|
||||
&t
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(ErrorKind::Bug)
|
||||
})?;
|
||||
assert_eq!(&res.0, &ImportCall::NAME);
|
||||
Ok(res.1)
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ pub struct MailboxGet {
|
|||
}
|
||||
impl MailboxGet {
|
||||
pub fn new(get_call: Get<MailboxObject>) -> Self {
|
||||
Self { get_call }
|
||||
MailboxGet { get_call }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ impl JmapOp {
|
|||
connection: Arc<FutureMutex<JmapConnection>>,
|
||||
store: Arc<Store>,
|
||||
) -> Self {
|
||||
Self {
|
||||
JmapOp {
|
||||
hash,
|
||||
connection,
|
||||
store,
|
||||
|
|
|
@ -63,7 +63,7 @@ pub struct Request {
|
|||
|
||||
impl Request {
|
||||
pub fn new(request_no: Arc<Mutex<usize>>) -> Self {
|
||||
Self {
|
||||
Request {
|
||||
using: USING,
|
||||
method_calls: Vec::new(),
|
||||
request_no,
|
||||
|
@ -177,7 +177,7 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
|
|||
})
|
||||
.collect();
|
||||
for key in ret.keys().cloned().collect::<SmallVec<[MailboxHash; 24]>>() {
|
||||
if let Some(parent_hash) = ret[&key].parent_hash {
|
||||
if let Some(parent_hash) = ret[&key].parent_hash.clone() {
|
||||
ret.entry(parent_hash).and_modify(|e| e.children.push(key));
|
||||
}
|
||||
}
|
||||
|
@ -190,7 +190,7 @@ pub async fn get_message_list(
|
|||
) -> Result<Vec<Id<EmailObject>>> {
|
||||
let email_call: EmailQuery = EmailQuery::new(
|
||||
Query::new()
|
||||
.account_id(conn.mail_account_id())
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.filter(Some(Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.in_mailbox(Some(mailbox.id.clone()))
|
||||
|
@ -264,7 +264,7 @@ pub async fn fetch(
|
|||
let mailbox_id = store.mailboxes.read().unwrap()[&mailbox_hash].id.clone();
|
||||
let email_query_call: EmailQuery = EmailQuery::new(
|
||||
Query::new()
|
||||
.account_id(conn.mail_account_id())
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.filter(Some(Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.in_mailbox(Some(mailbox_id))
|
||||
|
@ -283,7 +283,7 @@ pub async fn fetch(
|
|||
prev_seq,
|
||||
EmailQuery::RESULT_FIELD_IDS,
|
||||
)))
|
||||
.account_id(conn.mail_account_id()),
|
||||
.account_id(conn.mail_account_id().clone()),
|
||||
);
|
||||
|
||||
req.add_call(&email_call);
|
||||
|
|
|
@ -72,7 +72,7 @@ impl core::fmt::Debug for Id<String> {
|
|||
//, Hash, Eq, PartialEq, Default)]
|
||||
impl<OBJ> Clone for Id<OBJ> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
Id {
|
||||
inner: self.inner.clone(),
|
||||
_ph: PhantomData,
|
||||
}
|
||||
|
@ -101,7 +101,7 @@ impl<OBJ> Default for Id<OBJ> {
|
|||
|
||||
impl<OBJ> From<String> for Id<OBJ> {
|
||||
fn from(inner: String) -> Self {
|
||||
Self {
|
||||
Id {
|
||||
inner,
|
||||
_ph: PhantomData,
|
||||
}
|
||||
|
@ -146,7 +146,7 @@ pub struct State<OBJ> {
|
|||
//, Hash, Eq, PartialEq, Default)]
|
||||
impl<OBJ> Clone for State<OBJ> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
State {
|
||||
inner: self.inner.clone(),
|
||||
_ph: PhantomData,
|
||||
}
|
||||
|
@ -175,7 +175,7 @@ impl<OBJ> Default for State<OBJ> {
|
|||
|
||||
impl<OBJ> From<String> for State<OBJ> {
|
||||
fn from(inner: String) -> Self {
|
||||
Self {
|
||||
State {
|
||||
inner,
|
||||
_ph: PhantomData,
|
||||
}
|
||||
|
@ -284,9 +284,9 @@ impl Object for BlobObject {
|
|||
/// The id of the account to use.
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Get<OBJ>
|
||||
pub struct Get<OBJ: Object>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub account_id: Id<Account>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
@ -298,9 +298,9 @@ where
|
|||
_ph: PhantomData<fn() -> OBJ>,
|
||||
}
|
||||
|
||||
impl<OBJ> Get<OBJ>
|
||||
impl<OBJ: Object> Get<OBJ>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
@ -339,15 +339,6 @@ where
|
|||
);
|
||||
}
|
||||
|
||||
impl<OBJ> Default for Get<OBJ>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<OBJ: Object + Serialize + std::fmt::Debug> Serialize for Get<OBJ> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
|
@ -426,16 +417,17 @@ pub struct GetResponse<OBJ: Object> {
|
|||
|
||||
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for GetResponse<OBJ> {
|
||||
type Error = crate::error::Error;
|
||||
fn try_from(t: &RawValue) -> Result<Self, crate::error::Error> {
|
||||
let res: (String, Self, String) = serde_json::from_str(t.get()).map_err(|err| {
|
||||
crate::error::Error::new(format!(
|
||||
"BUG: Could not deserialize server JSON response properly, please report \
|
||||
this!\nReply from server: {}",
|
||||
&t
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(crate::error::ErrorKind::Bug)
|
||||
})?;
|
||||
fn try_from(t: &RawValue) -> Result<GetResponse<OBJ>, crate::error::Error> {
|
||||
let res: (String, GetResponse<OBJ>, String) =
|
||||
serde_json::from_str(t.get()).map_err(|err| {
|
||||
crate::error::Error::new(format!(
|
||||
"BUG: Could not deserialize server JSON response properly, please report \
|
||||
this!\nReply from server: {}",
|
||||
&t
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(crate::error::ErrorKind::Bug)
|
||||
})?;
|
||||
assert_eq!(&res.0, &format!("{}/get", OBJ::NAME));
|
||||
Ok(res.1)
|
||||
}
|
||||
|
@ -458,9 +450,9 @@ enum JmapError {
|
|||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Query<F: FilterTrait<OBJ>, OBJ>
|
||||
pub struct Query<F: FilterTrait<OBJ>, OBJ: Object>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub account_id: Id<Account>,
|
||||
pub filter: Option<F>,
|
||||
|
@ -480,9 +472,9 @@ where
|
|||
_ph: PhantomData<fn() -> OBJ>,
|
||||
}
|
||||
|
||||
impl<F: FilterTrait<OBJ>, OBJ> Query<F, OBJ>
|
||||
impl<F: FilterTrait<OBJ>, OBJ: Object> Query<F, OBJ>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
@ -508,15 +500,6 @@ where
|
|||
_impl!(calculate_total: bool);
|
||||
}
|
||||
|
||||
impl<F: FilterTrait<OBJ>, OBJ> Default for Query<F, OBJ>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn u64_zero(num: &u64) -> bool {
|
||||
*num == 0
|
||||
}
|
||||
|
@ -547,16 +530,17 @@ pub struct QueryResponse<OBJ: Object> {
|
|||
|
||||
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for QueryResponse<OBJ> {
|
||||
type Error = crate::error::Error;
|
||||
fn try_from(t: &RawValue) -> Result<Self, crate::error::Error> {
|
||||
let res: (String, Self, String) = serde_json::from_str(t.get()).map_err(|err| {
|
||||
crate::error::Error::new(format!(
|
||||
"BUG: Could not deserialize server JSON response properly, please report \
|
||||
this!\nReply from server: {}",
|
||||
&t
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(crate::error::ErrorKind::Bug)
|
||||
})?;
|
||||
fn try_from(t: &RawValue) -> Result<QueryResponse<OBJ>, crate::error::Error> {
|
||||
let res: (String, QueryResponse<OBJ>, String) =
|
||||
serde_json::from_str(t.get()).map_err(|err| {
|
||||
crate::error::Error::new(format!(
|
||||
"BUG: Could not deserialize server JSON response properly, please report \
|
||||
this!\nReply from server: {}",
|
||||
&t
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(crate::error::ErrorKind::Bug)
|
||||
})?;
|
||||
assert_eq!(&res.0, &format!("{}/query", OBJ::NAME));
|
||||
Ok(res.1)
|
||||
}
|
||||
|
@ -573,7 +557,7 @@ pub struct ResultField<M: Method<OBJ>, OBJ: Object> {
|
|||
|
||||
impl<M: Method<OBJ>, OBJ: Object> ResultField<M, OBJ> {
|
||||
pub fn new(field: &'static str) -> Self {
|
||||
Self {
|
||||
ResultField {
|
||||
field,
|
||||
_ph: PhantomData,
|
||||
}
|
||||
|
@ -620,9 +604,9 @@ impl<M: Method<OBJ>, OBJ: Object> ResultField<M, OBJ> {
|
|||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
/* ch-ch-ch-ch-ch-Changes */
|
||||
pub struct Changes<OBJ>
|
||||
pub struct Changes<OBJ: Object>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub account_id: Id<Account>,
|
||||
pub since_state: State<OBJ>,
|
||||
|
@ -632,9 +616,9 @@ where
|
|||
_ph: PhantomData<fn() -> OBJ>,
|
||||
}
|
||||
|
||||
impl<OBJ> Changes<OBJ>
|
||||
impl<OBJ: Object> Changes<OBJ>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
@ -670,15 +654,6 @@ where
|
|||
);
|
||||
}
|
||||
|
||||
impl<OBJ> Default for Changes<OBJ>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChangesResponse<OBJ: Object> {
|
||||
|
@ -695,16 +670,17 @@ pub struct ChangesResponse<OBJ: Object> {
|
|||
|
||||
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for ChangesResponse<OBJ> {
|
||||
type Error = crate::error::Error;
|
||||
fn try_from(t: &RawValue) -> Result<Self, crate::error::Error> {
|
||||
let res: (String, Self, String) = serde_json::from_str(t.get()).map_err(|err| {
|
||||
crate::error::Error::new(format!(
|
||||
"BUG: Could not deserialize server JSON response properly, please report \
|
||||
this!\nReply from server: {}",
|
||||
&t
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(crate::error::ErrorKind::Bug)
|
||||
})?;
|
||||
fn try_from(t: &RawValue) -> Result<ChangesResponse<OBJ>, crate::error::Error> {
|
||||
let res: (String, ChangesResponse<OBJ>, String) =
|
||||
serde_json::from_str(t.get()).map_err(|err| {
|
||||
crate::error::Error::new(format!(
|
||||
"BUG: Could not deserialize server JSON response properly, please report \
|
||||
this!\nReply from server: {}",
|
||||
&t
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(crate::error::ErrorKind::Bug)
|
||||
})?;
|
||||
assert_eq!(&res.0, &format!("{}/changes", OBJ::NAME));
|
||||
Ok(res.1)
|
||||
}
|
||||
|
@ -730,9 +706,9 @@ impl<OBJ: Object> ChangesResponse<OBJ> {
|
|||
///record type).
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Set<OBJ>
|
||||
pub struct Set<OBJ: Object>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
///o accountId: "Id"
|
||||
///
|
||||
|
@ -809,9 +785,9 @@ where
|
|||
pub destroy: Option<Vec<Id<OBJ>>>,
|
||||
}
|
||||
|
||||
impl<OBJ> Set<OBJ>
|
||||
impl<OBJ: Object> Set<OBJ>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
@ -837,15 +813,6 @@ where
|
|||
_impl!(update: Option<HashMap<Id<OBJ>, Value>>);
|
||||
}
|
||||
|
||||
impl<OBJ> Default for Set<OBJ>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetResponse<OBJ: Object> {
|
||||
|
@ -909,16 +876,17 @@ pub struct SetResponse<OBJ: Object> {
|
|||
|
||||
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for SetResponse<OBJ> {
|
||||
type Error = crate::error::Error;
|
||||
fn try_from(t: &RawValue) -> Result<Self, crate::error::Error> {
|
||||
let res: (String, Self, String) = serde_json::from_str(t.get()).map_err(|err| {
|
||||
crate::error::Error::new(format!(
|
||||
"BUG: Could not deserialize server JSON response properly, please report \
|
||||
this!\nReply from server: {}",
|
||||
&t
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(crate::error::ErrorKind::Bug)
|
||||
})?;
|
||||
fn try_from(t: &RawValue) -> Result<SetResponse<OBJ>, crate::error::Error> {
|
||||
let res: (String, SetResponse<OBJ>, String) =
|
||||
serde_json::from_str(t.get()).map_err(|err| {
|
||||
crate::error::Error::new(format!(
|
||||
"BUG: Could not deserialize server JSON response properly, please report \
|
||||
this!\nReply from server: {}",
|
||||
&t
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(crate::error::ErrorKind::Bug)
|
||||
})?;
|
||||
assert_eq!(&res.0, &format!("{}/set", OBJ::NAME));
|
||||
Ok(res.1)
|
||||
}
|
||||
|
@ -1034,7 +1002,7 @@ pub fn download_request_format(
|
|||
ret.push_str(blob_id.as_str());
|
||||
prev_pos += "{blobId}".len();
|
||||
} else if download_url[prev_pos..].starts_with("{name}") {
|
||||
ret.push_str(name.as_deref().unwrap_or(""));
|
||||
ret.push_str(name.as_ref().map(String::as_str).unwrap_or(""));
|
||||
prev_pos += "{name}".len();
|
||||
}
|
||||
}
|
||||
|
@ -1102,9 +1070,9 @@ pub struct UploadResponse {
|
|||
/// takes the following arguments:
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryChanges<F: FilterTrait<OBJ>, OBJ>
|
||||
pub struct QueryChanges<F: FilterTrait<OBJ>, OBJ: Object>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub account_id: Id<Account>,
|
||||
pub filter: Option<F>,
|
||||
|
@ -1146,9 +1114,9 @@ where
|
|||
_ph: PhantomData<fn() -> OBJ>,
|
||||
}
|
||||
|
||||
impl<F: FilterTrait<OBJ>, OBJ> QueryChanges<F, OBJ>
|
||||
impl<F: FilterTrait<OBJ>, OBJ: Object> QueryChanges<F, OBJ>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub fn new(account_id: Id<Account>, since_query_state: String) -> Self {
|
||||
Self {
|
||||
|
|
|
@ -37,7 +37,7 @@ pub enum JmapArgument<T> {
|
|||
|
||||
impl<T> JmapArgument<T> {
|
||||
pub fn value(v: T) -> Self {
|
||||
Self::Value(v)
|
||||
JmapArgument::Value(v)
|
||||
}
|
||||
|
||||
pub fn reference<M, OBJ>(result_of: usize, path: ResultField<M, OBJ>) -> Self
|
||||
|
@ -45,7 +45,7 @@ impl<T> JmapArgument<T> {
|
|||
M: Method<OBJ>,
|
||||
OBJ: Object,
|
||||
{
|
||||
Self::ResultReference {
|
||||
JmapArgument::ResultReference {
|
||||
result_of: format!("m{}", result_of),
|
||||
name: M::NAME.to_string(),
|
||||
path: path.field.to_string(),
|
||||
|
|
|
@ -51,9 +51,3 @@ impl<OBJ: Object> Comparator<OBJ> {
|
|||
_impl!(collation: Option<String>);
|
||||
_impl!(additional_properties: Vec<String>);
|
||||
}
|
||||
|
||||
impl<OBJ: Object> Default for Comparator<OBJ> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ pub struct FilterCondition<F: FilterTrait<OBJ>, OBJ: Object> {
|
|||
pub _ph: PhantomData<fn() -> OBJ>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Eq, PartialEq)]
|
||||
#[derive(Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum FilterOperator {
|
||||
And,
|
||||
|
@ -54,7 +54,7 @@ pub enum FilterOperator {
|
|||
|
||||
impl<F: FilterTrait<OBJ>, OBJ: Object> FilterCondition<F, OBJ> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
FilterCondition {
|
||||
cond: F::default(),
|
||||
_ph: PhantomData,
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ impl<F: FilterTrait<OBJ>, OBJ: Object> Default for FilterCondition<F, OBJ> {
|
|||
|
||||
impl<F: FilterTrait<OBJ>, OBJ: Object> Default for Filter<F, OBJ> {
|
||||
fn default() -> Self {
|
||||
Self::Condition(FilterCondition::default())
|
||||
Filter::Condition(FilterCondition::default())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,17 +78,17 @@ use std::ops::{BitAndAssign, BitOrAssign, Not};
|
|||
impl<F: FilterTrait<OBJ>, OBJ: Object> BitAndAssign for Filter<F, OBJ> {
|
||||
fn bitand_assign(&mut self, rhs: Self) {
|
||||
match self {
|
||||
Self::Operator {
|
||||
Filter::Operator {
|
||||
operator: FilterOperator::And,
|
||||
ref mut conditions,
|
||||
} => {
|
||||
conditions.push(rhs);
|
||||
}
|
||||
Self::Condition(_) | Self::Operator { .. } => {
|
||||
*self = Self::Operator {
|
||||
Filter::Condition(_) | Filter::Operator { .. } => {
|
||||
*self = Filter::Operator {
|
||||
operator: FilterOperator::And,
|
||||
conditions: vec![
|
||||
std::mem::replace(self, Self::Condition(FilterCondition::new())),
|
||||
std::mem::replace(self, Filter::Condition(FilterCondition::new())),
|
||||
rhs,
|
||||
],
|
||||
};
|
||||
|
@ -100,17 +100,17 @@ impl<F: FilterTrait<OBJ>, OBJ: Object> BitAndAssign for Filter<F, OBJ> {
|
|||
impl<F: FilterTrait<OBJ>, OBJ: Object> BitOrAssign for Filter<F, OBJ> {
|
||||
fn bitor_assign(&mut self, rhs: Self) {
|
||||
match self {
|
||||
Self::Operator {
|
||||
Filter::Operator {
|
||||
operator: FilterOperator::Or,
|
||||
ref mut conditions,
|
||||
} => {
|
||||
conditions.push(rhs);
|
||||
}
|
||||
Self::Condition(_) | Self::Operator { .. } => {
|
||||
*self = Self::Operator {
|
||||
Filter::Condition(_) | Filter::Operator { .. } => {
|
||||
*self = Filter::Operator {
|
||||
operator: FilterOperator::Or,
|
||||
conditions: vec![
|
||||
std::mem::replace(self, Self::Condition(FilterCondition::new())),
|
||||
std::mem::replace(self, Filter::Condition(FilterCondition::new())),
|
||||
rhs,
|
||||
],
|
||||
};
|
||||
|
@ -123,14 +123,14 @@ impl<F: FilterTrait<OBJ>, OBJ: Object> Not for Filter<F, OBJ> {
|
|||
type Output = Self;
|
||||
fn not(self) -> Self {
|
||||
match self {
|
||||
Self::Operator {
|
||||
Filter::Operator {
|
||||
operator,
|
||||
conditions,
|
||||
} if operator == FilterOperator::Not => Self::Operator {
|
||||
} if operator == FilterOperator::Not => Filter::Operator {
|
||||
operator: FilterOperator::Or,
|
||||
conditions,
|
||||
},
|
||||
Self::Condition(_) | Self::Operator { .. } => Self::Operator {
|
||||
Filter::Condition(_) | Filter::Operator { .. } => Filter::Operator {
|
||||
operator: FilterOperator::Not,
|
||||
conditions: vec![self],
|
||||
},
|
||||
|
|
|
@ -40,7 +40,7 @@ use crate::{
|
|||
backends::*,
|
||||
email::Flag,
|
||||
error::{Error, Result},
|
||||
utils::shellexpand::ShellExpandTrait,
|
||||
shellexpand::ShellExpandTrait,
|
||||
};
|
||||
|
||||
/// `BackendOp` implementor for Maildir
|
||||
|
@ -54,7 +54,7 @@ pub struct MaildirOp {
|
|||
|
||||
impl Clone for MaildirOp {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
MaildirOp {
|
||||
hash_index: self.hash_index.clone(),
|
||||
mailbox_hash: self.mailbox_hash,
|
||||
hash: self.hash,
|
||||
|
@ -65,7 +65,7 @@ impl Clone for MaildirOp {
|
|||
|
||||
impl MaildirOp {
|
||||
pub fn new(hash: EnvelopeHash, hash_index: HashIndexes, mailbox_hash: MailboxHash) -> Self {
|
||||
Self {
|
||||
MaildirOp {
|
||||
hash_index,
|
||||
mailbox_hash,
|
||||
hash,
|
||||
|
@ -95,7 +95,7 @@ impl MaildirOp {
|
|||
}
|
||||
}
|
||||
|
||||
impl BackendOp for MaildirOp {
|
||||
impl<'a> BackendOp for MaildirOp {
|
||||
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
|
||||
if self.slice.is_none() {
|
||||
let file = std::fs::OpenOptions::new()
|
||||
|
@ -163,7 +163,7 @@ impl MaildirMailbox {
|
|||
true
|
||||
};
|
||||
|
||||
let ret = Self {
|
||||
let ret = MaildirMailbox {
|
||||
hash: MailboxHash(h.finish()),
|
||||
name: file_name,
|
||||
path: fname.unwrap().to_path_buf(),
|
||||
|
|
|
@ -32,7 +32,7 @@ use crate::{
|
|||
conf::AccountSettings,
|
||||
email::{Envelope, EnvelopeHash, Flag},
|
||||
error::{Error, ErrorKind, Result},
|
||||
utils::shellexpand::ShellExpandTrait,
|
||||
shellexpand::ShellExpandTrait,
|
||||
Collection,
|
||||
};
|
||||
|
||||
|
@ -82,8 +82,8 @@ impl DerefMut for MaildirPath {
|
|||
}
|
||||
|
||||
impl From<PathBuf> for MaildirPath {
|
||||
fn from(val: PathBuf) -> Self {
|
||||
Self {
|
||||
fn from(val: PathBuf) -> MaildirPath {
|
||||
MaildirPath {
|
||||
buf: val,
|
||||
modified: None,
|
||||
removed: false,
|
||||
|
@ -211,17 +211,29 @@ impl MailBackend for MaildirType {
|
|||
let unseen = mailbox.unseen.clone();
|
||||
let total = mailbox.total.clone();
|
||||
let path: PathBuf = mailbox.fs_path().into();
|
||||
let root_mailbox = self.path.to_path_buf();
|
||||
let map = self.hash_indexes.clone();
|
||||
let mailbox_index = self.mailbox_index.clone();
|
||||
super::stream::MaildirStream::new(mailbox_hash, unseen, total, path, map, mailbox_index)
|
||||
super::stream::MaildirStream::new(
|
||||
&self.name,
|
||||
mailbox_hash,
|
||||
unseen,
|
||||
total,
|
||||
path,
|
||||
root_mailbox,
|
||||
map,
|
||||
mailbox_index,
|
||||
)
|
||||
}
|
||||
|
||||
fn refresh(&mut self, mailbox_hash: MailboxHash) -> ResultFuture<()> {
|
||||
let cache_dir = xdg::BaseDirectories::with_profile("meli", &self.name).unwrap();
|
||||
let account_hash = AccountHash::from_bytes(self.name.as_bytes());
|
||||
let sender = self.event_consumer.clone();
|
||||
|
||||
let mailbox: &MaildirMailbox = &self.mailboxes[&mailbox_hash];
|
||||
let path: PathBuf = mailbox.fs_path().into();
|
||||
let root_mailbox = self.path.to_path_buf();
|
||||
let map = self.hash_indexes.clone();
|
||||
let mailbox_index = self.mailbox_index.clone();
|
||||
|
||||
|
@ -256,6 +268,23 @@ impl MailBackend for MaildirType {
|
|||
.lock()
|
||||
.unwrap()
|
||||
.insert(env.hash(), mailbox_hash);
|
||||
let file_name = file.strip_prefix(&root_mailbox).unwrap().to_path_buf();
|
||||
if let Ok(cached) = cache_dir.place_cache_file(file_name) {
|
||||
/* place result in cache directory */
|
||||
let f = fs::File::create(cached)?;
|
||||
let metadata = f.metadata()?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
permissions.set_mode(0o600); // Read/write for owner only.
|
||||
f.set_permissions(permissions)?;
|
||||
|
||||
let writer = io::BufWriter::new(f);
|
||||
bincode::Options::serialize_into(
|
||||
bincode::config::DefaultOptions::new(),
|
||||
writer,
|
||||
&env,
|
||||
)?;
|
||||
}
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
|
@ -307,6 +336,7 @@ impl MailBackend for MaildirType {
|
|||
watcher
|
||||
.watch(&root_mailbox, RecursiveMode::Recursive)
|
||||
.unwrap();
|
||||
let cache_dir = xdg::BaseDirectories::with_profile("meli", &self.name).unwrap();
|
||||
debug!("watching {:?}", root_mailbox);
|
||||
let hash_indexes = self.hash_indexes.clone();
|
||||
let mailbox_index = self.mailbox_index.clone();
|
||||
|
@ -362,6 +392,7 @@ impl MailBackend for MaildirType {
|
|||
&hash_indexes,
|
||||
mailbox_hash,
|
||||
pathbuf.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
|
@ -416,6 +447,7 @@ impl MailBackend for MaildirType {
|
|||
&hash_indexes,
|
||||
mailbox_hash,
|
||||
pathbuf.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
|
@ -567,6 +599,7 @@ impl MailBackend for MaildirType {
|
|||
&hash_indexes,
|
||||
dest_mailbox,
|
||||
dest.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
|
@ -660,6 +693,7 @@ impl MailBackend for MaildirType {
|
|||
&hash_indexes,
|
||||
dest_mailbox.unwrap_or(mailbox_hash),
|
||||
dest.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
|
@ -706,6 +740,7 @@ impl MailBackend for MaildirType {
|
|||
&hash_indexes,
|
||||
dest_mailbox,
|
||||
dest.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
|
@ -800,7 +835,7 @@ impl MailBackend for MaildirType {
|
|||
) -> ResultFuture<()> {
|
||||
let path = self.mailboxes[&mailbox_hash].fs_path.clone();
|
||||
Ok(Box::pin(async move {
|
||||
Self::save_to_mailbox(path, bytes, flags)
|
||||
MaildirType::save_to_mailbox(path, bytes, flags)
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -1073,7 +1108,6 @@ impl MailBackend for MaildirType {
|
|||
}
|
||||
|
||||
impl MaildirType {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(
|
||||
settings: &AccountSettings,
|
||||
is_subscribed: Box<dyn Fn(&str) -> bool>,
|
||||
|
@ -1220,7 +1254,7 @@ impl MaildirType {
|
|||
},
|
||||
);
|
||||
}
|
||||
Ok(Box::new(Self {
|
||||
Ok(Box::new(MaildirType {
|
||||
name: settings.name.to_string(),
|
||||
mailboxes,
|
||||
hash_indexes: Arc::new(Mutex::new(hash_indexes)),
|
||||
|
@ -1342,17 +1376,23 @@ fn add_path_to_index(
|
|||
hash_index: &HashIndexes,
|
||||
mailbox_hash: MailboxHash,
|
||||
path: &Path,
|
||||
cache_dir: &xdg::BaseDirectories,
|
||||
file_name: PathBuf,
|
||||
buf: &mut Vec<u8>,
|
||||
) -> Result<Envelope> {
|
||||
debug!("add_path_to_index path {:?} filename{:?}", path, file_name);
|
||||
let env_hash = get_file_hash(path);
|
||||
hash_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.or_default()
|
||||
.insert(env_hash, path.to_path_buf().into());
|
||||
{
|
||||
let mut map = hash_index.lock().unwrap();
|
||||
let map = map.entry(mailbox_hash).or_default();
|
||||
map.insert(env_hash, path.to_path_buf().into());
|
||||
debug!(
|
||||
"inserted {} in {} map, len={}",
|
||||
env_hash,
|
||||
mailbox_hash,
|
||||
map.len()
|
||||
);
|
||||
}
|
||||
let mut reader = io::BufReader::new(fs::File::open(path)?);
|
||||
buf.clear();
|
||||
reader.read_to_end(buf)?;
|
||||
|
@ -1363,5 +1403,17 @@ fn add_path_to_index(
|
|||
env_hash,
|
||||
file_name.display()
|
||||
);
|
||||
if let Ok(cached) = cache_dir.place_cache_file(file_name) {
|
||||
debug!("putting in cache");
|
||||
/* place result in cache directory */
|
||||
let f = fs::File::create(cached)?;
|
||||
let metadata = f.metadata()?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
permissions.set_mode(0o600); // Read/write for owner only.
|
||||
f.set_permissions(permissions)?;
|
||||
let writer = io::BufWriter::new(f);
|
||||
bincode::Options::serialize_into(bincode::config::DefaultOptions::new(), writer, &env)?;
|
||||
}
|
||||
Ok(env)
|
||||
}
|
||||
|
|
|
@ -22,7 +22,9 @@
|
|||
use core::{future::Future, pin::Pin};
|
||||
use std::{
|
||||
io::{self, Read},
|
||||
os::unix::fs::PermissionsExt,
|
||||
path::PathBuf,
|
||||
result,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
|
@ -34,19 +36,22 @@ use futures::{
|
|||
use super::*;
|
||||
use crate::backends::maildir::backend::move_to_cur;
|
||||
|
||||
type Payload = Pin<Box<dyn Future<Output = Result<Vec<Envelope>>> + Send + 'static>>;
|
||||
|
||||
pub struct MaildirStream {
|
||||
payloads: Pin<Box<FuturesUnordered<Payload>>>,
|
||||
payloads: Pin<
|
||||
Box<
|
||||
FuturesUnordered<Pin<Box<dyn Future<Output = Result<Vec<Envelope>>> + Send + 'static>>>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
|
||||
impl MaildirStream {
|
||||
#[allow(clippy::type_complexity, clippy::new_ret_no_self)]
|
||||
pub fn new(
|
||||
name: &str,
|
||||
mailbox_hash: MailboxHash,
|
||||
unseen: Arc<Mutex<usize>>,
|
||||
total: Arc<Mutex<usize>>,
|
||||
mut path: PathBuf,
|
||||
root_mailbox: PathBuf,
|
||||
map: HashIndexes,
|
||||
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>> {
|
||||
|
@ -66,11 +71,14 @@ impl MaildirStream {
|
|||
files
|
||||
.chunks(chunk_size)
|
||||
.map(|chunk| {
|
||||
let cache_dir = xdg::BaseDirectories::with_profile("meli", name).unwrap();
|
||||
Box::pin(Self::chunk(
|
||||
SmallVec::from(chunk),
|
||||
cache_dir,
|
||||
mailbox_hash,
|
||||
unseen.clone(),
|
||||
total.clone(),
|
||||
root_mailbox.clone(),
|
||||
map.clone(),
|
||||
mailbox_index.clone(),
|
||||
)) as Pin<Box<dyn Future<Output = _> + Send + 'static>>
|
||||
|
@ -84,9 +92,11 @@ impl MaildirStream {
|
|||
|
||||
async fn chunk(
|
||||
chunk: SmallVec<[std::path::PathBuf; 2048]>,
|
||||
cache_dir: xdg::BaseDirectories,
|
||||
mailbox_hash: MailboxHash,
|
||||
unseen: Arc<Mutex<usize>>,
|
||||
total: Arc<Mutex<usize>>,
|
||||
root_mailbox: PathBuf,
|
||||
map: HashIndexes,
|
||||
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
|
||||
) -> Result<Vec<Envelope>> {
|
||||
|
@ -94,13 +104,44 @@ impl MaildirStream {
|
|||
let mut unseen_total: usize = 0;
|
||||
let mut buf = Vec::with_capacity(4096);
|
||||
for file in chunk {
|
||||
/* Check if we have a cache file with this email's
|
||||
* filename */
|
||||
let file_name = PathBuf::from(&file)
|
||||
.strip_prefix(&root_mailbox)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
if let Some(cached) = cache_dir.find_cache_file(&file_name) {
|
||||
/* Cached struct exists, try to load it */
|
||||
let cached_file = fs::File::open(&cached)?;
|
||||
let filesize = cached_file.metadata()?.len();
|
||||
let reader = io::BufReader::new(cached_file);
|
||||
let result: result::Result<Envelope, _> = bincode::Options::deserialize_from(
|
||||
bincode::Options::with_limit(
|
||||
bincode::config::DefaultOptions::new(),
|
||||
2 * filesize,
|
||||
),
|
||||
reader,
|
||||
);
|
||||
if let Ok(env) = result {
|
||||
let mut map = map.lock().unwrap();
|
||||
let map = map.entry(mailbox_hash).or_default();
|
||||
let hash = env.hash();
|
||||
map.insert(hash, file.clone().into());
|
||||
mailbox_index.lock().unwrap().insert(hash, mailbox_hash);
|
||||
if !env.is_seen() {
|
||||
unseen_total += 1;
|
||||
}
|
||||
local_r.push(env);
|
||||
continue;
|
||||
}
|
||||
/* Try delete invalid file */
|
||||
let _ = fs::remove_file(&cached);
|
||||
};
|
||||
let env_hash = get_file_hash(&file);
|
||||
{
|
||||
map.lock()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.or_default()
|
||||
.insert(env_hash, PathBuf::from(&file).into());
|
||||
let mut map = map.lock().unwrap();
|
||||
let map = map.entry(mailbox_hash).or_default();
|
||||
map.insert(env_hash, PathBuf::from(&file).into());
|
||||
}
|
||||
let mut reader = io::BufReader::new(fs::File::open(&file)?);
|
||||
buf.clear();
|
||||
|
@ -109,6 +150,22 @@ impl MaildirStream {
|
|||
Ok(mut env) => {
|
||||
env.set_hash(env_hash);
|
||||
mailbox_index.lock().unwrap().insert(env_hash, mailbox_hash);
|
||||
if let Ok(cached) = cache_dir.place_cache_file(file_name) {
|
||||
/* place result in cache directory */
|
||||
let f = fs::File::create(cached)?;
|
||||
let metadata = f.metadata()?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
permissions.set_mode(0o600); // Read/write for owner only.
|
||||
f.set_permissions(permissions)?;
|
||||
|
||||
let writer = io::BufWriter::new(f);
|
||||
bincode::Options::serialize_into(
|
||||
bincode::config::DefaultOptions::new(),
|
||||
writer,
|
||||
&env,
|
||||
)?;
|
||||
}
|
||||
if !env.is_seen() {
|
||||
unseen_total += 1;
|
||||
}
|
||||
|
|
|
@ -146,7 +146,7 @@ use crate::{
|
|||
email::{parser::BytesExt, *},
|
||||
error::{Error, ErrorKind, Result},
|
||||
get_path_hash,
|
||||
utils::shellexpand::ShellExpandTrait,
|
||||
shellexpand::ShellExpandTrait,
|
||||
};
|
||||
|
||||
extern crate notify;
|
||||
|
@ -233,7 +233,7 @@ impl BackendMailbox for MboxMailbox {
|
|||
}
|
||||
|
||||
fn clone(&self) -> Mailbox {
|
||||
Box::new(Self {
|
||||
Box::new(MboxMailbox {
|
||||
hash: self.hash,
|
||||
name: self.name.clone(),
|
||||
path: self.path.clone(),
|
||||
|
@ -294,7 +294,7 @@ pub struct MboxOp {
|
|||
|
||||
impl MboxOp {
|
||||
pub fn new(_hash: EnvelopeHash, path: &Path, offset: Offset, length: Length) -> Self {
|
||||
Self {
|
||||
MboxOp {
|
||||
_hash,
|
||||
path: path.to_path_buf(),
|
||||
slice: std::cell::RefCell::new(None),
|
||||
|
@ -317,10 +317,10 @@ impl BackendOp for MboxOp {
|
|||
buf_reader.read_to_end(&mut contents)?;
|
||||
*self.slice.get_mut() = Some(contents);
|
||||
}
|
||||
let ret = self.slice.get_mut().as_ref().unwrap().as_slice()
|
||||
let ret = Ok(self.slice.get_mut().as_ref().unwrap().as_slice()
|
||||
[self.offset..self.offset + self.length]
|
||||
.to_vec();
|
||||
Ok(Box::pin(async move { Ok(ret) }))
|
||||
.to_vec());
|
||||
Ok(Box::pin(async move { ret }))
|
||||
}
|
||||
|
||||
fn fetch_flags(&self) -> ResultFuture<Flag> {
|
||||
|
@ -784,18 +784,8 @@ pub fn mbox_parse(
|
|||
}
|
||||
};
|
||||
let start: Offset = input[offset + file_offset..]
|
||||
.find(b"From ")
|
||||
.map(|from_offset| {
|
||||
input[offset + file_offset + from_offset..]
|
||||
.find(b"\n")
|
||||
.map(|v| v + 1)
|
||||
.unwrap_or_else(|| {
|
||||
input[offset + file_offset + from_offset..]
|
||||
.len()
|
||||
.saturating_sub(2)
|
||||
})
|
||||
})
|
||||
.map(|v| v + 2)
|
||||
.find(b"\n")
|
||||
.map(|v| v + 1)
|
||||
.unwrap_or(0);
|
||||
let len = input.len() - next_input.len() - offset - file_offset - start;
|
||||
index.insert(env.hash(), (offset + file_offset + start, len));
|
||||
|
@ -848,17 +838,7 @@ impl<'a> Iterator for MessageIterator<'a> {
|
|||
}
|
||||
};
|
||||
let start: Offset = self.input[self.offset + self.file_offset..]
|
||||
.find(b"From ")
|
||||
.map(|from_offset| {
|
||||
self.input[self.offset + self.file_offset + from_offset..]
|
||||
.find(b"\n")
|
||||
.map(|v| v + 1)
|
||||
.unwrap_or_else(|| {
|
||||
self.input[self.offset + self.file_offset + from_offset..]
|
||||
.len()
|
||||
.saturating_sub(2)
|
||||
})
|
||||
})
|
||||
.find(b"\n")
|
||||
.map(|v| v + 1)
|
||||
.unwrap_or(0);
|
||||
let len = self.input.len() - next_input.len() - self.offset - self.file_offset - start;
|
||||
|
@ -953,10 +933,9 @@ impl MailBackend for MboxType {
|
|||
if payload.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
let contents = std::mem::take(&mut self.contents);
|
||||
self.mailboxes
|
||||
.lock()
|
||||
.unwrap()
|
||||
let mut mailbox_lock = self.mailboxes.lock().unwrap();
|
||||
let contents = std::mem::replace(&mut self.contents, vec![]);
|
||||
mailbox_lock
|
||||
.entry(self.mailbox_hash)
|
||||
.and_modify(|f| f.content = contents);
|
||||
Ok(Some(payload))
|
||||
|
@ -1011,15 +990,12 @@ impl MailBackend for MboxType {
|
|||
let mut watcher = watcher(tx, std::time::Duration::from_secs(10))
|
||||
.map_err(|e| e.to_string())
|
||||
.map_err(Error::new)?;
|
||||
{
|
||||
let mailboxes_lck = self.mailboxes.lock().unwrap();
|
||||
for f in mailboxes_lck.values() {
|
||||
watcher
|
||||
.watch(&f.fs_path, RecursiveMode::Recursive)
|
||||
.map_err(|e| e.to_string())
|
||||
.map_err(Error::new)?;
|
||||
log::debug!("watching {:?}", f.fs_path.as_path());
|
||||
}
|
||||
for f in self.mailboxes.lock().unwrap().values() {
|
||||
watcher
|
||||
.watch(&f.fs_path, RecursiveMode::Recursive)
|
||||
.map_err(|e| e.to_string())
|
||||
.map_err(Error::new)?;
|
||||
debug!("watching {:?}", f.fs_path.as_path());
|
||||
}
|
||||
let account_hash = AccountHash::from_bytes(self.account_name.as_bytes());
|
||||
let mailboxes = self.mailboxes.clone();
|
||||
|
@ -1138,8 +1114,7 @@ impl MailBackend for MboxType {
|
|||
}
|
||||
/* Trigger rescan of mailboxes */
|
||||
DebouncedEvent::Rescan => {
|
||||
let mailboxes_lck = mailboxes.lock().unwrap();
|
||||
for &mailbox_hash in mailboxes_lck.keys() {
|
||||
for &mailbox_hash in mailboxes.lock().unwrap().keys() {
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
|
@ -1153,12 +1128,7 @@ impl MailBackend for MboxType {
|
|||
}
|
||||
_ => {}
|
||||
},
|
||||
Err(e) => {
|
||||
log::debug!("watch error: {:?}", e);
|
||||
return Err(Error::new(format!(
|
||||
"Mbox watching thread exited with error: {e}"
|
||||
)));
|
||||
}
|
||||
Err(e) => debug!("watch error: {:?}", e),
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
@ -1210,7 +1180,7 @@ impl MailBackend for MboxType {
|
|||
_flags: SmallVec<[(std::result::Result<Flag, String>, bool); 8]>,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new(
|
||||
"Setting flags is currently unimplemented for mbox backend",
|
||||
"Settings flags is currently unimplemented for mbox backend",
|
||||
))
|
||||
}
|
||||
|
||||
|
@ -1335,7 +1305,6 @@ macro_rules! get_conf_val {
|
|||
}
|
||||
|
||||
impl MboxType {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(
|
||||
s: &AccountSettings,
|
||||
_is_subscribed: Box<dyn Fn(&str) -> bool>,
|
||||
|
@ -1350,7 +1319,7 @@ impl MboxType {
|
|||
)));
|
||||
}
|
||||
let prefer_mbox_type: String = get_conf_val!(s["prefer_mbox_type"], "auto".to_string())?;
|
||||
let ret = Self {
|
||||
let ret = MboxType {
|
||||
account_name: s.name.to_string(),
|
||||
event_consumer,
|
||||
path,
|
||||
|
|
|
@ -20,10 +20,8 @@
|
|||
*/
|
||||
|
||||
use super::*;
|
||||
use crate::utils::datetime;
|
||||
|
||||
impl MboxFormat {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn append(
|
||||
&self,
|
||||
writer: &mut dyn std::io::Write,
|
||||
|
@ -51,9 +49,9 @@ impl MboxFormat {
|
|||
}
|
||||
writer.write_all(&b" "[..])?;
|
||||
writer.write_all(
|
||||
datetime::timestamp_to_string(
|
||||
delivery_date.unwrap_or_else(datetime::now),
|
||||
Some(datetime::formats::ASCTIME_FMT),
|
||||
crate::datetime::timestamp_to_string(
|
||||
delivery_date.unwrap_or_else(crate::datetime::now),
|
||||
Some(crate::datetime::ASCTIME_FMT),
|
||||
true,
|
||||
)
|
||||
.trim()
|
||||
|
@ -154,8 +152,8 @@ impl MboxFormat {
|
|||
};
|
||||
|
||||
match self {
|
||||
Self::MboxO | Self::MboxRd => Err(Error::new("Unimplemented.")),
|
||||
Self::MboxCl => {
|
||||
MboxFormat::MboxO | MboxFormat::MboxRd => Err(Error::new("Unimplemented.")),
|
||||
MboxFormat::MboxCl => {
|
||||
let len = (body_len
|
||||
+ body
|
||||
.windows(b"\nFrom ".len())
|
||||
|
@ -217,7 +215,7 @@ impl MboxFormat {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
Self::MboxCl2 => {
|
||||
MboxFormat::MboxCl2 => {
|
||||
let len = body_len.to_string();
|
||||
for (h, v) in headers
|
||||
.into_iter()
|
||||
|
|
|
@ -52,9 +52,9 @@ use futures::{lock::Mutex as FutureMutex, stream::Stream};
|
|||
use crate::{
|
||||
backends::*,
|
||||
conf::AccountSettings,
|
||||
connections::timeout,
|
||||
email::*,
|
||||
error::{Error, Result, ResultIntoError},
|
||||
utils::futures::timeout,
|
||||
Collection,
|
||||
};
|
||||
pub type UID = usize;
|
||||
|
@ -119,7 +119,7 @@ type Capabilities = HashSet<String>;
|
|||
#[derive(Debug)]
|
||||
pub struct UIDStore {
|
||||
account_hash: AccountHash,
|
||||
account_name: Arc<str>,
|
||||
account_name: Arc<String>,
|
||||
capabilities: Arc<Mutex<Capabilities>>,
|
||||
message_id_index: Arc<Mutex<HashMap<String, EnvelopeHash>>>,
|
||||
hash_index: Arc<Mutex<HashMap<EnvelopeHash, (UID, MailboxHash)>>>,
|
||||
|
@ -134,10 +134,10 @@ pub struct UIDStore {
|
|||
impl UIDStore {
|
||||
fn new(
|
||||
account_hash: AccountHash,
|
||||
account_name: Arc<str>,
|
||||
account_name: Arc<String>,
|
||||
event_consumer: BackendEventConsumer,
|
||||
) -> Self {
|
||||
Self {
|
||||
UIDStore {
|
||||
account_hash,
|
||||
account_name,
|
||||
event_consumer,
|
||||
|
@ -286,11 +286,8 @@ impl MailBackend for NntpType {
|
|||
let mut conn = timeout(Some(Duration::from_secs(60 * 16)), connection.lock()).await?;
|
||||
if let Some(mut latest_article) = latest_article {
|
||||
let timestamp = latest_article - 10 * 60;
|
||||
let datetime_str = crate::utils::datetime::timestamp_to_string(
|
||||
timestamp,
|
||||
Some("%Y%m%d %H%M%S"),
|
||||
true,
|
||||
);
|
||||
let datetime_str =
|
||||
crate::datetime::timestamp_to_string(timestamp, Some("%Y%m%d %H%M%S"), true);
|
||||
|
||||
if newnews_support {
|
||||
conn.send_command(
|
||||
|
@ -356,7 +353,7 @@ impl MailBackend for NntpType {
|
|||
let uid_store = self.uid_store.clone();
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
Self::nntp_mailboxes(&connection).await?;
|
||||
NntpType::nntp_mailboxes(&connection).await?;
|
||||
let mailboxes_lck = uid_store.mailboxes.lock().await;
|
||||
let ret = mailboxes_lck
|
||||
.iter()
|
||||
|
@ -552,7 +549,6 @@ impl MailBackend for NntpType {
|
|||
}
|
||||
|
||||
impl NntpType {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(
|
||||
s: &AccountSettings,
|
||||
is_subscribed: Box<dyn Fn(&str) -> bool + Send + Sync>,
|
||||
|
@ -611,7 +607,7 @@ impl NntpType {
|
|||
},
|
||||
};
|
||||
let account_hash = AccountHash::from_bytes(s.name.as_bytes());
|
||||
let account_name = s.name.to_string().into();
|
||||
let account_name = Arc::new(s.name.to_string());
|
||||
let mut mailboxes = HashMap::default();
|
||||
for (k, _f) in s.mailboxes.iter() {
|
||||
let mailbox_hash = MailboxHash(get_path_hash!(&k));
|
||||
|
@ -640,7 +636,7 @@ impl NntpType {
|
|||
});
|
||||
let connection = NntpConnection::new_connection(&server_conf, uid_store.clone());
|
||||
|
||||
Ok(Box::new(Self {
|
||||
Ok(Box::new(NntpType {
|
||||
server_conf,
|
||||
_is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
|
||||
_can_create_flags: Arc::new(Mutex::new(false)),
|
||||
|
@ -792,7 +788,7 @@ struct FetchState {
|
|||
|
||||
impl FetchState {
|
||||
async fn fetch_envs(&mut self) -> Result<Option<Vec<Envelope>>> {
|
||||
let Self {
|
||||
let FetchState {
|
||||
mailbox_hash,
|
||||
ref connection,
|
||||
ref uid_store,
|
||||
|
|
|
@ -21,10 +21,10 @@
|
|||
|
||||
use crate::{
|
||||
backends::{BackendMailbox, MailboxHash},
|
||||
connections::{lookup_ipv4, Connection},
|
||||
email::parser::BytesExt,
|
||||
error::*,
|
||||
log,
|
||||
utils::connections::{lookup_ipv4, Connection},
|
||||
};
|
||||
extern crate native_tls;
|
||||
use std::{collections::HashSet, future::Future, pin::Pin, sync::Arc, time::Instant};
|
||||
|
@ -35,12 +35,21 @@ pub use smol::Async as AsyncWrapper;
|
|||
|
||||
use super::{Capabilities, NntpServerConf, UIDStore};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct NntpExtensionUse {
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
pub deflate: bool,
|
||||
}
|
||||
|
||||
impl Default for NntpExtensionUse {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
deflate: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NntpStream {
|
||||
pub stream: AsyncWrapper<Connection>,
|
||||
|
@ -57,7 +66,7 @@ pub enum MailboxSelection {
|
|||
|
||||
impl MailboxSelection {
|
||||
pub fn take(&mut self) -> Self {
|
||||
std::mem::replace(self, Self::None)
|
||||
std::mem::replace(self, MailboxSelection::None)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,7 +82,9 @@ pub struct NntpConnection {
|
|||
}
|
||||
|
||||
impl NntpStream {
|
||||
pub async fn new_connection(server_conf: &NntpServerConf) -> Result<(Capabilities, Self)> {
|
||||
pub async fn new_connection(
|
||||
server_conf: &NntpServerConf,
|
||||
) -> Result<(Capabilities, NntpStream)> {
|
||||
use std::net::TcpStream;
|
||||
let path = &server_conf.server_hostname;
|
||||
|
||||
|
@ -85,7 +96,7 @@ impl NntpStream {
|
|||
)?))?
|
||||
};
|
||||
let mut res = String::with_capacity(8 * 1024);
|
||||
let mut ret = Self {
|
||||
let mut ret = NntpStream {
|
||||
stream,
|
||||
extension_use: server_conf.extension_use,
|
||||
current_mailbox: MailboxSelection::None,
|
||||
|
@ -262,7 +273,7 @@ impl NntpStream {
|
|||
server_conf.server_hostname, res
|
||||
)
|
||||
})?;
|
||||
let Self {
|
||||
let NntpStream {
|
||||
stream,
|
||||
extension_use,
|
||||
current_mailbox,
|
||||
|
@ -271,7 +282,7 @@ impl NntpStream {
|
|||
let stream = stream.into_inner()?;
|
||||
return Ok((
|
||||
capabilities,
|
||||
Self {
|
||||
NntpStream {
|
||||
stream: AsyncWrapper::new(stream.deflate())?,
|
||||
extension_use,
|
||||
current_mailbox,
|
||||
|
@ -410,8 +421,11 @@ impl NntpStream {
|
|||
}
|
||||
|
||||
impl NntpConnection {
|
||||
pub fn new_connection(server_conf: &NntpServerConf, uid_store: Arc<UIDStore>) -> Self {
|
||||
Self {
|
||||
pub fn new_connection(
|
||||
server_conf: &NntpServerConf,
|
||||
uid_store: Arc<UIDStore>,
|
||||
) -> NntpConnection {
|
||||
NntpConnection {
|
||||
stream: Err(Error::new("Offline".to_string())),
|
||||
server_conf: server_conf.clone(),
|
||||
uid_store,
|
||||
|
|
|
@ -40,7 +40,7 @@ impl NntpOp {
|
|||
connection: Arc<FutureMutex<NntpConnection>>,
|
||||
uid_store: Arc<UIDStore>,
|
||||
) -> Self {
|
||||
Self {
|
||||
NntpOp {
|
||||
uid,
|
||||
connection,
|
||||
mailbox_hash,
|
||||
|
|
|
@ -35,7 +35,7 @@ use crate::{
|
|||
conf::AccountSettings,
|
||||
email::{Envelope, EnvelopeHash, Flag},
|
||||
error::{Error, Result},
|
||||
utils::shellexpand::ShellExpandTrait,
|
||||
shellexpand::ShellExpandTrait,
|
||||
Collection,
|
||||
};
|
||||
|
||||
|
@ -88,7 +88,6 @@ impl DbConnection {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)] // Don't judge me clippy.
|
||||
fn refresh(
|
||||
&mut self,
|
||||
mailboxes: Arc<RwLock<HashMap<MailboxHash, NotmuchMailbox>>>,
|
||||
|
@ -116,7 +115,9 @@ impl DbConnection {
|
|||
let mut tag_lock = tag_index.write().unwrap();
|
||||
for tag in tags.1.iter() {
|
||||
let num = TagHash::from_bytes(tag.as_bytes());
|
||||
tag_lock.entry(num).or_insert_with(|| tag.clone());
|
||||
if !tag_lock.contains_key(&num) {
|
||||
tag_lock.insert(num, tag.clone());
|
||||
}
|
||||
}
|
||||
for &mailbox_hash in mailbox_hashes {
|
||||
(event_consumer)(
|
||||
|
@ -223,7 +224,7 @@ pub struct NotmuchDb {
|
|||
mailbox_index: Arc<RwLock<HashMap<EnvelopeHash, SmallVec<[MailboxHash; 16]>>>>,
|
||||
collection: Collection,
|
||||
path: PathBuf,
|
||||
_account_name: Arc<str>,
|
||||
_account_name: Arc<String>,
|
||||
account_hash: AccountHash,
|
||||
event_consumer: BackendEventConsumer,
|
||||
save_messages_to: Option<PathBuf>,
|
||||
|
@ -301,7 +302,6 @@ unsafe impl Send for NotmuchMailbox {}
|
|||
unsafe impl Sync for NotmuchMailbox {}
|
||||
|
||||
impl NotmuchDb {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(
|
||||
s: &AccountSettings,
|
||||
_is_subscribed: Box<dyn Fn(&str) -> bool>,
|
||||
|
@ -422,7 +422,7 @@ impl NotmuchDb {
|
|||
}
|
||||
|
||||
let account_hash = AccountHash::from_bytes(s.name.as_bytes());
|
||||
Ok(Box::new(Self {
|
||||
Ok(Box::new(NotmuchDb {
|
||||
lib,
|
||||
revision_uuid: Arc::new(RwLock::new(0)),
|
||||
path,
|
||||
|
@ -432,7 +432,7 @@ impl NotmuchDb {
|
|||
|
||||
mailboxes: Arc::new(RwLock::new(mailboxes)),
|
||||
save_messages_to: None,
|
||||
_account_name: s.name.to_string().into(),
|
||||
_account_name: Arc::new(s.name.to_string()),
|
||||
account_hash,
|
||||
event_consumer,
|
||||
}))
|
||||
|
@ -538,7 +538,7 @@ impl NotmuchDb {
|
|||
} else {
|
||||
notmuch_database_mode_t_NOTMUCH_DATABASE_MODE_READ_ONLY
|
||||
},
|
||||
std::ptr::addr_of_mut!(database),
|
||||
&mut database as *mut _,
|
||||
)
|
||||
};
|
||||
if status != 0 {
|
||||
|
@ -635,7 +635,7 @@ impl MailBackend for NotmuchDb {
|
|||
}
|
||||
}
|
||||
}
|
||||
let database = Arc::new(Self::new_connection(
|
||||
let database = Arc::new(NotmuchDb::new_connection(
|
||||
self.path.as_path(),
|
||||
self.revision_uuid.clone(),
|
||||
self.lib.clone(),
|
||||
|
@ -659,6 +659,7 @@ impl MailBackend for NotmuchDb {
|
|||
let mut index_lck = index.write().unwrap();
|
||||
v = query
|
||||
.search()?
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
index_lck.insert(m.env_hash(), m.msg_id_cstr().into());
|
||||
m.msg_id_cstr().into()
|
||||
|
@ -684,7 +685,7 @@ impl MailBackend for NotmuchDb {
|
|||
|
||||
fn refresh(&mut self, _mailbox_hash: MailboxHash) -> ResultFuture<()> {
|
||||
let account_hash = self.account_hash;
|
||||
let mut database = Self::new_connection(
|
||||
let mut database = NotmuchDb::new_connection(
|
||||
self.path.as_path(),
|
||||
self.revision_uuid.clone(),
|
||||
self.lib.clone(),
|
||||
|
@ -736,7 +737,7 @@ impl MailBackend for NotmuchDb {
|
|||
loop {
|
||||
let _ = rx.recv().map_err(|err| err.to_string())?;
|
||||
{
|
||||
let mut database = Self::new_connection(
|
||||
let mut database = NotmuchDb::new_connection(
|
||||
path.as_path(),
|
||||
revision_uuid.clone(),
|
||||
lib.clone(),
|
||||
|
@ -941,7 +942,7 @@ impl MailBackend for NotmuchDb {
|
|||
melib_query: crate::search::Query,
|
||||
mailbox_hash: Option<MailboxHash>,
|
||||
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
|
||||
let database = Self::new_connection(
|
||||
let database = NotmuchDb::new_connection(
|
||||
self.path.as_path(),
|
||||
self.revision_uuid.clone(),
|
||||
self.lib.clone(),
|
||||
|
@ -1097,10 +1098,7 @@ impl<'s> Query<'s> {
|
|||
unsafe {
|
||||
try_call!(
|
||||
self.lib,
|
||||
call!(self.lib, notmuch_query_count_messages)(
|
||||
self.ptr,
|
||||
std::ptr::addr_of_mut!(count)
|
||||
)
|
||||
call!(self.lib, notmuch_query_count_messages)(self.ptr, &mut count as *mut _)
|
||||
)
|
||||
.map_err(|err| err.0)?;
|
||||
}
|
||||
|
@ -1110,10 +1108,7 @@ impl<'s> Query<'s> {
|
|||
fn search(&'s self) -> Result<MessageIterator<'s>> {
|
||||
let mut messages: *mut notmuch_messages_t = std::ptr::null_mut();
|
||||
let status = unsafe {
|
||||
call!(self.lib, notmuch_query_search_messages)(
|
||||
self.ptr,
|
||||
std::ptr::addr_of_mut!(messages),
|
||||
)
|
||||
call!(self.lib, notmuch_query_search_messages)(self.ptr, &mut messages as *mut _)
|
||||
};
|
||||
if status != 0 {
|
||||
return Err(Error::new(format!(
|
||||
|
@ -1263,10 +1258,6 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
|
|||
q.query_to_string(ret);
|
||||
ret.push_str("))");
|
||||
}
|
||||
Answered => todo!(),
|
||||
AnsweredBy { .. } => todo!(),
|
||||
Larger { .. } => todo!(),
|
||||
Smaller { .. } => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ impl<'m> Message<'m> {
|
|||
call!(lib, notmuch_database_find_message)(
|
||||
*db.inner.read().unwrap(),
|
||||
msg_id.as_ptr(),
|
||||
std::ptr::addr_of_mut!(message),
|
||||
&mut message as *mut _,
|
||||
)
|
||||
};
|
||||
if message.is_null() {
|
||||
|
@ -81,7 +81,7 @@ impl<'m> Message<'m> {
|
|||
unsafe { CStr::from_ptr(msg_id) }
|
||||
}
|
||||
|
||||
pub fn date(&self) -> crate::UnixTimestamp {
|
||||
pub fn date(&self) -> crate::datetime::UnixTimestamp {
|
||||
(unsafe { call!(self.lib, notmuch_message_get_date)(self.message) }) as u64
|
||||
}
|
||||
|
||||
|
@ -100,7 +100,9 @@ impl<'m> Message<'m> {
|
|||
let (flags, tags) = TagIterator::new(&self).collect_flags_and_tags();
|
||||
for tag in tags {
|
||||
let num = TagHash::from_bytes(tag.as_bytes());
|
||||
tag_lock.entry(num).or_insert(tag);
|
||||
if !tag_lock.contains_key(&num) {
|
||||
tag_lock.insert(num, tag);
|
||||
}
|
||||
env.tags_mut().push(num);
|
||||
}
|
||||
unsafe {
|
||||
|
|
|
@ -34,7 +34,7 @@ impl<'q> Thread<'q> {
|
|||
ThreadHash::from(c_str.to_bytes())
|
||||
}
|
||||
|
||||
pub fn date(&self) -> crate::UnixTimestamp {
|
||||
pub fn date(&self) -> crate::datetime::UnixTimestamp {
|
||||
(unsafe { call!(self.lib, notmuch_thread_get_newest_date)(self.ptr) }) as u64
|
||||
}
|
||||
|
||||
|
@ -42,10 +42,6 @@ impl<'q> Thread<'q> {
|
|||
(unsafe { call!(self.lib, notmuch_thread_get_total_messages)(self.ptr) }) as usize
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
pub fn iter(&'q self) -> MessageIterator<'q> {
|
||||
let ptr = unsafe { call!(self.lib, notmuch_thread_get_messages)(self.ptr) };
|
||||
MessageIterator {
|
||||
|
|
|
@ -49,8 +49,32 @@ impl Default for Collection {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
impl Drop for Collection {
|
||||
fn drop(&mut self) {
|
||||
let cache_dir: xdg::BaseDirectories =
|
||||
xdg::BaseDirectories::with_profile("meli", "threads".to_string()).unwrap();
|
||||
if let Ok(cached) = cache_dir.place_cache_file("threads") {
|
||||
/* place result in cache directory */
|
||||
let f = match fs::File::create(cached) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
panic!("{}", e);
|
||||
}
|
||||
};
|
||||
let writer = io::BufWriter::new(f);
|
||||
let _ = bincode::Options::serialize_into(
|
||||
bincode::config::DefaultOptions::new(),
|
||||
writer,
|
||||
&self.thread,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
impl Collection {
|
||||
pub fn new() -> Self {
|
||||
pub fn new() -> Collection {
|
||||
let message_id_index = Arc::new(RwLock::new(HashMap::with_capacity_and_hasher(
|
||||
16,
|
||||
Default::default(),
|
||||
|
@ -64,7 +88,7 @@ impl Collection {
|
|||
Default::default(),
|
||||
)));
|
||||
|
||||
Self {
|
||||
Collection {
|
||||
envelopes: Arc::new(RwLock::new(Default::default())),
|
||||
tag_index: Arc::new(RwLock::new(BTreeMap::default())),
|
||||
message_id_index,
|
||||
|
@ -145,7 +169,9 @@ impl Collection {
|
|||
if *h == mailbox_hash {
|
||||
continue;
|
||||
}
|
||||
_ = t.update_envelope(&self.envelopes, old_hash, new_hash);
|
||||
t.update_envelope(&self.envelopes, old_hash, new_hash)
|
||||
.ok()
|
||||
.take();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
@ -160,7 +186,7 @@ impl Collection {
|
|||
) -> Option<SmallVec<[MailboxHash; 8]>> {
|
||||
*self.sent_mailbox.write().unwrap() = sent_mailbox;
|
||||
|
||||
let Self {
|
||||
let Collection {
|
||||
ref threads,
|
||||
ref envelopes,
|
||||
ref mailboxes,
|
||||
|
@ -170,8 +196,8 @@ impl Collection {
|
|||
|
||||
let mut threads_lck = threads.write().unwrap();
|
||||
let mut mailboxes_lck = mailboxes.write().unwrap();
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = threads_lck.entry(mailbox_hash) {
|
||||
e.insert(Threads::new(new_envelopes.len()));
|
||||
if !threads_lck.contains_key(&mailbox_hash) {
|
||||
threads_lck.insert(mailbox_hash, Threads::new(new_envelopes.len()));
|
||||
mailboxes_lck.insert(mailbox_hash, new_envelopes.keys().cloned().collect());
|
||||
for (h, e) in new_envelopes {
|
||||
envelopes.write().unwrap().insert(h, e);
|
||||
|
@ -301,7 +327,8 @@ impl Collection {
|
|||
.unwrap_or(false)
|
||||
{
|
||||
for (_, t) in threads_lck.iter_mut() {
|
||||
_ = t.update_envelope(&self.envelopes, old_hash, new_hash);
|
||||
t.update_envelope(&self.envelopes, old_hash, new_hash)
|
||||
.unwrap_or(());
|
||||
}
|
||||
}
|
||||
{
|
||||
|
@ -323,7 +350,9 @@ impl Collection {
|
|||
if *h == mailbox_hash {
|
||||
continue;
|
||||
}
|
||||
_ = t.update_envelope(&self.envelopes, old_hash, new_hash);
|
||||
t.update_envelope(&self.envelopes, old_hash, new_hash)
|
||||
.ok()
|
||||
.take();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -337,7 +366,8 @@ impl Collection {
|
|||
.unwrap_or(false)
|
||||
{
|
||||
for (_, t) in threads_lck.iter_mut() {
|
||||
_ = t.update_envelope(&self.envelopes, env_hash, env_hash);
|
||||
t.update_envelope(&self.envelopes, env_hash, env_hash)
|
||||
.unwrap_or(());
|
||||
}
|
||||
}
|
||||
{
|
||||
|
@ -359,7 +389,9 @@ impl Collection {
|
|||
if *h == mailbox_hash {
|
||||
continue;
|
||||
}
|
||||
_ = t.update_envelope(&self.envelopes, env_hash, env_hash);
|
||||
t.update_envelope(&self.envelopes, env_hash, env_hash)
|
||||
.ok()
|
||||
.take();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -393,8 +425,7 @@ impl Collection {
|
|||
|
||||
pub fn insert_reply(&self, env_hash: EnvelopeHash) {
|
||||
debug_assert!(self.envelopes.read().unwrap().contains_key(&env_hash));
|
||||
let mut iter = self.threads.write().unwrap();
|
||||
for (_, t) in iter.iter_mut() {
|
||||
for (_, t) in self.threads.write().unwrap().iter_mut() {
|
||||
t.insert_reply(&self.envelopes, env_hash);
|
||||
}
|
||||
}
|
||||
|
@ -428,8 +459,8 @@ impl Collection {
|
|||
|
||||
pub fn new_mailbox(&self, mailbox_hash: MailboxHash) {
|
||||
let mut mailboxes_lck = self.mailboxes.write().unwrap();
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = mailboxes_lck.entry(mailbox_hash) {
|
||||
e.insert(Default::default());
|
||||
if !mailboxes_lck.contains_key(&mailbox_hash) {
|
||||
mailboxes_lck.insert(mailbox_hash, Default::default());
|
||||
self.threads
|
||||
.write()
|
||||
.unwrap()
|
||||
|
|
|
@ -97,10 +97,10 @@ impl AccountSettings {
|
|||
} else if let Some(pass) = self.extra.get("server_password") {
|
||||
Ok(pass.to_owned())
|
||||
} else {
|
||||
Err(Error::new(
|
||||
Err(Error::new(format!(
|
||||
"Configuration error: connection requires either server_password or \
|
||||
server_password_command",
|
||||
))
|
||||
server_password_command"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -128,7 +128,7 @@ pub struct MailboxConf {
|
|||
|
||||
impl Default for MailboxConf {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
MailboxConf {
|
||||
alias: None,
|
||||
autoload: false,
|
||||
subscribe: ToggleFlag::Unset,
|
||||
|
@ -190,9 +190,8 @@ mod strings {
|
|||
named_unit_variant!(ask);
|
||||
}
|
||||
|
||||
#[derive(Copy, Default, Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Copy, Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ToggleFlag {
|
||||
#[default]
|
||||
Unset,
|
||||
InternalVal(bool),
|
||||
False,
|
||||
|
@ -203,32 +202,37 @@ pub enum ToggleFlag {
|
|||
impl From<bool> for ToggleFlag {
|
||||
fn from(val: bool) -> Self {
|
||||
if val {
|
||||
Self::True
|
||||
ToggleFlag::True
|
||||
} else {
|
||||
Self::False
|
||||
ToggleFlag::False
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ToggleFlag {
|
||||
fn default() -> Self {
|
||||
ToggleFlag::Unset
|
||||
}
|
||||
}
|
||||
|
||||
impl ToggleFlag {
|
||||
pub fn is_unset(&self) -> bool {
|
||||
Self::Unset == *self
|
||||
ToggleFlag::Unset == *self
|
||||
}
|
||||
|
||||
pub fn is_internal(&self) -> bool {
|
||||
matches!(self, Self::InternalVal(_))
|
||||
matches!(self, ToggleFlag::InternalVal(_))
|
||||
}
|
||||
|
||||
pub fn is_ask(&self) -> bool {
|
||||
matches!(self, Self::Ask)
|
||||
matches!(self, ToggleFlag::Ask)
|
||||
}
|
||||
|
||||
pub fn is_false(&self) -> bool {
|
||||
matches!(self, Self::False | Self::InternalVal(false))
|
||||
matches!(self, ToggleFlag::False | ToggleFlag::InternalVal(false))
|
||||
}
|
||||
|
||||
pub fn is_true(&self) -> bool {
|
||||
matches!(self, Self::True | Self::InternalVal(true))
|
||||
matches!(self, ToggleFlag::True | ToggleFlag::InternalVal(true))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -238,10 +242,10 @@ impl Serialize for ToggleFlag {
|
|||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Unset | Self::InternalVal(_) => serializer.serialize_none(),
|
||||
Self::False => serializer.serialize_bool(false),
|
||||
Self::True => serializer.serialize_bool(true),
|
||||
Self::Ask => serializer.serialize_str("ask"),
|
||||
ToggleFlag::Unset | ToggleFlag::InternalVal(_) => serializer.serialize_none(),
|
||||
ToggleFlag::False => serializer.serialize_bool(false),
|
||||
ToggleFlag::True => serializer.serialize_bool(true),
|
||||
ToggleFlag::Ask => serializer.serialize_str("ask"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -266,9 +270,9 @@ impl<'de> Deserialize<'de> for ToggleFlag {
|
|||
err
|
||||
))
|
||||
})? {
|
||||
InnerToggleFlag::Bool(true) => Self::True,
|
||||
InnerToggleFlag::Bool(false) => Self::False,
|
||||
InnerToggleFlag::Ask => Self::Ask,
|
||||
InnerToggleFlag::Bool(true) => ToggleFlag::True,
|
||||
InnerToggleFlag::Bool(false) => ToggleFlag::False,
|
||||
InnerToggleFlag::Ask => ToggleFlag::Ask,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ impl Connection {
|
|||
pub const IO_BUF_SIZE: usize = 64 * 1024;
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
pub fn deflate(self) -> Self {
|
||||
Self::Deflate {
|
||||
Connection::Deflate {
|
||||
inner: DeflateEncoder::new(
|
||||
DeflateDecoder::new_with_buf(Box::new(self), vec![0; Self::IO_BUF_SIZE]),
|
||||
Compression::default(),
|
||||
|
@ -157,7 +157,7 @@ impl Connection {
|
|||
where
|
||||
T: Copy,
|
||||
{
|
||||
let payload = std::ptr::addr_of!(payload) as *const c_void;
|
||||
let payload = &payload as *const T as *const c_void;
|
||||
syscall!(setsockopt(
|
||||
self.as_raw_fd(),
|
||||
opt,
|
||||
|
@ -175,7 +175,7 @@ impl Connection {
|
|||
self.as_raw_fd(),
|
||||
opt,
|
||||
val,
|
||||
std::ptr::addr_of_mut!(slot) as *mut _,
|
||||
&mut slot as *mut _ as *mut _,
|
||||
&mut len,
|
||||
))?;
|
||||
assert_eq!(len as usize, std::mem::size_of::<T>());
|
||||
|
@ -275,3 +275,24 @@ pub fn lookup_ipv4(host: &str, port: u16) -> crate::Result<std::net::SocketAddr>
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
use futures::future::{self, Either, Future};
|
||||
|
||||
pub async fn timeout<O>(dur: Option<Duration>, f: impl Future<Output = O>) -> crate::Result<O> {
|
||||
futures::pin_mut!(f);
|
||||
if let Some(dur) = dur {
|
||||
match future::select(f, smol::Timer::after(dur)).await {
|
||||
Either::Left((out, _)) => Ok(out),
|
||||
Either::Right(_) => {
|
||||
Err(crate::error::Error::new("Timed out.")
|
||||
.set_kind(crate::error::ErrorKind::Timeout))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(f.await)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sleep(dur: Duration) {
|
||||
smol::Timer::after(dur).await;
|
||||
}
|
|
@ -47,27 +47,15 @@ use std::{
|
|||
use crate::error::{Result, ResultIntoError};
|
||||
|
||||
pub type UnixTimestamp = u64;
|
||||
|
||||
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;
|
||||
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";
|
||||
|
||||
extern "C" {
|
||||
fn strptime(
|
||||
|
@ -90,7 +78,6 @@ extern "C" {
|
|||
fn gettimeofday(tv: *mut libc::timeval, tz: *mut libc::timezone) -> i32;
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "openbsd"))]
|
||||
#[repr(i32)]
|
||||
#[derive(Copy, Clone)]
|
||||
#[allow(dead_code)]
|
||||
|
@ -99,18 +86,6 @@ enum LocaleCategoryMask {
|
|||
All = libc::LC_ALL_MASK,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "openbsd")]
|
||||
const _LC_LAST: c_int = 7;
|
||||
|
||||
#[cfg(target_os = "openbsd")]
|
||||
#[repr(i32)]
|
||||
#[derive(Copy, Clone)]
|
||||
#[allow(dead_code)]
|
||||
enum LocaleCategoryMask {
|
||||
Time = 1 << libc::LC_TIME,
|
||||
All = (1 << _LC_LAST) - 2,
|
||||
}
|
||||
|
||||
#[repr(i32)]
|
||||
#[derive(Copy, Clone)]
|
||||
#[allow(dead_code)]
|
||||
|
@ -169,7 +144,7 @@ impl Locale {
|
|||
unsafe { libc::freelocale(new_locale) };
|
||||
return Err(nix::Error::last().into());
|
||||
}
|
||||
Ok(Self {
|
||||
Ok(Locale {
|
||||
mask,
|
||||
category,
|
||||
new_locale,
|
||||
|
@ -203,7 +178,7 @@ pub fn timestamp_to_string(timestamp: UnixTimestamp, fmt: Option<&str>, posix: b
|
|||
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
|
||||
unsafe {
|
||||
let i: i64 = timestamp.try_into().unwrap_or(0);
|
||||
localtime_r(std::ptr::addr_of!(i), std::ptr::addr_of_mut!(new_tm));
|
||||
localtime_r(&i as *const i64, &mut new_tm as *mut libc::tm);
|
||||
}
|
||||
let format: Cow<'_, CStr> = if let Some(cs) = fmt
|
||||
.map(str::as_bytes)
|
||||
|
@ -218,7 +193,7 @@ pub fn timestamp_to_string(timestamp: UnixTimestamp, fmt: Option<&str>, posix: b
|
|||
{
|
||||
Cow::from(cstring)
|
||||
} else {
|
||||
unsafe { CStr::from_bytes_with_nul_unchecked(fmt::DEFAULT_FMT.as_bytes()).into() }
|
||||
unsafe { CStr::from_bytes_with_nul_unchecked(DEFAULT_FMT.as_bytes()).into() }
|
||||
};
|
||||
|
||||
let mut vec: [u8; 256] = [0; 256];
|
||||
|
@ -243,7 +218,7 @@ pub fn timestamp_to_string(timestamp: UnixTimestamp, fmt: Option<&str>, posix: b
|
|||
vec.as_mut_ptr() as *mut _,
|
||||
256,
|
||||
format.as_ptr(),
|
||||
std::ptr::addr_of!(new_tm),
|
||||
&new_tm as *const _,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
@ -333,7 +308,7 @@ fn year_to_secs(year: i64, is_leap: &mut bool) -> std::result::Result<i64, ()> {
|
|||
}
|
||||
}
|
||||
|
||||
const fn month_to_secs(month: usize, is_leap: bool) -> i64 {
|
||||
fn month_to_secs(month: usize, is_leap: bool) -> i64 {
|
||||
const SECS_THROUGH_MONTH: [i64; 12] = [
|
||||
0,
|
||||
31 * 86400,
|
||||
|
@ -361,7 +336,7 @@ where
|
|||
{
|
||||
let s = CString::new(s)?;
|
||||
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
|
||||
for fmt in &[fmt::RFC822_FMT_WITH_TIME, fmt::RFC822_FMT, fmt::ASCTIME_FMT] {
|
||||
for fmt in &[RFC822_FMT_WITH_TIME, RFC822_FMT, ASCTIME_FMT] {
|
||||
let fmt = unsafe { CStr::from_bytes_with_nul_unchecked(fmt.as_bytes()) };
|
||||
let ret = {
|
||||
let _with_locale = Locale::new(
|
||||
|
@ -372,7 +347,7 @@ where
|
|||
)
|
||||
.chain_err_summary(|| "Could not set locale for datetime conversion")
|
||||
.chain_err_kind(crate::error::ErrorKind::External)?;
|
||||
unsafe { strptime(s.as_ptr(), fmt.as_ptr(), std::ptr::addr_of_mut!(new_tm)) }
|
||||
unsafe { strptime(s.as_ptr(), fmt.as_ptr(), &mut new_tm as *mut _) }
|
||||
};
|
||||
|
||||
if ret.is_null() {
|
||||
|
@ -403,15 +378,12 @@ where
|
|||
rest.to_bytes()
|
||||
};
|
||||
|
||||
TIMEZONE_ABBR
|
||||
.binary_search_by(|probe| probe.0.cmp(rest))
|
||||
.map_or_else(
|
||||
|_| 0,
|
||||
|idx| {
|
||||
let (hr_offset, min_offset) = TIMEZONE_ABBR[idx].1;
|
||||
(hr_offset as i64) * 60 * 60 + (min_offset as i64) * 60
|
||||
},
|
||||
)
|
||||
if let Ok(idx) = TIMEZONE_ABBR.binary_search_by(|probe| probe.0.cmp(rest)) {
|
||||
let (hr_offset, min_offset) = TIMEZONE_ABBR[idx].1;
|
||||
(hr_offset as i64) * 60 * 60 + (min_offset as i64) * 60
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
return Ok(tm_to_secs(new_tm)
|
||||
.map(|res| (res - tm_gmtoff) as u64)
|
||||
|
@ -426,7 +398,7 @@ where
|
|||
{
|
||||
let s = CString::new(s)?;
|
||||
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
|
||||
for fmt in &[fmt::RFC3339_DATETIME, fmt::RFC3339_DATE] {
|
||||
for fmt in &[RFC3339_FMT_WITH_TIME, RFC3339_FMT] {
|
||||
let fmt = unsafe { CStr::from_bytes_with_nul_unchecked(fmt.as_bytes()) };
|
||||
let ret = {
|
||||
let _with_locale = Locale::new(
|
||||
|
@ -437,7 +409,7 @@ where
|
|||
)
|
||||
.chain_err_summary(|| "Could not set locale for datetime conversion")
|
||||
.chain_err_kind(crate::error::ErrorKind::External)?;
|
||||
unsafe { strptime(s.as_ptr(), fmt.as_ptr(), std::ptr::addr_of_mut!(new_tm)) }
|
||||
unsafe { strptime(s.as_ptr(), fmt.as_ptr(), &mut new_tm as *mut _) }
|
||||
};
|
||||
if ret.is_null() {
|
||||
continue;
|
||||
|
@ -468,15 +440,12 @@ where
|
|||
rest.to_bytes()
|
||||
};
|
||||
|
||||
TIMEZONE_ABBR
|
||||
.binary_search_by(|probe| probe.0.cmp(rest))
|
||||
.map_or_else(
|
||||
|_| 0,
|
||||
|idx| {
|
||||
let (hr_offset, min_offset) = TIMEZONE_ABBR[idx].1;
|
||||
(hr_offset as i64) * 60 * 60 + (min_offset as i64) * 60
|
||||
},
|
||||
)
|
||||
if let Ok(idx) = TIMEZONE_ABBR.binary_search_by(|probe| probe.0.cmp(rest)) {
|
||||
let (hr_offset, min_offset) = TIMEZONE_ABBR[idx].1;
|
||||
(hr_offset as i64) * 60 * 60 + (min_offset as i64) * 60
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
return Ok(tm_to_secs(new_tm)
|
||||
.map(|res| (res - tm_gmtoff) as u64)
|
||||
|
@ -485,14 +454,8 @@ 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>>,
|
||||
{
|
||||
|
@ -503,16 +466,15 @@ where
|
|||
Cow::from(CString::new(fmt.as_bytes())?)
|
||||
};
|
||||
unsafe {
|
||||
let val = CString::new(s)?;
|
||||
let ret = strptime(val.as_ptr(), fmt.as_ptr(), std::ptr::addr_of_mut!(new_tm));
|
||||
let ret = strptime(
|
||||
CString::new(s)?.as_ptr(),
|
||||
fmt.as_ptr(),
|
||||
&mut new_tm as *mut _,
|
||||
);
|
||||
if ret.is_null() {
|
||||
return Err("Could not parse time with strptime.".into());
|
||||
return Ok(None);
|
||||
}
|
||||
let rest: isize = val.as_ptr().offset_from(ret);
|
||||
Ok((
|
||||
rest.unsigned_abs(),
|
||||
mktime(std::ptr::addr_of!(new_tm)) as u64,
|
||||
))
|
||||
Ok(Some(mktime(&new_tm as *const _) as u64))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -529,6 +491,124 @@ 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)),
|
||||
|
@ -721,126 +801,3 @@ 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 *const i8) }.is_null() {
|
||||
eprintln!("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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -19,80 +19,82 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Email parsing and composing.
|
||||
//!
|
||||
//! # Parsing bytes into an `Envelope`
|
||||
//!
|
||||
//! An [`Envelope`](Envelope) represents the information you can get from an
|
||||
//! email's headers and body structure. Addresses in `To`, `From` fields etc
|
||||
//! are parsed into [`Address`](crate::email::Address) types.
|
||||
//!
|
||||
//! ```
|
||||
//! use melib::{Attachment, Envelope};
|
||||
//!
|
||||
//! let raw_mail = r#"From: "some name" <some@example.com>
|
||||
//! To: "me" <myself@example.com>
|
||||
//! Cc:
|
||||
//! Subject: =?utf-8?Q?gratuitously_encoded_subject?=
|
||||
//! Message-ID: <h2g7f.z0gy2pgaen5m@example.com>
|
||||
//! MIME-Version: 1.0
|
||||
//! Content-Type: multipart/mixed; charset="utf-8";
|
||||
//! boundary="bzz_bzz__bzz__"
|
||||
//!
|
||||
//! This is a MIME formatted message with attachments. Use a MIME-compliant client to view it properly.
|
||||
//! --bzz_bzz__bzz__
|
||||
//!
|
||||
//! hello world.
|
||||
//! --bzz_bzz__bzz__
|
||||
//! Content-Type: image/gif; name="test_image.gif"; charset="utf-8"
|
||||
//! Content-Disposition: attachment
|
||||
//! Content-Transfer-Encoding: base64
|
||||
//!
|
||||
//! R0lGODdhKAAXAOfZAAABzAADzQAEzgQFtBEAxAAGxBcAxwALvRcFwAAPwBcLugATuQEUuxoNuxYQ
|
||||
//! sxwOvAYVvBsStSAVtx8YsRUcuhwhth4iuCQsyDAwuDc1vTc3uDg4uT85rkc9ukJBvENCvURGukdF
|
||||
//! wUVKt0hLuUxPvVZSvFlYu1hbt2BZuFxdul5joGhqlnNuf3FvlnBvwXJyt3Jxw3N0oXx1gH12gV99
|
||||
//! z317f3N7spFxwHp5wH99gYB+goF/g25+26tziIOBhWqD3oiBjICAuudkjIN+zHeC2n6Bzc1vh4eF
|
||||
//! iYaBw8F0kImHi4KFxYyHmIWIvI2Lj4uIvYaJyY+IuJGMi5iJl4qKxZSMmIuLxpONnpGPk42NvI2M
|
||||
//! 1LKGl46OvZePm5ORlZiQnJqSnpaUmLyJnJuTn5iVmZyUoJGVyZ2VoZSVw5iXoZmWrO18rJiUyp6W
|
||||
//! opuYnKaVnZ+Xo5yZncaMoaCYpJiaqo+Z2Z2annuf5qGZpa2WoJybpZmayZ2Z0KCZypydrZ6dp6Cd
|
||||
//! oZ6a0aGay5ucy5+eqKGeouWMgp+b0qKbzKCfqdqPnp2ezaGgqqOgpKafqrScpp+gz6ajqKujr62j
|
||||
//! qayksKmmq62lsaiosqqorOyWnaqqtKeqzLGptaurta2rr7Kqtq+ssLOrt6+uuLGusuqhfbWtubCv
|
||||
//! ubKvs7GwurOwtPSazbevu+ali7SxtbiwvOykjLOyvLWytuCmqOankrSzvbazuLmyvrW0vre0uba1
|
||||
//! wLi1ury0wLm2u721wbe3wbq3vMC2vLi4wr+3w7m5w8C4xLi6yry6vsG5xbu7xcC6zMK6xry8xry+
|
||||
//! u8O7x729x8C9wb++yMG+wsO+vMK/w8a+y8e/zMnBzcXH18nL2///////////////////////////
|
||||
//! ////////////////////////////////////////////////////////////////////////////
|
||||
//! /////////////////////////////////////////////////////ywAAAAAKAAXAAAI/gBP4Cjh
|
||||
//! IYMLEh0w4EgBgsMLEyFGFBEB5cOFABgzatS4AVssZAOsLOHCxooVMzCyoNmzaBOkJlS0VEDyZMjG
|
||||
//! mxk3XOMF60CDBgsoPABK9KcDCRImPCiQYAECAgQCRMU4VSrGCjFarBgUSJCgQ10FBTrkNRCfPnz4
|
||||
//! dA3UNa1btnDZqgU7Ntqzu3ej2X2mFy9eaHuhNRtMGJrhwYYN930G2K7eaNIY34U2mfJkwpgzI9Yr
|
||||
//! GBqwR2KSvAlMOXHnw5pTNzPdLNoWIWtU9XjGjDEYS8LAlFm1SrVvzIKj5TH0KpORSZOryPgCZgqL
|
||||
//! Ob+jG0YVRBErUrOiiGJ8KxgtYsh27xWL/tswnTtEbsiRVYdJNMHk4yOGhswGjR88UKjQ9Ey+/8TL
|
||||
//! XKKGGn7Akph/8XX2WDTTcAYfguVt9hhrEPqmzIOJ3VUheb48WJiHG6amC4i+WVJKKCimqGIoYxyj
|
||||
//! WWK8kKjaJ9bA18sxvXjYhourmbbMMrjI+OIn1QymDCVXANGFK4S1gQw0PxozzC+33FLLKUJq9gk1
|
||||
//! gyWDhyNwrMLkYGUEM4wvuLRiCiieXIJJJVlmJskcZ9TZRht1lnFGGmTMkMoonVQSSSOFAGJHHI0w
|
||||
//! ouiijDaaCCGQRgrpH3q4QYYXWDihxBE+7KCDDjnUIEVAADs=
|
||||
//! --bzz_bzz__bzz__--"#;
|
||||
//!
|
||||
//! let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
|
||||
//! assert_eq!(envelope.subject().as_ref(), "gratuitously encoded subject");
|
||||
//! assert_eq!(
|
||||
//! envelope.message_id_display().as_ref(),
|
||||
//! "<h2g7f.z0gy2pgaen5m@example.com>"
|
||||
//! );
|
||||
//!
|
||||
//! let body = envelope.body_bytes(raw_mail.as_bytes());
|
||||
//! assert_eq!(body.content_type().to_string().as_str(), "multipart/mixed");
|
||||
//!
|
||||
//! let body_text = body.text();
|
||||
//! assert_eq!(body_text.as_str(), "hello world.");
|
||||
//!
|
||||
//! let subattachments: Vec<Attachment> = body.attachments();
|
||||
//! assert_eq!(subattachments.len(), 3);
|
||||
//! assert_eq!(
|
||||
//! subattachments[2].content_type().name().unwrap(),
|
||||
//! "test_image.gif"
|
||||
//! );
|
||||
//! ```
|
||||
/*!
|
||||
* Email parsing and composing.
|
||||
*
|
||||
* # Parsing bytes into an `Envelope`
|
||||
*
|
||||
* An [`Envelope`](Envelope) represents the information you can get from an
|
||||
* email's headers and body structure. Addresses in `To`, `From` fields etc
|
||||
* are parsed into [`Address`](crate::email::Address) types.
|
||||
*
|
||||
* ```
|
||||
* use melib::{Attachment, Envelope};
|
||||
*
|
||||
* let raw_mail = r#"From: "some name" <some@example.com>
|
||||
* To: "me" <myself@example.com>
|
||||
* Cc:
|
||||
* Subject: =?utf-8?Q?gratuitously_encoded_subject?=
|
||||
* Message-ID: <h2g7f.z0gy2pgaen5m@example.com>
|
||||
* MIME-Version: 1.0
|
||||
* Content-Type: multipart/mixed; charset="utf-8";
|
||||
* boundary="bzz_bzz__bzz__"
|
||||
*
|
||||
* This is a MIME formatted message with attachments. Use a MIME-compliant client to view it properly.
|
||||
* --bzz_bzz__bzz__
|
||||
*
|
||||
* hello world.
|
||||
* --bzz_bzz__bzz__
|
||||
* Content-Type: image/gif; name="test_image.gif"; charset="utf-8"
|
||||
* Content-Disposition: attachment
|
||||
* Content-Transfer-Encoding: base64
|
||||
*
|
||||
* R0lGODdhKAAXAOfZAAABzAADzQAEzgQFtBEAxAAGxBcAxwALvRcFwAAPwBcLugATuQEUuxoNuxYQ
|
||||
* sxwOvAYVvBsStSAVtx8YsRUcuhwhth4iuCQsyDAwuDc1vTc3uDg4uT85rkc9ukJBvENCvURGukdF
|
||||
* wUVKt0hLuUxPvVZSvFlYu1hbt2BZuFxdul5joGhqlnNuf3FvlnBvwXJyt3Jxw3N0oXx1gH12gV99
|
||||
* z317f3N7spFxwHp5wH99gYB+goF/g25+26tziIOBhWqD3oiBjICAuudkjIN+zHeC2n6Bzc1vh4eF
|
||||
* iYaBw8F0kImHi4KFxYyHmIWIvI2Lj4uIvYaJyY+IuJGMi5iJl4qKxZSMmIuLxpONnpGPk42NvI2M
|
||||
* 1LKGl46OvZePm5ORlZiQnJqSnpaUmLyJnJuTn5iVmZyUoJGVyZ2VoZSVw5iXoZmWrO18rJiUyp6W
|
||||
* opuYnKaVnZ+Xo5yZncaMoaCYpJiaqo+Z2Z2annuf5qGZpa2WoJybpZmayZ2Z0KCZypydrZ6dp6Cd
|
||||
* oZ6a0aGay5ucy5+eqKGeouWMgp+b0qKbzKCfqdqPnp2ezaGgqqOgpKafqrScpp+gz6ajqKujr62j
|
||||
* qayksKmmq62lsaiosqqorOyWnaqqtKeqzLGptaurta2rr7Kqtq+ssLOrt6+uuLGusuqhfbWtubCv
|
||||
* ubKvs7GwurOwtPSazbevu+ali7SxtbiwvOykjLOyvLWytuCmqOankrSzvbazuLmyvrW0vre0uba1
|
||||
* wLi1ury0wLm2u721wbe3wbq3vMC2vLi4wr+3w7m5w8C4xLi6yry6vsG5xbu7xcC6zMK6xry8xry+
|
||||
* u8O7x729x8C9wb++yMG+wsO+vMK/w8a+y8e/zMnBzcXH18nL2///////////////////////////
|
||||
* ////////////////////////////////////////////////////////////////////////////
|
||||
* /////////////////////////////////////////////////////ywAAAAAKAAXAAAI/gBP4Cjh
|
||||
* IYMLEh0w4EgBgsMLEyFGFBEB5cOFABgzatS4AVssZAOsLOHCxooVMzCyoNmzaBOkJlS0VEDyZMjG
|
||||
* mxk3XOMF60CDBgsoPABK9KcDCRImPCiQYAECAgQCRMU4VSrGCjFarBgUSJCgQ10FBTrkNRCfPnz4
|
||||
* dA3UNa1btnDZqgU7Ntqzu3ej2X2mFy9eaHuhNRtMGJrhwYYN930G2K7eaNIY34U2mfJkwpgzI9Yr
|
||||
* GBqwR2KSvAlMOXHnw5pTNzPdLNoWIWtU9XjGjDEYS8LAlFm1SrVvzIKj5TH0KpORSZOryPgCZgqL
|
||||
* Ob+jG0YVRBErUrOiiGJ8KxgtYsh27xWL/tswnTtEbsiRVYdJNMHk4yOGhswGjR88UKjQ9Ey+/8TL
|
||||
* XKKGGn7Akph/8XX2WDTTcAYfguVt9hhrEPqmzIOJ3VUheb48WJiHG6amC4i+WVJKKCimqGIoYxyj
|
||||
* WWK8kKjaJ9bA18sxvXjYhourmbbMMrjI+OIn1QymDCVXANGFK4S1gQw0PxozzC+33FLLKUJq9gk1
|
||||
* gyWDhyNwrMLkYGUEM4wvuLRiCiieXIJJJVlmJskcZ9TZRht1lnFGGmTMkMoonVQSSSOFAGJHHI0w
|
||||
* ouiijDaaCCGQRgrpH3q4QYYXWDihxBE+7KCDDjnUIEVAADs=
|
||||
* --bzz_bzz__bzz__--"#;
|
||||
*
|
||||
* let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
|
||||
* assert_eq!(envelope.subject().as_ref(), "gratuitously encoded subject");
|
||||
* assert_eq!(
|
||||
* envelope.message_id_display().as_ref(),
|
||||
* "<h2g7f.z0gy2pgaen5m@example.com>"
|
||||
* );
|
||||
*
|
||||
* let body = envelope.body_bytes(raw_mail.as_bytes());
|
||||
* assert_eq!(body.content_type().to_string().as_str(), "multipart/mixed");
|
||||
*
|
||||
* let body_text = body.text();
|
||||
* assert_eq!(body_text.as_str(), "hello world.");
|
||||
*
|
||||
* let subattachments: Vec<Attachment> = body.attachments();
|
||||
* assert_eq!(subattachments.len(), 3);
|
||||
* assert_eq!(
|
||||
* subattachments[2].content_type().name().unwrap(),
|
||||
* "test_image.gif"
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
|
||||
pub mod address;
|
||||
pub mod attachment_types;
|
||||
|
@ -110,41 +112,18 @@ pub use address::{Address, MessageID, References, StrBuild, StrBuilder};
|
|||
pub use attachments::{Attachment, AttachmentBuilder};
|
||||
pub use compose::{attachment_from_file, Draft};
|
||||
pub use headers::*;
|
||||
#[cfg(feature = "imap_backend")]
|
||||
use imap_codec::{
|
||||
core::{AString, Atom, NonEmptyVec},
|
||||
fetch::{MacroOrMessageDataItemNames, MessageDataItemName},
|
||||
flag::Flag as ImapCodecFlag,
|
||||
section::Section,
|
||||
};
|
||||
use imap_codec::message::{Flag as ImapCodecFlag, FlagFetch as ImapCodecFlagFetch};
|
||||
pub use mailto::*;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
datetime::UnixTimestamp,
|
||||
error::{Error, Result},
|
||||
parser::BytesExt,
|
||||
thread::ThreadNodeHash,
|
||||
TagHash, UnixTimestamp,
|
||||
TagHash,
|
||||
};
|
||||
|
||||
#[cfg(feature = "imap_backend")]
|
||||
// TODO(#222): Make this `const` as soon as it is possible.
|
||||
pub(crate) fn common_attributes() -> MacroOrMessageDataItemNames<'static> {
|
||||
MacroOrMessageDataItemNames::MessageDataItemNames(vec![
|
||||
MessageDataItemName::Uid,
|
||||
MessageDataItemName::Flags,
|
||||
MessageDataItemName::Envelope,
|
||||
MessageDataItemName::BodyExt {
|
||||
section: Some(Section::HeaderFields(
|
||||
None,
|
||||
NonEmptyVec::from(AString::from(Atom::unvalidated("REFERENCES"))),
|
||||
)),
|
||||
partial: None,
|
||||
peek: true,
|
||||
},
|
||||
MessageDataItemName::BodyStructure,
|
||||
])
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct Flag: u8 {
|
||||
|
@ -159,15 +138,30 @@ bitflags! {
|
|||
|
||||
impl PartialEq<&str> for Flag {
|
||||
fn eq(&self, other: &&str) -> bool {
|
||||
(other.eq_ignore_ascii_case("passed") && self.contains(Self::PASSED))
|
||||
|| (other.eq_ignore_ascii_case("replied") && self.contains(Self::REPLIED))
|
||||
|| (other.eq_ignore_ascii_case("seen") && self.contains(Self::SEEN))
|
||||
|| (other.eq_ignore_ascii_case("read") && self.contains(Self::SEEN))
|
||||
|| (other.eq_ignore_ascii_case("junk") && self.contains(Self::TRASHED))
|
||||
|| (other.eq_ignore_ascii_case("trash") && self.contains(Self::TRASHED))
|
||||
|| (other.eq_ignore_ascii_case("trashed") && self.contains(Self::TRASHED))
|
||||
|| (other.eq_ignore_ascii_case("draft") && self.contains(Self::DRAFT))
|
||||
|| (other.eq_ignore_ascii_case("flagged") && self.contains(Self::FLAGGED))
|
||||
(other.eq_ignore_ascii_case("passed") && self.contains(Flag::PASSED))
|
||||
|| (other.eq_ignore_ascii_case("replied") && self.contains(Flag::REPLIED))
|
||||
|| (other.eq_ignore_ascii_case("seen") && self.contains(Flag::SEEN))
|
||||
|| (other.eq_ignore_ascii_case("read") && self.contains(Flag::SEEN))
|
||||
|| (other.eq_ignore_ascii_case("junk") && self.contains(Flag::TRASHED))
|
||||
|| (other.eq_ignore_ascii_case("trash") && self.contains(Flag::TRASHED))
|
||||
|| (other.eq_ignore_ascii_case("trashed") && self.contains(Flag::TRASHED))
|
||||
|| (other.eq_ignore_ascii_case("draft") && self.contains(Flag::DRAFT))
|
||||
|| (other.eq_ignore_ascii_case("flagged") && self.contains(Flag::FLAGGED))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ImapCodecFlagFetch<'_>> for Flag {
|
||||
fn from(value: ImapCodecFlagFetch) -> Self {
|
||||
match value {
|
||||
ImapCodecFlagFetch::Flag(flag) => match flag {
|
||||
ImapCodecFlag::Seen => Flag::SEEN,
|
||||
ImapCodecFlag::Draft => Flag::DRAFT,
|
||||
ImapCodecFlag::Flagged => Flag::FLAGGED,
|
||||
// TODO
|
||||
_ => Flag::empty(),
|
||||
},
|
||||
ImapCodecFlagFetch::Recent => Flag::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -180,43 +174,12 @@ macro_rules! flag_impl {
|
|||
}
|
||||
|
||||
impl Flag {
|
||||
flag_impl!(fn is_passed, Self::PASSED);
|
||||
flag_impl!(fn is_replied, Self::REPLIED);
|
||||
flag_impl!(fn is_seen, Self::SEEN);
|
||||
flag_impl!(fn is_trashed, Self::TRASHED);
|
||||
flag_impl!(fn is_draft, Self::DRAFT);
|
||||
flag_impl!(fn is_flagged, Self::FLAGGED);
|
||||
|
||||
#[cfg(feature = "imap_backend")]
|
||||
pub(crate) fn derive_imap_codec_flags(&self) -> Vec<ImapCodecFlag> {
|
||||
let mut flags = Vec::new();
|
||||
|
||||
if self.is_passed() {
|
||||
// This is from http://cr.yp.to/proto/maildir.html and not meaningful in IMAP.
|
||||
}
|
||||
|
||||
if self.is_replied() {
|
||||
flags.push(ImapCodecFlag::Answered);
|
||||
}
|
||||
|
||||
if self.is_seen() {
|
||||
flags.push(ImapCodecFlag::Seen);
|
||||
}
|
||||
|
||||
if self.is_trashed() {
|
||||
flags.push(ImapCodecFlag::Deleted);
|
||||
}
|
||||
|
||||
if self.is_draft() {
|
||||
flags.push(ImapCodecFlag::Draft);
|
||||
}
|
||||
|
||||
if self.is_flagged() {
|
||||
flags.push(ImapCodecFlag::Flagged);
|
||||
}
|
||||
|
||||
flags
|
||||
}
|
||||
flag_impl!(fn is_seen, Flag::SEEN);
|
||||
flag_impl!(fn is_draft, Flag::DRAFT);
|
||||
flag_impl!(fn is_trashed, Flag::TRASHED);
|
||||
flag_impl!(fn is_passed, Flag::PASSED);
|
||||
flag_impl!(fn is_replied, Flag::REPLIED);
|
||||
flag_impl!(fn is_flagged, Flag::FLAGGED);
|
||||
}
|
||||
|
||||
///`Mail` holds both the envelope info of an email in its `envelope` field and
|
||||
|
@ -239,7 +202,7 @@ impl Deref for Mail {
|
|||
|
||||
impl Mail {
|
||||
pub fn new(bytes: Vec<u8>, flags: Option<Flag>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
Ok(Mail {
|
||||
envelope: Envelope::from_bytes(&bytes, flags)?,
|
||||
bytes,
|
||||
})
|
||||
|
@ -273,8 +236,8 @@ pub struct Envelope {
|
|||
pub date: String,
|
||||
pub subject: Option<String>,
|
||||
pub from: SmallVec<[Address; 1]>,
|
||||
// pub sender
|
||||
// pub reply_to
|
||||
// TODO: sender
|
||||
// TODO: reply_to
|
||||
pub to: SmallVec<[Address; 1]>,
|
||||
pub cc: SmallVec<[Address; 1]>,
|
||||
pub bcc: Vec<Address>,
|
||||
|
@ -309,13 +272,13 @@ impl core::fmt::Debug for Envelope {
|
|||
|
||||
impl Default for Envelope {
|
||||
fn default() -> Self {
|
||||
Self::new(EnvelopeHash::default())
|
||||
Envelope::new(EnvelopeHash::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Envelope {
|
||||
pub fn new(hash: EnvelopeHash) -> Self {
|
||||
Self {
|
||||
Envelope {
|
||||
hash,
|
||||
date: String::new(),
|
||||
timestamp: 0,
|
||||
|
@ -340,8 +303,8 @@ impl Envelope {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8], flags: Option<Flag>) -> Result<Self> {
|
||||
let mut e = Self::new(EnvelopeHash::from_bytes(bytes.trim()));
|
||||
pub fn from_bytes(bytes: &[u8], flags: Option<Flag>) -> Result<Envelope> {
|
||||
let mut e = Envelope::new(EnvelopeHash::from_bytes(bytes));
|
||||
let res = e.populate_headers(bytes).ok();
|
||||
if res.is_some() {
|
||||
if let Some(f) = flags {
|
||||
|
@ -855,19 +818,19 @@ impl Envelope {
|
|||
impl Eq for Envelope {}
|
||||
|
||||
impl Ord for Envelope {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
fn cmp(&self, other: &Envelope) -> std::cmp::Ordering {
|
||||
self.datetime().cmp(&other.datetime())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Envelope {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
fn partial_cmp(&self, other: &Envelope) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Envelope {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
fn eq(&self, other: &Envelope) -> bool {
|
||||
self.hash == other.hash
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ pub struct MailboxAddress {
|
|||
impl Eq for MailboxAddress {}
|
||||
|
||||
impl PartialEq for MailboxAddress {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
fn eq(&self, other: &MailboxAddress) -> bool {
|
||||
self.address_spec.display_bytes(&self.raw) == other.address_spec.display_bytes(&other.raw)
|
||||
}
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ pub enum Address {
|
|||
|
||||
impl Address {
|
||||
pub fn new(display_name: Option<String>, address: String) -> Self {
|
||||
Self::Mailbox(if let Some(d) = display_name {
|
||||
Address::Mailbox(if let Some(d) = display_name {
|
||||
MailboxAddress {
|
||||
raw: format!("{} <{}>", d, address).into_bytes(),
|
||||
display_name: StrBuilder {
|
||||
|
@ -135,8 +135,8 @@ impl Address {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn new_group(display_name: String, mailbox_list: Vec<Self>) -> Self {
|
||||
Self::Group(GroupAddress {
|
||||
pub fn new_group(display_name: String, mailbox_list: Vec<Address>) -> Self {
|
||||
Address::Group(GroupAddress {
|
||||
raw: format!(
|
||||
"{}:{};",
|
||||
display_name,
|
||||
|
@ -157,8 +157,8 @@ impl Address {
|
|||
|
||||
pub fn raw(&self) -> &[u8] {
|
||||
match self {
|
||||
Self::Mailbox(m) => m.raw.as_slice(),
|
||||
Self::Group(g) => g.raw.as_slice(),
|
||||
Address::Mailbox(m) => m.raw.as_slice(),
|
||||
Address::Group(g) => g.raw.as_slice(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,8 +179,8 @@ impl Address {
|
|||
/// ```
|
||||
pub fn get_display_name(&self) -> Option<String> {
|
||||
let ret = match self {
|
||||
Self::Mailbox(m) => m.display_name.display(&m.raw),
|
||||
Self::Group(g) => g.display_name.display(&g.raw),
|
||||
Address::Mailbox(m) => m.display_name.display(&m.raw),
|
||||
Address::Group(g) => g.display_name.display(&g.raw),
|
||||
};
|
||||
if ret.is_empty() {
|
||||
None
|
||||
|
@ -193,26 +193,26 @@ impl Address {
|
|||
/// `String`.
|
||||
pub fn get_email(&self) -> String {
|
||||
match self {
|
||||
Self::Mailbox(m) => m.address_spec.display(&m.raw),
|
||||
Self::Group(_) => String::new(),
|
||||
Address::Mailbox(m) => m.address_spec.display(&m.raw),
|
||||
Address::Group(_) => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn address_spec_raw(&self) -> &[u8] {
|
||||
match self {
|
||||
Self::Mailbox(m) => m.address_spec.display_bytes(&m.raw),
|
||||
Self::Group(g) => &g.raw,
|
||||
Address::Mailbox(m) => m.address_spec.display_bytes(&m.raw),
|
||||
Address::Group(g) => &g.raw,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_fqdn(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::Mailbox(m) => {
|
||||
Address::Mailbox(m) => {
|
||||
let raw_address = m.address_spec.display_bytes(&m.raw);
|
||||
let fqdn_pos = raw_address.iter().position(|&b| b == b'@')? + 1;
|
||||
Some(String::from_utf8_lossy(&raw_address[fqdn_pos..]).into())
|
||||
}
|
||||
Self::Group(_) => None,
|
||||
Address::Group(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -231,16 +231,16 @@ impl Address {
|
|||
.collect::<_>()
|
||||
}
|
||||
|
||||
pub fn list_try_from<T: AsRef<[u8]>>(val: T) -> Result<Vec<Self>> {
|
||||
Ok(parser::address::rfc2822address_list(val.as_ref())?
|
||||
pub fn list_try_from(val: &str) -> Result<Vec<Address>> {
|
||||
Ok(parser::address::rfc2822address_list(val.as_bytes())?
|
||||
.1
|
||||
.to_vec())
|
||||
}
|
||||
|
||||
pub fn contains_address(&self, other: &Self) -> bool {
|
||||
pub fn contains_address(&self, other: &Address) -> bool {
|
||||
match self {
|
||||
Self::Mailbox(_) => self == other,
|
||||
Self::Group(g) => g
|
||||
Address::Mailbox(_) => self == other,
|
||||
Address::Group(g) => g
|
||||
.mailbox_list
|
||||
.iter()
|
||||
.any(|addr| addr.contains_address(other)),
|
||||
|
@ -269,7 +269,7 @@ impl Address {
|
|||
/// ```
|
||||
pub fn subaddress(&self, separator: &str) -> Option<(Self, String)> {
|
||||
match self {
|
||||
Self::Mailbox(_) => {
|
||||
Address::Mailbox(_) => {
|
||||
let email = self.get_email();
|
||||
let (local_part, domain) =
|
||||
match super::parser::address::addr_spec_raw(email.as_bytes())
|
||||
|
@ -292,18 +292,7 @@ impl Address {
|
|||
subaddress.to_string(),
|
||||
))
|
||||
}
|
||||
Self::Group(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the display name of this address in bytes.
|
||||
///
|
||||
/// For a string, see the [`display_name`](fn@Self::get_display_name)
|
||||
/// method.
|
||||
pub fn display_name_bytes(&self) -> &[u8] {
|
||||
match self {
|
||||
Self::Mailbox(m) => m.display_name.display_bytes(&m.raw),
|
||||
Self::Group(g) => g.display_name.display_bytes(&g.raw),
|
||||
Address::Group(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -311,12 +300,14 @@ impl Address {
|
|||
impl Eq for Address {}
|
||||
|
||||
impl PartialEq for Address {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
fn eq(&self, other: &Address) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Mailbox(_), Self::Group(_)) | (Self::Group(_), Self::Mailbox(_)) => false,
|
||||
(Self::Mailbox(s), Self::Mailbox(o)) => s == o,
|
||||
(Self::Group(s), Self::Group(o)) => {
|
||||
self.display_name_bytes() == other.display_name_bytes()
|
||||
(Address::Mailbox(_), Address::Group(_)) | (Address::Group(_), Address::Mailbox(_)) => {
|
||||
false
|
||||
}
|
||||
(Address::Mailbox(s), Address::Mailbox(o)) => s == o,
|
||||
(Address::Group(s), Address::Group(o)) => {
|
||||
s.display_name.display_bytes(&s.raw) == o.display_name.display_bytes(&o.raw)
|
||||
&& s.mailbox_list.iter().collect::<HashSet<_>>()
|
||||
== o.mailbox_list.iter().collect::<HashSet<_>>()
|
||||
}
|
||||
|
@ -327,10 +318,10 @@ impl PartialEq for Address {
|
|||
impl Hash for Address {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
match self {
|
||||
Self::Mailbox(s) => {
|
||||
Address::Mailbox(s) => {
|
||||
s.address_spec.display_bytes(&s.raw).hash(state);
|
||||
}
|
||||
Self::Group(s) => {
|
||||
Address::Group(s) => {
|
||||
s.display_name.display_bytes(&s.raw).hash(state);
|
||||
for sub in &s.mailbox_list {
|
||||
sub.hash(state);
|
||||
|
@ -343,13 +334,15 @@ impl Hash for Address {
|
|||
impl core::fmt::Display for Address {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::Mailbox(m) if m.display_name.length > 0 => match m.display_name.display(&m.raw) {
|
||||
d if d.contains('.') || d.contains(',') => {
|
||||
write!(f, "\"{}\" <{}>", d, m.address_spec.display(&m.raw))
|
||||
Address::Mailbox(m) if m.display_name.length > 0 => {
|
||||
match m.display_name.display(&m.raw) {
|
||||
d if d.contains('.') || d.contains(',') => {
|
||||
write!(f, "\"{}\" <{}>", d, m.address_spec.display(&m.raw))
|
||||
}
|
||||
d => write!(f, "{} <{}>", d, m.address_spec.display(&m.raw)),
|
||||
}
|
||||
d => write!(f, "{} <{}>", d, m.address_spec.display(&m.raw)),
|
||||
},
|
||||
Self::Group(g) => {
|
||||
}
|
||||
Address::Group(g) => {
|
||||
let attachment_strings: Vec<String> =
|
||||
g.mailbox_list.iter().map(|a| format!("{}", a)).collect();
|
||||
write!(
|
||||
|
@ -359,7 +352,7 @@ impl core::fmt::Display for Address {
|
|||
attachment_strings.join(", ")
|
||||
)
|
||||
}
|
||||
Self::Mailbox(m) => write!(f, "{}", m.address_spec.display(&m.raw)),
|
||||
Address::Mailbox(m) => write!(f, "{}", m.address_spec.display(&m.raw)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -367,12 +360,12 @@ impl core::fmt::Display for Address {
|
|||
impl core::fmt::Debug for Address {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::Mailbox(m) => f
|
||||
Address::Mailbox(m) => f
|
||||
.debug_struct("Address::Mailbox")
|
||||
.field("display_name", &m.display_name.display(&m.raw))
|
||||
.field("address_spec", &m.address_spec.display(&m.raw))
|
||||
.finish(),
|
||||
Self::Group(g) => {
|
||||
Address::Group(g) => {
|
||||
let attachment_strings: Vec<String> =
|
||||
g.mailbox_list.iter().map(|a| format!("{}", a)).collect();
|
||||
|
||||
|
@ -387,8 +380,7 @@ impl core::fmt::Debug for Address {
|
|||
|
||||
impl TryFrom<&str> for Address {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(val: &str) -> Result<Self> {
|
||||
fn try_from(val: &str) -> Result<Address> {
|
||||
Ok(parser::address::address(val.as_bytes())?.1)
|
||||
}
|
||||
}
|
||||
|
@ -429,7 +421,7 @@ pub struct MessageID(pub Vec<u8>, pub StrBuilder);
|
|||
impl StrBuild for MessageID {
|
||||
fn new(string: &[u8], slice: &[u8]) -> Self {
|
||||
let offset = string.find(slice).unwrap_or(0);
|
||||
Self(
|
||||
MessageID(
|
||||
string.to_owned(),
|
||||
StrBuilder {
|
||||
offset,
|
||||
|
@ -437,13 +429,11 @@ impl StrBuild for MessageID {
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn raw(&self) -> &[u8] {
|
||||
let offset = self.1.offset;
|
||||
let length = self.1.length;
|
||||
&self.0[offset..offset + length.saturating_sub(1)]
|
||||
}
|
||||
|
||||
fn val(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
|
@ -478,11 +468,10 @@ impl core::fmt::Display for MessageID {
|
|||
}
|
||||
|
||||
impl PartialEq for MessageID {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
fn eq(&self, other: &MessageID) -> bool {
|
||||
self.raw() == other.raw()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for MessageID {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "{}", String::from_utf8(self.raw().to_vec()).unwrap())
|
||||
|
|
|
@ -28,10 +28,9 @@ use crate::email::{
|
|||
parser::BytesExt,
|
||||
};
|
||||
|
||||
#[derive(Clone, Default, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Charset {
|
||||
Ascii,
|
||||
#[default]
|
||||
UTF8,
|
||||
UTF16,
|
||||
ISO8859_1,
|
||||
|
@ -61,102 +60,110 @@ pub enum Charset {
|
|||
KOI8U,
|
||||
}
|
||||
|
||||
impl Default for Charset {
|
||||
fn default() -> Self {
|
||||
Charset::UTF8
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [u8]> for Charset {
|
||||
fn from(b: &'a [u8]) -> Self {
|
||||
match b.trim() {
|
||||
b if b.eq_ignore_ascii_case(b"us-ascii") || b.eq_ignore_ascii_case(b"ascii") => {
|
||||
Self::Ascii
|
||||
Charset::Ascii
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"utf-8") || b.eq_ignore_ascii_case(b"utf8") => {
|
||||
Charset::UTF8
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"utf-8") || b.eq_ignore_ascii_case(b"utf8") => Self::UTF8,
|
||||
b if b.eq_ignore_ascii_case(b"utf-16") || b.eq_ignore_ascii_case(b"utf16") => {
|
||||
Self::UTF16
|
||||
Charset::UTF16
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-1") || b.eq_ignore_ascii_case(b"iso8859-1") => {
|
||||
Self::ISO8859_1
|
||||
Charset::ISO8859_1
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-2") || b.eq_ignore_ascii_case(b"iso8859-2") => {
|
||||
Self::ISO8859_2
|
||||
Charset::ISO8859_2
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-3") || b.eq_ignore_ascii_case(b"iso8859-3") => {
|
||||
Self::ISO8859_3
|
||||
Charset::ISO8859_3
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-4") || b.eq_ignore_ascii_case(b"iso8859-4") => {
|
||||
Self::ISO8859_4
|
||||
Charset::ISO8859_4
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-5") || b.eq_ignore_ascii_case(b"iso8859-5") => {
|
||||
Self::ISO8859_5
|
||||
Charset::ISO8859_5
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-6") || b.eq_ignore_ascii_case(b"iso8859-6") => {
|
||||
Self::ISO8859_6
|
||||
Charset::ISO8859_6
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-7") || b.eq_ignore_ascii_case(b"iso8859-7") => {
|
||||
Self::ISO8859_7
|
||||
Charset::ISO8859_7
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-8") || b.eq_ignore_ascii_case(b"iso8859-8") => {
|
||||
Self::ISO8859_8
|
||||
Charset::ISO8859_8
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-10")
|
||||
|| b.eq_ignore_ascii_case(b"iso8859-10") =>
|
||||
{
|
||||
Self::ISO8859_10
|
||||
Charset::ISO8859_10
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-13")
|
||||
|| b.eq_ignore_ascii_case(b"iso8859-13") =>
|
||||
{
|
||||
Self::ISO8859_13
|
||||
Charset::ISO8859_13
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-14")
|
||||
|| b.eq_ignore_ascii_case(b"iso8859-14") =>
|
||||
{
|
||||
Self::ISO8859_14
|
||||
Charset::ISO8859_14
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-15")
|
||||
|| b.eq_ignore_ascii_case(b"iso8859-15") =>
|
||||
{
|
||||
Self::ISO8859_15
|
||||
Charset::ISO8859_15
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-16")
|
||||
|| b.eq_ignore_ascii_case(b"iso8859-16") =>
|
||||
{
|
||||
Self::ISO8859_16
|
||||
Charset::ISO8859_16
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"windows-1250")
|
||||
|| b.eq_ignore_ascii_case(b"windows1250") =>
|
||||
{
|
||||
Self::Windows1250
|
||||
Charset::Windows1250
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"windows-1251")
|
||||
|| b.eq_ignore_ascii_case(b"windows1251") =>
|
||||
{
|
||||
Self::Windows1251
|
||||
Charset::Windows1251
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"windows-1252")
|
||||
|| b.eq_ignore_ascii_case(b"windows1252") =>
|
||||
{
|
||||
Self::Windows1252
|
||||
Charset::Windows1252
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"windows-1253")
|
||||
|| b.eq_ignore_ascii_case(b"windows1253")
|
||||
|| b.eq_ignore_ascii_case(b"cp1253")
|
||||
|| b.eq_ignore_ascii_case(b"cp-1253") =>
|
||||
{
|
||||
Self::Windows1253
|
||||
Charset::Windows1253
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"gbk") => Self::GBK,
|
||||
b if b.eq_ignore_ascii_case(b"gbk") => Charset::GBK,
|
||||
b if b.eq_ignore_ascii_case(b"gb18030") || b.eq_ignore_ascii_case(b"gb-18030") => {
|
||||
Self::GB18030
|
||||
Charset::GB18030
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"gb2312") || b.eq_ignore_ascii_case(b"gb-2312") => {
|
||||
Self::GB2312
|
||||
Charset::GB2312
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"big5") => Self::BIG5,
|
||||
b if b.eq_ignore_ascii_case(b"iso-2022-jp") => Self::ISO2022JP,
|
||||
b if b.eq_ignore_ascii_case(b"euc-jp") => Self::EUCJP,
|
||||
b if b.eq_ignore_ascii_case(b"koi8-r") => Self::KOI8R,
|
||||
b if b.eq_ignore_ascii_case(b"koi8-u") => Self::KOI8U,
|
||||
b if b.eq_ignore_ascii_case(b"big5") => Charset::BIG5,
|
||||
b if b.eq_ignore_ascii_case(b"iso-2022-jp") => Charset::ISO2022JP,
|
||||
b if b.eq_ignore_ascii_case(b"euc-jp") => Charset::EUCJP,
|
||||
b if b.eq_ignore_ascii_case(b"koi8-r") => Charset::KOI8R,
|
||||
b if b.eq_ignore_ascii_case(b"koi8-u") => Charset::KOI8U,
|
||||
_ => {
|
||||
debug!("unknown tag is {:?}", str::from_utf8(b));
|
||||
Self::Ascii
|
||||
Charset::Ascii
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -165,80 +172,85 @@ impl<'a> From<&'a [u8]> for Charset {
|
|||
impl Display for Charset {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
match self {
|
||||
Self::Ascii => write!(f, "us-ascii"),
|
||||
Self::UTF8 => write!(f, "utf-8"),
|
||||
Self::UTF16 => write!(f, "utf-16"),
|
||||
Self::ISO8859_1 => write!(f, "iso-8859-1"),
|
||||
Self::ISO8859_2 => write!(f, "iso-8859-2"),
|
||||
Self::ISO8859_3 => write!(f, "iso-8859-3"),
|
||||
Self::ISO8859_4 => write!(f, "iso-8859-4"),
|
||||
Self::ISO8859_5 => write!(f, "iso-8859-5"),
|
||||
Self::ISO8859_6 => write!(f, "iso-8859-6"),
|
||||
Self::ISO8859_7 => write!(f, "iso-8859-7"),
|
||||
Self::ISO8859_8 => write!(f, "iso-8859-8"),
|
||||
Self::ISO8859_10 => write!(f, "iso-8859-10"),
|
||||
Self::ISO8859_13 => write!(f, "iso-8859-13"),
|
||||
Self::ISO8859_14 => write!(f, "iso-8859-14"),
|
||||
Self::ISO8859_15 => write!(f, "iso-8859-15"),
|
||||
Self::ISO8859_16 => write!(f, "iso-8859-16"),
|
||||
Self::Windows1250 => write!(f, "windows-1250"),
|
||||
Self::Windows1251 => write!(f, "windows-1251"),
|
||||
Self::Windows1252 => write!(f, "windows-1252"),
|
||||
Self::Windows1253 => write!(f, "windows-1253"),
|
||||
Self::GBK => write!(f, "gbk"),
|
||||
Self::GB2312 => write!(f, "gb2312"),
|
||||
Self::GB18030 => write!(f, "gb18030"),
|
||||
Self::BIG5 => write!(f, "big5"),
|
||||
Self::ISO2022JP => write!(f, "iso-2022-jp"),
|
||||
Self::EUCJP => write!(f, "euc-jp"),
|
||||
Self::KOI8R => write!(f, "koi8-r"),
|
||||
Self::KOI8U => write!(f, "koi8-u"),
|
||||
Charset::Ascii => write!(f, "us-ascii"),
|
||||
Charset::UTF8 => write!(f, "utf-8"),
|
||||
Charset::UTF16 => write!(f, "utf-16"),
|
||||
Charset::ISO8859_1 => write!(f, "iso-8859-1"),
|
||||
Charset::ISO8859_2 => write!(f, "iso-8859-2"),
|
||||
Charset::ISO8859_3 => write!(f, "iso-8859-3"),
|
||||
Charset::ISO8859_4 => write!(f, "iso-8859-4"),
|
||||
Charset::ISO8859_5 => write!(f, "iso-8859-5"),
|
||||
Charset::ISO8859_6 => write!(f, "iso-8859-6"),
|
||||
Charset::ISO8859_7 => write!(f, "iso-8859-7"),
|
||||
Charset::ISO8859_8 => write!(f, "iso-8859-8"),
|
||||
Charset::ISO8859_10 => write!(f, "iso-8859-10"),
|
||||
Charset::ISO8859_13 => write!(f, "iso-8859-13"),
|
||||
Charset::ISO8859_14 => write!(f, "iso-8859-14"),
|
||||
Charset::ISO8859_15 => write!(f, "iso-8859-15"),
|
||||
Charset::ISO8859_16 => write!(f, "iso-8859-16"),
|
||||
Charset::Windows1250 => write!(f, "windows-1250"),
|
||||
Charset::Windows1251 => write!(f, "windows-1251"),
|
||||
Charset::Windows1252 => write!(f, "windows-1252"),
|
||||
Charset::Windows1253 => write!(f, "windows-1253"),
|
||||
Charset::GBK => write!(f, "gbk"),
|
||||
Charset::GB2312 => write!(f, "gb2312"),
|
||||
Charset::GB18030 => write!(f, "gb18030"),
|
||||
Charset::BIG5 => write!(f, "big5"),
|
||||
Charset::ISO2022JP => write!(f, "iso-2022-jp"),
|
||||
Charset::EUCJP => write!(f, "euc-jp"),
|
||||
Charset::KOI8R => write!(f, "koi8-r"),
|
||||
Charset::KOI8U => write!(f, "koi8-u"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum MultipartType {
|
||||
Alternative,
|
||||
Digest,
|
||||
Encrypted,
|
||||
#[default]
|
||||
Mixed,
|
||||
Related,
|
||||
Signed,
|
||||
}
|
||||
|
||||
impl Default for MultipartType {
|
||||
fn default() -> Self {
|
||||
MultipartType::Mixed
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MultipartType {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Alternative => "multipart/alternative",
|
||||
Self::Digest => "multipart/digest",
|
||||
Self::Encrypted => "multipart/encrypted",
|
||||
Self::Mixed => "multipart/mixed",
|
||||
Self::Related => "multipart/related",
|
||||
Self::Signed => "multipart/signed",
|
||||
MultipartType::Alternative => "multipart/alternative",
|
||||
MultipartType::Digest => "multipart/digest",
|
||||
MultipartType::Encrypted => "multipart/encrypted",
|
||||
MultipartType::Mixed => "multipart/mixed",
|
||||
MultipartType::Related => "multipart/related",
|
||||
MultipartType::Signed => "multipart/signed",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for MultipartType {
|
||||
fn from(val: &[u8]) -> Self {
|
||||
fn from(val: &[u8]) -> MultipartType {
|
||||
if val.eq_ignore_ascii_case(b"mixed") {
|
||||
Self::Mixed
|
||||
MultipartType::Mixed
|
||||
} else if val.eq_ignore_ascii_case(b"alternative") {
|
||||
Self::Alternative
|
||||
MultipartType::Alternative
|
||||
} else if val.eq_ignore_ascii_case(b"digest") {
|
||||
Self::Digest
|
||||
MultipartType::Digest
|
||||
} else if val.eq_ignore_ascii_case(b"encrypted") {
|
||||
Self::Encrypted
|
||||
MultipartType::Encrypted
|
||||
} else if val.eq_ignore_ascii_case(b"signed") {
|
||||
Self::Signed
|
||||
MultipartType::Signed
|
||||
} else if val.eq_ignore_ascii_case(b"related") {
|
||||
Self::Related
|
||||
MultipartType::Related
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
|
@ -274,7 +286,7 @@ pub enum ContentType {
|
|||
|
||||
impl Default for ContentType {
|
||||
fn default() -> Self {
|
||||
Self::Text {
|
||||
ContentType::Text {
|
||||
kind: Text::Plain,
|
||||
parameters: Vec::new(),
|
||||
charset: Charset::UTF8,
|
||||
|
@ -286,64 +298,64 @@ impl PartialEq<&[u8]> for ContentType {
|
|||
fn eq(&self, other: &&[u8]) -> bool {
|
||||
match (self, *other) {
|
||||
(
|
||||
Self::Text {
|
||||
ContentType::Text {
|
||||
kind: Text::Plain, ..
|
||||
},
|
||||
b"text/plain",
|
||||
) => true,
|
||||
(
|
||||
Self::Text {
|
||||
ContentType::Text {
|
||||
kind: Text::Html, ..
|
||||
},
|
||||
b"text/html",
|
||||
) => true,
|
||||
(
|
||||
Self::Multipart {
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Alternative,
|
||||
..
|
||||
},
|
||||
b"multipart/alternative",
|
||||
) => true,
|
||||
(
|
||||
Self::Multipart {
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Digest,
|
||||
..
|
||||
},
|
||||
b"multipart/digest",
|
||||
) => true,
|
||||
(
|
||||
Self::Multipart {
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Encrypted,
|
||||
..
|
||||
},
|
||||
b"multipart/encrypted",
|
||||
) => true,
|
||||
(
|
||||
Self::Multipart {
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Mixed,
|
||||
..
|
||||
},
|
||||
b"multipart/mixed",
|
||||
) => true,
|
||||
(
|
||||
Self::Multipart {
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Related,
|
||||
..
|
||||
},
|
||||
b"multipart/related",
|
||||
) => true,
|
||||
(
|
||||
Self::Multipart {
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Signed,
|
||||
..
|
||||
},
|
||||
b"multipart/signed",
|
||||
) => true,
|
||||
(Self::PGPSignature, b"application/pgp-signature") => true,
|
||||
(Self::CMSSignature, b"application/pkcs7-signature") => true,
|
||||
(Self::MessageRfc822, b"message/rfc822") => true,
|
||||
(Self::Other { tag, .. }, _) => other.eq_ignore_ascii_case(tag),
|
||||
(Self::OctetStream { .. }, b"application/octet-stream") => true,
|
||||
(ContentType::PGPSignature, b"application/pgp-signature") => true,
|
||||
(ContentType::CMSSignature, b"application/pkcs7-signature") => true,
|
||||
(ContentType::MessageRfc822, b"message/rfc822") => true,
|
||||
(ContentType::Other { tag, .. }, _) => other.eq_ignore_ascii_case(tag),
|
||||
(ContentType::OctetStream { .. }, b"application/octet-stream") => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
@ -358,26 +370,26 @@ impl PartialEq<&str> for ContentType {
|
|||
impl Display for ContentType {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
match self {
|
||||
Self::Text { kind: t, .. } => t.fmt(f),
|
||||
Self::Multipart { kind: k, .. } => k.fmt(f),
|
||||
Self::Other { ref tag, .. } => write!(f, "{}", String::from_utf8_lossy(tag)),
|
||||
Self::PGPSignature => write!(f, "application/pgp-signature"),
|
||||
Self::CMSSignature => write!(f, "application/pkcs7-signature"),
|
||||
Self::MessageRfc822 => write!(f, "message/rfc822"),
|
||||
Self::OctetStream { .. } => write!(f, "application/octet-stream"),
|
||||
ContentType::Text { kind: t, .. } => t.fmt(f),
|
||||
ContentType::Multipart { kind: k, .. } => k.fmt(f),
|
||||
ContentType::Other { ref tag, .. } => write!(f, "{}", String::from_utf8_lossy(tag)),
|
||||
ContentType::PGPSignature => write!(f, "application/pgp-signature"),
|
||||
ContentType::CMSSignature => write!(f, "application/pkcs7-signature"),
|
||||
ContentType::MessageRfc822 => write!(f, "message/rfc822"),
|
||||
ContentType::OctetStream { .. } => write!(f, "application/octet-stream"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContentType {
|
||||
pub fn is_text(&self) -> bool {
|
||||
matches!(self, Self::Text { .. })
|
||||
matches!(self, ContentType::Text { .. })
|
||||
}
|
||||
|
||||
pub fn is_text_html(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Text {
|
||||
ContentType::Text {
|
||||
kind: Text::Html,
|
||||
..
|
||||
}
|
||||
|
@ -423,8 +435,8 @@ impl ContentType {
|
|||
|
||||
pub fn name(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Other { ref name, .. } => name.as_ref().map(|n| n.as_ref()),
|
||||
Self::OctetStream {
|
||||
ContentType::Other { ref name, .. } => name.as_ref().map(|n| n.as_ref()),
|
||||
ContentType::OctetStream {
|
||||
ref name,
|
||||
parameters: _,
|
||||
} => name.as_ref().map(|n| n.as_ref()),
|
||||
|
@ -433,7 +445,7 @@ impl ContentType {
|
|||
}
|
||||
|
||||
pub fn parts(&self) -> Option<&[Attachment]> {
|
||||
if let Self::Multipart { ref parts, .. } = self {
|
||||
if let ContentType::Multipart { ref parts, .. } = self {
|
||||
Some(parts)
|
||||
} else {
|
||||
None
|
||||
|
@ -451,58 +463,61 @@ pub enum Text {
|
|||
|
||||
impl Text {
|
||||
pub fn is_html(&self) -> bool {
|
||||
matches!(self, Self::Html)
|
||||
matches!(self, Text::Html)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Text {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
match *self {
|
||||
Self::Plain => write!(f, "text/plain"),
|
||||
Self::Html => write!(f, "text/html"),
|
||||
Self::Rfc822 => write!(f, "text/rfc822"),
|
||||
Self::Other { tag: ref t } => write!(f, "text/{}", String::from_utf8_lossy(t)),
|
||||
Text::Plain => write!(f, "text/plain"),
|
||||
Text::Html => write!(f, "text/html"),
|
||||
Text::Rfc822 => write!(f, "text/rfc822"),
|
||||
Text::Other { tag: ref t } => write!(f, "text/{}", String::from_utf8_lossy(t)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum ContentTransferEncoding {
|
||||
#[default]
|
||||
_8Bit,
|
||||
_7Bit,
|
||||
Base64,
|
||||
QuotedPrintable,
|
||||
Other {
|
||||
tag: Vec<u8>,
|
||||
},
|
||||
Other { tag: Vec<u8> },
|
||||
}
|
||||
|
||||
impl Default for ContentTransferEncoding {
|
||||
fn default() -> Self {
|
||||
ContentTransferEncoding::_8Bit
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ContentTransferEncoding {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
match *self {
|
||||
Self::_7Bit => write!(f, "7bit"),
|
||||
Self::_8Bit => write!(f, "8bit"),
|
||||
Self::Base64 => write!(f, "base64"),
|
||||
Self::QuotedPrintable => write!(f, "quoted-printable"),
|
||||
Self::Other { tag: ref t } => {
|
||||
ContentTransferEncoding::_7Bit => write!(f, "7bit"),
|
||||
ContentTransferEncoding::_8Bit => write!(f, "8bit"),
|
||||
ContentTransferEncoding::Base64 => write!(f, "base64"),
|
||||
ContentTransferEncoding::QuotedPrintable => write!(f, "quoted-printable"),
|
||||
ContentTransferEncoding::Other { tag: ref t } => {
|
||||
panic!("unknown encoding {:?}", str::from_utf8(t))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<&[u8]> for ContentTransferEncoding {
|
||||
fn from(val: &[u8]) -> Self {
|
||||
fn from(val: &[u8]) -> ContentTransferEncoding {
|
||||
if val.eq_ignore_ascii_case(b"base64") {
|
||||
Self::Base64
|
||||
ContentTransferEncoding::Base64
|
||||
} else if val.eq_ignore_ascii_case(b"7bit") {
|
||||
Self::_7Bit
|
||||
ContentTransferEncoding::_7Bit
|
||||
} else if val.eq_ignore_ascii_case(b"8bit") {
|
||||
Self::_8Bit
|
||||
ContentTransferEncoding::_8Bit
|
||||
} else if val.eq_ignore_ascii_case(b"quoted-printable") {
|
||||
Self::QuotedPrintable
|
||||
ContentTransferEncoding::QuotedPrintable
|
||||
} else {
|
||||
Self::Other {
|
||||
ContentTransferEncoding::Other {
|
||||
tag: val.to_ascii_lowercase(),
|
||||
}
|
||||
}
|
||||
|
@ -520,33 +535,38 @@ pub struct ContentDisposition {
|
|||
pub parameter: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum ContentDispositionKind {
|
||||
#[default]
|
||||
Inline,
|
||||
Attachment,
|
||||
}
|
||||
|
||||
impl ContentDispositionKind {
|
||||
pub fn is_inline(&self) -> bool {
|
||||
matches!(self, Self::Inline)
|
||||
matches!(self, ContentDispositionKind::Inline)
|
||||
}
|
||||
|
||||
pub fn is_attachment(&self) -> bool {
|
||||
matches!(self, Self::Attachment)
|
||||
matches!(self, ContentDispositionKind::Attachment)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ContentDispositionKind {
|
||||
fn default() -> Self {
|
||||
ContentDispositionKind::Inline
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ContentDispositionKind {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
match *self {
|
||||
Self::Inline => write!(f, "inline"),
|
||||
Self::Attachment => write!(f, "attachment"),
|
||||
ContentDispositionKind::Inline => write!(f, "inline"),
|
||||
ContentDispositionKind::Attachment => write!(f, "attachment"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<&[u8]> for ContentDisposition {
|
||||
fn from(val: &[u8]) -> Self {
|
||||
fn from(val: &[u8]) -> ContentDisposition {
|
||||
crate::email::parser::attachments::content_disposition(val)
|
||||
.map(|(_, v)| v)
|
||||
.unwrap_or_default()
|
||||
|
@ -554,10 +574,10 @@ impl From<&[u8]> for ContentDisposition {
|
|||
}
|
||||
|
||||
impl From<ContentDispositionKind> for ContentDisposition {
|
||||
fn from(kind: ContentDispositionKind) -> Self {
|
||||
Self {
|
||||
fn from(kind: ContentDispositionKind) -> ContentDisposition {
|
||||
ContentDisposition {
|
||||
kind,
|
||||
..Self::default()
|
||||
..ContentDisposition::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,34 +19,24 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Attachment encoding and decoding.
|
||||
|
||||
/*! Encoding/decoding of attachments */
|
||||
use core::{fmt, str};
|
||||
|
||||
use data_encoding::BASE64_MIME;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
email::{
|
||||
address::StrBuilder,
|
||||
attachment_types::*,
|
||||
parser::{self, BytesExt},
|
||||
Mail,
|
||||
},
|
||||
BytesDisplay,
|
||||
use crate::email::{
|
||||
address::StrBuilder,
|
||||
attachment_types::*,
|
||||
parser::{self, BytesExt},
|
||||
Mail,
|
||||
};
|
||||
|
||||
/// Type alias for function that takes an [`Attachment`] and appends a bytes
|
||||
/// representation in its second argument.
|
||||
pub type Filter<'a> = Box<dyn FnMut(&Attachment, &mut Vec<u8>) + 'a>;
|
||||
|
||||
#[derive(Default)]
|
||||
/// Options for decoding an [`Attachment`].
|
||||
pub struct DecodeOptions<'att> {
|
||||
/// [`Filter`] to use.
|
||||
pub filter: Option<Filter<'att>>,
|
||||
/// Override the attachment's [`Charset`], if any, with a user-provided
|
||||
/// value.
|
||||
pub force_charset: Option<Charset>,
|
||||
}
|
||||
|
||||
|
@ -60,9 +50,6 @@ impl<'att> From<Option<Charset>> for DecodeOptions<'att> {
|
|||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
/// A struct analogous to [`Attachment`] which can have incomplete and partial
|
||||
/// content before being turned into an [`Attachment`] with
|
||||
/// [`AttachmentBuilder::build`].
|
||||
pub struct AttachmentBuilder {
|
||||
pub content_type: ContentType,
|
||||
pub content_transfer_encoding: ContentTransferEncoding,
|
||||
|
@ -76,13 +63,13 @@ impl AttachmentBuilder {
|
|||
pub fn new(content: &[u8]) -> Self {
|
||||
let (headers, body) = match parser::attachments::attachment(content) {
|
||||
Ok((_, v)) => v,
|
||||
Err(err) => {
|
||||
log::debug!("error in parsing attachment: {}", err);
|
||||
log::debug!("\n-------------------------------");
|
||||
log::debug!("{}\n", String::from_utf8_lossy(content));
|
||||
log::debug!("-------------------------------\n");
|
||||
Err(_) => {
|
||||
debug!("error in parsing attachment");
|
||||
debug!("\n-------------------------------");
|
||||
debug!("{}\n", ::std::string::String::from_utf8_lossy(content));
|
||||
debug!("-------------------------------\n");
|
||||
|
||||
return Self {
|
||||
return AttachmentBuilder {
|
||||
content_type: Default::default(),
|
||||
content_transfer_encoding: ContentTransferEncoding::_7Bit,
|
||||
content_disposition: ContentDisposition::default(),
|
||||
|
@ -100,7 +87,7 @@ impl AttachmentBuilder {
|
|||
offset: content.len() - body.len(),
|
||||
length: body.len(),
|
||||
};
|
||||
let mut builder = Self {
|
||||
let mut builder = AttachmentBuilder {
|
||||
raw,
|
||||
body,
|
||||
..Default::default()
|
||||
|
@ -270,11 +257,11 @@ impl AttachmentBuilder {
|
|||
};
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::debug!(
|
||||
"parsing error in content_type: {:?} {}",
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"parsing error in content_type: {:?} {:?}",
|
||||
String::from_utf8_lossy(value),
|
||||
err
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -300,14 +287,14 @@ impl AttachmentBuilder {
|
|||
Ok((_, attachments)) => {
|
||||
let mut vec = Vec::with_capacity(attachments.len());
|
||||
for a in attachments {
|
||||
let mut builder = Self::default();
|
||||
let mut builder = AttachmentBuilder::default();
|
||||
let (headers, body) = match parser::attachments::attachment(a) {
|
||||
Ok((_, v)) => v,
|
||||
Err(err) => {
|
||||
log::debug!("error in parsing attachment: {}", err);
|
||||
log::debug!("\n-------------------------------");
|
||||
log::debug!("{}\n", String::from_utf8_lossy(a));
|
||||
log::debug!("-------------------------------\n");
|
||||
Err(_) => {
|
||||
debug!("error in parsing attachment");
|
||||
debug!("\n-------------------------------");
|
||||
debug!("{}\n", ::std::string::String::from_utf8_lossy(a));
|
||||
debug!("-------------------------------\n");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
@ -334,7 +321,7 @@ impl AttachmentBuilder {
|
|||
vec
|
||||
}
|
||||
a => {
|
||||
log::debug!(
|
||||
debug!(
|
||||
"error {:?}\n\traw: {:?}\n\tboundary: {:?}",
|
||||
a,
|
||||
str::from_utf8(raw).unwrap(),
|
||||
|
@ -355,7 +342,7 @@ impl From<Attachment> for AttachmentBuilder {
|
|||
raw,
|
||||
body,
|
||||
} = val;
|
||||
Self {
|
||||
AttachmentBuilder {
|
||||
content_type,
|
||||
content_disposition,
|
||||
content_transfer_encoding,
|
||||
|
@ -374,7 +361,7 @@ impl From<AttachmentBuilder> for Attachment {
|
|||
raw,
|
||||
body,
|
||||
} = val;
|
||||
Self {
|
||||
Attachment {
|
||||
content_type,
|
||||
content_transfer_encoding,
|
||||
content_disposition,
|
||||
|
@ -400,9 +387,9 @@ impl fmt::Debug for Attachment {
|
|||
let mut text = Vec::with_capacity(4096);
|
||||
self.get_text_recursive(&mut text);
|
||||
f.debug_struct("Attachment")
|
||||
.field("Content-Type", &self.content_type)
|
||||
.field("Content-Transfer-Encoding", &self.content_transfer_encoding)
|
||||
.field("bytes", &self.raw.len())
|
||||
.field("content_type", &self.content_type)
|
||||
.field("content_transfer_encoding", &self.content_transfer_encoding)
|
||||
.field("raw bytes length", &self.raw.len())
|
||||
.field("body", &String::from_utf8_lossy(&text))
|
||||
.finish()
|
||||
}
|
||||
|
@ -415,17 +402,17 @@ impl fmt::Display for Attachment {
|
|||
match Mail::new(self.body.display_bytes(&self.raw).to_vec(), None) {
|
||||
Ok(wrapper) => write!(
|
||||
f,
|
||||
"{} {} {} [message/rfc822] {}",
|
||||
wrapper.subject(),
|
||||
"{} - {} - {} [message/rfc822] {}",
|
||||
wrapper.date(),
|
||||
wrapper.field_from_to_string(),
|
||||
wrapper.date_as_str(),
|
||||
BytesDisplay(self.raw.len()),
|
||||
wrapper.subject(),
|
||||
crate::Bytes(self.raw.len()),
|
||||
),
|
||||
Err(err) => write!(
|
||||
f,
|
||||
"could not parse: {} [message/rfc822] {}",
|
||||
err,
|
||||
BytesDisplay(self.raw.len()),
|
||||
crate::Bytes(self.raw.len()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@ -438,14 +425,14 @@ impl fmt::Display for Attachment {
|
|||
"\"{}\", [{}] {}",
|
||||
name,
|
||||
self.mime_type(),
|
||||
BytesDisplay(self.raw.len())
|
||||
crate::Bytes(self.raw.len())
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"Data attachment [{}] {}",
|
||||
self.mime_type(),
|
||||
BytesDisplay(self.raw.len())
|
||||
crate::Bytes(self.raw.len())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -456,14 +443,14 @@ impl fmt::Display for Attachment {
|
|||
"\"{}\", [{}] {}",
|
||||
name,
|
||||
self.mime_type(),
|
||||
BytesDisplay(self.raw.len())
|
||||
crate::Bytes(self.raw.len())
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"Text attachment [{}] {}",
|
||||
self.mime_type(),
|
||||
BytesDisplay(self.raw.len())
|
||||
crate::Bytes(self.raw.len())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -486,7 +473,7 @@ impl Attachment {
|
|||
content_transfer_encoding: ContentTransferEncoding,
|
||||
raw: Vec<u8>,
|
||||
) -> Self {
|
||||
Self {
|
||||
Attachment {
|
||||
content_type,
|
||||
content_disposition: ContentDisposition::default(),
|
||||
content_transfer_encoding,
|
||||
|
@ -516,11 +503,11 @@ impl Attachment {
|
|||
match parser::attachments::multipart_parts(self.body(), boundary) {
|
||||
Ok((_, v)) => v,
|
||||
Err(e) => {
|
||||
log::debug!("error in parsing attachment");
|
||||
log::debug!("\n-------------------------------");
|
||||
log::debug!("{}\n", String::from_utf8_lossy(&self.raw));
|
||||
log::debug!("-------------------------------\n");
|
||||
log::debug!("{:?}\n", e);
|
||||
debug!("error in parsing attachment");
|
||||
debug!("\n-------------------------------");
|
||||
debug!("{}\n", ::std::string::String::from_utf8_lossy(&self.raw));
|
||||
debug!("-------------------------------\n");
|
||||
debug!("{:?}\n", e);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
@ -575,18 +562,18 @@ impl Attachment {
|
|||
None
|
||||
})
|
||||
{
|
||||
if Self::check_if_has_attachments_quick(body, boundary) {
|
||||
if Attachment::check_if_has_attachments_quick(body, boundary) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("error in parsing multipart_parts");
|
||||
log::debug!("\n-------------------------------");
|
||||
log::debug!("{}\n", String::from_utf8_lossy(bytes));
|
||||
log::debug!("-------------------------------\n");
|
||||
log::debug!("{:?}\n", e);
|
||||
debug!("error in parsing multipart_parts");
|
||||
debug!("\n-------------------------------");
|
||||
debug!("{}\n", ::std::string::String::from_utf8_lossy(bytes));
|
||||
debug!("-------------------------------\n");
|
||||
debug!("{:?}\n", e);
|
||||
}
|
||||
}
|
||||
false
|
||||
|
@ -663,7 +650,7 @@ impl Attachment {
|
|||
self.content_type.to_string()
|
||||
}
|
||||
|
||||
pub fn attachments(&self) -> Vec<Self> {
|
||||
pub fn attachments(&self) -> Vec<Attachment> {
|
||||
let mut ret = Vec::new();
|
||||
fn count_recursive(att: &Attachment, ret: &mut Vec<Attachment>) {
|
||||
match att.content_type {
|
||||
|
@ -713,12 +700,12 @@ impl Attachment {
|
|||
kind: MultipartType::Alternative,
|
||||
ref parts,
|
||||
..
|
||||
} => parts.iter().all(Self::is_html),
|
||||
} => parts.iter().all(Attachment::is_html),
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Related,
|
||||
..
|
||||
} => false,
|
||||
ContentType::Multipart { ref parts, .. } => parts.iter().any(Self::is_html),
|
||||
ContentType::Multipart { ref parts, .. } => parts.iter().any(Attachment::is_html),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
@ -893,7 +880,7 @@ impl Attachment {
|
|||
.map(|n| n.replace(|c| std::path::is_separator(c) || c.is_ascii_control(), "_"))
|
||||
}
|
||||
|
||||
fn decode_rec_helper(&self, options: &mut DecodeOptions<'_>) -> Vec<u8> {
|
||||
fn decode_rec_helper<'a, 'b>(&'a self, options: &mut DecodeOptions<'b>) -> Vec<u8> {
|
||||
match self.content_type {
|
||||
ContentType::Other { .. } => Vec::new(),
|
||||
ContentType::Text { .. } => self.decode_helper(options),
|
||||
|
@ -962,11 +949,11 @@ impl Attachment {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn decode_rec(&self, mut options: DecodeOptions<'_>) -> Vec<u8> {
|
||||
pub fn decode_rec<'a, 'b>(&'a self, mut options: DecodeOptions<'b>) -> Vec<u8> {
|
||||
self.decode_rec_helper(&mut options)
|
||||
}
|
||||
|
||||
fn decode_helper(&self, options: &mut DecodeOptions<'_>) -> Vec<u8> {
|
||||
fn decode_helper<'a, 'b>(&'a self, options: &mut DecodeOptions<'b>) -> Vec<u8> {
|
||||
let charset = options
|
||||
.force_charset
|
||||
.unwrap_or_else(|| match self.content_type {
|
||||
|
@ -1006,7 +993,7 @@ impl Attachment {
|
|||
ret
|
||||
}
|
||||
|
||||
pub fn decode(&self, mut options: DecodeOptions<'_>) -> Vec<u8> {
|
||||
pub fn decode<'a, 'b>(&'a self, mut options: DecodeOptions<'b>) -> Vec<u8> {
|
||||
self.decode_helper(&mut options)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,9 +19,8 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Compose a [``Draft`], with `MIME` and attachment support.
|
||||
/*! Compose a `Draft`, with MIME and attachment support */
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
ffi::OsStr,
|
||||
io::Read,
|
||||
path::{Path, PathBuf},
|
||||
|
@ -37,12 +36,14 @@ use crate::{
|
|||
attachment_types::{Charset, ContentTransferEncoding, ContentType, MultipartType},
|
||||
attachments::AttachmentBuilder,
|
||||
},
|
||||
utils::{datetime, shellexpand::ShellExpandTrait},
|
||||
shellexpand::ShellExpandTrait,
|
||||
};
|
||||
|
||||
pub mod mime;
|
||||
pub mod random;
|
||||
|
||||
//use self::mime::*;
|
||||
|
||||
use super::parser;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
|
@ -58,20 +59,20 @@ impl Default for Draft {
|
|||
fn default() -> Self {
|
||||
let mut headers = HeaderMap::default();
|
||||
headers.insert(
|
||||
HeaderName::DATE,
|
||||
datetime::timestamp_to_string(
|
||||
datetime::now(),
|
||||
Some(datetime::formats::RFC822_DATE),
|
||||
HeaderName::new_unchecked("Date"),
|
||||
crate::datetime::timestamp_to_string(
|
||||
crate::datetime::now(),
|
||||
Some(crate::datetime::RFC822_DATE),
|
||||
true,
|
||||
),
|
||||
);
|
||||
headers.insert(HeaderName::FROM, "".into());
|
||||
headers.insert(HeaderName::TO, "".into());
|
||||
headers.insert(HeaderName::CC, "".into());
|
||||
headers.insert(HeaderName::BCC, "".into());
|
||||
headers.insert(HeaderName::SUBJECT, "".into());
|
||||
headers.insert(HeaderName::new_unchecked("From"), "".into());
|
||||
headers.insert(HeaderName::new_unchecked("To"), "".into());
|
||||
headers.insert(HeaderName::new_unchecked("Cc"), "".into());
|
||||
headers.insert(HeaderName::new_unchecked("Bcc"), "".into());
|
||||
headers.insert(HeaderName::new_unchecked("Subject"), "".into());
|
||||
|
||||
Self {
|
||||
Draft {
|
||||
headers,
|
||||
body: String::new(),
|
||||
wrap_header_preamble: None,
|
||||
|
@ -89,7 +90,7 @@ impl FromStr for Draft {
|
|||
}
|
||||
|
||||
let (headers, _) = parser::mail(s.as_bytes())?;
|
||||
let mut ret = Self::default();
|
||||
let mut ret = Draft::default();
|
||||
|
||||
for (k, v) in headers {
|
||||
ret.headers
|
||||
|
@ -105,7 +106,7 @@ impl FromStr for Draft {
|
|||
|
||||
impl Draft {
|
||||
pub fn edit(envelope: &Envelope, bytes: &[u8]) -> Result<Self> {
|
||||
let mut ret = Self::default();
|
||||
let mut ret = Draft::default();
|
||||
for (k, v) in envelope.headers(bytes).unwrap_or_else(|_| Vec::new()) {
|
||||
ret.headers.insert(k.try_into()?, v.into());
|
||||
}
|
||||
|
@ -117,20 +118,12 @@ impl Draft {
|
|||
Ok(ret)
|
||||
}
|
||||
|
||||
pub fn set_header(&mut self, header: HeaderName, value: String) -> &mut Self {
|
||||
self.headers.insert(header, value);
|
||||
pub fn set_header(&mut self, header: &str, value: String) -> &mut Self {
|
||||
self.headers
|
||||
.insert(HeaderName::new_unchecked(header), value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn try_set_header(
|
||||
&mut self,
|
||||
header: &str,
|
||||
value: String,
|
||||
) -> std::result::Result<&mut Self, InvalidHeaderName> {
|
||||
self.headers.insert(HeaderName::try_from(header)?, value);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn set_wrap_header_preamble(&mut self, value: Option<(String, String)>) -> &mut Self {
|
||||
self.wrap_header_preamble = value;
|
||||
self
|
||||
|
@ -157,7 +150,7 @@ impl Draft {
|
|||
.into();
|
||||
}
|
||||
}
|
||||
let new = Self::from_str(value.as_ref())?;
|
||||
let new = Draft::from_str(value.as_ref())?;
|
||||
let changes: bool = self.headers != new.headers || self.body != new.body;
|
||||
self.headers = new.headers;
|
||||
self.body = new.body;
|
||||
|
@ -165,9 +158,9 @@ impl Draft {
|
|||
}
|
||||
|
||||
pub fn new_reply(envelope: &Envelope, bytes: &[u8], reply_to_all: bool) -> Self {
|
||||
let mut ret = Self::default();
|
||||
let mut ret = Draft::default();
|
||||
ret.headers_mut().insert(
|
||||
HeaderName::REFERENCES,
|
||||
HeaderName::new_unchecked("References"),
|
||||
format!(
|
||||
"{} {}",
|
||||
envelope
|
||||
|
@ -184,7 +177,7 @@ impl Draft {
|
|||
),
|
||||
);
|
||||
ret.headers_mut().insert(
|
||||
HeaderName::IN_REPLY_TO,
|
||||
HeaderName::new_unchecked("In-Reply-To"),
|
||||
envelope.message_id_display().into(),
|
||||
);
|
||||
// "Mail-Followup-To/(To+Cc+(Mail-Reply-To/Reply-To/From)) for follow-up,
|
||||
|
@ -193,27 +186,33 @@ impl Draft {
|
|||
if reply_to_all {
|
||||
if let Some(reply_to) = envelope.other_headers().get("Mail-Followup-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::TO, reply_to.to_string());
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
} else if let Some(reply_to) = envelope.other_headers().get("Reply-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::TO, reply_to.to_string());
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
} else {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::TO, envelope.field_from_to_string());
|
||||
ret.headers_mut().insert(
|
||||
HeaderName::new_unchecked("To"),
|
||||
envelope.field_from_to_string(),
|
||||
);
|
||||
}
|
||||
// FIXME: add To/Cc
|
||||
} else if let Some(reply_to) = envelope.other_headers().get("Mail-Reply-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::TO, reply_to.to_string());
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
} else if let Some(reply_to) = envelope.other_headers().get("Reply-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::TO, reply_to.to_string());
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
} else {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::TO, envelope.field_from_to_string());
|
||||
ret.headers_mut().insert(
|
||||
HeaderName::new_unchecked("To"),
|
||||
envelope.field_from_to_string(),
|
||||
);
|
||||
}
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::CC, envelope.field_cc_to_string());
|
||||
ret.headers_mut().insert(
|
||||
HeaderName::new_unchecked("Cc"),
|
||||
envelope.field_cc_to_string(),
|
||||
);
|
||||
let body = envelope.body_bytes(bytes);
|
||||
ret.body = {
|
||||
let reply_body_bytes = body.decode_rec(Default::default());
|
||||
|
@ -222,7 +221,7 @@ impl Draft {
|
|||
let mut ret = format!(
|
||||
"On {} {} wrote:\n",
|
||||
envelope.date_as_str(),
|
||||
&ret.headers()[HeaderName::TO]
|
||||
&ret.headers()["To"]
|
||||
);
|
||||
for l in lines {
|
||||
ret.push('>');
|
||||
|
@ -305,8 +304,10 @@ impl Draft {
|
|||
if let Ok((_, addr)) = super::parser::address::mailbox(self.headers["From"].as_bytes())
|
||||
{
|
||||
if let Some(fqdn) = addr.get_fqdn() {
|
||||
self.headers
|
||||
.insert(HeaderName::MESSAGE_ID, random::gen_message_id(&fqdn));
|
||||
self.headers.insert(
|
||||
HeaderName::new_unchecked("Message-ID"),
|
||||
random::gen_message_id(&fqdn),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -525,7 +526,11 @@ where
|
|||
.set_body_to_raw()
|
||||
.set_content_type(ContentType::Other {
|
||||
name: path.file_name().map(|s| s.to_string_lossy().into()),
|
||||
tag: query_mime_info(&path).unwrap_or_else(|_| b"application/octet-stream".to_vec()),
|
||||
tag: if let Ok(mime_type) = query_mime_info(&path) {
|
||||
mime_type
|
||||
} else {
|
||||
b"application/octet-stream".to_vec()
|
||||
},
|
||||
parameters: vec![],
|
||||
});
|
||||
|
||||
|
@ -554,8 +559,8 @@ mod tests {
|
|||
default
|
||||
.set_wrap_header_preamble(Some(("<!--".to_string(), "-->".to_string())))
|
||||
.set_body("αδφαφσαφασ".to_string())
|
||||
.set_header(HeaderName::SUBJECT, "test_update()".into())
|
||||
.set_header(HeaderName::DATE, "Sun, 16 Jun 2013 17:56:45 +0200".into());
|
||||
.set_header("Subject", "test_update()".into())
|
||||
.set_header("Date", "Sun, 16 Jun 2013 17:56:45 +0200".into());
|
||||
|
||||
let original = default.clone();
|
||||
let s = default.to_edit_string();
|
||||
|
|
|
@ -19,7 +19,31 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::utils::random::{clock, random_u64};
|
||||
use std::{char, fs::File, io::prelude::*, time::SystemTime};
|
||||
|
||||
fn random_u64() -> u64 {
|
||||
let mut f = File::open("/dev/urandom").unwrap();
|
||||
let mut buffer = [0; 8];
|
||||
|
||||
// read exactly 10 bytes
|
||||
f.read_exact(&mut buffer).unwrap();
|
||||
|
||||
u64::from(buffer[0])
|
||||
| (u64::from(buffer[1]) << 8)
|
||||
| (u64::from(buffer[2]) << 16)
|
||||
| (u64::from(buffer[3]) << 24)
|
||||
| (u64::from(buffer[4]) << 32)
|
||||
| (u64::from(buffer[5]) << 40)
|
||||
| (u64::from(buffer[6]) << 48)
|
||||
| (u64::from(buffer[7]) << 56)
|
||||
}
|
||||
|
||||
fn clock() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
fn base36(mut m: u64) -> String {
|
||||
let mut stack = Vec::with_capacity(32);
|
||||
|
|
|
@ -19,19 +19,103 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Wrapper type [`HeaderName`] for case-insensitive comparisons.
|
||||
|
||||
pub mod names;
|
||||
/*! Wrapper type `HeaderName` for case-insensitive comparisons */
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
cmp::{Eq, PartialEq},
|
||||
convert::{TryFrom, TryInto},
|
||||
convert::TryFrom,
|
||||
fmt,
|
||||
hash::{Hash, Hasher},
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
use indexmap::IndexMap;
|
||||
pub use names::{HeaderName, InvalidHeaderName, Protocol, Standard, StandardHeader, Status};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct HeaderNameType<S>(S);
|
||||
|
||||
/// Case insensitive wrapper for a header name. As of `RFC5322` it's
|
||||
/// guaranteed to be ASCII.
|
||||
pub type HeaderName = HeaderNameType<SmallVec<[u8; 32]>>;
|
||||
|
||||
impl HeaderName {
|
||||
pub fn new_unchecked(from: &str) -> Self {
|
||||
HeaderNameType(from.as_bytes().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<[u8]>> fmt::Display for HeaderNameType<S> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.normalize())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<[u8]>> fmt::Debug for HeaderNameType<S> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<[u8]>> PartialEq<[u8]> for HeaderNameType<S> {
|
||||
fn eq(&self, other: &[u8]) -> bool {
|
||||
self.0.as_ref().eq_ignore_ascii_case(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<[u8]>> PartialEq<&str> for HeaderNameType<S> {
|
||||
fn eq(&self, other: &&str) -> bool {
|
||||
self.0.as_ref().eq_ignore_ascii_case(other.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S1: AsRef<[u8]>, S2: AsRef<[u8]>> PartialEq<HeaderNameType<S2>> for HeaderNameType<S1> {
|
||||
fn eq(&self, other: &HeaderNameType<S2>) -> bool {
|
||||
self.0.as_ref().eq_ignore_ascii_case(other.0.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<[u8]>> Eq for HeaderNameType<S> {}
|
||||
|
||||
impl<S: AsRef<[u8]>> Hash for HeaderNameType<S> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
for b in self.0.as_ref().iter() {
|
||||
b.to_ascii_lowercase().hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for HeaderName {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||
if value.is_ascii() {
|
||||
Ok(HeaderNameType(value.into()))
|
||||
} else {
|
||||
Err(Error::new(format!(
|
||||
"Header value is not ascii: {:?}",
|
||||
value
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for HeaderName {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
if value.is_ascii() {
|
||||
Ok(HeaderNameType(value.as_bytes().into()))
|
||||
} else {
|
||||
Err(Error::new(format!(
|
||||
"Header value is not ascii: {:?}",
|
||||
value
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait HeaderKey {
|
||||
fn to_key(&self) -> &[u8];
|
||||
|
@ -53,9 +137,9 @@ impl PartialEq for dyn HeaderKey + '_ {
|
|||
|
||||
impl Eq for dyn HeaderKey + '_ {}
|
||||
|
||||
impl HeaderKey for HeaderName {
|
||||
impl<S: AsRef<[u8]>> HeaderKey for HeaderNameType<S> {
|
||||
fn to_key(&self) -> &[u8] {
|
||||
self.as_bytes()
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,100 +151,119 @@ impl<'a> Borrow<dyn HeaderKey + 'a> for HeaderName {
|
|||
}
|
||||
}
|
||||
|
||||
/// Map of mail headers and values.
|
||||
///
|
||||
/// Can be indexed by:
|
||||
///
|
||||
/// - `usize`
|
||||
/// - `&[u8]`, which panics if it's not a valid header value.
|
||||
/// - `&str`, which also panics if it's not a valid header value.
|
||||
/// - [HeaderName], which is guaranteed to be valid.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Except for the above, indexing will also panic if index is out of range or
|
||||
/// header key is not present in the map.
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
pub struct HeaderMap(indexmap::IndexMap<HeaderName, String>);
|
||||
impl<S: AsRef<[u8]>> HeaderNameType<S> {
|
||||
pub fn as_str(&self) -> &str {
|
||||
// HeadersType are ascii so valid utf8
|
||||
unsafe { std::str::from_utf8_unchecked(self.0.as_ref()) }
|
||||
}
|
||||
|
||||
impl std::ops::Index<usize> for HeaderMap {
|
||||
type Output = str;
|
||||
fn index(&self, k: usize) -> &Self::Output {
|
||||
(self.0)[k].as_str()
|
||||
pub fn normalize(&self) -> &str {
|
||||
if self == &b"subject"[..] {
|
||||
"Subject"
|
||||
} else if self == &b"from"[..] {
|
||||
"From"
|
||||
} else if self == &b"to"[..] {
|
||||
"To"
|
||||
} else if self == &b"cc"[..] {
|
||||
"Cc"
|
||||
} else if self == &b"bcc"[..] {
|
||||
"Bcc"
|
||||
} else if self == &b"reply-to"[..] {
|
||||
"Reply-To"
|
||||
} else if self == &b"in-reply-to"[..] {
|
||||
"In-Reply-To"
|
||||
} else if self == &b"references"[..] {
|
||||
"References"
|
||||
} else if self == &b"sender"[..] {
|
||||
"Sender"
|
||||
} else if self == &b"mail-reply-to"[..] {
|
||||
"Mail-Reply-To"
|
||||
} else if self == &b"mail-followup-to"[..] {
|
||||
"Mail-Followup-To"
|
||||
} else if self == &b"mime-version"[..] {
|
||||
"MIME-Version"
|
||||
} else if self == &b"content-disposition"[..] {
|
||||
"Content-Disposition"
|
||||
} else if self == &b"content-transfer-encoding"[..] {
|
||||
"Content-Transfer-Encoding"
|
||||
} else if self == &b"content-type"[..] {
|
||||
"Content-Type"
|
||||
} else if self == &b"content-id"[..] {
|
||||
"Content-ID"
|
||||
} else if self == &b"content-description"[..] {
|
||||
"Content-Description"
|
||||
} else if self == &b"authentication-results"[..] {
|
||||
"Authentication-Results"
|
||||
} else if self == &b"dkim-signature"[..] {
|
||||
"DKIM-Signature"
|
||||
} else if self == &b"delivered-to"[..] {
|
||||
"Delivered-To"
|
||||
} else if self == &b"message-id"[..] {
|
||||
"Message-ID"
|
||||
} else if self == &b"comments"[..] {
|
||||
"Comments"
|
||||
} else if self == &b"keywords"[..] {
|
||||
"Keywords"
|
||||
} else if self == &b"resent-from"[..] {
|
||||
"Resent-From"
|
||||
} else if self == &b"resent-sender"[..] {
|
||||
"Resent-Sender"
|
||||
} else if self == &b"resent-to"[..] {
|
||||
"Resent-To"
|
||||
} else if self == &b"resent-cc"[..] {
|
||||
"Resent-Cc"
|
||||
} else if self == &b"resent-bcc"[..] {
|
||||
"Resent-Bcc"
|
||||
} else if self == &b"resent-date"[..] {
|
||||
"Resent-Date"
|
||||
} else if self == &b"resent-message-id"[..] {
|
||||
"Resent-Message-ID"
|
||||
} else if self == &b"resent-reply-to"[..] {
|
||||
"Resent-Reply-To"
|
||||
} else if self == &b"return-path"[..] {
|
||||
"Return-Path"
|
||||
} else if self == &b"received"[..] {
|
||||
"Received"
|
||||
} else {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
pub struct HeaderMap(indexmap::IndexMap<HeaderName, String>);
|
||||
|
||||
impl std::ops::Index<&[u8]> for HeaderMap {
|
||||
type Output = str;
|
||||
fn index(&self, k: &[u8]) -> &Self::Output {
|
||||
(self.0)[&HeaderName::try_from(k).expect("Invalid bytes in header name.")].as_str()
|
||||
(self.0)[HeaderNameType(k).borrow() as &dyn HeaderKey].as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<&str> for HeaderMap {
|
||||
type Output = str;
|
||||
fn index(&self, k: &str) -> &Self::Output {
|
||||
(self.0)[&HeaderName::try_from(k).expect("Invalid bytes in header name.")].as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<&HeaderName> for HeaderMap {
|
||||
type Output = str;
|
||||
fn index(&self, k: &HeaderName) -> &Self::Output {
|
||||
(self.0)[k].as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<HeaderName> for HeaderMap {
|
||||
type Output = str;
|
||||
fn index(&self, k: HeaderName) -> &Self::Output {
|
||||
(self.0)[&k].as_str()
|
||||
(self.0)[HeaderNameType(k).borrow() as &dyn HeaderKey].as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl HeaderMap {
|
||||
pub fn empty() -> Self {
|
||||
Self::default()
|
||||
pub fn get_mut(&mut self, key: &str) -> Option<&mut String> {
|
||||
(self.0).get_mut(HeaderNameType(key).borrow() as &dyn HeaderKey)
|
||||
}
|
||||
|
||||
pub fn get_mut<T: TryInto<HeaderName> + std::fmt::Debug>(
|
||||
&mut self,
|
||||
key: T,
|
||||
) -> Option<&mut String>
|
||||
where
|
||||
<T as TryInto<HeaderName>>::Error: std::fmt::Debug,
|
||||
{
|
||||
let k = key.try_into().ok()?;
|
||||
(self.0).get_mut(&k)
|
||||
pub fn get(&self, key: &str) -> Option<&str> {
|
||||
(self.0)
|
||||
.get(HeaderNameType(key).borrow() as &dyn HeaderKey)
|
||||
.map(|x| x.as_str())
|
||||
}
|
||||
|
||||
pub fn get<T: TryInto<HeaderName> + std::fmt::Debug>(&self, key: T) -> Option<&str>
|
||||
where
|
||||
<T as TryInto<HeaderName>>::Error: std::fmt::Debug,
|
||||
{
|
||||
let k = key.try_into().ok()?;
|
||||
(self.0).get(&k).map(|x| x.as_str())
|
||||
pub fn contains_key(&self, key: &str) -> bool {
|
||||
(self.0).contains_key(HeaderNameType(key).borrow() as &dyn HeaderKey)
|
||||
}
|
||||
|
||||
pub fn contains_key<T: TryInto<HeaderName> + std::fmt::Debug>(&self, key: T) -> bool
|
||||
where
|
||||
<T as TryInto<HeaderName>>::Error: std::fmt::Debug,
|
||||
{
|
||||
key.try_into()
|
||||
.ok()
|
||||
.map(|k| (self.0).contains_key(&k))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn remove<T: TryInto<HeaderName> + std::fmt::Debug>(&mut self, key: T) -> Option<String>
|
||||
where
|
||||
<T as TryInto<HeaderName>>::Error: std::fmt::Debug,
|
||||
{
|
||||
key.try_into().ok().and_then(|k| (self.0).remove(&k))
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> indexmap::IndexMap<HeaderName, String> {
|
||||
self.0
|
||||
pub fn remove(&mut self, key: &str) -> Option<String> {
|
||||
(self.0).remove(HeaderNameType(key).borrow() as &dyn HeaderKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -178,34 +281,16 @@ impl DerefMut for HeaderMap {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_headers_case_sensitivity() {
|
||||
let mut headers = HeaderMap::default();
|
||||
headers.insert("from".try_into().unwrap(), "Myself <a@b.c>".into());
|
||||
assert_eq!(&headers["From"], "Myself <a@b.c>");
|
||||
assert_eq!(&headers["From"], &headers["from"]);
|
||||
assert_eq!(&headers["fROm"], &headers["from"]);
|
||||
headers.get_mut("from").unwrap().pop();
|
||||
assert_eq!(&headers["From"], "Myself <a@b.c");
|
||||
headers.insert("frOM".try_into().unwrap(), "nada".into());
|
||||
assert_eq!(&headers["fROm"], "nada");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_headers_map_index() {
|
||||
let mut headers = HeaderMap::default();
|
||||
headers.insert(HeaderName::SUBJECT, "foobar".into());
|
||||
headers.insert(HeaderName::MESSAGE_ID, "foobar@examplecom".into());
|
||||
assert_eq!(&headers[0], "foobar");
|
||||
assert_eq!(&headers[HeaderName::SUBJECT], "foobar");
|
||||
assert_eq!(&headers[&HeaderName::SUBJECT], "foobar");
|
||||
assert_eq!(&headers["subject"], "foobar");
|
||||
assert_eq!(&headers["Subject"], "foobar");
|
||||
assert_eq!(&headers[b"Subject".as_slice()], "foobar");
|
||||
assert!(&headers[HeaderName::MESSAGE_ID] != "foobar");
|
||||
}
|
||||
#[test]
|
||||
fn test_headers_case_sensitivity() {
|
||||
use std::convert::TryInto;
|
||||
let mut headers = HeaderMap::default();
|
||||
headers.insert("from".try_into().unwrap(), "Myself <a@b.c>".into());
|
||||
assert_eq!(&headers["From"], "Myself <a@b.c>");
|
||||
assert_eq!(&headers["From"], &headers["from"]);
|
||||
assert_eq!(&headers["fROm"], &headers["from"]);
|
||||
headers.get_mut("from").unwrap().pop();
|
||||
assert_eq!(&headers["From"], "Myself <a@b.c");
|
||||
headers.insert("frOM".try_into().unwrap(), "nada".into());
|
||||
assert_eq!(&headers["fROm"], "nada");
|
||||
}
|
||||
|
|
|
@ -1,954 +0,0 @@
|
|||
/*
|
||||
* meli - melib crate.
|
||||
*
|
||||
* Copyright 2023 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/>.
|
||||
*/
|
||||
|
||||
//! E-mail header names. Also referred to as `Fields` in `RFC5322`.
|
||||
#![allow(non_upper_case_globals)]
|
||||
|
||||
use std::{
|
||||
borrow::{Borrow, Cow},
|
||||
cmp::Ordering,
|
||||
convert::TryFrom,
|
||||
error::Error,
|
||||
hash::{Hash, Hasher},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::email::parser::BytesExt;
|
||||
|
||||
bitflags! {
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct Protocol: u32 {
|
||||
const None = 0b00000001;
|
||||
const Mail = Self::None.bits() << 1;
|
||||
const NNTP = Self::Mail.bits() << 1;
|
||||
const MIME = Self::NNTP.bits() << 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Case insensitive wrapper for a header name. As of `RFC5322` it's
|
||||
/// guaranteed to be ASCII.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct HeaderName {
|
||||
inner: Repr<Custom>,
|
||||
}
|
||||
|
||||
impl Custom {
|
||||
fn as_str(&self) -> &str {
|
||||
unsafe { std::str::from_utf8_unchecked(&self.0) }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
enum Repr<T> {
|
||||
Standard(StandardHeader),
|
||||
Custom(T),
|
||||
}
|
||||
|
||||
impl<T: std::fmt::Display> std::fmt::Display for Repr<T> {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Standard(inner) => write!(fmt, "{}", inner.as_str()),
|
||||
Self::Custom(inner) => inner.fmt(fmt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Used to hijack the Hash impl
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
struct Custom(SmallVec<[u8; 32]>);
|
||||
|
||||
/// A possible error when converting a `HeaderName` from another type.
|
||||
pub struct InvalidHeaderName;
|
||||
|
||||
impl Error for InvalidHeaderName {}
|
||||
|
||||
impl std::fmt::Debug for InvalidHeaderName {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(fmt, "Invalid header name.")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for InvalidHeaderName {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(fmt, "{}", stringify!(InvalidHeaderName))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! standard_headers {
|
||||
(
|
||||
$(
|
||||
$(#[$docs:meta])*
|
||||
($konst:ident, $upcase:ident, $name:literal, $template:expr, $(Protocol::$var:tt)|+,$status:expr,$standards:expr);
|
||||
)+
|
||||
) => {
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum StandardHeader {
|
||||
$(
|
||||
$konst,
|
||||
)+
|
||||
}
|
||||
|
||||
$(
|
||||
$(#[$docs])*
|
||||
pub const $upcase: HeaderName = HeaderName {
|
||||
inner: Repr::Standard(StandardHeader::$konst),
|
||||
};
|
||||
)+
|
||||
|
||||
impl HeaderName {
|
||||
$(
|
||||
pub const $upcase: Self = $upcase;
|
||||
)+
|
||||
}
|
||||
|
||||
impl StandardHeader {
|
||||
#[inline]
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match *self {
|
||||
$(
|
||||
Self::$konst => $name,
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn protocol(&self) -> Protocol {
|
||||
match *self {
|
||||
$(
|
||||
Self::$konst => Protocol::from_bits_truncate($(Protocol::$var.bits()|)* u32::MAX),
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn status(&self) -> Status {
|
||||
match *self {
|
||||
$(
|
||||
Self::$konst => $status,
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn standards(&self) -> &[Standard] {
|
||||
match *self {
|
||||
$(
|
||||
Self::$konst => $standards,
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
// invalid clippy lint match here
|
||||
#[allow(clippy::string_lit_as_bytes)]
|
||||
pub fn from_bytes(name_bytes: &[u8]) -> Option<Self> {
|
||||
match name_bytes {
|
||||
$(
|
||||
_ if name_bytes.eq_ignore_ascii_case($name.as_bytes()) => Some(Self::$konst),
|
||||
)+
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
const TEST_HEADERS: &[(StandardHeader, &str)] = &[
|
||||
$(
|
||||
(StandardHeader::$konst, $name),
|
||||
)+
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! standards {
|
||||
(
|
||||
$(
|
||||
$(#[$docs:meta])*
|
||||
($konst:ident, $upcase:ident, $name:literal, $lowername:literal );
|
||||
)+
|
||||
) => {
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Standard {
|
||||
$(
|
||||
$konst,
|
||||
)+
|
||||
}
|
||||
|
||||
$(
|
||||
$(#[$docs])*
|
||||
pub const $upcase: Standard = Standard::$konst;
|
||||
)+
|
||||
|
||||
impl Standard {
|
||||
#[inline]
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match *self {
|
||||
$(
|
||||
Self::$konst => $name,
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn url(&self) -> &str {
|
||||
match *self {
|
||||
$(
|
||||
Self::$konst => concat!("https://www.rfc-editor.org/rfc/", $lowername, ".html"),
|
||||
)+
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// invalid clippy lint match here
|
||||
#[allow(clippy::string_lit_as_bytes)]
|
||||
pub fn from_bytes(name_bytes: &[u8]) -> Option<Self> {
|
||||
match name_bytes {
|
||||
$(
|
||||
_ if name_bytes.eq_ignore_ascii_case($name.as_bytes()) => Some(Self::$konst),
|
||||
)+
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
standards! {
|
||||
(RFC0850, RFC0850, "RFC0850", "rfc0850");
|
||||
(RFC1808, RFC1808, "RFC1808", "rfc1808");
|
||||
(RFC1849, RFC1849, "RFC1849", "rfc1849");
|
||||
(RFC2068, RFC2068, "RFC2068", "rfc2068");
|
||||
(RFC2076, RFC2076, "RFC2076", "rfc2076");
|
||||
(RFC2110, RFC2110, "RFC2110", "rfc2110");
|
||||
(RFC2156, RFC2156, "RFC2156", "rfc2156");
|
||||
(RFC2557, RFC2557, "RFC2557", "rfc2557");
|
||||
(RFC2616, RFC2616, "RFC2616", "rfc2616");
|
||||
(RFC2980, RFC2980, "RFC2980", "rfc2980");
|
||||
(RFC3798, RFC3798, "RFC3798", "rfc3798");
|
||||
(RFC3834, RFC3834, "RFC3834", "rfc3834");
|
||||
(RFC3865, RFC3865, "RFC3865", "rfc3865");
|
||||
(RFC3977, RFC3977, "RFC3977", "rfc3977");
|
||||
(RFC4021, RFC4021, "RFC4021", "rfc4021");
|
||||
(RFC5064, RFC5064, "RFC5064", "rfc5064");
|
||||
(RFC5321, RFC5321, "RFC5321", "rfc5321");
|
||||
(RFC5322, RFC5322, "RFC5322", "rfc5322");
|
||||
(RFC5337, RFC5337, "RFC5337", "rfc5337");
|
||||
(RFC5504, RFC5504, "RFC5504", "rfc5504");
|
||||
(RFC5518, RFC5518, "RFC5518", "rfc5518");
|
||||
(RFC5536, RFC5536, "RFC5536", "rfc5536");
|
||||
(RFC5537, RFC5537, "RFC5537", "rfc5537");
|
||||
(RFC5703, RFC5703, "RFC5703", "rfc5703");
|
||||
(RFC6017, RFC6017, "RFC6017", "rfc6017");
|
||||
(RFC6068, RFC6068, "RFC6068", "rfc6068");
|
||||
(RFC6109, RFC6109, "RFC6109", "rfc6109");
|
||||
(RFC6376, RFC6376, "RFC6376", "rfc6376");
|
||||
(RFC6477, RFC6477, "RFC6477", "rfc6477");
|
||||
(RFC6758, RFC6758, "RFC6758", "rfc6758");
|
||||
(RFC6854, RFC6854, "RFC6854", "rfc6854");
|
||||
(RFC6857, RFC6857, "RFC6857", "rfc6857");
|
||||
(RFC7208, RFC7208, "RFC7208", "rfc7208");
|
||||
(RFC7259, RFC7259, "RFC7259", "rfc7259");
|
||||
(RFC7293, RFC7293, "RFC7293", "rfc7293");
|
||||
(RFC7444, RFC7444, "RFC7444", "rfc7444");
|
||||
(RFC7681, RFC7681, "RFC7681", "rfc7681");
|
||||
(RFC8058, RFC8058, "RFC8058", "rfc8058");
|
||||
(RFC8255, RFC8255, "RFC8255", "rfc8255");
|
||||
(RFC8315, RFC8315, "RFC8315", "rfc8315");
|
||||
(RFC8460, RFC8460, "RFC8460", "rfc8460");
|
||||
(RFC8601, RFC8601, "RFC8601", "rfc8601");
|
||||
(RFC8617, RFC8617, "RFC8617", "rfc8617");
|
||||
(RFC8689, RFC8689, "RFC8689", "rfc8689");
|
||||
(RFC9057, RFC9057, "RFC9057", "rfc9057");
|
||||
(RFC9228, RFC9228, "RFC9228", "rfc9228");
|
||||
}
|
||||
|
||||
/// Status of field at the moment of writing.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Status {
|
||||
/// Deprecated,
|
||||
Deprecated,
|
||||
/// Experimental,
|
||||
Experimental,
|
||||
/// Informational,
|
||||
Informational,
|
||||
/// None,
|
||||
None,
|
||||
/// Obsoleted,
|
||||
Obsoleted,
|
||||
/// Reserved,
|
||||
Reserved,
|
||||
/// Standard,
|
||||
Standard,
|
||||
}
|
||||
|
||||
// Generate constants for all standard e-mail field headers.
|
||||
standard_headers! {
|
||||
/* Unit Variant |Constant ident |Actual field value |Template value |Protocols |Status |Standards */
|
||||
/* -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */
|
||||
(Subject, SUBJECT, "Subject", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5536, Standard::RFC5322]);
|
||||
(ReplyTo, REPLY_TO, "Reply-To", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5536, Standard::RFC5322]);
|
||||
(InReplyTo, IN_REPLY_TO, "In-Reply-To", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(References, REFERENCES, "References", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5536, Standard::RFC5322]);
|
||||
(MailReplyTo, MAIL_REPLY_TO, "Mail-Reply-To", None, Protocol::Mail, Status::None, &[]);
|
||||
(MailFollowupTo, MAIL_FOLLOWUP_TO, "Mail-Followup-To", None, Protocol::Mail, Status::None, &[]);
|
||||
(DeliveredTo, DELIVERED_TO, "Delivered-To", None, Protocol::Mail, Status::None, &[Standard::RFC9228]);
|
||||
(Comments, COMMENTS, "Comments", None, Protocol::Mail, Status::None, &[]);
|
||||
(Keywords, KEYWORDS, "Keywords", None, Protocol::Mail, Status::None, &[]);
|
||||
(Received, RECEIVED, "Received", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322, Standard::RFC5321]);
|
||||
(ContentLanguage, CONTENT_LANGUAGE, "Content-Language", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentLength, CONTENT_LENGTH, "Content-Length", None, Protocol::Mail, Status::None, &[]);
|
||||
(Forwarded, FORWARDED, "Forwarded", None, Protocol::Mail, Status::None, &[]);
|
||||
(AcceptLanguage, ACCEPT_LANGUAGE, "Accept-Language", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(AlsoControl, ALSO_CONTROL, "Also-Control", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC1849, Standard::RFC5536]);
|
||||
(AlternateRecipient, ALTERNATE_RECIPIENT, "Alternate-Recipient", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Approved, APPROVED, "Approved", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(ArcAuthenticationResults, ARC_AUTHENTICATION_RESULTS, "ARC-Authentication-Results", None, Protocol::Mail, Status::Experimental, &[Standard::RFC8617]);
|
||||
(ArcMessageSignature, ARC_MESSAGE_SIGNATURE, "ARC-Message-Signature", None, Protocol::Mail, Status::Experimental, &[Standard::RFC8617]);
|
||||
(ArcSeal, ARC_SEAL, "ARC-Seal", None, Protocol::Mail, Status::Experimental, &[Standard::RFC8617]);
|
||||
(Archive, ARCHIVE, "Archive", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(ArchivedAt, ARCHIVED_AT, "Archived-At", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5064]);
|
||||
(ArticleNames, ARTICLE_NAMES, "Article-Names", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC1849, Standard::RFC5536]);
|
||||
(ArticleUpdates, ARTICLE_UPDATES, "Article-Updates", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC1849, Standard::RFC5536]);
|
||||
(AuthenticationResults, AUTHENTICATION_RESULTS, "Authentication-Results", None, Protocol::Mail, Status::Standard, &[Standard::RFC8601]);
|
||||
(AutoSubmitted, AUTO_SUBMITTED, "Auto-Submitted", None, Protocol::Mail, Status::Standard, &[Standard::RFC3834]);
|
||||
(Autoforwarded, AUTOFORWARDED, "Autoforwarded", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Autosubmitted, AUTOSUBMITTED, "Autosubmitted", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Base, BASE, "Base", None, Protocol::MIME, Status::Obsoleted, &[Standard::RFC1808, Standard::RFC2068]);
|
||||
(Bcc, BCC, "Bcc", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(Body, BODY, "Body", None, Protocol::None, Status::Reserved, &[Standard::RFC6068]);
|
||||
(CancelKey, CANCEL_KEY, "Cancel-Key", None, Protocol::NNTP, Status::Standard, &[Standard::RFC8315]);
|
||||
(CancelLock, CANCEL_LOCK, "Cancel-Lock", None, Protocol::NNTP, Status::Standard, &[Standard::RFC8315]);
|
||||
(Cc, CC, "Cc", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(ContentAlternative, CONTENT_ALTERNATIVE, "Content-Alternative", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentBase, CONTENT_BASE, "Content-Base", None, Protocol::MIME, Status::Obsoleted, &[Standard::RFC2110, Standard::RFC2557]);
|
||||
(ContentDescription, CONTENT_DESCRIPTION, "Content-Description", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentDisposition, CONTENT_DISPOSITION, "Content-Disposition", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentDuration, CONTENT_DURATION, "Content-Duration", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentFeatures, CONTENT_FEATURES, "Content-Features", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentId, CONTENT_ID, "Content-ID", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentIdentifier, CONTENT_IDENTIFIER, "Content-Identifier", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ContentLocation, CONTENT_LOCATION, "Content-Location", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentMd5, CONTENT_MD5, "Content-MD5", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentReturn, CONTENT_RETURN, "Content-Return", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ContentTransferEncoding, CONTENT_TRANSFER_ENCODING, "Content-Transfer-Encoding", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentTranslationType, CONTENT_TRANSLATION_TYPE, "Content-Translation-Type", None, Protocol::MIME, Status::Standard, &[Standard::RFC8255]);
|
||||
(ContentType, CONTENT_TYPE, "Content-Type", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(Control, CONTROL, "Control", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(Conversion, CONVERSION, "Conversion", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ConversionWithLoss, CONVERSION_WITH_LOSS, "Conversion-With-Loss", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(DlExpansionHistory, DL_EXPANSION_HISTORY, "DL-Expansion-History", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Date, DATE, "Date", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5536, Standard::RFC5322]);
|
||||
(DateReceived, DATE_RECEIVED, "Date-Received", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC0850, Standard::RFC5536]);
|
||||
(DeferredDelivery, DEFERRED_DELIVERY, "Deferred-Delivery", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(DeliveryDate, DELIVERY_DATE, "Delivery-Date", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(DiscardedX400IpmsExtensions, DISCARDED_X400_IPMS_EXTENSIONS, "Discarded-X400-IPMS-Extensions", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(DiscardedX400MtsExtensions, DISCARDED_X400_MTS_EXTENSIONS, "Discarded-X400-MTS-Extensions", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(DiscloseRecipients, DISCLOSE_RECIPIENTS, "Disclose-Recipients", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(DispositionNotificationOptions, DISPOSITION_NOTIFICATION_OPTIONS, "Disposition-Notification-Options", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(DispositionNotificationTo, DISPOSITION_NOTIFICATION_TO, "Disposition-Notification-To", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Distribution, DISTRIBUTION, "Distribution", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(DkimSignature, DKIM_SIGNATURE, "DKIM-Signature", None, Protocol::Mail, Status::Standard, &[Standard::RFC6376]);
|
||||
(DowngradedBcc, DOWNGRADED_BCC, "Downgraded-Bcc", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedCc, DOWNGRADED_CC, "Downgraded-Cc", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedDispositionNotificationTo, DOWNGRADED_DISPOSITION_NOTIFICATION_TO, "Downgraded-Disposition-Notification-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedFinalRecipient, DOWNGRADED_FINAL_RECIPIENT, "Downgraded-Final-Recipient", None, Protocol::Mail, Status::Standard, &[Standard::RFC6857]);
|
||||
(DowngradedFrom, DOWNGRADED_FROM, "Downgraded-From", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedInReplyTo, DOWNGRADED_IN_REPLY_TO, "Downgraded-In-Reply-To", None, Protocol::Mail, Status::Standard, &[Standard::RFC6857]);
|
||||
(DowngradedMailFrom, DOWNGRADED_MAIL_FROM, "Downgraded-Mail-From", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedMessageId, DOWNGRADED_MESSAGE_ID, "Downgraded-Message-Id", None, Protocol::Mail, Status::Standard, &[Standard::RFC6857]);
|
||||
(DowngradedOriginalRecipient, DOWNGRADED_ORIGINAL_RECIPIENT, "Downgraded-Original-Recipient", None, Protocol::Mail, Status::Standard, &[Standard::RFC6857]);
|
||||
(DowngradedRcptTo, DOWNGRADED_RCPT_TO, "Downgraded-Rcpt-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedReferences, DOWNGRADED_REFERENCES, "Downgraded-References", None, Protocol::Mail, Status::Standard, &[Standard::RFC6857]);
|
||||
(DowngradedReplyTo, DOWNGRADED_REPLY_TO, "Downgraded-Reply-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedResentBcc, DOWNGRADED_RESENT_BCC, "Downgraded-Resent-Bcc", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedResentCc, DOWNGRADED_RESENT_CC, "Downgraded-Resent-Cc", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedResentFrom, DOWNGRADED_RESENT_FROM, "Downgraded-Resent-From", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedResentReplyTo, DOWNGRADED_RESENT_REPLY_TO, "Downgraded-Resent-Reply-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedResentSender, DOWNGRADED_RESENT_SENDER, "Downgraded-Resent-Sender", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedResentTo, DOWNGRADED_RESENT_TO, "Downgraded-Resent-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedReturnPath, DOWNGRADED_RETURN_PATH, "Downgraded-Return-Path", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedSender, DOWNGRADED_SENDER, "Downgraded-Sender", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedTo, DOWNGRADED_TO, "Downgraded-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(Encoding, ENCODING, "Encoding", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Encrypted, ENCRYPTED, "Encrypted", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Expires, EXPIRES, "Expires", None, Protocol::Mail | Protocol::NNTP, Status::None, &[Standard::RFC4021, Standard::RFC5536]);
|
||||
(ExpiryDate, EXPIRY_DATE, "Expiry-Date", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(FollowupTo, FOLLOWUP_TO, "Followup-To", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(From, FROM, "From", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5322, Standard::RFC6854]);
|
||||
(GenerateDeliveryReport, GENERATE_DELIVERY_REPORT, "Generate-Delivery-Report", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Importance, IMPORTANCE, "Importance", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(IncompleteCopy, INCOMPLETE_COPY, "Incomplete-Copy", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(InjectionDate, INJECTION_DATE, "Injection-Date", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(InjectionInfo, INJECTION_INFO, "Injection-Info", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(Language, LANGUAGE, "Language", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(LatestDeliveryTime, LATEST_DELIVERY_TIME, "Latest-Delivery-Time", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Lines, LINES, "Lines", None, Protocol::NNTP, Status::Deprecated, &[Standard::RFC5536, Standard::RFC3977]);
|
||||
(ListArchive, LIST_ARCHIVE, "List-Archive", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ListHelp, LIST_HELP, "List-Help", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ListId, LIST_ID, "List-ID", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ListOwner, LIST_OWNER, "List-Owner", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ListPost, LIST_POST, "List-Post", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ListSubscribe, LIST_SUBSCRIBE, "List-Subscribe", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ListUnsubscribe, LIST_UNSUBSCRIBE, "List-Unsubscribe", Some("perm/list-unsubscribe"), Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ListUnsubscribePost, LIST_UNSUBSCRIBE_POST, "List-Unsubscribe-Post", None, Protocol::Mail, Status::Standard, &[Standard::RFC8058]);
|
||||
(MessageContext, MESSAGE_CONTEXT, "Message-Context", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(MessageId, MESSAGE_ID, "Message-ID", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5322, Standard::RFC5536]);
|
||||
(MessageType, MESSAGE_TYPE, "Message-Type", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(MimeVersion, MIME_VERSION, "MIME-Version", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(MtPriority, MT_PRIORITY, "MT-Priority", None, Protocol::Mail, Status::Standard, &[Standard::RFC6758]);
|
||||
(Newsgroups, NEWSGROUPS, "Newsgroups", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(NntpPostingDate, NNTP_POSTING_DATE, "NNTP-Posting-Date", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC5536]);
|
||||
(NntpPostingHost, NNTP_POSTING_HOST, "NNTP-Posting-Host", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC2980, Standard::RFC5536]);
|
||||
(Obsoletes, OBSOLETES, "Obsoletes", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Organization, ORGANIZATION, "Organization", None, Protocol::Mail | Protocol::NNTP, Status::Informational, &[Standard::RFC7681, Standard::RFC5536]);
|
||||
(OriginalEncodedInformationTypes, ORIGINAL_ENCODED_INFORMATION_TYPES, "Original-Encoded-Information-Types", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(OriginalFrom, ORIGINAL_FROM, "Original-From", None, Protocol::Mail, Status::Standard, &[Standard::RFC5703]);
|
||||
(OriginalMessageId, ORIGINAL_MESSAGE_ID, "Original-Message-ID", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(OriginalRecipient, ORIGINAL_RECIPIENT, "Original-Recipient", Some("perm/original-recipient"),Protocol::Mail, Status::Standard, &[Standard::RFC3798, Standard::RFC5337]);
|
||||
(OriginalSender, ORIGINAL_SENDER, "Original-Sender", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5537]);
|
||||
(OriginatorReturnAddress, ORIGINATOR_RETURN_ADDRESS, "Originator-Return-Address", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(OriginalSubject, ORIGINAL_SUBJECT, "Original-Subject", None, Protocol::Mail, Status::Standard, &[Standard::RFC5703]);
|
||||
(Path, PATH, "Path", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(PicsLabel, PICS_LABEL, "PICS-Label", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(PostingVersion, POSTING_VERSION, "Posting-Version", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC0850, Standard::RFC5536]);
|
||||
(PreventNondeliveryReport, PREVENT_NONDELIVERY_REPORT, "Prevent-NonDelivery-Report", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Priority, PRIORITY, "Priority", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ReceivedSpf, RECEIVED_SPF, "Received-SPF", None, Protocol::Mail, Status::Standard, &[Standard::RFC7208]);
|
||||
(RelayVersion, RELAY_VERSION, "Relay-Version", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC0850, Standard::RFC5536]);
|
||||
(ReplyBy, REPLY_BY, "Reply-By", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(RequireRecipientValidSince, REQUIRE_RECIPIENT_VALID_SINCE, "Require-Recipient-Valid-Since", None, Protocol::Mail, Status::Standard, &[Standard::RFC7293]);
|
||||
(ResentBcc, RESENT_BCC, "Resent-Bcc", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(ResentCc, RESENT_CC, "Resent-Cc", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(ResentDate, RESENT_DATE, "Resent-Date", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(ResentFrom, RESENT_FROM, "Resent-From", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322, Standard::RFC6854]);
|
||||
(ResentMessageId, RESENT_MESSAGE_ID, "Resent-Message-ID", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(ResentReplyTo, RESENT_REPLY_TO, "Resent-Reply-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5322]);
|
||||
(ResentSender, RESENT_SENDER, "Resent-Sender", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322, Standard::RFC6854]);
|
||||
(ResentTo, RESENT_TO, "Resent-To", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(ReturnPath, RETURN_PATH, "Return-Path", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(SeeAlso, SEE_ALSO, "See-Also", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC1849, Standard::RFC5536]);
|
||||
(Sender, SENDER, "Sender", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5322, Standard::RFC6854]);
|
||||
(Sensitivity, SENSITIVITY, "Sensitivity", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Solicitation, SOLICITATION, "Solicitation", None, Protocol::Mail, Status::None, &[Standard::RFC3865]);
|
||||
(Summary, SUMMARY, "Summary", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(Supersedes, SUPERSEDES, "Supersedes", None, Protocol::Mail | Protocol::NNTP, Status::None, &[Standard::RFC5536, Standard::RFC2156]);
|
||||
(TlsReportDomain, TLS_REPORT_DOMAIN, "TLS-Report-Domain", None, Protocol::Mail, Status::Standard, &[Standard::RFC8460]);
|
||||
(TlsReportSubmitter, TLS_REPORT_SUBMITTER, "TLS-Report-Submitter", None, Protocol::Mail, Status::Standard, &[Standard::RFC8460]);
|
||||
(TlsRequired, TLS_REQUIRED, "TLS-Required", None, Protocol::Mail, Status::Standard, &[Standard::RFC8689]);
|
||||
(To, TO, "To", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(UserAgent, USER_AGENT, "User-Agent", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536, Standard::RFC2616]);
|
||||
(VbrInfo, VBR_INFO, "VBR-Info", None, Protocol::Mail, Status::Standard, &[Standard::RFC5518]);
|
||||
(X400ContentIdentifier, X400_CONTENT_IDENTIFIER, "X400-Content-Identifier", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(X400ContentReturn, X400_CONTENT_RETURN, "X400-Content-Return", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(X400ContentType, X400_CONTENT_TYPE, "X400-Content-Type", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(X400MtsIdentifier, X400_MTS_IDENTIFIER, "X400-MTS-Identifier", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(X400Originator, X400_ORIGINATOR, "X400-Originator", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(X400Received, X400_RECEIVED, "X400-Received", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(X400Recipients, X400_RECIPIENTS, "X400-Recipients", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(X400Trace, X400_TRACE, "X400-Trace", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Xref, XREF, "Xref", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(ApparentlyTo, APPARENTLY_TO, "Apparently-To", Some("prov/apparently-to"), Protocol::Mail, Status::None, &[Standard::RFC2076]);
|
||||
(Author, AUTHOR, "Author", None, Protocol::Mail, Status::None, &[Standard::RFC9057]);
|
||||
(EdiintFeatures, EDIINT_FEATURES, "EDIINT-Features", None, Protocol::Mail, Status::None, &[Standard::RFC6017]);
|
||||
(EesstVersion, EESST_VERSION, "Eesst-Version", None, Protocol::Mail, Status::None, &[Standard::RFC7681]);
|
||||
(ErrorsTo, ERRORS_TO, "Errors-To", Some("prov/errors-to"), Protocol::Mail, Status::None, &[Standard::RFC2076]);
|
||||
(JabberId, JABBER_ID, "Jabber-ID", Some("prov/jabber-id"), Protocol::Mail | Protocol::NNTP, Status::None, &[Standard::RFC7259]);
|
||||
(SioLabel, SIO_LABEL, "SIO-Label", None, Protocol::Mail, Status::None, &[Standard::RFC7444]);
|
||||
(SioLabelHistory, SIO_LABEL_HISTORY, "SIO-Label-History", None, Protocol::Mail, Status::None, &[Standard::RFC7444]);
|
||||
(XArchivedAt, X_ARCHIVED_AT, "X-Archived-At", Some("prov/x-archived-at"), Protocol::Mail | Protocol::NNTP, Status::Deprecated, &[Standard::RFC5064]);
|
||||
(XMittente, X_MITTENTE, "X-Mittente", None, Protocol::Mail, Status::None, &[Standard::RFC6109]);
|
||||
(XRicevuta, X_RICEVUTA, "X-Ricevuta", None, Protocol::Mail, Status::None, &[Standard::RFC6109]);
|
||||
(XRiferimentoMessageId, X_RIFERIMENTO_MESSAGE_ID, "X-Riferimento-Message-ID", None, Protocol::Mail, Status::None, &[Standard::RFC6109]);
|
||||
(XTiporicevuta, X_TIPORICEVUTA, "X-TipoRicevuta", None, Protocol::Mail, Status::None, &[Standard::RFC6109]);
|
||||
(XTrasporto, X_TRASPORTO, "X-Trasporto", None, Protocol::Mail, Status::None, &[Standard::RFC6109]);
|
||||
(XVerificasicurezza, X_VERIFICASICUREZZA, "X-VerificaSicurezza", None, Protocol::Mail, Status::None, &[Standard::RFC6109]);
|
||||
}
|
||||
|
||||
/// Valid header name ASCII bytes
|
||||
///
|
||||
/// Source: [RFC5322 3.6.8.](https://datatracker.ietf.org/doc/html/rfc5322#autoid-35)
|
||||
/// ```text
|
||||
/// field-name = 1*ftext
|
||||
///
|
||||
/// ftext = %d33-57 / ; Printable US-ASCII
|
||||
/// %d59-126 ; characters not including
|
||||
/// ; ":".
|
||||
/// ```
|
||||
const HEADER_CHARS: [u8; 128] = [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // x
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1x
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2x
|
||||
0, 0, 0, b'!', b'"', b'#', b'$', b'%', b'&', b'\'', // 3x
|
||||
0, 0, b'*', b'+', 0, b'-', b'.', 0, b'0', b'1', // 4x
|
||||
b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', 0, 0, // 5x
|
||||
0, 0, 0, 0, 0, b'a', b'b', b'c', b'd', b'e', // 6x
|
||||
b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', b'n', b'o', // 7x
|
||||
b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', b'x', b'y', // 8x
|
||||
b'z', 0, 0, 0, b'^', b'_', b'`', b'a', b'b', b'c', // 9x
|
||||
b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', // 10x
|
||||
b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', // 11x
|
||||
b'x', b'y', b'z', 0, b'|', 0, b'~', 0, // 128
|
||||
];
|
||||
|
||||
impl HeaderName {
|
||||
/// Returns a `str` representation of the header.
|
||||
///
|
||||
/// The returned string will always be lower case. Use `Display` for a
|
||||
/// properly formatted representation.
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self.inner {
|
||||
Repr::Standard(v) => v.as_str(),
|
||||
Repr::Custom(ref v) => v.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a `&[u8]` representation of the header.
|
||||
///
|
||||
/// The returned string will always be lower case. Use `Display` for a
|
||||
/// properly formatted representation.
|
||||
#[inline]
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
match self.inner {
|
||||
Repr::Standard(v) => v.as_str().as_bytes(),
|
||||
Repr::Custom(ref v) => v.0.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_bytes(src: &[u8]) -> Result<Self, InvalidHeaderName> {
|
||||
if let Some(std) = StandardHeader::from_bytes(src.trim()) {
|
||||
Ok(Self {
|
||||
inner: Repr::Standard(std),
|
||||
})
|
||||
} else {
|
||||
let mut buf = SmallVec::<[u8; 32]>::new();
|
||||
for b in src {
|
||||
if let Some(b) = HEADER_CHARS.get(*b as usize).filter(|b| **b != 0) {
|
||||
buf.push(*b);
|
||||
} else {
|
||||
return Err(InvalidHeaderName::new());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
inner: Repr::Custom(Custom(buf)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_standard(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self {
|
||||
inner: Repr::Standard(_)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for HeaderName {
|
||||
type Err = InvalidHeaderName;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, InvalidHeaderName> {
|
||||
Self::from_bytes(s.as_bytes()).map_err(|_| InvalidHeaderName::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for HeaderName {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for HeaderName {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.as_str().as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<str> for HeaderName {
|
||||
fn borrow(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for HeaderName {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(fmt, "{}", &self.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for HeaderName {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Helper {
|
||||
S(String),
|
||||
B(Vec<u8>),
|
||||
}
|
||||
<Helper>::deserialize(deserializer)
|
||||
.map_err(|_| de::Error::custom("invalid header name value"))
|
||||
.and_then(|s| {
|
||||
Self::from_bytes(match &s {
|
||||
Helper::S(v) => v.as_bytes(),
|
||||
Helper::B(v) => v.as_slice(),
|
||||
})
|
||||
.map_err(|_| de::Error::custom("invalid header name value"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for HeaderName {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl InvalidHeaderName {
|
||||
const fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Self> for HeaderName {
|
||||
fn from(src: &'a Self) -> Self {
|
||||
src.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&HeaderName> for Cow<'static, str> {
|
||||
fn from(src: &HeaderName) -> Self {
|
||||
match src.inner {
|
||||
Repr::Standard(s) => Cow::Borrowed(s.as_str()),
|
||||
Repr::Custom(_) => Cow::Owned(src.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for HeaderName {
|
||||
type Error = InvalidHeaderName;
|
||||
#[inline]
|
||||
fn try_from(s: &'a str) -> Result<Self, Self::Error> {
|
||||
Self::from_bytes(s.as_bytes()).map_err(|_| InvalidHeaderName::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a String> for HeaderName {
|
||||
type Error = InvalidHeaderName;
|
||||
#[inline]
|
||||
fn try_from(s: &'a String) -> Result<Self, Self::Error> {
|
||||
Self::from_bytes(s.as_bytes()).map_err(|_| InvalidHeaderName::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a [u8]> for HeaderName {
|
||||
type Error = InvalidHeaderName;
|
||||
#[inline]
|
||||
fn try_from(s: &'a [u8]) -> Result<Self, Self::Error> {
|
||||
Self::from_bytes(s).map_err(|_| InvalidHeaderName::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for HeaderName {
|
||||
type Error = InvalidHeaderName;
|
||||
|
||||
#[inline]
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
Self::from_bytes(s.as_bytes()).map_err(|_| InvalidHeaderName::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for HeaderName {
|
||||
type Error = InvalidHeaderName;
|
||||
|
||||
#[inline]
|
||||
fn try_from(vec: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
Self::from_bytes(&vec).map_err(|_| InvalidHeaderName::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
impl From<StandardHeader> for HeaderName {
|
||||
fn from(src: StandardHeader) -> Self {
|
||||
Self {
|
||||
inner: Repr::Standard(src),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
impl From<Custom> for HeaderName {
|
||||
fn from(src: Custom) -> Self {
|
||||
Self {
|
||||
inner: Repr::Custom(src),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialEq<&'a Self> for HeaderName {
|
||||
#[inline]
|
||||
fn eq(&self, other: &&'a Self) -> bool {
|
||||
*self == **other
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialEq<HeaderName> for &'a HeaderName {
|
||||
#[inline]
|
||||
fn eq(&self, other: &HeaderName) -> bool {
|
||||
*other == *self
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<str> for HeaderName {
|
||||
/// Performs a case-insensitive comparison of the string against the header
|
||||
/// name
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use melib::email::headers::HeaderName;
|
||||
///
|
||||
/// assert_eq!(HeaderName::CONTENT_LENGTH, "content-length");
|
||||
/// assert_eq!(HeaderName::CONTENT_LENGTH, "Content-Length");
|
||||
/// assert_ne!(HeaderName::CONTENT_LENGTH, "content length");
|
||||
/// ```
|
||||
#[inline]
|
||||
fn eq(&self, other: &str) -> bool {
|
||||
self.as_str().eq_ignore_ascii_case(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<HeaderName> for str {
|
||||
/// Performs a case-insensitive comparison of the string against the header
|
||||
/// name
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::convert::TryFrom;
|
||||
///
|
||||
/// use melib::email::headers::HeaderName;
|
||||
///
|
||||
/// assert_eq!(HeaderName::CONTENT_LENGTH, "content-length");
|
||||
/// assert_eq!(HeaderName::CONTENT_LENGTH, "Content-Length");
|
||||
/// assert_ne!(HeaderName::CONTENT_LENGTH, "content length");
|
||||
/// assert_eq!(
|
||||
/// HeaderName::CONTENT_LENGTH,
|
||||
/// HeaderName::try_from("content-length").unwrap()
|
||||
/// );
|
||||
/// ```
|
||||
#[inline]
|
||||
fn eq(&self, other: &HeaderName) -> bool {
|
||||
*other == *self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialEq<&'a str> for HeaderName {
|
||||
/// Performs a case-insensitive comparison of the string against the header
|
||||
/// name
|
||||
#[inline]
|
||||
fn eq(&self, other: &&'a str) -> bool {
|
||||
*self == **other
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialEq<HeaderName> for &'a str {
|
||||
/// Performs a case-insensitive comparison of the string against the header
|
||||
/// name
|
||||
#[inline]
|
||||
fn eq(&self, other: &HeaderName) -> bool {
|
||||
*other == *self
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Custom {
|
||||
#[inline]
|
||||
fn hash<H: Hasher>(&self, hasher: &mut H) {
|
||||
for b in self.0.as_slice() {
|
||||
hasher.write_u8(b.to_ascii_lowercase())
|
||||
}
|
||||
}
|
||||
}
|
||||
const UPPERCASE_TOKENS: &[&str] = &[
|
||||
"ARC", "DKIM", "DL", "EDIINT", "ID", "IPMS", "MD5", "MIME", "MT", "MTS", "NNTP", "PICS", "RSS",
|
||||
"SIO", "SPF", "TLS", "VBR",
|
||||
];
|
||||
|
||||
impl std::fmt::Display for Custom {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let as_str = self.as_str();
|
||||
let len = as_str.len();
|
||||
let mut bytes_count = 0;
|
||||
for chunk in as_str.split('-') {
|
||||
if let Ok(tok) = UPPERCASE_TOKENS.binary_search_by(|probe| {
|
||||
if probe.eq_ignore_ascii_case(chunk) {
|
||||
Ordering::Equal
|
||||
} else {
|
||||
let mut iter = AsciiIgnoreCaseCmp {
|
||||
ord: Ordering::Equal,
|
||||
a: probe.as_bytes(),
|
||||
b: chunk.as_bytes(),
|
||||
};
|
||||
let _cnt: usize = iter.by_ref().fuse().count();
|
||||
debug_assert!(
|
||||
_cnt <= probe.len(),
|
||||
"_cnt {} should be lte probe.len() {}, for probe {} and chunk {}",
|
||||
_cnt,
|
||||
probe.len(),
|
||||
probe,
|
||||
chunk
|
||||
);
|
||||
debug_assert!(
|
||||
_cnt <= chunk.len(),
|
||||
"_cnt {} should be lte chunk.len() {}, for probe {} and chunk {}",
|
||||
_cnt,
|
||||
chunk.len(),
|
||||
probe,
|
||||
chunk
|
||||
);
|
||||
iter.ord
|
||||
}
|
||||
}) {
|
||||
write!(fmt, "{}", UPPERCASE_TOKENS[tok])?;
|
||||
} else {
|
||||
if let Some(first) = chunk.chars().next() {
|
||||
write!(fmt, "{}", first.to_ascii_uppercase())?;
|
||||
}
|
||||
for ch in chunk.chars().skip(1) {
|
||||
write!(fmt, "{}", ch.to_ascii_lowercase())?
|
||||
}
|
||||
}
|
||||
bytes_count += chunk.len();
|
||||
if bytes_count != len {
|
||||
bytes_count += 1;
|
||||
write!(fmt, "-")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// an iterator which alternates between Some and None
|
||||
struct AsciiIgnoreCaseCmp<'a, 'b> {
|
||||
ord: Ordering,
|
||||
a: &'a [u8],
|
||||
b: &'b [u8],
|
||||
}
|
||||
|
||||
impl<'a, 'b> Iterator for AsciiIgnoreCaseCmp<'a, 'b> {
|
||||
type Item = ();
|
||||
|
||||
fn next(&mut self) -> Option<()> {
|
||||
match (self.a.first(), self.b.first()) {
|
||||
(Some(a_char), Some(b_char)) => {
|
||||
self.ord = a_char
|
||||
.to_ascii_lowercase()
|
||||
.cmp(&b_char.to_ascii_lowercase());
|
||||
self.a = &self.a[1..];
|
||||
self.b = &self.b[1..];
|
||||
if self.ord == Ordering::Equal {
|
||||
Some(())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
(Some(_), None) => {
|
||||
self.ord = Ordering::Greater;
|
||||
None
|
||||
}
|
||||
(None, Some(_)) => {
|
||||
self.ord = Ordering::Less;
|
||||
None
|
||||
}
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_email_headers_headername_display() {
|
||||
assert_eq!(&HeaderName::SUBJECT.to_string(), "Subject");
|
||||
assert_eq!(&HeaderName::CC.to_string(), "Cc");
|
||||
assert_eq!(&HeaderName::IN_REPLY_TO.to_string(), "In-Reply-To");
|
||||
assert_eq!(
|
||||
&HeaderName::ORIGINAL_MESSAGE_ID.to_string(),
|
||||
"Original-Message-ID"
|
||||
);
|
||||
assert_eq!(
|
||||
&HeaderName::try_from("x-user-agent").unwrap().to_string(),
|
||||
"X-User-Agent"
|
||||
);
|
||||
assert_eq!(
|
||||
&HeaderName::try_from("arc-foobar").unwrap().to_string(),
|
||||
"ARC-Foobar"
|
||||
);
|
||||
assert_eq!(
|
||||
&HeaderName::try_from("x-rss-feed").unwrap().to_string(),
|
||||
"X-RSS-Feed"
|
||||
);
|
||||
assert_eq!(
|
||||
&HeaderName::try_from("With-regards-to").unwrap().to_string(),
|
||||
"With-Regards-To"
|
||||
);
|
||||
assert_eq!(
|
||||
&HeaderName::try_from("in-response-to-id")
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
"In-Response-To-ID"
|
||||
);
|
||||
assert_eq!(
|
||||
&HeaderName::try_from("something-dKim").unwrap().to_string(),
|
||||
"Something-DKIM"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email_headers_parse_standard_headers() {
|
||||
for &(std, name) in TEST_HEADERS {
|
||||
// Test lower case
|
||||
assert_eq!(
|
||||
HeaderName::from_bytes(name.to_ascii_lowercase().as_bytes()).unwrap(),
|
||||
HeaderName::from(std)
|
||||
);
|
||||
|
||||
// Test upper case
|
||||
let upper = std::str::from_utf8(name.as_bytes())
|
||||
.expect("byte string constants are all utf-8")
|
||||
.to_uppercase();
|
||||
assert_eq!(
|
||||
HeaderName::from_bytes(upper.as_bytes()).unwrap(),
|
||||
HeaderName::from(std)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,8 +19,7 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Parsing of `RFC2369` and `RFC2919` `List-*` headers.
|
||||
|
||||
/*! Parsing of rfc2369/rfc2919 `List-*` headers */
|
||||
use std::convert::From;
|
||||
|
||||
use smallvec::SmallVec;
|
||||
|
@ -107,10 +106,9 @@ pub fn list_id(header: Option<&'_ str>) -> Option<&'_ str> {
|
|||
|
||||
impl<'a> ListActions<'a> {
|
||||
pub fn detect(envelope: &'a Envelope) -> Option<ListActions<'a>> {
|
||||
let mut ret = Self {
|
||||
id: list_id_header(envelope),
|
||||
..Self::default()
|
||||
};
|
||||
let mut ret = ListActions::default();
|
||||
|
||||
ret.id = list_id_header(envelope);
|
||||
|
||||
if let Some(archive) = envelope.other_headers().get("List-Archive") {
|
||||
if archive.starts_with('<') {
|
||||
|
|
|
@ -19,118 +19,42 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Parsing of `mailto` addresses.
|
||||
//!
|
||||
//! Conforming to [RFC6068](https://www.rfc-editor.org/rfc/rfc6068) which obsoletes
|
||||
//! [RFC2368](https://www.rfc-editor.org/rfc/rfc2368).
|
||||
|
||||
/*! Parsing of `mailto` addresses */
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
email::headers::HeaderMap,
|
||||
utils::percent_encoding::{AsciiSet, CONTROLS},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Mailto {
|
||||
pub address: Vec<Address>,
|
||||
pub address: Address,
|
||||
pub subject: Option<String>,
|
||||
pub cc: Option<String>,
|
||||
pub bcc: Option<String>,
|
||||
pub body: Option<String>,
|
||||
pub headers: HeaderMap,
|
||||
}
|
||||
|
||||
impl Mailto {
|
||||
pub const IGNORE_HEADERS: &[HeaderName] = &[
|
||||
HeaderName::FROM,
|
||||
HeaderName::DATE,
|
||||
HeaderName::MESSAGE_ID,
|
||||
HeaderName::APPARENTLY_TO,
|
||||
HeaderName::ARC_AUTHENTICATION_RESULTS,
|
||||
HeaderName::ARC_MESSAGE_SIGNATURE,
|
||||
HeaderName::ARC_SEAL,
|
||||
HeaderName::AUTHENTICATION_RESULTS,
|
||||
HeaderName::AUTOFORWARDED,
|
||||
HeaderName::AUTO_SUBMITTED,
|
||||
HeaderName::AUTOSUBMITTED,
|
||||
HeaderName::BASE,
|
||||
HeaderName::CONTENT_ALTERNATIVE,
|
||||
HeaderName::CONTENT_BASE,
|
||||
HeaderName::CONTENT_DESCRIPTION,
|
||||
HeaderName::CONTENT_DISPOSITION,
|
||||
HeaderName::CONTENT_DURATION,
|
||||
HeaderName::CONTENT_FEATURES,
|
||||
HeaderName::CONTENT_ID,
|
||||
HeaderName::CONTENT_IDENTIFIER,
|
||||
HeaderName::CONTENT_LANGUAGE,
|
||||
HeaderName::CONTENT_LENGTH,
|
||||
HeaderName::CONTENT_LOCATION,
|
||||
HeaderName::CONTENT_MD5,
|
||||
HeaderName::CONTENT_RETURN,
|
||||
HeaderName::CONTENT_TRANSFER_ENCODING,
|
||||
HeaderName::CONTENT_TRANSLATION_TYPE,
|
||||
HeaderName::CONTENT_TYPE,
|
||||
HeaderName::DELIVERED_TO,
|
||||
HeaderName::DKIM_SIGNATURE,
|
||||
HeaderName::ENCRYPTED,
|
||||
HeaderName::FORWARDED,
|
||||
HeaderName::MAIL_FOLLOWUP_TO,
|
||||
HeaderName::MAIL_REPLY_TO,
|
||||
HeaderName::MIME_VERSION,
|
||||
HeaderName::ORIGINAL_ENCODED_INFORMATION_TYPES,
|
||||
HeaderName::ORIGINAL_FROM,
|
||||
HeaderName::ORIGINAL_MESSAGE_ID,
|
||||
HeaderName::ORIGINAL_RECIPIENT,
|
||||
HeaderName::ORIGINAL_SUBJECT,
|
||||
HeaderName::ORIGINATOR_RETURN_ADDRESS,
|
||||
HeaderName::RECEIVED,
|
||||
HeaderName::RECEIVED_SPF,
|
||||
HeaderName::RESENT_BCC,
|
||||
HeaderName::RESENT_CC,
|
||||
HeaderName::RESENT_DATE,
|
||||
HeaderName::RESENT_FROM,
|
||||
HeaderName::RESENT_MESSAGE_ID,
|
||||
HeaderName::RESENT_REPLY_TO,
|
||||
HeaderName::RESENT_SENDER,
|
||||
HeaderName::RESENT_TO,
|
||||
HeaderName::RETURN_PATH,
|
||||
HeaderName::SENDER,
|
||||
HeaderName::USER_AGENT,
|
||||
];
|
||||
|
||||
pub const MAILTO_CHARSET: &AsciiSet = &CONTROLS
|
||||
.add(b' ')
|
||||
.add(b'"')
|
||||
.add(b'"')
|
||||
.add(b'#')
|
||||
.add(b'%')
|
||||
.add(b'/')
|
||||
.add(b'<')
|
||||
.add(b'>')
|
||||
.add(b'?')
|
||||
.add(b'`')
|
||||
.add(b'{')
|
||||
.add(b'}');
|
||||
}
|
||||
|
||||
impl From<Mailto> for Draft {
|
||||
fn from(val: Mailto) -> Self {
|
||||
let mut ret = Self::default();
|
||||
let mut ret = Draft::default();
|
||||
let Mailto {
|
||||
address: _,
|
||||
address,
|
||||
subject,
|
||||
cc,
|
||||
bcc,
|
||||
body,
|
||||
headers,
|
||||
} = val;
|
||||
for (hdr, val) in headers.into_inner() {
|
||||
ret.set_header(hdr, val);
|
||||
}
|
||||
ret.set_header("Subject", subject.unwrap_or_default());
|
||||
ret.set_header("Cc", cc.unwrap_or_default());
|
||||
ret.set_header("Bcc", bcc.unwrap_or_default());
|
||||
ret.set_body(body.unwrap_or_default());
|
||||
ret.set_header("To", address.to_string());
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Mailto> for Draft {
|
||||
fn from(val: &Mailto) -> Self {
|
||||
Self::from(val.clone())
|
||||
Draft::from(val.clone())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,316 +62,95 @@ impl TryFrom<&[u8]> for Mailto {
|
|||
type Error = String;
|
||||
|
||||
fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
|
||||
super::parser::generic::mailto(value)
|
||||
.map(|(_, v)| v)
|
||||
.map_err(|err| {
|
||||
log::debug!(
|
||||
"parser::mailto returned error while parsing {}:\n{:?}",
|
||||
String::from_utf8_lossy(value),
|
||||
&err,
|
||||
);
|
||||
format!("{:?}", err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Mailto {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
|
||||
super::parser::generic::mailto(value.as_bytes())
|
||||
.map(|(_, v)| v)
|
||||
.map_err(|err| {
|
||||
log::debug!(
|
||||
"parser::mailto returned error while parsing {}:\n{:?}",
|
||||
value,
|
||||
&err
|
||||
);
|
||||
format!("{:?}", err)
|
||||
})
|
||||
let parse_res = super::parser::generic::mailto(value).map(|(_, v)| v);
|
||||
if let Ok(res) = parse_res {
|
||||
Ok(res)
|
||||
} else {
|
||||
debug!(
|
||||
"parser::mailto returned error while parsing {}:\n{:?}",
|
||||
String::from_utf8_lossy(value),
|
||||
parse_res.as_ref().err().unwrap()
|
||||
);
|
||||
Err(format!("{:?}", parse_res.err().unwrap()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use HeaderName as HDR;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_mailto() {
|
||||
macro_rules! addr {
|
||||
($lit:literal) => {
|
||||
Address::try_from($lit).unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! mlt {
|
||||
($lit:literal) => {
|
||||
Mailto::try_from($lit).expect("Could not parse mailto link.")
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! hdr {
|
||||
($lit:literal) => {
|
||||
HeaderName::try_from($lit).expect("Could not parse header name.")
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! hdrmap {
|
||||
($(($field:literal, $val:literal)),+) => {{
|
||||
let mut m = HeaderMap::empty();
|
||||
$(
|
||||
m.insert(hdr!($field), $val.into());
|
||||
)+
|
||||
|
||||
m
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! test_case {
|
||||
($mailto:literal, addresses => $($addr:literal),*; body => $body:expr; $(($field:literal, $val:literal)),+) => {{
|
||||
let addresses = &[
|
||||
$(
|
||||
addr!($addr)
|
||||
),*
|
||||
];
|
||||
let Mailto {
|
||||
address,
|
||||
body,
|
||||
headers,
|
||||
} = mlt!($mailto);
|
||||
assert_eq!(
|
||||
(address.as_slice(), body.as_ref().map(|b| b.as_str()), headers),
|
||||
(addresses.as_slice(), $body, hdrmap!($(($field, $val)),*))
|
||||
);
|
||||
}}
|
||||
}
|
||||
|
||||
test_case!("mailto:info@example.com?subject=email%20subject",
|
||||
addresses=> "info@example.com";
|
||||
body => None;
|
||||
("To", "info@example.com"), ("Subject", "email subject")
|
||||
);
|
||||
test_case!("mailto:info@example.com?cc=8cc9@example.com",
|
||||
addresses=> "info@example.com";
|
||||
body => None;
|
||||
("To", "info@example.com"), ("Cc", "8cc9@example.com")
|
||||
);
|
||||
test_case!("mailto:info@example.com?bcc=7bcc8@example.com&body=line%20first%0Abut%20not%0Alast",
|
||||
addresses=> "info@example.com";
|
||||
body => Some("line first\nbut not\nlast");
|
||||
("To", "info@example.com"), ("Bcc", "7bcc8@example.com")
|
||||
);
|
||||
|
||||
test_case!("mailto:info@example.com?In-Reply-To=%3C20230526204845.673031-1-manos.pitsidianakis@linaro.org%3E&Cc=kraxel%40redhat.com%2Cqemu-devel%40nongnu.org&Subject=Re%3A%20%5BPATCH%5D%20Add%20virtio-sound%20and%20virtio-sound-pci%20devices",
|
||||
addresses=> "info@example.com";
|
||||
body => None;
|
||||
("To", "info@example.com"), ("Subject", "Re: [PATCH] Add virtio-sound and virtio-sound-pci devices"), ("Cc", "kraxel@redhat.com,qemu-devel@nongnu.org"), ("In-Reply-To", "<20230526204845.673031-1-manos.pitsidianakis@linaro.org>")
|
||||
);
|
||||
let test_address = super::parser::address::address(b"info@example.com")
|
||||
.map(|(_, v)| v)
|
||||
.unwrap();
|
||||
let mailto = Mailto::try_from(&b"mailto:info@example.com?subject=email%20subject"[0..])
|
||||
.expect("Could not parse mailto link.");
|
||||
let Mailto {
|
||||
ref address,
|
||||
ref subject,
|
||||
ref cc,
|
||||
ref bcc,
|
||||
ref body,
|
||||
} = mailto;
|
||||
|
||||
assert_eq!(
|
||||
mlt!("mailto:chris@example.com%2C%20tony@example.com"),
|
||||
mlt!("mailto:?to=chris@example.com%2C%20tony@example.com")
|
||||
(
|
||||
address,
|
||||
subject.as_ref().map(String::as_str),
|
||||
cc.as_ref().map(String::as_str),
|
||||
bcc.as_ref().map(String::as_str),
|
||||
body.as_ref().map(String::as_str),
|
||||
),
|
||||
(&test_address, Some("email subject"), None, None, None)
|
||||
);
|
||||
|
||||
/* address plus to= should be ignored */
|
||||
assert!(
|
||||
Mailto::try_from("mailto:?to=chris@example.com%2C%20tony@example.com")
|
||||
!= Mailto::try_from("mailto:chris@example.com?to=tony@example.com"),
|
||||
"{:?} == {:?}",
|
||||
Mailto::try_from("mailto:?to=chris@example.com%2C%20tony@example.com"),
|
||||
Mailto::try_from("mailto:chris@example.com?to=tony@example.com")
|
||||
);
|
||||
|
||||
// URLs for an ordinary individual mailing address:
|
||||
test_case!("mailto:chris@example.com",
|
||||
addresses=> "chris@example.com";
|
||||
body => None;
|
||||
("To", "chris@example.com")
|
||||
);
|
||||
|
||||
// A URL for a mail response system that requires the name of the file in the
|
||||
// subject:
|
||||
|
||||
test_case!("mailto:infobot@example.com?subject=current-issue",
|
||||
addresses => "infobot@example.com";
|
||||
body => None;
|
||||
("To", "infobot@example.com"), ("Subject", "current-issue")
|
||||
);
|
||||
|
||||
// A mail response system that requires a "send" request in the body:
|
||||
|
||||
test_case!("mailto:infobot@example.com?body=send%20current-issue",
|
||||
addresses => "infobot@example.com";
|
||||
body => Some("send current-issue");
|
||||
("To", "infobot@example.com")
|
||||
);
|
||||
|
||||
//A similar URL could have two lines with different "send" requests (in this
|
||||
// case, "send current-issue" and, on the next line, "send index".)
|
||||
|
||||
test_case!("mailto:infobot@example.com?body=send%20current-issue%0D%0Asend%20index",
|
||||
addresses => "infobot@example.com";
|
||||
body => Some("send current-issue\r\nsend index");
|
||||
("To", "infobot@example.com")
|
||||
);
|
||||
// An interesting use of your mailto URL is when browsing archives of messages.
|
||||
// Each browsed message might contain a mailto URL like:
|
||||
|
||||
test_case!("mailto:foobar@example.com?In-Reply-To=%3c3469A91.D10AF4C@example.com%3e",
|
||||
addresses => "foobar@example.com";
|
||||
body => None;
|
||||
("To", "foobar@example.com"), ("In-Reply-To", "<3469A91.D10AF4C@example.com>")
|
||||
);
|
||||
|
||||
// A request to subscribe to a mailing list:
|
||||
|
||||
test_case!("mailto:majordomo@example.com?body=subscribe%20bamboo-l",
|
||||
addresses => "majordomo@example.com";
|
||||
body => Some("subscribe bamboo-l");
|
||||
("To", "majordomo@example.com")
|
||||
);
|
||||
|
||||
// A URL for a single user which includes a CC of another user:
|
||||
|
||||
test_case!("mailto:joe@example.com?cc=bob@example.com&body=hello",
|
||||
addresses => "joe@example.com";
|
||||
body => Some("hello");
|
||||
("To", "joe@example.com"), ("Cc", "bob@example.com")
|
||||
);
|
||||
|
||||
// Another way of expressing the same thing:
|
||||
|
||||
test_case!("mailto:?to=joe@example.com&cc=bob@example.com&body=hello",
|
||||
addresses => "joe@example.com";
|
||||
body => Some("hello");
|
||||
("To", "joe@example.com"), ("Cc", "bob@example.com")
|
||||
);
|
||||
|
||||
// Note the use of the "&" reserved character, above. The following example,
|
||||
// by using "?" twice, is incorrect: <mailto:joe@example.com?cc=bob@
|
||||
// example.com?body=hello> ; WRONG!
|
||||
|
||||
Mailto::try_from("mailto:joe@example.com?cc=bob@example.com?body=hello").unwrap_err();
|
||||
|
||||
// <a href="mailto:?to=joe@xyz.com&cc=bob@xyz.com&body=hello"> assert
|
||||
// these are equal
|
||||
|
||||
test_case!("mailto:?to=joe@example.com&cc=bob@example.com&body=hello",
|
||||
addresses => "joe@example.com";
|
||||
body => Some("hello");
|
||||
("To", "joe@example.com"), ("Cc", "bob@example.com")
|
||||
);
|
||||
|
||||
// To indicate the address "gorby%kremvax@example.com" one would do:
|
||||
// <mailto:gorby%25kremvax@example.com>
|
||||
|
||||
test_case!("mailto:gorby%25kremvax@example.com",
|
||||
addresses => "gorby%kremvax@example.com";
|
||||
body => None;
|
||||
("To", "gorby%kremvax@example.com")
|
||||
);
|
||||
|
||||
// Custom header is ignored
|
||||
// <mailto:address@example.com?blat=foop>
|
||||
|
||||
test_case!("mailto:address@example.com?blat=foop",
|
||||
addresses => "address@example.com";
|
||||
body => None;
|
||||
("To", "address@example.com")
|
||||
);
|
||||
|
||||
// 6.2. Examples of Complicated Email Addresses
|
||||
|
||||
let mailto = Mailto::try_from(&b"mailto:info@example.com?cc=8cc9@example.com"[0..])
|
||||
.expect("Could not parse mailto link.");
|
||||
let Mailto {
|
||||
ref address,
|
||||
ref subject,
|
||||
ref cc,
|
||||
ref bcc,
|
||||
ref body,
|
||||
} = mailto;
|
||||
assert_eq!(
|
||||
mlt!("mailto:%22not%40me%22@example.org").address,
|
||||
vec![addr!(r#""not@me"@example.org"#)]
|
||||
(
|
||||
address,
|
||||
subject.as_ref().map(String::as_str),
|
||||
cc.as_ref().map(String::as_str),
|
||||
bcc.as_ref().map(String::as_str),
|
||||
body.as_ref().map(String::as_str),
|
||||
),
|
||||
(&test_address, None, Some("8cc9@example.com"), None, None)
|
||||
);
|
||||
|
||||
// Email address: "oh\\no"@example.org; corresponding 'mailto' URI:
|
||||
|
||||
// <mailto:%22oh%5C%5Cno%22@example.org>.
|
||||
|
||||
// Email address: "\\\"it's\ ugly\\\""@example.org; corresponding
|
||||
// 'mailto' URI:
|
||||
|
||||
// <mailto:%22%5C%5C%5C%22it's%5C%20ugly%5C%5C%5C%22%22@example.org>.
|
||||
// [tag:FIXME]
|
||||
//assert_eq!(
|
||||
// mlt!("mailto:%22%5C%5C%5C%22it's%5C%20ugly%5C%5C%5C%22%22@example.org").
|
||||
// address, vec![addr!(r#"\"it's ugly\"@example.org"#)]
|
||||
//);
|
||||
|
||||
// When an email address itself includes an "&" (ampersand) character, that
|
||||
// character has to be percent-encoded. For example, the 'mailto' URI
|
||||
// to send mail to "Mike&family@example.org" is
|
||||
// <mailto:Mike%26family@example.org>.
|
||||
let mailto = Mailto::try_from(
|
||||
&b"mailto:info@example.com?bcc=7bcc8@example.com&body=line%20first%0Abut%20not%0Alast"
|
||||
[0..],
|
||||
)
|
||||
.expect("Could not parse mailto link.");
|
||||
let Mailto {
|
||||
ref address,
|
||||
ref subject,
|
||||
ref cc,
|
||||
ref bcc,
|
||||
ref body,
|
||||
} = mailto;
|
||||
assert_eq!(
|
||||
mlt!("mailto:Mike%26family@example.org").address,
|
||||
vec![addr!("Mike&family@example.org")]
|
||||
);
|
||||
|
||||
// Sending a mail with the subject "coffee" in French, i.e., "cafe" where the
|
||||
// final e is an e-acute, using UTF-8 and percent-encoding:
|
||||
// <mailto:user@example.org?subject=caf%C3%A9>
|
||||
assert_eq!(
|
||||
&mlt!("mailto:user@example.org?subject=caf%C3%A9").headers[HDR::SUBJECT],
|
||||
"café"
|
||||
);
|
||||
|
||||
// The same subject, this time using an encoded-word (escaping the "="
|
||||
// and "?" characters used in the encoded-word syntax, because they are
|
||||
// reserved):
|
||||
// [tag:FIXME]
|
||||
// <mailto:user@example.org?subject=%3D%3Futf-8%3FQ%3Fcaf%3DC3%3DA9%3F%3D>
|
||||
assert_eq!(
|
||||
&mlt!("mailto:user@example.org?subject=%3D%3Futf-8%3FQ%3Fcaf%3DC3%3DA9%3F%3D").headers
|
||||
[HDR::SUBJECT],
|
||||
"=?utf-8?Q?caf=C3=A9?="
|
||||
);
|
||||
|
||||
// The same subject, this time encoded as iso-8859-1:
|
||||
|
||||
// <mailto:user@example.org?subject=%3D%3Fiso-8859-1%3FQ%3Fcaf%3DE9%3F%3D>
|
||||
// [tag:FIXME]
|
||||
assert_eq!(
|
||||
&mlt!("mailto:user@example.org?subject=%3D%3Fiso-8859-1%3FQ%3Fcaf%3DE9%3F%3D").headers
|
||||
[HDR::SUBJECT],
|
||||
"=?iso-8859-1?Q?caf=E9?="
|
||||
);
|
||||
|
||||
// Going back to straight UTF-8 and adding a body with the same value:
|
||||
//
|
||||
// <mailto:user@example.org?subject=caf%C3%A9&body=caf%C3%A9>
|
||||
test_case!("mailto:user@example.org?subject=caf%C3%A9&body=caf%C3%A9",
|
||||
addresses => "user@example.org";
|
||||
body => Some("café");
|
||||
("To", "user@example.org"),
|
||||
("Subject", "café")
|
||||
);
|
||||
|
||||
// The following example uses the Japanese word "natto" (Unicode
|
||||
// characters U+7D0D U+8C46) as a domain name label, sending a mail to a
|
||||
// user at "natto".example.org:
|
||||
|
||||
// <mailto:user@%E7%B4%8D%E8%B1%86.example.org?subject=Test&body=NATTO>
|
||||
|
||||
// When constructing the email, the domain name label is converted to
|
||||
// punycode. The resulting message may look as follows:
|
||||
|
||||
// From: sender@example.net
|
||||
// To: user@xn--99zt52a.example.org
|
||||
// Subject: Test
|
||||
// Content-Type: text/plain
|
||||
// Content-Transfer-Encoding: 7bit
|
||||
//
|
||||
// NATTO
|
||||
test_case!("mailto:user@%E7%B4%8D%E8%B1%86.example.org?subject=Test&body=NATTO",
|
||||
addresses => "user@納豆.example.org";
|
||||
body => Some("NATTO");
|
||||
("To", "user@納豆.example.org"),
|
||||
("Subject", "Test")
|
||||
(
|
||||
address,
|
||||
subject.as_ref().map(String::as_str),
|
||||
cc.as_ref().map(String::as_str),
|
||||
bcc.as_ref().map(String::as_str),
|
||||
body.as_ref().map(String::as_str),
|
||||
),
|
||||
(
|
||||
&test_address,
|
||||
None,
|
||||
None,
|
||||
Some("7bcc8@example.com"),
|
||||
Some("line first\nbut not\nlast")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,10 +19,8 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Parsers for email. See submodules.
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
use std::{borrow::Cow, convert::TryFrom, fmt::Write};
|
||||
/*! Parsers for email. See submodules */
|
||||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
branch::alt,
|
||||
|
@ -36,15 +34,7 @@ use nom::{
|
|||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
email::{
|
||||
address::Address,
|
||||
headers::{HeaderMap, HeaderName},
|
||||
mailto::Mailto,
|
||||
},
|
||||
error::{Error, Result, ResultIntoError},
|
||||
utils::{html_escape::HtmlEntity, percent_encoding::percent_decode},
|
||||
};
|
||||
use crate::error::{Error, Result, ResultIntoError};
|
||||
|
||||
macro_rules! to_str {
|
||||
($l:expr) => {{
|
||||
|
@ -144,8 +134,8 @@ impl<I, E> nom::error::FromExternalError<I, E> for ParsingError<I> {
|
|||
impl<I> nom::error::ContextError<I> for ParsingError<I> {}
|
||||
|
||||
impl<'i> From<ParsingError<&'i [u8]>> for Error {
|
||||
fn from(val: ParsingError<&'i [u8]>) -> Self {
|
||||
Self::new("Parsing error").set_summary(format!(
|
||||
fn from(val: ParsingError<&'i [u8]>) -> Error {
|
||||
Error::new("Parsing error").set_summary(format!(
|
||||
r#"In input: "{}...",
|
||||
Error: {}"#,
|
||||
String::from_utf8_lossy(val.input)
|
||||
|
@ -158,8 +148,8 @@ Error: {}"#,
|
|||
}
|
||||
|
||||
impl<'i> From<ParsingError<&'i str>> for Error {
|
||||
fn from(val: ParsingError<&'i str>) -> Self {
|
||||
Self::new("Parsing error").set_summary(format!(
|
||||
fn from(val: ParsingError<&'i str>) -> Error {
|
||||
Error::new("Parsing error").set_summary(format!(
|
||||
r#"In input: "{}...",
|
||||
Error: {}"#,
|
||||
val.input.chars().take(30).collect::<String>(),
|
||||
|
@ -169,34 +159,34 @@ Error: {}"#,
|
|||
}
|
||||
|
||||
impl<'i> From<nom::Err<ParsingError<&'i [u8]>>> for Error {
|
||||
fn from(val: nom::Err<ParsingError<&'i [u8]>>) -> Self {
|
||||
fn from(val: nom::Err<ParsingError<&'i [u8]>>) -> Error {
|
||||
match val {
|
||||
nom::Err::Incomplete(_) => Self::new("Parsing Error: Incomplete"),
|
||||
nom::Err::Incomplete(_) => Error::new("Parsing Error: Incomplete"),
|
||||
nom::Err::Error(err) | nom::Err::Failure(err) => err.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'i> From<nom::Err<ParsingError<&'i str>>> for Error {
|
||||
fn from(val: nom::Err<ParsingError<&'i str>>) -> Self {
|
||||
fn from(val: nom::Err<ParsingError<&'i str>>) -> Error {
|
||||
match val {
|
||||
nom::Err::Incomplete(_) => Self::new("Parsing Error: Incomplete"),
|
||||
nom::Err::Incomplete(_) => Error::new("Parsing Error: Incomplete"),
|
||||
nom::Err::Error(err) | nom::Err::Failure(err) => err.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nom::Err<nom::error::Error<&[u8]>>> for Error {
|
||||
fn from(val: nom::Err<nom::error::Error<&[u8]>>) -> Self {
|
||||
fn from(val: nom::Err<nom::error::Error<&[u8]>>) -> Error {
|
||||
match val {
|
||||
nom::Err::Incomplete(_) => Self::new("Parsing Error: Incomplete"),
|
||||
nom::Err::Error(_) | nom::Err::Failure(_) => Self::new("Parsing Error"),
|
||||
nom::Err::Incomplete(_) => Error::new("Parsing Error: Incomplete"),
|
||||
nom::Err::Error(_) | nom::Err::Failure(_) => Error::new("Parsing Error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'i> From<ParsingError<&'i [u8]>> for nom::error::Error<&'i [u8]> {
|
||||
fn from(val: ParsingError<&'i [u8]>) -> Self {
|
||||
fn from(val: ParsingError<&'i [u8]>) -> nom::error::Error<&'i [u8]> {
|
||||
nom::error::Error::new(val.input, ErrorKind::Satisfy)
|
||||
}
|
||||
}
|
||||
|
@ -328,9 +318,9 @@ pub fn mail(input: &[u8]) -> Result<(Vec<(&[u8], &[u8])>, &[u8])> {
|
|||
}
|
||||
|
||||
pub mod dates {
|
||||
//! Date values in headers.
|
||||
/*! Date values in headers */
|
||||
use super::{generic::*, *};
|
||||
use crate::utils::datetime::UnixTimestamp;
|
||||
use crate::datetime::UnixTimestamp;
|
||||
|
||||
fn take_n_digits(n: usize) -> impl Fn(&[u8]) -> IResult<&[u8], &[u8]> {
|
||||
move |input: &[u8]| {
|
||||
|
@ -344,15 +334,15 @@ pub mod dates {
|
|||
}
|
||||
}
|
||||
|
||||
/// In the obsolete time zone, "UT" and "GMT" are indications of
|
||||
/// "Universal Time" and "Greenwich Mean Time", respectively, and are
|
||||
/// both semantically identical to "+0000".
|
||||
///In the obsolete time zone, "UT" and "GMT" are indications of
|
||||
///"Universal Time" and "Greenwich Mean Time", respectively, and are
|
||||
///both semantically identical to "+0000".
|
||||
|
||||
/// The remaining three character zones are the US time zones. The first
|
||||
/// letter, "E", "C", "M", or "P" stands for "Eastern", "Central",
|
||||
/// "Mountain", and "Pacific". The second letter is either "S" for
|
||||
/// "Standard" time, or "D" for "Daylight Savings" (or summer) time.
|
||||
/// Their interpretations are as follows:
|
||||
///The remaining three character zones are the US time zones. The first
|
||||
///letter, "E", "C", "M", or "P" stands for "Eastern", "Central",
|
||||
///"Mountain", and "Pacific". The second letter is either "S" for
|
||||
///"Standard" time, or "D" for "Daylight Savings" (or summer) time.
|
||||
///Their interpretations are as follows:
|
||||
|
||||
/// EDT is semantically equivalent to -0400
|
||||
/// EST is semantically equivalent to -0500
|
||||
|
@ -363,21 +353,21 @@ pub mod dates {
|
|||
/// PDT is semantically equivalent to -0700
|
||||
/// PST is semantically equivalent to -0800
|
||||
|
||||
/// The 1 character military time zones were defined in a non-standard
|
||||
/// way in RFC0822 and are therefore unpredictable in their meaning.
|
||||
/// The original definitions of the military zones "A" through "I" are
|
||||
/// equivalent to "+0100" through "+0900", respectively; "K", "L", and
|
||||
/// "M" are equivalent to "+1000", "+1100", and "+1200", respectively;
|
||||
/// "N" through "Y" are equivalent to "-0100" through "-1200".
|
||||
/// respectively; and "Z" is equivalent to "+0000". However, because of
|
||||
/// the error in RFC0822, they SHOULD all be considered equivalent to
|
||||
/// "-0000" unless there is out-of-band information confirming their
|
||||
/// meaning.
|
||||
///The 1 character military time zones were defined in a non-standard
|
||||
///way in [RFC0822] and are therefore unpredictable in their meaning.
|
||||
///The original definitions of the military zones "A" through "I" are
|
||||
///equivalent to "+0100" through "+0900", respectively; "K", "L", and
|
||||
///"M" are equivalent to "+1000", "+1100", and "+1200", respectively;
|
||||
///"N" through "Y" are equivalent to "-0100" through "-1200".
|
||||
///respectively; and "Z" is equivalent to "+0000". However, because of
|
||||
///the error in [RFC0822], they SHOULD all be considered equivalent to
|
||||
///"-0000" unless there is out-of-band information confirming their
|
||||
///meaning.
|
||||
|
||||
/// Other multi-character (usually between 3 and 5) alphabetic time zones
|
||||
/// have been used in Internet messages. Any such time zone whose
|
||||
/// meaning is not known SHOULD be considered equivalent to "-0000"
|
||||
/// unless there is out-of-band information confirming their meaning.
|
||||
///Other multi-character (usually between 3 and 5) alphabetic time zones
|
||||
///have been used in Internet messages. Any such time zone whose
|
||||
///meaning is not known SHOULD be considered equivalent to "-0000"
|
||||
///unless there is out-of-band information confirming their meaning.
|
||||
fn obs_zone(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
|
||||
alt((
|
||||
map(tag("UT"), |_| (&b"+"[..], &b"0000"[..])),
|
||||
|
@ -394,9 +384,7 @@ pub mod dates {
|
|||
))(input)
|
||||
}
|
||||
|
||||
/// ```text
|
||||
/// zone = (FWS ( "+" / "-" ) 4DIGIT) / obs-zone
|
||||
/// ```
|
||||
///zone = (FWS ( "+" / "-" ) 4DIGIT) / obs-zone
|
||||
fn zone(input: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
|
||||
alt((
|
||||
|input| {
|
||||
|
@ -408,15 +396,13 @@ pub mod dates {
|
|||
))(input)
|
||||
}
|
||||
|
||||
/// ```text
|
||||
/// date-time = [ day-of-week "," ] date time [CFWS]
|
||||
/// date = day month year
|
||||
/// time = time-of-day zone
|
||||
/// time-of-day = hour ":" minute [ ":" second ]
|
||||
/// hour = 2DIGIT / obs-hour
|
||||
/// minute = 2DIGIT / obs-minute
|
||||
/// second = 2DIGIT / obs-second
|
||||
/// ```
|
||||
///date-time = [ day-of-week "," ] date time [CFWS]
|
||||
///date = day month year
|
||||
///time = time-of-day zone
|
||||
///time-of-day = hour ":" minute [ ":" second ]
|
||||
///hour = 2DIGIT / obs-hour
|
||||
///minute = 2DIGIT / obs-minute
|
||||
///second = 2DIGIT / obs-second
|
||||
fn date_time(input: &[u8]) -> IResult<&[u8], UnixTimestamp> {
|
||||
let orig_input = input;
|
||||
let mut accum: SmallVec<[u8; 32]> = SmallVec::new();
|
||||
|
@ -451,7 +437,7 @@ pub mod dates {
|
|||
accum.extend_from_slice(b" ");
|
||||
accum.extend_from_slice(sign);
|
||||
accum.extend_from_slice(zone);
|
||||
match crate::utils::datetime::rfc822_to_timestamp(accum.to_vec()) {
|
||||
match crate::datetime::rfc822_to_timestamp(accum.to_vec()) {
|
||||
Ok(t) => Ok((input, t)),
|
||||
Err(_err) => Err(nom::Err::Error(
|
||||
(
|
||||
|
@ -463,9 +449,8 @@ pub mod dates {
|
|||
}
|
||||
}
|
||||
|
||||
/// e.g Wed Sep 9 00:27:54 2020
|
||||
///
|
||||
/// ```text
|
||||
///e.g Wed Sep 9 00:27:54 2020
|
||||
///```text
|
||||
/// day-of-week month day time year
|
||||
/// date-time = [ day-of-week "," ] date time [CFWS]
|
||||
/// date = day month year
|
||||
|
@ -513,7 +498,7 @@ pub mod dates {
|
|||
accum.extend_from_slice(sign);
|
||||
accum.extend_from_slice(zone);
|
||||
}
|
||||
match crate::utils::datetime::rfc822_to_timestamp(accum.to_vec()) {
|
||||
match crate::datetime::rfc822_to_timestamp(accum.to_vec()) {
|
||||
Ok(t) => Ok((input, t)),
|
||||
Err(_err) => Err(nom::Err::Error(
|
||||
(
|
||||
|
@ -525,11 +510,9 @@ pub mod dates {
|
|||
}
|
||||
}
|
||||
|
||||
/// ```text
|
||||
/// day-of-week = ([FWS] day-name) / obs-day-of-week
|
||||
/// day-name = "Mon" / "Tue" / "Wed" / "Thu" /
|
||||
///`day-of-week = ([FWS] day-name) / obs-day-of-week`
|
||||
///day-name = "Mon" / "Tue" / "Wed" / "Thu" /
|
||||
/// "Fri" / "Sat" / "Sun"
|
||||
/// ```
|
||||
fn day_of_week(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
|
||||
let (input, day_name) = alt((
|
||||
tag("Mon"),
|
||||
|
@ -543,7 +526,7 @@ pub mod dates {
|
|||
Ok((input, day_name.into()))
|
||||
}
|
||||
|
||||
/// `day = ([FWS] 1*2DIGIT FWS) / obs-day`
|
||||
///day = ([FWS] 1*2DIGIT FWS) / obs-day
|
||||
fn day(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
let (input, _) = opt(fws)(input)?;
|
||||
let (input, ret) = alt((take_n_digits(2), take_n_digits(1)))(input)?;
|
||||
|
@ -552,11 +535,9 @@ pub mod dates {
|
|||
Ok((input, ret))
|
||||
}
|
||||
|
||||
/// ```text
|
||||
/// month = "Jan" / "Feb" / "Mar" / "Apr" /
|
||||
///month = "Jan" / "Feb" / "Mar" / "Apr" /
|
||||
/// "May" / "Jun" / "Jul" / "Aug" /
|
||||
/// "Sep" / "Oct" / "Nov" / "Dec"
|
||||
/// ```
|
||||
fn month(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
alt((
|
||||
tag("Jan"),
|
||||
|
@ -582,7 +563,7 @@ pub mod dates {
|
|||
Ok((input, ret))
|
||||
}
|
||||
|
||||
pub fn rfc5322_date(input: &[u8]) -> Result<crate::UnixTimestamp> {
|
||||
pub fn rfc5322_date(input: &[u8]) -> Result<crate::datetime::UnixTimestamp> {
|
||||
date_time(input)
|
||||
.or_else(|_| {
|
||||
//let (_, mut parsed_result) = encodings::phrase(&eat_comments(input), false)?;
|
||||
|
@ -616,7 +597,7 @@ pub mod dates {
|
|||
parsed_result[pos] = b'+';
|
||||
}
|
||||
|
||||
crate::utils::datetime::rfc822_to_timestamp(parsed_result.trim())
|
||||
crate::datetime::rfc822_to_timestamp(parsed_result.trim())
|
||||
*/
|
||||
}
|
||||
|
||||
|
@ -635,7 +616,7 @@ pub mod dates {
|
|||
}
|
||||
|
||||
pub mod generic {
|
||||
//! Generally useful parser combinators.
|
||||
/*! Generally useful parser combinators */
|
||||
use super::*;
|
||||
#[inline(always)]
|
||||
pub fn byte_in_slice<'a>(slice: &'static [u8]) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], u8> {
|
||||
|
@ -665,9 +646,9 @@ pub mod generic {
|
|||
}
|
||||
}
|
||||
|
||||
/// UTF-8 characters can be defined in terms of octets using the
|
||||
/// following ABNF `[RFC5234]`, taken from `[RFC3629]`:
|
||||
/// UTF8-non-ascii = UTF8-2 / UTF8-3 / UTF8-4
|
||||
///UTF-8 characters can be defined in terms of octets using the
|
||||
///following ABNF [RFC5234], taken from [RFC3629]:
|
||||
///UTF8-non-ascii = UTF8-2 / UTF8-3 / UTF8-4
|
||||
fn utf8_non_ascii(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
|
||||
/// UTF8-2 = %xC2-DF UTF8-tail
|
||||
fn utf8_2(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
|
@ -932,157 +913,85 @@ pub mod generic {
|
|||
}
|
||||
}
|
||||
|
||||
use crate::email::{address::Address, mailto::Mailto};
|
||||
pub fn mailto(mut input: &[u8]) -> IResult<&[u8], Mailto> {
|
||||
let orig_input = input;
|
||||
if !input.starts_with(b"mailto:") {
|
||||
return Err(nom::Err::Error(
|
||||
(input, "mailto(): input doesn't start with `mailto:`").into(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut body = None;
|
||||
let mut headers = HeaderMap::empty();
|
||||
let mut address: Vec<Address>;
|
||||
|
||||
if String::from_utf8_lossy(input).matches('?').count() > 1 {
|
||||
return Err(nom::Err::Error(
|
||||
(input, "mailto(): Using '?' twice is invalid.").into(),
|
||||
));
|
||||
}
|
||||
input = &input[b"mailto:".len()..];
|
||||
let mut decoded_owned = percent_decode(input).decode_utf8().unwrap().to_string();
|
||||
|
||||
let mut substitutions = vec![];
|
||||
for (i, _) in decoded_owned.match_indices('&') {
|
||||
if let Some(j) = HtmlEntity::ALL
|
||||
.iter()
|
||||
.position(|e| decoded_owned[i..].starts_with(e))
|
||||
{
|
||||
substitutions.push((i, HtmlEntity::ALL[j].len(), HtmlEntity::GLYPHS[j]));
|
||||
}
|
||||
}
|
||||
let end = input.iter().position(|e| *e == b'?').unwrap_or(input.len());
|
||||
let address: Address;
|
||||
|
||||
for (i, len, g) in substitutions.into_iter().rev() {
|
||||
decoded_owned.replace_range(i..(i + len), g);
|
||||
}
|
||||
|
||||
let mut decoded = decoded_owned.as_str();
|
||||
|
||||
let end = decoded.as_bytes().iter().position(|e| *e == b'?');
|
||||
let end_or_len = end.unwrap_or(decoded.len());
|
||||
|
||||
if let Ok(addr) = Address::list_try_from(&decoded[..end_or_len]) {
|
||||
if let Ok((_, addr)) = crate::email::parser::address::address(&input[..end]) {
|
||||
address = addr;
|
||||
decoded = if decoded[end_or_len..].is_empty() {
|
||||
&decoded[end_or_len..]
|
||||
input = if input[end..].is_empty() {
|
||||
&input[end..]
|
||||
} else {
|
||||
&decoded[end_or_len + 1..]
|
||||
&input[end + 1..]
|
||||
};
|
||||
} else if end.is_some() {
|
||||
decoded = &decoded[1..];
|
||||
address = vec![];
|
||||
} else {
|
||||
return Err(nom::Err::Error(
|
||||
(
|
||||
input,
|
||||
format!("input {:?}", String::from_utf8_lossy(orig_input)),
|
||||
)
|
||||
.into(),
|
||||
(input, "mailto(): address not found in input").into(),
|
||||
));
|
||||
}
|
||||
|
||||
if !address.is_empty() {
|
||||
let mut full_address = String::new();
|
||||
for address in &address {
|
||||
write!(&mut full_address, "{}, ", address)
|
||||
.expect("Could not write into a String, are you out of memory?");
|
||||
}
|
||||
if full_address.ends_with(", ") {
|
||||
let len = full_address.len();
|
||||
full_address.truncate(len - ", ".len());
|
||||
}
|
||||
headers.insert(HeaderName::TO, full_address);
|
||||
}
|
||||
|
||||
while !decoded.is_empty() {
|
||||
if decoded.starts_with("&") {
|
||||
decoded = &decoded["&".len()..];
|
||||
continue;
|
||||
}
|
||||
|
||||
let tag = if let Some(tag_pos) = decoded.as_bytes().iter().position(|e| *e == b'=') {
|
||||
let ret = &decoded[0..tag_pos];
|
||||
decoded = &decoded[tag_pos + 1..];
|
||||
let mut subject = None;
|
||||
let mut cc = None;
|
||||
let mut bcc = None;
|
||||
let mut body = None;
|
||||
while !input.is_empty() {
|
||||
let tag = if let Some(tag_pos) = input.iter().position(|e| *e == b'=') {
|
||||
let ret = &input[0..tag_pos];
|
||||
input = &input[tag_pos + 1..];
|
||||
ret
|
||||
} else {
|
||||
return Err(nom::Err::Error(
|
||||
(
|
||||
input,
|
||||
format!("mailto(): extra characters found in input: {}", decoded),
|
||||
)
|
||||
.into(),
|
||||
(input, "mailto(): extra characters found in input").into(),
|
||||
));
|
||||
};
|
||||
|
||||
let value_end = decoded
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.position(|e| *e == b'&')
|
||||
.unwrap_or(decoded.len());
|
||||
let value_end = input.iter().position(|e| *e == b'&').unwrap_or(input.len());
|
||||
|
||||
let value = decoded[..value_end].to_string();
|
||||
let value = String::from_utf8_lossy(&input[..value_end]).to_string();
|
||||
match tag {
|
||||
"body" if body.is_none() => {
|
||||
body = Some(value);
|
||||
b"subject" if subject.is_none() => {
|
||||
subject = Some(value.replace("%20", " "));
|
||||
}
|
||||
b"cc" if cc.is_none() => {
|
||||
cc = Some(value);
|
||||
}
|
||||
b"bcc" if bcc.is_none() => {
|
||||
bcc = Some(value);
|
||||
}
|
||||
b"body" if body.is_none() => {
|
||||
/* FIXME:
|
||||
* Parse escaped characters properly.
|
||||
*/
|
||||
body = Some(value.replace("%20", " ").replace("%0A", "\n"));
|
||||
}
|
||||
_ => {
|
||||
return Err(nom::Err::Error(
|
||||
(input, "mailto(): unknown tag in input").into(),
|
||||
));
|
||||
}
|
||||
other => match HeaderName::try_from(other) {
|
||||
Ok(hdr) if hdr == HeaderName::TO => {
|
||||
if !headers.contains_key(&hdr) {
|
||||
if let Ok(address_val) = Address::list_try_from(value.as_str()) {
|
||||
address.extend(address_val.into_iter());
|
||||
}
|
||||
headers.insert(HeaderName::TO, value);
|
||||
}
|
||||
}
|
||||
Ok(hdr) if hdr.is_standard() => {
|
||||
if Mailto::IGNORE_HEADERS.contains(&hdr) {
|
||||
log::warn!(
|
||||
"parsing mailto(): header {} is not allowed in mailto URIs for \
|
||||
safety and will be ignored. Value was {:?}",
|
||||
hdr,
|
||||
value
|
||||
);
|
||||
}
|
||||
if !headers.contains_key(&hdr) {
|
||||
headers.insert(hdr, value);
|
||||
}
|
||||
}
|
||||
Ok(hdr) => {
|
||||
log::warn!(
|
||||
"parsing mailto(): header {} is not a known header and it will be \
|
||||
ignored.Value was {:?}",
|
||||
hdr,
|
||||
value
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
return Err(nom::Err::Error(
|
||||
(input, "mailto(): unknown tag in input").into(),
|
||||
));
|
||||
}
|
||||
},
|
||||
}
|
||||
if decoded[value_end..].is_empty() {
|
||||
if input[value_end..].is_empty() {
|
||||
break;
|
||||
}
|
||||
decoded = &decoded[value_end + 1..];
|
||||
input = &input[value_end + 1..];
|
||||
}
|
||||
Ok((
|
||||
input,
|
||||
Mailto {
|
||||
address,
|
||||
subject,
|
||||
cc,
|
||||
bcc,
|
||||
body,
|
||||
headers,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
@ -1415,7 +1324,7 @@ List-Archive: <http://www.host.com/list/archive/> (Web Archive)
|
|||
}
|
||||
|
||||
pub mod headers {
|
||||
//! Email headers.
|
||||
/*! Email headers */
|
||||
use super::*;
|
||||
|
||||
pub fn headers(input: &[u8]) -> IResult<&[u8], Vec<(&[u8], &[u8])>> {
|
||||
|
@ -1676,7 +1585,7 @@ pub mod headers {
|
|||
}
|
||||
|
||||
pub mod attachments {
|
||||
//! Email attachments.
|
||||
/*! Email attachments */
|
||||
use super::*;
|
||||
use crate::email::{
|
||||
address::*,
|
||||
|
@ -1946,7 +1855,7 @@ pub mod attachments {
|
|||
}
|
||||
|
||||
pub mod encodings {
|
||||
//! Email encodings (quoted printable, `MIME`).
|
||||
/*! Email encodings (quoted printable, MIME) */
|
||||
use data_encoding::BASE64_MIME;
|
||||
use encoding::{all::*, DecoderTrap, Encoding};
|
||||
|
||||
|
@ -2045,9 +1954,10 @@ pub mod encodings {
|
|||
let encoded_text = &input[3 + tag_end_idx..encoded_end_idx];
|
||||
|
||||
let s: Vec<u8> = match input[tag_end_idx + 1] {
|
||||
b'b' | b'B' => BASE64_MIME
|
||||
.decode(encoded_text)
|
||||
.map_or_else(|_| encoded_text.to_vec(), |v| v),
|
||||
b'b' | b'B' => match BASE64_MIME.decode(encoded_text) {
|
||||
Ok(v) => v,
|
||||
Err(_) => encoded_text.to_vec(),
|
||||
},
|
||||
b'q' | b'Q' => match quoted_printable_bytes_header(encoded_text) {
|
||||
Ok((b"", s)) => s,
|
||||
_ => {
|
||||
|
@ -2065,21 +1975,19 @@ pub mod encodings {
|
|||
|
||||
let charset = Charset::from(&input[2..tag_end_idx]);
|
||||
|
||||
if Charset::UTF8 == charset {
|
||||
if let Charset::UTF8 = charset {
|
||||
Ok((&input[encoded_end_idx + 2..], s))
|
||||
} else {
|
||||
decode_charset(&s, charset).map_or_else(
|
||||
|_| {
|
||||
Err(nom::Err::Error(
|
||||
(
|
||||
input,
|
||||
format!("encoded_word(): unknown charset {:?}", charset),
|
||||
)
|
||||
.into(),
|
||||
))
|
||||
},
|
||||
|v| Ok((&input[encoded_end_idx + 2..], v.into_bytes())),
|
||||
)
|
||||
match decode_charset(&s, charset) {
|
||||
Ok(v) => Ok((&input[encoded_end_idx + 2..], v.into_bytes())),
|
||||
_ => Err(nom::Err::Error(
|
||||
(
|
||||
input,
|
||||
format!("encoded_word(): unknown charset {:?}", charset),
|
||||
)
|
||||
.into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2800,31 +2708,32 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
// //FIXME: add file
|
||||
//#[test]
|
||||
//fn test_attachments() {
|
||||
// use std::io::Read;
|
||||
// let mut buffer: Vec<u8> = Vec::new();
|
||||
// let _ = std::fs::File::open("").unwrap().read_to_end(&mut buffer);
|
||||
// let boundary = b"b1_4382d284f0c601a737bb32aaeda53160";
|
||||
// let (_, body) = match mail(&buffer) {
|
||||
// Ok(v) => v,
|
||||
// Err(_) => panic!(),
|
||||
// };
|
||||
// let attachments = parts(body, boundary).unwrap().1;
|
||||
// assert_eq!(attachments.len(), 4);
|
||||
// let v: Vec<&str> = attachments
|
||||
// .iter()
|
||||
// .map(|v| std::str::from_utf8(v).unwrap())
|
||||
// .collect();
|
||||
// //println!("attachments {:?}", v);
|
||||
//}
|
||||
|
||||
#[test]
|
||||
fn test_attachments() {
|
||||
//FIXME: add file
|
||||
return;
|
||||
/*
|
||||
use std::io::Read;
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
let _ = std::fs::File::open("").unwrap().read_to_end(&mut buffer);
|
||||
let boundary = b"b1_4382d284f0c601a737bb32aaeda53160";
|
||||
let (_, body) = match mail(&buffer) {
|
||||
Ok(v) => v,
|
||||
Err(_) => panic!(),
|
||||
};
|
||||
let attachments = parts(body, boundary).unwrap().1;
|
||||
assert_eq!(attachments.len(), 4);
|
||||
let v: Vec<&str> = attachments
|
||||
.iter()
|
||||
.map(|v| std::str::from_utf8(v).unwrap())
|
||||
.collect();
|
||||
//println!("attachments {:?}", v);
|
||||
*/
|
||||
}
|
||||
#[test]
|
||||
fn test_addresses() {
|
||||
macro_rules! assert_parse {
|
||||
($name:literal, $addr:literal, $raw:literal) => {{
|
||||
#[allow(clippy::string_lit_as_bytes)]
|
||||
let s = $raw.as_bytes();
|
||||
let r = address(s).unwrap().1;
|
||||
match r {
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Verification of OpenPGP signatures.
|
||||
/*! Verification of OpenPGP signatures */
|
||||
use crate::{
|
||||
email::{
|
||||
attachment_types::{ContentType, MultipartType},
|
||||
|
@ -28,10 +28,9 @@ use crate::{
|
|||
Error, Result,
|
||||
};
|
||||
|
||||
/// Convert raw attachment to the form needed for signature verification ([RFC3156](https://tools.ietf.org/html/rfc3156))
|
||||
///
|
||||
/// ## RFC3156
|
||||
/// Convert raw attachment to the form needed for signature verification ([rfc3156](https://tools.ietf.org/html/rfc3156))
|
||||
///
|
||||
/// ## rfc3156
|
||||
/// ```text
|
||||
/// Upon receipt of a signed message, an application MUST:
|
||||
///
|
||||
|
|
|
@ -19,7 +19,9 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Library error type.
|
||||
/*!
|
||||
* An error object for `melib`
|
||||
*/
|
||||
|
||||
use std::{borrow::Cow, fmt, io, result, str, string, sync::Arc};
|
||||
|
||||
|
@ -181,7 +183,7 @@ pub enum NetworkErrorKind {
|
|||
}
|
||||
|
||||
impl NetworkErrorKind {
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
use NetworkErrorKind::*;
|
||||
match self {
|
||||
None => "Network",
|
||||
|
@ -240,19 +242,6 @@ impl NetworkErrorKind {
|
|||
NetworkAuthenticationRequired => "Network Authentication Required",
|
||||
}
|
||||
}
|
||||
|
||||
/// Error kind means network is certainly down.
|
||||
pub const fn is_network_down(&self) -> bool {
|
||||
use NetworkErrorKind::*;
|
||||
matches!(
|
||||
self,
|
||||
BadGateway
|
||||
| ServiceUnavailable
|
||||
| GatewayTimeout
|
||||
| NetworkAuthenticationRequired
|
||||
| ConnectionFailed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NetworkErrorKind {
|
||||
|
@ -317,26 +306,17 @@ impl From<isahc::http::StatusCode> for NetworkErrorKind {
|
|||
}
|
||||
|
||||
#[derive(Debug, Copy, PartialEq, Eq, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum ErrorKind {
|
||||
None,
|
||||
External,
|
||||
Authentication,
|
||||
Configuration,
|
||||
/// Protocol error.
|
||||
///
|
||||
/// `EPROTO 71 Protocol error`
|
||||
ProtocolError,
|
||||
/// Protocol is not supported.
|
||||
/// It could be the wrong type or version.
|
||||
ProtocolNotSupported,
|
||||
Bug,
|
||||
Network(NetworkErrorKind),
|
||||
Timeout,
|
||||
OSError,
|
||||
NotImplemented,
|
||||
NotSupported,
|
||||
ValueError,
|
||||
}
|
||||
|
||||
impl fmt::Display for ErrorKind {
|
||||
|
@ -345,48 +325,33 @@ impl fmt::Display for ErrorKind {
|
|||
fmt,
|
||||
"{}",
|
||||
match self {
|
||||
Self::None => "None",
|
||||
Self::External => "External",
|
||||
Self::Authentication => "Authentication",
|
||||
Self::Bug => "Bug, please report this!",
|
||||
Self::Network(ref inner) => inner.as_str(),
|
||||
Self::ProtocolError => "Protocol error",
|
||||
Self::ProtocolNotSupported =>
|
||||
"Protocol is not supported. It could be the wrong type or version.",
|
||||
Self::Timeout => "Timeout",
|
||||
Self::OSError => "OS Error",
|
||||
Self::Configuration => "Configuration",
|
||||
Self::NotImplemented => "Not implemented",
|
||||
Self::NotSupported => "Not supported",
|
||||
Self::ValueError => "Invalid value",
|
||||
ErrorKind::None => "None",
|
||||
ErrorKind::External => "External",
|
||||
ErrorKind::Authentication => "Authentication",
|
||||
ErrorKind::Bug => "Bug, please report this!",
|
||||
ErrorKind::Network(ref inner) => inner.as_str(),
|
||||
ErrorKind::Timeout => "Timeout",
|
||||
ErrorKind::OSError => "OS Error",
|
||||
ErrorKind::Configuration => "Configuration",
|
||||
ErrorKind::NotImplemented => "Not implemented",
|
||||
ErrorKind::NotSupported => "Not supported",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! is_variant {
|
||||
($n:ident, $($var:tt)+) => {
|
||||
#[inline]
|
||||
pub fn $n(&self) -> bool {
|
||||
matches!(self, Self::$($var)*)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl ErrorKind {
|
||||
is_variant! { is_authentication, Authentication }
|
||||
is_variant! { is_bug, Bug }
|
||||
is_variant! { is_configuration, Configuration }
|
||||
is_variant! { is_external, External }
|
||||
is_variant! { is_network, Network(_) }
|
||||
is_variant! { is_network_down, Network(ref k) if k.is_network_down() }
|
||||
is_variant! { is_not_implemented, NotImplemented }
|
||||
is_variant! { is_not_supported, NotSupported }
|
||||
is_variant! { is_oserror, OSError }
|
||||
is_variant! { is_protocol_error, ProtocolError }
|
||||
is_variant! { is_protocol_not_supported, ProtocolNotSupported }
|
||||
is_variant! { is_timeout, Timeout }
|
||||
is_variant! { is_value_error, ValueError }
|
||||
pub fn is_network(&self) -> bool {
|
||||
matches!(self, ErrorKind::Network(_))
|
||||
}
|
||||
|
||||
pub fn is_timeout(&self) -> bool {
|
||||
matches!(self, ErrorKind::Timeout)
|
||||
}
|
||||
|
||||
pub fn is_authentication(&self) -> bool {
|
||||
matches!(self, ErrorKind::Authentication)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -460,11 +425,11 @@ impl<T, I: Into<Error>> ResultIntoError<T> for std::result::Result<T, I> {
|
|||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new<M>(msg: M) -> Self
|
||||
pub fn new<M>(msg: M) -> Error
|
||||
where
|
||||
M: Into<Cow<'static, str>>,
|
||||
{
|
||||
Self {
|
||||
Error {
|
||||
summary: msg.into(),
|
||||
details: None,
|
||||
source: None,
|
||||
|
@ -472,7 +437,7 @@ impl Error {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_details<M>(mut self, details: M) -> Self
|
||||
pub fn set_details<M>(mut self, details: M) -> Error
|
||||
where
|
||||
M: Into<Cow<'static, str>>,
|
||||
{
|
||||
|
@ -484,7 +449,7 @@ impl Error {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn set_summary<M>(mut self, summary: M) -> Self
|
||||
pub fn set_summary<M>(mut self, summary: M) -> Error
|
||||
where
|
||||
M: Into<Cow<'static, str>>,
|
||||
{
|
||||
|
@ -499,12 +464,12 @@ impl Error {
|
|||
pub fn set_source(
|
||||
mut self,
|
||||
new_val: Option<std::sync::Arc<dyn std::error::Error + Send + Sync + 'static>>,
|
||||
) -> Self {
|
||||
) -> Error {
|
||||
self.source = new_val;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_kind(mut self, new_val: ErrorKind) -> Self {
|
||||
pub fn set_kind(mut self, new_val: ErrorKind) -> Error {
|
||||
self.kind = new_val;
|
||||
self
|
||||
}
|
||||
|
@ -528,16 +493,14 @@ impl fmt::Display for Error {
|
|||
|
||||
impl std::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
self.source
|
||||
.as_ref()
|
||||
.map(|s| &(*(*s)) as &(dyn std::error::Error + 'static))
|
||||
self.source.as_ref().map(|s| &(*(*s)) as _)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
#[inline]
|
||||
fn from(kind: io::Error) -> Self {
|
||||
Self::new(kind.to_string())
|
||||
fn from(kind: io::Error) -> Error {
|
||||
Error::new(kind.to_string())
|
||||
.set_details(kind.kind().to_string())
|
||||
.set_source(Some(Arc::new(kind)))
|
||||
.set_kind(ErrorKind::OSError)
|
||||
|
@ -546,22 +509,22 @@ impl From<io::Error> for Error {
|
|||
|
||||
impl<'a> From<Cow<'a, str>> for Error {
|
||||
#[inline]
|
||||
fn from(kind: Cow<'_, str>) -> Self {
|
||||
Self::new(kind.to_string())
|
||||
fn from(kind: Cow<'_, str>) -> Error {
|
||||
Error::new(kind.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<string::FromUtf8Error> for Error {
|
||||
#[inline]
|
||||
fn from(kind: string::FromUtf8Error) -> Self {
|
||||
Self::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
fn from(kind: string::FromUtf8Error) -> Error {
|
||||
Error::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<str::Utf8Error> for Error {
|
||||
#[inline]
|
||||
fn from(kind: str::Utf8Error) -> Self {
|
||||
Self::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
fn from(kind: str::Utf8Error) -> Error {
|
||||
Error::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
//use std::option;
|
||||
|
@ -574,16 +537,16 @@ impl From<str::Utf8Error> for Error {
|
|||
|
||||
impl<T> From<std::sync::PoisonError<T>> for Error {
|
||||
#[inline]
|
||||
fn from(kind: std::sync::PoisonError<T>) -> Self {
|
||||
Self::new(kind.to_string()).set_kind(ErrorKind::Bug)
|
||||
fn from(kind: std::sync::PoisonError<T>) -> Error {
|
||||
Error::new(kind.to_string()).set_kind(ErrorKind::Bug)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tls")]
|
||||
impl<T: Sync + Send + 'static + core::fmt::Debug> From<native_tls::HandshakeError<T>> for Error {
|
||||
#[inline]
|
||||
fn from(kind: native_tls::HandshakeError<T>) -> Self {
|
||||
Self::new(kind.to_string())
|
||||
fn from(kind: native_tls::HandshakeError<T>) -> Error {
|
||||
Error::new(kind.to_string())
|
||||
.set_source(Some(Arc::new(kind)))
|
||||
.set_kind(ErrorKind::Network(NetworkErrorKind::InvalidTLSConnection))
|
||||
}
|
||||
|
@ -592,8 +555,8 @@ impl<T: Sync + Send + 'static + core::fmt::Debug> From<native_tls::HandshakeErro
|
|||
#[cfg(feature = "tls")]
|
||||
impl From<native_tls::Error> for Error {
|
||||
#[inline]
|
||||
fn from(kind: native_tls::Error) -> Self {
|
||||
Self::new(kind.to_string())
|
||||
fn from(kind: native_tls::Error) -> Error {
|
||||
Error::new(kind.to_string())
|
||||
.set_source(Some(Arc::new(kind)))
|
||||
.set_kind(ErrorKind::Network(NetworkErrorKind::InvalidTLSConnection))
|
||||
}
|
||||
|
@ -601,49 +564,49 @@ impl From<native_tls::Error> for Error {
|
|||
|
||||
impl From<std::num::ParseIntError> for Error {
|
||||
#[inline]
|
||||
fn from(kind: std::num::ParseIntError) -> Self {
|
||||
Self::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
fn from(kind: std::num::ParseIntError) -> Error {
|
||||
Error::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
impl From<&isahc::error::ErrorKind> for NetworkErrorKind {
|
||||
#[inline]
|
||||
fn from(val: &isahc::error::ErrorKind) -> Self {
|
||||
fn from(val: &isahc::error::ErrorKind) -> NetworkErrorKind {
|
||||
use isahc::error::ErrorKind::*;
|
||||
match val {
|
||||
BadClientCertificate => Self::BadClientCertificate,
|
||||
BadServerCertificate => Self::BadServerCertificate,
|
||||
ClientInitialization => Self::ClientInitialization,
|
||||
ConnectionFailed => Self::ConnectionFailed,
|
||||
InvalidContentEncoding => Self::InvalidContentEncoding,
|
||||
InvalidCredentials => Self::InvalidCredentials,
|
||||
InvalidRequest => Self::BadRequest,
|
||||
Io => Self::Io,
|
||||
NameResolution => Self::HostLookupFailed,
|
||||
ProtocolViolation => Self::ProtocolViolation,
|
||||
RequestBodyNotRewindable => Self::RequestBodyNotRewindable,
|
||||
Timeout => Self::Timeout,
|
||||
TlsEngine => Self::InvalidTLSConnection,
|
||||
TooManyRedirects => Self::TooManyRedirects,
|
||||
_ => Self::None,
|
||||
BadClientCertificate => NetworkErrorKind::BadClientCertificate,
|
||||
BadServerCertificate => NetworkErrorKind::BadServerCertificate,
|
||||
ClientInitialization => NetworkErrorKind::ClientInitialization,
|
||||
ConnectionFailed => NetworkErrorKind::ConnectionFailed,
|
||||
InvalidContentEncoding => NetworkErrorKind::InvalidContentEncoding,
|
||||
InvalidCredentials => NetworkErrorKind::InvalidCredentials,
|
||||
InvalidRequest => NetworkErrorKind::BadRequest,
|
||||
Io => NetworkErrorKind::Io,
|
||||
NameResolution => NetworkErrorKind::HostLookupFailed,
|
||||
ProtocolViolation => NetworkErrorKind::ProtocolViolation,
|
||||
RequestBodyNotRewindable => NetworkErrorKind::RequestBodyNotRewindable,
|
||||
Timeout => NetworkErrorKind::Timeout,
|
||||
TlsEngine => NetworkErrorKind::InvalidTLSConnection,
|
||||
TooManyRedirects => NetworkErrorKind::TooManyRedirects,
|
||||
_ => NetworkErrorKind::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NetworkErrorKind> for ErrorKind {
|
||||
#[inline]
|
||||
fn from(kind: NetworkErrorKind) -> Self {
|
||||
Self::Network(kind)
|
||||
fn from(kind: NetworkErrorKind) -> ErrorKind {
|
||||
ErrorKind::Network(kind)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
impl From<isahc::Error> for Error {
|
||||
#[inline]
|
||||
fn from(val: isahc::Error) -> Self {
|
||||
fn from(val: isahc::Error) -> Error {
|
||||
let kind: NetworkErrorKind = val.kind().into();
|
||||
Self::new(val.to_string())
|
||||
Error::new(val.to_string())
|
||||
.set_source(Some(Arc::new(val)))
|
||||
.set_kind(ErrorKind::Network(kind))
|
||||
}
|
||||
|
@ -652,103 +615,106 @@ impl From<isahc::Error> for Error {
|
|||
#[cfg(feature = "jmap_backend")]
|
||||
impl From<serde_json::error::Error> for Error {
|
||||
#[inline]
|
||||
fn from(kind: serde_json::error::Error) -> Self {
|
||||
Self::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
fn from(kind: serde_json::error::Error) -> Error {
|
||||
Error::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<dyn std::error::Error + Sync + Send + 'static>> for Error {
|
||||
#[inline]
|
||||
fn from(kind: Box<dyn std::error::Error + Sync + Send + 'static>) -> Self {
|
||||
Self::new(kind.to_string()).set_source(Some(kind.into()))
|
||||
fn from(kind: Box<dyn std::error::Error + Sync + Send + 'static>) -> Error {
|
||||
Error::new(kind.to_string()).set_source(Some(kind.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::ffi::NulError> for Error {
|
||||
#[inline]
|
||||
fn from(kind: std::ffi::NulError) -> Self {
|
||||
Self::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
fn from(kind: std::ffi::NulError) -> Error {
|
||||
Error::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<bincode::ErrorKind>> for Error {
|
||||
#[inline]
|
||||
fn from(kind: Box<bincode::ErrorKind>) -> Error {
|
||||
Error::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nix::Error> for Error {
|
||||
#[inline]
|
||||
fn from(kind: nix::Error) -> Self {
|
||||
Self::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
fn from(kind: nix::Error) -> Error {
|
||||
Error::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite3")]
|
||||
impl From<rusqlite::Error> for Error {
|
||||
#[inline]
|
||||
fn from(kind: rusqlite::Error) -> Self {
|
||||
Self::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
fn from(kind: rusqlite::Error) -> Error {
|
||||
Error::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<libloading::Error> for Error {
|
||||
#[inline]
|
||||
fn from(kind: libloading::Error) -> Self {
|
||||
Self::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
fn from(kind: libloading::Error) -> Error {
|
||||
Error::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Error {
|
||||
#[inline]
|
||||
fn from(kind: &str) -> Self {
|
||||
Self::new(kind.to_string())
|
||||
fn from(kind: &str) -> Error {
|
||||
Error::new(kind.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
#[inline]
|
||||
fn from(kind: String) -> Self {
|
||||
Self::new(kind)
|
||||
fn from(kind: String) -> Error {
|
||||
Error::new(kind)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nom::Err<(&[u8], nom::error::ErrorKind)>> for Error {
|
||||
#[inline]
|
||||
fn from(kind: nom::Err<(&[u8], nom::error::ErrorKind)>) -> Self {
|
||||
Self::new("Parsing error").set_source(Some(Arc::new(Self::new(kind.to_string()))))
|
||||
fn from(kind: nom::Err<(&[u8], nom::error::ErrorKind)>) -> Error {
|
||||
Error::new("Parsing error").set_source(Some(Arc::new(Error::new(kind.to_string()))))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nom::Err<(&str, nom::error::ErrorKind)>> for Error {
|
||||
#[inline]
|
||||
fn from(kind: nom::Err<(&str, nom::error::ErrorKind)>) -> Self {
|
||||
Self::new("Parsing error").set_details(kind.to_string())
|
||||
fn from(kind: nom::Err<(&str, nom::error::ErrorKind)>) -> Error {
|
||||
Error::new("Parsing error").set_details(kind.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::email::InvalidHeaderName> for Error {
|
||||
impl<'a> From<&'a mut Error> for Error {
|
||||
#[inline]
|
||||
fn from(kind: crate::email::InvalidHeaderName) -> Self {
|
||||
Self::new(kind.to_string())
|
||||
.set_source(Some(Arc::new(kind)))
|
||||
.set_kind(ErrorKind::Network(NetworkErrorKind::InvalidTLSConnection))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a mut Self> for Error {
|
||||
#[inline]
|
||||
fn from(kind: &'a mut Self) -> Self {
|
||||
fn from(kind: &'a mut Error) -> Error {
|
||||
kind.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Self> for Error {
|
||||
impl<'a> From<&'a Error> for Error {
|
||||
#[inline]
|
||||
fn from(kind: &'a Self) -> Self {
|
||||
fn from(kind: &'a Error) -> Error {
|
||||
kind.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<base64::DecodeError> for Error {
|
||||
// ----- imap-codec -----
|
||||
|
||||
impl From<imap_codec::core::LiteralError> for Error {
|
||||
#[inline]
|
||||
fn from(kind: base64::DecodeError) -> Self {
|
||||
Self::new("base64 decoding failed")
|
||||
.set_source(Some(Arc::new(kind)))
|
||||
.set_kind(ErrorKind::ValueError)
|
||||
fn from(error: imap_codec::core::LiteralError) -> Error {
|
||||
Error{
|
||||
summary: error.to_string().into(),
|
||||
details: None,
|
||||
source: Some(Arc::new(error)),
|
||||
kind: ErrorKind::Configuration,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
#![allow(unused)]
|
||||
#![allow(dead_code)]
|
||||
#![allow(clippy::useless_transmute)]
|
||||
#![allow(clippy::borrow_as_ptr)]
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
use libc::{off_t, time_t, FILE};
|
||||
|
||||
|
@ -4994,10 +4993,10 @@ pub type gpgme_set_global_flag = extern "C" fn(
|
|||
value: *const ::std::os::raw::c_char,
|
||||
) -> ::std::os::raw::c_int;
|
||||
pub type gpgme_check_version = unsafe extern "C" fn(
|
||||
req_version: *const ::std::os::raw::c_uchar,
|
||||
req_version: *const ::std::os::raw::c_char,
|
||||
) -> *const ::std::os::raw::c_char;
|
||||
pub type gpgme_check_version_internal = extern "C" fn(
|
||||
req_version: *const ::std::os::raw::c_uchar,
|
||||
req_version: *const ::std::os::raw::c_char,
|
||||
offset_sig_validity: usize,
|
||||
) -> *const ::std::os::raw::c_char;
|
||||
pub type gpgme_get_dirinfo =
|
||||
|
@ -5380,7 +5379,7 @@ fn bindgen_test_layout___va_list_tag() {
|
|||
concat!("Alignment of ", stringify!(__va_list_tag))
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { std::ptr::addr_of!((*(::std::ptr::null::<__va_list_tag>())).gp_offset) as usize },
|
||||
unsafe { &(*(::std::ptr::null::<__va_list_tag>())).gp_offset as *const _ as usize },
|
||||
0usize,
|
||||
concat!(
|
||||
"Offset of field: ",
|
||||
|
@ -5390,7 +5389,7 @@ fn bindgen_test_layout___va_list_tag() {
|
|||
)
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { std::ptr::addr_of!((*(::std::ptr::null::<__va_list_tag>())).fp_offset) as usize },
|
||||
unsafe { &(*(::std::ptr::null::<__va_list_tag>())).fp_offset as *const _ as usize },
|
||||
4usize,
|
||||
concat!(
|
||||
"Offset of field: ",
|
||||
|
@ -5400,9 +5399,7 @@ fn bindgen_test_layout___va_list_tag() {
|
|||
)
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
std::ptr::addr_of!((*(::std::ptr::null::<__va_list_tag>())).overflow_arg_area) as usize
|
||||
},
|
||||
unsafe { &(*(::std::ptr::null::<__va_list_tag>())).overflow_arg_area as *const _ as usize },
|
||||
8usize,
|
||||
concat!(
|
||||
"Offset of field: ",
|
||||
|
@ -5412,9 +5409,7 @@ fn bindgen_test_layout___va_list_tag() {
|
|||
)
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
std::ptr::addr_of!((*(::std::ptr::null::<__va_list_tag>())).reg_save_area) as usize
|
||||
},
|
||||
unsafe { &(*(::std::ptr::null::<__va_list_tag>())).reg_save_area as *const _ as usize },
|
||||
16usize,
|
||||
concat!(
|
||||
"Offset of field: ",
|
||||
|
|
|
@ -108,10 +108,11 @@ impl<'de> Deserialize<'de> for LocateKey {
|
|||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
<String>::deserialize(deserializer).map_or_else(
|
||||
|_| Err(de::Error::custom("LocateKey value must be a string.")),
|
||||
|s| Self::from_string_de::<'de, D, String>(s),
|
||||
)
|
||||
if let Ok(s) = <String>::deserialize(deserializer) {
|
||||
LocateKey::from_string_de::<'de, D, String>(s)
|
||||
} else {
|
||||
Err(de::Error::custom("LocateKey value must be a string."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,16 +131,16 @@ impl LocateKey {
|
|||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(match s.as_ref().trim() {
|
||||
s if s.eq_ignore_ascii_case("cert") => Self::CERT,
|
||||
s if s.eq_ignore_ascii_case("pka") => Self::PKA,
|
||||
s if s.eq_ignore_ascii_case("dane") => Self::DANE,
|
||||
s if s.eq_ignore_ascii_case("wkd") => Self::WKD,
|
||||
s if s.eq_ignore_ascii_case("ldap") => Self::LDAP,
|
||||
s if s.eq_ignore_ascii_case("keyserver") => Self::KEYSERVER,
|
||||
s if s.eq_ignore_ascii_case("keyserver-url") => Self::KEYSERVER_URL,
|
||||
s if s.eq_ignore_ascii_case("local") => Self::LOCAL,
|
||||
s if s.eq_ignore_ascii_case("cert") => LocateKey::CERT,
|
||||
s if s.eq_ignore_ascii_case("pka") => LocateKey::PKA,
|
||||
s if s.eq_ignore_ascii_case("dane") => LocateKey::DANE,
|
||||
s if s.eq_ignore_ascii_case("wkd") => LocateKey::WKD,
|
||||
s if s.eq_ignore_ascii_case("ldap") => LocateKey::LDAP,
|
||||
s if s.eq_ignore_ascii_case("keyserver") => LocateKey::KEYSERVER,
|
||||
s if s.eq_ignore_ascii_case("keyserver-url") => LocateKey::KEYSERVER_URL,
|
||||
s if s.eq_ignore_ascii_case("local") => LocateKey::LOCAL,
|
||||
combination if combination.contains(',') => {
|
||||
let mut ret = Self::NODEFAULT;
|
||||
let mut ret = LocateKey::NODEFAULT;
|
||||
for c in combination.trim().split(',') {
|
||||
ret |= Self::from_string_de::<'de, D, &str>(c.trim())?;
|
||||
}
|
||||
|
@ -156,7 +157,7 @@ impl LocateKey {
|
|||
|
||||
impl std::fmt::Display for LocateKey {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
if *self == Self::NODEFAULT {
|
||||
if *self == LocateKey::NODEFAULT {
|
||||
write!(fmt, "clear,nodefault")
|
||||
} else {
|
||||
let mut accum = String::new();
|
||||
|
@ -168,13 +169,13 @@ impl std::fmt::Display for LocateKey {
|
|||
}
|
||||
}};
|
||||
}
|
||||
is_set!(Self::CERT, "cert");
|
||||
is_set!(Self::PKA, "pka");
|
||||
is_set!(Self::WKD, "wkd");
|
||||
is_set!(Self::LDAP, "ldap");
|
||||
is_set!(Self::KEYSERVER, "keyserver");
|
||||
is_set!(Self::KEYSERVER_URL, "keyserver-url");
|
||||
is_set!(Self::LOCAL, "local");
|
||||
is_set!(LocateKey::CERT, "cert");
|
||||
is_set!(LocateKey::PKA, "pka");
|
||||
is_set!(LocateKey::WKD, "wkd");
|
||||
is_set!(LocateKey::LDAP, "ldap");
|
||||
is_set!(LocateKey::KEYSERVER, "keyserver");
|
||||
is_set!(LocateKey::KEYSERVER_URL, "keyserver-url");
|
||||
is_set!(LocateKey::LOCAL, "local");
|
||||
accum.pop();
|
||||
write!(fmt, "{}", accum)
|
||||
}
|
||||
|
@ -222,7 +223,8 @@ impl Context {
|
|||
pub fn new() -> Result<Self> {
|
||||
let lib =
|
||||
Arc::new(unsafe { libloading::Library::new(libloading::library_filename("gpgme")) }?);
|
||||
if unsafe { call!(&lib, gpgme_check_version)(GPGME_VERSION.as_bytes().as_ptr()) }.is_null()
|
||||
if unsafe { call!(&lib, gpgme_check_version)(GPGME_VERSION.as_bytes().as_ptr() as *mut _) }
|
||||
.is_null()
|
||||
{
|
||||
return Err(Error::new(format!(
|
||||
"Could not use libgpgme: requested version compatible with {} but got {}",
|
||||
|
@ -263,7 +265,7 @@ impl Context {
|
|||
gpgme_error_try(&lib, call!(&lib, gpgme_new)(&mut ptr))?;
|
||||
call!(&lib, gpgme_set_io_cbs)(ptr, &mut io_cbs);
|
||||
}
|
||||
let ret = Self {
|
||||
let ret = Context {
|
||||
inner: Arc::new(ContextInner {
|
||||
inner: core::ptr::NonNull::new(ptr)
|
||||
.ok_or_else(|| Error::new("Could not use libgpgme").set_kind(ErrorKind::Bug))?,
|
||||
|
@ -584,7 +586,7 @@ impl Context {
|
|||
.map(|cs| cs.as_ptr())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
as *const ::std::os::raw::c_char,
|
||||
secret.into(),
|
||||
if secret { 1 } else { 0 },
|
||||
),
|
||||
)?;
|
||||
}
|
||||
|
@ -661,9 +663,8 @@ impl Context {
|
|||
call!(&ctx.lib, gpgme_op_keylist_end)(ctx.inner.as_ptr()),
|
||||
)?;
|
||||
}
|
||||
io_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
let io_state_lck = io_state.lock().unwrap();
|
||||
io_state_lck
|
||||
.done
|
||||
.lock()
|
||||
.unwrap()
|
||||
|
@ -781,13 +782,13 @@ impl Context {
|
|||
}
|
||||
}))
|
||||
.await;
|
||||
{
|
||||
let rcv = io_state.lock().unwrap().receiver.clone();
|
||||
let _ = rcv.recv().await;
|
||||
}
|
||||
io_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
let rcv = {
|
||||
let io_state_lck = io_state.lock().unwrap();
|
||||
io_state_lck.receiver.clone()
|
||||
};
|
||||
let _ = rcv.recv().await;
|
||||
let io_state_lck = io_state.lock().unwrap();
|
||||
io_state_lck
|
||||
.done
|
||||
.lock()
|
||||
.unwrap()
|
||||
|
@ -886,11 +887,13 @@ impl Context {
|
|||
}
|
||||
}))
|
||||
.await;
|
||||
let rcv = { io_state.lock().unwrap().receiver.clone() };
|
||||
let rcv = {
|
||||
let io_state_lck = io_state.lock().unwrap();
|
||||
io_state_lck.receiver.clone()
|
||||
};
|
||||
let _ = rcv.recv().await;
|
||||
io_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
let io_state_lck = io_state.lock().unwrap();
|
||||
io_state_lck
|
||||
.done
|
||||
.lock()
|
||||
.unwrap()
|
||||
|
@ -1094,11 +1097,13 @@ impl Context {
|
|||
}
|
||||
}))
|
||||
.await;
|
||||
let rcv = { io_state.lock().unwrap().receiver.clone() };
|
||||
let rcv = {
|
||||
let io_state_lck = io_state.lock().unwrap();
|
||||
io_state_lck.receiver.clone()
|
||||
};
|
||||
let _ = rcv.recv().await;
|
||||
io_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
let io_state_lck = io_state.lock().unwrap();
|
||||
io_state_lck
|
||||
.done
|
||||
.lock()
|
||||
.unwrap()
|
||||
|
@ -1225,7 +1230,7 @@ impl Clone for Key {
|
|||
unsafe {
|
||||
call!(&self.lib, gpgme_key_ref)(self.inner.inner.as_ptr());
|
||||
}
|
||||
Self {
|
||||
Key {
|
||||
inner: self.inner.clone(),
|
||||
lib,
|
||||
}
|
||||
|
@ -1235,7 +1240,7 @@ impl Clone for Key {
|
|||
impl Key {
|
||||
#[inline(always)]
|
||||
fn new(inner: KeyInner, lib: Arc<libloading::Library>) -> Self {
|
||||
Self { inner, lib }
|
||||
Key { inner, lib }
|
||||
}
|
||||
|
||||
pub fn primary_uid(&self) -> Option<Address> {
|
||||
|
@ -1314,7 +1319,7 @@ impl std::fmt::Debug for Key {
|
|||
}
|
||||
|
||||
impl PartialEq for Key {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
fn eq(&self, other: &Key) -> bool {
|
||||
self.fingerprint() == other.fingerprint()
|
||||
}
|
||||
}
|
||||
|
|
350
melib/src/lib.rs
350
melib/src/lib.rs
|
@ -19,41 +19,6 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#![deny(
|
||||
/* groups */
|
||||
clippy::correctness,
|
||||
clippy::suspicious,
|
||||
clippy::complexity,
|
||||
clippy::perf,
|
||||
clippy::cargo,
|
||||
clippy::nursery,
|
||||
clippy::style,
|
||||
/* restriction */
|
||||
clippy::dbg_macro,
|
||||
clippy::rc_buffer,
|
||||
clippy::as_underscore,
|
||||
clippy::assertions_on_result_states,
|
||||
/* rustdoc */
|
||||
rustdoc::broken_intra_doc_links,
|
||||
/* pedantic */
|
||||
//clippy::cast_lossless,
|
||||
//clippy::cast_possible_wrap,
|
||||
//clippy::ptr_as_ptr,
|
||||
//clippy::bool_to_int_with_if,
|
||||
clippy::borrow_as_ptr,
|
||||
clippy::case_sensitive_file_extension_comparisons,
|
||||
//clippy::cast_lossless,
|
||||
//clippy::cast_ptr_alignment,
|
||||
)]
|
||||
#![allow(
|
||||
clippy::option_if_let_else,
|
||||
clippy::missing_const_for_fn,
|
||||
clippy::significant_drop_tightening,
|
||||
clippy::multiple_crate_versions,
|
||||
clippy::significant_drop_in_scrutinee,
|
||||
clippy::cognitive_complexity
|
||||
)]
|
||||
|
||||
//! A crate that performs mail client operations such as
|
||||
//! - Hold an [`Envelope`](./email/struct.Envelope.html) with methods convenient
|
||||
//! for mail client use. (see module [`email`](./email/index.html))
|
||||
|
@ -74,6 +39,8 @@
|
|||
//! - Basic mail account configuration to use with
|
||||
//! [`backends`](./backends/index.html) (see module
|
||||
//! [`conf`](./conf/index.html))
|
||||
//! - Parser combinators (see module [`parsec`](./parsec/index.html))
|
||||
//! - A `ShellExpandTrait` to expand paths like a shell.
|
||||
//! - A `debug` macro that works like `std::dbg` but for multiple threads. (see
|
||||
//! [`debug` macro](./macro.debug.html))
|
||||
|
||||
|
@ -117,10 +84,12 @@ pub mod dbg {
|
|||
#[cfg(feature = "unicode_algorithms")]
|
||||
pub mod text_processing;
|
||||
|
||||
pub use utils::{
|
||||
datetime::UnixTimestamp,
|
||||
logging::{LogLevel, StderrLogger},
|
||||
};
|
||||
pub mod datetime;
|
||||
pub use datetime::UnixTimestamp;
|
||||
|
||||
#[macro_use]
|
||||
mod logging;
|
||||
pub use self::logging::{LogLevel, StderrLogger};
|
||||
|
||||
pub mod addressbook;
|
||||
pub use addressbook::*;
|
||||
|
@ -134,18 +103,19 @@ pub use conf::*;
|
|||
pub mod email;
|
||||
pub use email::*;
|
||||
pub mod error;
|
||||
pub use error::*;
|
||||
pub use crate::error::*;
|
||||
pub mod thread;
|
||||
pub use thread::*;
|
||||
pub mod connections;
|
||||
pub mod parsec;
|
||||
pub mod search;
|
||||
|
||||
#[macro_use]
|
||||
pub mod utils;
|
||||
|
||||
#[cfg(feature = "gpgme")]
|
||||
pub mod gpgme;
|
||||
#[cfg(feature = "smtp")]
|
||||
pub mod smtp;
|
||||
#[cfg(feature = "sqlite3")]
|
||||
pub mod sqlite3;
|
||||
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
@ -166,16 +136,16 @@ pub extern crate xdg_utils;
|
|||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[repr(transparent)]
|
||||
pub struct BytesDisplay(pub usize);
|
||||
pub struct Bytes(pub usize);
|
||||
|
||||
impl BytesDisplay {
|
||||
impl Bytes {
|
||||
pub const KILOBYTE: f64 = 1024.0;
|
||||
pub const MEGABYTE: f64 = Self::KILOBYTE * 1024.0;
|
||||
pub const GIGABYTE: f64 = Self::MEGABYTE * 1024.0;
|
||||
pub const PETABYTE: f64 = Self::GIGABYTE * 1024.0;
|
||||
}
|
||||
|
||||
impl core::fmt::Display for BytesDisplay {
|
||||
impl core::fmt::Display for Bytes {
|
||||
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
let bytes: f64 = self.0 as f64;
|
||||
if bytes == 0.0 {
|
||||
|
@ -194,4 +164,290 @@ impl core::fmt::Display for BytesDisplay {
|
|||
}
|
||||
}
|
||||
|
||||
pub use utils::shellexpand::ShellExpandTrait;
|
||||
pub use shellexpand::ShellExpandTrait;
|
||||
pub mod shellexpand {
|
||||
|
||||
#[cfg(not(any(target_os = "netbsd", target_os = "macos")))]
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
os::unix::ffi::OsStrExt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use smallvec::SmallVec;
|
||||
|
||||
pub trait ShellExpandTrait {
|
||||
fn expand(&self) -> PathBuf;
|
||||
fn complete(&self, force: bool) -> SmallVec<[String; 128]>;
|
||||
}
|
||||
|
||||
impl ShellExpandTrait for Path {
|
||||
fn expand(&self) -> PathBuf {
|
||||
let mut ret = PathBuf::new();
|
||||
for c in self.components() {
|
||||
let c_to_str = c.as_os_str().to_str();
|
||||
match c_to_str {
|
||||
Some("~") => {
|
||||
if let Ok(home_dir) = std::env::var("HOME") {
|
||||
ret.push(home_dir)
|
||||
} else {
|
||||
return PathBuf::new();
|
||||
}
|
||||
}
|
||||
Some(var) if var.starts_with('$') => {
|
||||
let env_name = var.split_at(1).1;
|
||||
if env_name.chars().all(char::is_uppercase) {
|
||||
ret.push(std::env::var(env_name).unwrap_or_default());
|
||||
} else {
|
||||
ret.push(c);
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
ret.push(c);
|
||||
}
|
||||
None => {
|
||||
/* path is invalid */
|
||||
return PathBuf::new();
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn complete(&self, force: bool) -> SmallVec<[String; 128]> {
|
||||
use libc::dirent64;
|
||||
use nix::fcntl::OFlag;
|
||||
const BUF_SIZE: ::libc::size_t = 8 << 10;
|
||||
|
||||
let (prefix, _match) = if self.as_os_str().as_bytes().ends_with(b"/.") {
|
||||
(self.components().as_path(), OsStr::from_bytes(b"."))
|
||||
} else if self.exists() && (!force || self.as_os_str().as_bytes().ends_with(b"/")) {
|
||||
return SmallVec::new();
|
||||
} else {
|
||||
let last_component = self
|
||||
.components()
|
||||
.last()
|
||||
.map(|c| c.as_os_str())
|
||||
.unwrap_or_else(|| OsStr::from_bytes(b""));
|
||||
let prefix = if let Some(p) = self.parent() {
|
||||
p
|
||||
} else {
|
||||
return SmallVec::new();
|
||||
};
|
||||
(prefix, last_component)
|
||||
};
|
||||
|
||||
let dir = match ::nix::dir::Dir::openat(
|
||||
::libc::AT_FDCWD,
|
||||
prefix,
|
||||
OFlag::O_DIRECTORY | OFlag::O_NOATIME | OFlag::O_RDONLY | OFlag::O_CLOEXEC,
|
||||
::nix::sys::stat::Mode::S_IRUSR | ::nix::sys::stat::Mode::S_IXUSR,
|
||||
)
|
||||
.or_else(|_| {
|
||||
::nix::dir::Dir::openat(
|
||||
::libc::AT_FDCWD,
|
||||
prefix,
|
||||
OFlag::O_DIRECTORY | OFlag::O_RDONLY | OFlag::O_CLOEXEC,
|
||||
::nix::sys::stat::Mode::S_IRUSR | ::nix::sys::stat::Mode::S_IXUSR,
|
||||
)
|
||||
}) {
|
||||
Ok(dir) => dir,
|
||||
Err(err) => {
|
||||
debug!(prefix);
|
||||
debug!(err);
|
||||
return SmallVec::new();
|
||||
}
|
||||
};
|
||||
|
||||
let mut buf: Vec<u8> = Vec::with_capacity(BUF_SIZE);
|
||||
let mut entries = SmallVec::new();
|
||||
loop {
|
||||
let n: i64 = unsafe {
|
||||
::libc::syscall(
|
||||
::libc::SYS_getdents64,
|
||||
dir.as_raw_fd(),
|
||||
buf.as_ptr(),
|
||||
BUF_SIZE - 256,
|
||||
)
|
||||
};
|
||||
if n < 0 {
|
||||
return SmallVec::new();
|
||||
} else if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let n = n as usize;
|
||||
unsafe {
|
||||
buf.set_len(n);
|
||||
}
|
||||
let mut pos = 0;
|
||||
while pos < n {
|
||||
let dir = unsafe { std::mem::transmute::<&[u8], &[dirent64]>(&buf[pos..]) };
|
||||
let entry = unsafe { std::ffi::CStr::from_ptr(dir[0].d_name.as_ptr()) };
|
||||
if entry.to_bytes() != b"." && entry.to_bytes() != b".." {
|
||||
if entry.to_bytes().starts_with(_match.as_bytes()) {
|
||||
if dir[0].d_type == ::libc::DT_DIR && !entry.to_bytes().ends_with(b"/")
|
||||
{
|
||||
let mut s = unsafe {
|
||||
String::from_utf8_unchecked(
|
||||
entry.to_bytes()[_match.as_bytes().len()..].to_vec(),
|
||||
)
|
||||
};
|
||||
s.push('/');
|
||||
entries.push(s);
|
||||
} else {
|
||||
entries.push(unsafe {
|
||||
String::from_utf8_unchecked(
|
||||
entry.to_bytes()[_match.as_bytes().len()..].to_vec(),
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
pos += dir[0].d_reclen as usize;
|
||||
}
|
||||
// https://github.com/romkatv/gitstatus/blob/caf44f7aaf33d0f46e6749e50595323c277e0908/src/dir.cc
|
||||
// "It's tempting to bail here if n + sizeof(linux_dirent64) +
|
||||
// 512 <= n. After all, there was enough space
|
||||
// for another entry but SYS_getdents64 didn't write it, so this
|
||||
// must be the end of the directory listing,
|
||||
// right? Unfortunately, no. SYS_getdents64 is finicky.
|
||||
// It sometimes writes a partial list of entries even if the
|
||||
// full list would fit."
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn complete(&self, force: bool) -> SmallVec<[String; 128]> {
|
||||
let mut entries = SmallVec::new();
|
||||
let (prefix, _match) = {
|
||||
if self.exists() && (!force || self.as_os_str().as_bytes().ends_with(b"/")) {
|
||||
// println!("{} {:?}", self.display(), self.components().last());
|
||||
return entries;
|
||||
} else {
|
||||
let last_component = self
|
||||
.components()
|
||||
.last()
|
||||
.map(|c| c.as_os_str())
|
||||
.unwrap_or_else(|| OsStr::from_bytes(b""));
|
||||
let prefix = if let Some(p) = self.parent() {
|
||||
p
|
||||
} else {
|
||||
return entries;
|
||||
};
|
||||
(prefix, last_component)
|
||||
}
|
||||
};
|
||||
if force && self.is_dir() && !self.as_os_str().as_bytes().ends_with(b"/") {
|
||||
entries.push("/".to_string());
|
||||
}
|
||||
|
||||
if let Ok(iter) = std::fs::read_dir(&prefix) {
|
||||
for entry in iter.flatten() {
|
||||
if entry.path().as_os_str().as_bytes() != b"."
|
||||
&& entry.path().as_os_str().as_bytes() != b".."
|
||||
&& entry
|
||||
.path()
|
||||
.as_os_str()
|
||||
.as_bytes()
|
||||
.starts_with(_match.as_bytes())
|
||||
{
|
||||
if entry.path().is_dir()
|
||||
&& !entry.path().as_os_str().as_bytes().ends_with(b"/")
|
||||
{
|
||||
let mut s = unsafe {
|
||||
String::from_utf8_unchecked(
|
||||
entry.path().as_os_str().as_bytes()[_match.as_bytes().len()..]
|
||||
.to_vec(),
|
||||
)
|
||||
};
|
||||
s.push('/');
|
||||
entries.push(s);
|
||||
} else {
|
||||
entries.push(unsafe {
|
||||
String::from_utf8_unchecked(
|
||||
entry.path().as_os_str().as_bytes()[_match.as_bytes().len()..]
|
||||
.to_vec(),
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
entries
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shellexpandtrait() {
|
||||
assert!(Path::new("~").expand().complete(false).is_empty());
|
||||
assert!(!Path::new("~").expand().complete(true).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! declare_u64_hash {
|
||||
($type_name:ident) => {
|
||||
#[derive(
|
||||
Hash,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Debug,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Copy,
|
||||
Clone,
|
||||
)]
|
||||
#[repr(transparent)]
|
||||
pub struct $type_name(pub u64);
|
||||
|
||||
impl $type_name {
|
||||
#[inline(always)]
|
||||
pub fn from_bytes(bytes: &[u8]) -> Self {
|
||||
use std::{collections::hash_map::DefaultHasher, hash::Hasher};
|
||||
let mut h = DefaultHasher::new();
|
||||
h.write(bytes);
|
||||
Self(h.finish())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn to_be_bytes(self) -> [u8; 8] {
|
||||
self.0.to_be_bytes()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn is_null(self) -> bool {
|
||||
self.0 == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for $type_name {
|
||||
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(fmt, "{}", self.0)
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "sqlite3")]
|
||||
impl rusqlite::types::ToSql for $type_name {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
Ok(rusqlite::types::ToSqlOutput::from(self.0 as i64))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite3")]
|
||||
impl rusqlite::types::FromSql for $type_name {
|
||||
fn column_result(
|
||||
value: rusqlite::types::ValueRef,
|
||||
) -> rusqlite::types::FromSqlResult<Self> {
|
||||
let b: i64 = rusqlite::types::FromSql::column_result(value)?;
|
||||
|
||||
Ok($type_name(b as u64))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ use std::{
|
|||
|
||||
use log::{Level, LevelFilter, Log, Metadata, Record};
|
||||
|
||||
#[derive(Copy, Clone, Default, Eq, PartialEq, PartialOrd, Hash, Debug, Serialize, Deserialize)]
|
||||
#[derive(Copy, Clone, Default, PartialEq, PartialOrd, Hash, Debug, Serialize, Deserialize)]
|
||||
#[repr(u8)]
|
||||
pub enum LogLevel {
|
||||
OFF = 0,
|
||||
|
@ -46,12 +46,12 @@ pub enum LogLevel {
|
|||
impl From<u8> for LogLevel {
|
||||
fn from(verbosity: u8) -> Self {
|
||||
match verbosity {
|
||||
0 => Self::OFF,
|
||||
1 => Self::ERROR,
|
||||
2 => Self::WARN,
|
||||
3 => Self::INFO,
|
||||
4 => Self::DEBUG,
|
||||
_ => Self::TRACE,
|
||||
0 => LogLevel::OFF,
|
||||
1 => LogLevel::ERROR,
|
||||
2 => LogLevel::WARN,
|
||||
3 => LogLevel::INFO,
|
||||
4 => LogLevel::DEBUG,
|
||||
_ => LogLevel::TRACE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ impl std::fmt::Display for LogLevel {
|
|||
|
||||
use LogLevel::*;
|
||||
|
||||
#[derive(Copy, Clone, Default, Eq, PartialEq, PartialOrd, Hash, Debug, Serialize, Deserialize)]
|
||||
#[derive(Copy, Clone, Default, PartialEq, PartialOrd, Hash, Debug, Serialize, Deserialize)]
|
||||
pub enum Destination {
|
||||
File,
|
||||
#[default]
|
||||
|
@ -175,8 +175,8 @@ impl StderrLogger {
|
|||
.create(true) /* a new file will be created if the file does not yet already exist.*/
|
||||
.read(true)
|
||||
.open(data_dir.place_data_file("meli.log").unwrap()).unwrap();
|
||||
Self {
|
||||
dest: Arc::new(Mutex::new(BufWriter::new(log_file))),
|
||||
StderrLogger {
|
||||
dest: Arc::new(Mutex::new(BufWriter::new(log_file).into())),
|
||||
level: Arc::new(AtomicU8::new(level as u8)),
|
||||
print_level: true,
|
||||
print_module_names: true,
|
||||
|
@ -188,8 +188,8 @@ impl StderrLogger {
|
|||
};
|
||||
#[cfg(test)]
|
||||
let logger = {
|
||||
Self {
|
||||
dest: Arc::new(Mutex::new(BufWriter::new(std::io::stderr()))),
|
||||
StderrLogger {
|
||||
dest: Arc::new(Mutex::new(BufWriter::new(std::io::stderr()).into())),
|
||||
level: Arc::new(AtomicU8::new(level as u8)),
|
||||
print_level: true,
|
||||
print_module_names: true,
|
||||
|
@ -220,14 +220,14 @@ impl StderrLogger {
|
|||
|
||||
#[cfg(not(test))]
|
||||
pub fn change_log_dest(&mut self, path: PathBuf) {
|
||||
use crate::utils::shellexpand::ShellExpandTrait;
|
||||
use crate::shellexpand::ShellExpandTrait;
|
||||
|
||||
let path = path.expand(); // expand shell stuff
|
||||
let mut dest = self.dest.lock().unwrap();
|
||||
*dest = BufWriter::new(OpenOptions::new().append(true) /* writes will append to a file instead of overwriting previous contents */
|
||||
.create(true) /* a new file will be created if the file does not yet already exist.*/
|
||||
.read(true)
|
||||
.open(path).unwrap());
|
||||
.open(path).unwrap()).into();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,7 +254,7 @@ impl Log for StderrLogger {
|
|||
) -> Option<()> {
|
||||
writer
|
||||
.write_all(
|
||||
super::datetime::timestamp_to_string(super::datetime::now(), None, false)
|
||||
crate::datetime::timestamp_to_string(crate::datetime::now(), None, false)
|
||||
.as_bytes(),
|
||||
)
|
||||
.ok()?;
|
||||
|
@ -279,7 +279,7 @@ impl Log for StderrLogger {
|
|||
self.debug_dest,
|
||||
record.metadata().level() <= Level::from(self.log_level()),
|
||||
) {
|
||||
(Destination::None, false) => {}
|
||||
(Destination::None, false) => return,
|
||||
(Destination::None | Destination::File, _) => {
|
||||
_ = self.dest.lock().ok().and_then(|mut d| {
|
||||
write(
|
|
@ -21,10 +21,6 @@
|
|||
|
||||
//! Parser combinators.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::utils::datetime::{parse_timestamp_from_string, UnixTimestamp};
|
||||
|
||||
pub type Result<'a, Output> = std::result::Result<(&'a str, Output), &'a str>;
|
||||
|
||||
pub trait Parser<'a, Output> {
|
||||
|
@ -104,7 +100,11 @@ where
|
|||
{
|
||||
move |input| {
|
||||
parser.parse(input).and_then(|(next_input, result)| {
|
||||
map_fn(result).map_or_else(|_| Err(next_input), |res| Ok((next_input, res)))
|
||||
if let Ok(res) = map_fn(result) {
|
||||
Ok((next_input, res))
|
||||
} else {
|
||||
Err(next_input)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -383,17 +383,14 @@ where
|
|||
}
|
||||
|
||||
pub fn any_char(input: &str) -> Result<char> {
|
||||
input
|
||||
.chars()
|
||||
.next()
|
||||
.map_or_else(|| Err(input), |next| Ok((&input[next.len_utf8()..], next)))
|
||||
match input.chars().next() {
|
||||
Some(next) => Ok((&input[next.len_utf8()..], next)),
|
||||
_ => Err(input),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn string<'a>() -> impl Parser<'a, String> {
|
||||
one_or_more(pred(any_char, |c| {
|
||||
c.is_alphanumeric() || (c.is_ascii_graphic() && !['"', '(', ')', ' '].contains(c))
|
||||
}))
|
||||
.map(|r| r.into_iter().collect())
|
||||
one_or_more(pred(any_char, |c| c.is_alphanumeric())).map(|r| r.into_iter().collect())
|
||||
}
|
||||
|
||||
pub fn space1<'a>() -> impl Parser<'a, Vec<char>> {
|
||||
|
@ -441,8 +438,8 @@ pub fn is_not<'a>(slice: &'static [u8]) -> impl Parser<'a, &'a str> {
|
|||
/// Try alternative parsers in order until one succeeds.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use melib::utils::parsec::{Parser, quoted_slice, match_literal, alt, delimited, prefix};
|
||||
/// #
|
||||
/// # use melib::parsec::{Parser, quoted_slice, match_literal, alt, delimited, prefix};
|
||||
///
|
||||
/// let parser = |input| {
|
||||
/// alt([
|
||||
/// delimited(match_literal("{"), quoted_slice(), match_literal("}")),
|
||||
|
@ -452,8 +449,9 @@ pub fn is_not<'a>(slice: &'static [u8]) -> impl Parser<'a, &'a str> {
|
|||
/// };
|
||||
///
|
||||
/// let input1: &str = "{\"quoted\"}";
|
||||
/// assert_eq!(Ok(("", "quoted")), parser.parse(input1));
|
||||
/// let input2: &str = "[\"quoted\"]";
|
||||
/// assert_eq!(Ok(("", "quoted")), parser.parse(input1));
|
||||
///
|
||||
/// assert_eq!(Ok(("", "quoted")), parser.parse(input2));
|
||||
/// ```
|
||||
pub fn alt<'a, P, A, const N: usize>(parsers: [P; N]) -> impl Parser<'a, A>
|
||||
|
@ -579,7 +577,7 @@ pub fn take<'a>(count: usize) -> impl Parser<'a, &'a str> {
|
|||
///
|
||||
///```rust
|
||||
/// # use std::str::FromStr;
|
||||
/// # use melib::utils::parsec::{Parser, delimited, match_literal, map_res, is_a, take_literal};
|
||||
/// # use melib::parsec::{Parser, delimited, match_literal, map_res, is_a, take_literal};
|
||||
/// let lit: &str = "{31}\r\nThere is no script by that name\r\n";
|
||||
/// assert_eq!(
|
||||
/// take_literal(delimited(
|
||||
|
@ -601,19 +599,6 @@ 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"), usize::from_str)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashMap;
|
||||
|
@ -624,12 +609,12 @@ mod test {
|
|||
fn test_parsec() {
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum JsonValue {
|
||||
String(String),
|
||||
Number(f64),
|
||||
Bool(bool),
|
||||
Null,
|
||||
Object(HashMap<String, JsonValue>),
|
||||
Array(Vec<JsonValue>),
|
||||
JsonString(String),
|
||||
JsonNumber(f64),
|
||||
JsonBool(bool),
|
||||
JsonNull,
|
||||
JsonObject(HashMap<String, JsonValue>),
|
||||
JsonArray(Vec<JsonValue>),
|
||||
}
|
||||
|
||||
fn parse_value<'a>() -> impl Parser<'a, JsonValue> {
|
||||
|
@ -639,16 +624,16 @@ mod test {
|
|||
either(
|
||||
either(
|
||||
either(
|
||||
map(parse_bool(), JsonValue::Bool),
|
||||
map(parse_null(), |()| JsonValue::Null),
|
||||
map(parse_bool(), JsonValue::JsonBool),
|
||||
map(parse_null(), |()| JsonValue::JsonNull),
|
||||
),
|
||||
map(parse_array(), JsonValue::Array),
|
||||
map(parse_array(), JsonValue::JsonArray),
|
||||
),
|
||||
map(parse_object(), JsonValue::Object),
|
||||
map(parse_object(), JsonValue::JsonObject),
|
||||
),
|
||||
map(parse_number(), JsonValue::Number),
|
||||
map(parse_number(), JsonValue::JsonNumber),
|
||||
),
|
||||
map(quoted_string(), JsonValue::String),
|
||||
map(quoted_string(), JsonValue::JsonString),
|
||||
)
|
||||
.parse(input)
|
||||
}
|
||||
|
@ -710,15 +695,15 @@ mod test {
|
|||
}
|
||||
}
|
||||
assert_eq!(
|
||||
Ok(("", JsonValue::String("a".to_string()))),
|
||||
Ok(("", JsonValue::JsonString("a".to_string()))),
|
||||
parse_value().parse(r#""a""#)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(("", JsonValue::Bool(true))),
|
||||
Ok(("", JsonValue::JsonBool(true))),
|
||||
parse_value().parse(r#"true"#)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(("", JsonValue::Object(HashMap::default()))),
|
||||
Ok(("", JsonValue::JsonObject(HashMap::default()))),
|
||||
parse_value().parse(r#"{}"#)
|
||||
);
|
||||
println!("{:?}", parse_value().parse(r#"{"a":true}"#));
|
|
@ -24,10 +24,7 @@ use std::{borrow::Cow, convert::TryFrom};
|
|||
pub use query_parser::query;
|
||||
use Query::*;
|
||||
|
||||
use crate::utils::{
|
||||
datetime::{formats, UnixTimestamp},
|
||||
parsec::*,
|
||||
};
|
||||
use crate::{parsec::*, UnixTimestamp};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize)]
|
||||
pub enum Query {
|
||||
|
@ -53,18 +50,6 @@ 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 {
|
||||
|
@ -100,45 +85,14 @@ 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),
|
||||
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
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Query {
|
||||
type Error = crate::error::Error;
|
||||
fn try_from(t: &str) -> crate::error::Result<Self> {
|
||||
fn try_from(t: &str) -> crate::error::Result<Query> {
|
||||
query()
|
||||
.parse_complete(t)
|
||||
.map(|(_, q)| q)
|
||||
|
@ -149,88 +103,6 @@ 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::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:")),
|
||||
|
@ -271,14 +143,6 @@ 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"))
|
||||
|
@ -369,8 +233,8 @@ pub mod query_parser {
|
|||
/// # Invocation
|
||||
/// ```
|
||||
/// use melib::{
|
||||
/// parsec::Parser,
|
||||
/// search::{query, Query},
|
||||
/// utils::parsec::Parser,
|
||||
/// };
|
||||
///
|
||||
/// let input = "test";
|
||||
|
@ -385,17 +249,8 @@ 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)
|
||||
|
@ -427,12 +282,105 @@ 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, And(Box::new(query_a), Box::new(query_b))))
|
||||
Ok((rest, Or(Box::new(query_a), Box::new(query_b))))
|
||||
} else {
|
||||
Ok((rest, query_a))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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((
|
||||
"",
|
||||
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!(
|
||||
Ok(("", Flags(vec!["f".to_string()]))),
|
||||
query().parse_complete("tags:f")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
|
@ -458,114 +406,3 @@ impl<'de> Deserialize<'de> for Query {
|
|||
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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::utils::parsec::*;
|
||||
use crate::parsec::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RuleBlock(pub Vec<Rule>);
|
||||
|
@ -142,7 +142,7 @@ pub enum ZoneRule {
|
|||
/// "iso8601" => the date and time in restricted ISO 8601 format.
|
||||
Iso8601,
|
||||
/// "std11" => the date and time in a format appropriate
|
||||
/// for use in a Date: header field (`RFC2822`).
|
||||
/// for use in a Date: header field [RFC2822].
|
||||
Std11,
|
||||
/// "zone" => the time zone in use. If the user specified a
|
||||
///time zone with ":zone", "zone" will
|
||||
|
@ -525,7 +525,7 @@ pub mod parser {
|
|||
either(
|
||||
map(
|
||||
right(ws(parse_token("exists")), ws(parse_string_list())),
|
||||
ConditionRule::Exists,
|
||||
|l| ConditionRule::Exists(l),
|
||||
),
|
||||
map(
|
||||
right(ws(parse_token("size")), ws(parse_sieve_integer_operator())),
|
||||
|
@ -540,14 +540,12 @@ pub mod parser {
|
|||
either(parse_sieve_header(), parse_sieve_address()),
|
||||
),
|
||||
either(
|
||||
map(
|
||||
right(ws(parse_token("allof")), parse_test_list()),
|
||||
ConditionRule::AllOf,
|
||||
),
|
||||
map(
|
||||
right(ws(parse_token("anyof")), parse_test_list()),
|
||||
ConditionRule::AnyOf,
|
||||
),
|
||||
map(right(ws(parse_token("allof")), parse_test_list()), |l| {
|
||||
ConditionRule::AllOf(l)
|
||||
}),
|
||||
map(right(ws(parse_token("anyof")), parse_test_list()), |l| {
|
||||
ConditionRule::AnyOf(l)
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -576,14 +574,14 @@ pub mod parser {
|
|||
either(parse_sieve_stop(), parse_sieve_require()),
|
||||
parse_sieve_if(),
|
||||
),
|
||||
Rule::Control,
|
||||
|c| Rule::Control(c),
|
||||
),
|
||||
map(
|
||||
either(
|
||||
either(parse_sieve_keep(), parse_sieve_fileinto()),
|
||||
either(parse_sieve_redirect(), parse_sieve_discard()),
|
||||
),
|
||||
Rule::Action,
|
||||
|ac| Rule::Action(ac),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -596,7 +594,7 @@ pub mod parser {
|
|||
ws(zero_or_more(parse_sieve_rule())),
|
||||
parse_token("}"),
|
||||
)),
|
||||
RuleBlock,
|
||||
|v| RuleBlock(v),
|
||||
)
|
||||
.parse(input)
|
||||
}
|
||||
|
@ -685,7 +683,7 @@ mod test {
|
|||
parser::*, ActionCommand::*, AddressOperator::*, CharacterOperator::*, ConditionRule::*,
|
||||
ControlCommand::*, IntegerOperator::*, MatchOperator::*, Rule::*, RuleBlock,
|
||||
};
|
||||
use crate::utils::parsec::Parser;
|
||||
use crate::parsec::Parser;
|
||||
|
||||
#[test]
|
||||
fn test_sieve_parse_strings() {
|
||||
|
|
|
@ -80,9 +80,9 @@ use smallvec::SmallVec;
|
|||
use smol::{unblock, Async as AsyncWrapper};
|
||||
|
||||
use crate::{
|
||||
connections::{lookup_ipv4, Connection},
|
||||
email::{parser::BytesExt, Address, Envelope},
|
||||
error::{Error, Result, ResultIntoError},
|
||||
utils::connections::{lookup_ipv4, Connection},
|
||||
};
|
||||
|
||||
/// Kind of server security (StartTLS/TLS/None) the client should attempt
|
||||
|
@ -157,7 +157,7 @@ pub struct SmtpAuthType {
|
|||
}
|
||||
|
||||
impl SmtpAuth {
|
||||
pub const fn require_auth(&self) -> bool {
|
||||
fn require_auth(&self) -> bool {
|
||||
use SmtpAuth::*;
|
||||
match self {
|
||||
None => false,
|
||||
|
@ -202,7 +202,7 @@ pub struct SmtpExtensionSupport {
|
|||
#[serde(default = "crate::conf::true_val")]
|
||||
binarymime: bool,
|
||||
/// Resources:
|
||||
/// - <http://www.postfix.org/SMTPUTF8_README.html>
|
||||
/// - http://www.postfix.org/SMTPUTF8_README.html
|
||||
#[serde(default = "crate::conf::true_val")]
|
||||
smtputf8: bool,
|
||||
#[serde(default = "crate::conf::true_val")]
|
||||
|
@ -274,8 +274,8 @@ impl SmtpConnection {
|
|||
)
|
||||
.await?;
|
||||
drop(pre_ehlo_extensions_reply);
|
||||
|
||||
if matches!(server_conf.security, SmtpSecurity::Auto { .. }) {
|
||||
//debug!(pre_ehlo_extensions_reply);
|
||||
if let SmtpSecurity::Auto { .. } = server_conf.security {
|
||||
if server_conf.port == 465 {
|
||||
server_conf.security = SmtpSecurity::Tls {
|
||||
danger_accept_invalid_certs,
|
||||
|
@ -292,7 +292,7 @@ impl SmtpConnection {
|
|||
}
|
||||
}
|
||||
socket.write_all(b"EHLO meli.delivery\r\n").await?;
|
||||
if matches!(server_conf.security, SmtpSecurity::StartTLS { .. }) {
|
||||
if let SmtpSecurity::StartTLS { .. } = server_conf.security {
|
||||
let pre_tls_extensions_reply = read_lines(
|
||||
&mut socket,
|
||||
&mut res,
|
||||
|
@ -372,7 +372,7 @@ impl SmtpConnection {
|
|||
ret
|
||||
}
|
||||
};
|
||||
let mut ret = Self {
|
||||
let mut ret = SmtpConnection {
|
||||
stream,
|
||||
read_buffer: String::new(),
|
||||
server_conf: server_conf.clone(),
|
||||
|
@ -608,7 +608,7 @@ impl SmtpConnection {
|
|||
}
|
||||
let mut current_command: SmallVec<[&[u8]; 16]> = SmallVec::new();
|
||||
//first step in the procedure is the MAIL command.
|
||||
// `MAIL FROM:<reverse-path> [SP <mail-parameters> ] <CRLF>`
|
||||
// MAIL FROM:<reverse-path> [SP <mail-parameters> ] <CRLF>
|
||||
current_command.push(b"MAIL FROM:<");
|
||||
if !envelope_from.is_empty() {
|
||||
current_command.push(envelope_from.trim().as_bytes());
|
||||
|
@ -668,8 +668,8 @@ impl SmtpConnection {
|
|||
}
|
||||
self.send_command(¤t_command).await?;
|
||||
|
||||
//`RCPT TO:<forward-path> [ SP <rcpt-parameters> ] <CRLF>`
|
||||
// If accepted, the SMTP server returns a "250 OK" reply and stores the
|
||||
//RCPT TO:<forward-path> [ SP <rcpt-parameters> ] <CRLF>
|
||||
//If accepted, the SMTP server returns a "250 OK" reply and stores the
|
||||
// forward-path.
|
||||
if !self.server_conf.extensions.pipelining {
|
||||
self.read_lines(&mut res, Some((ReplyCode::_250, &[])))
|
||||
|
@ -679,7 +679,7 @@ impl SmtpConnection {
|
|||
}
|
||||
}
|
||||
|
||||
// Since it has been a common source of errors, it is worth noting that spaces
|
||||
//Since it has been a common source of errors, it is worth noting that spaces
|
||||
// are not permitted on either side of the colon following FROM in the
|
||||
// MAIL command or TO in the RCPT command. The syntax is exactly as
|
||||
// given above.
|
||||
|
@ -692,7 +692,7 @@ impl SmtpConnection {
|
|||
} else {
|
||||
//The third step in the procedure is the DATA command
|
||||
//(or some alternative specified in a service extension).
|
||||
//DATA `<CRLF>`
|
||||
//DATA <CRLF>
|
||||
self.send_command(&[b"DATA"]).await?;
|
||||
//Client SMTP implementations that employ pipelining MUST check ALL statuses
|
||||
// associated with each command in a group. For example, if none of
|
||||
|
@ -739,7 +739,7 @@ impl SmtpConnection {
|
|||
}
|
||||
|
||||
//The mail data are terminated by a line containing only a period, that is, the
|
||||
// character sequence "`<CRLF>`.`<CRLF>`", where the first `<CRLF>` is
|
||||
// character sequence "<CRLF>.<CRLF>", where the first <CRLF> is
|
||||
// actually the terminator of the previous line (see Section 4.5.2).
|
||||
// This is the end of mail data indication.
|
||||
self.stream.write_all(b".\r\n").await?;
|
||||
|
@ -788,15 +788,15 @@ pub enum ReplyCode {
|
|||
/// particular non-standard command; this reply is useful only to the human
|
||||
/// user)
|
||||
_214,
|
||||
/// `<domain>` Service ready
|
||||
/// <domain> Service ready
|
||||
_220,
|
||||
/// `<domain>` Service closing transmission channel
|
||||
/// <domain> Service closing transmission channel
|
||||
_221,
|
||||
/// Authentication successful,
|
||||
_235,
|
||||
/// Requested mail action okay, completed
|
||||
_250,
|
||||
/// User not local; will forward to `<forward-path>` (See Section 3.4)
|
||||
/// User not local; will forward to <forward-path> (See Section 3.4)
|
||||
_251,
|
||||
/// Cannot VRFY user, but will accept message and attempt delivery (See
|
||||
/// Section 3.5.3)
|
||||
|
@ -805,9 +805,9 @@ pub enum ReplyCode {
|
|||
_334,
|
||||
/// PRDR specific, eg "content analysis has started|
|
||||
_353,
|
||||
/// Start mail input; end with `<CRLF>`.`<CRLF>`
|
||||
/// Start mail input; end with <CRLF>.<CRLF>
|
||||
_354,
|
||||
/// `<domain>` Service not available, closing transmission channel (This may
|
||||
/// <domain> Service not available, closing transmission channel (This may
|
||||
/// be a reply to any command if the service knows it must shut down)
|
||||
_421,
|
||||
/// Requested mail action not taken: mailbox unavailable (e.g., mailbox busy
|
||||
|
@ -835,7 +835,7 @@ pub enum ReplyCode {
|
|||
/// Requested action not taken: mailbox unavailable (e.g., mailbox not
|
||||
/// found, no access, or command rejected for policy reasons)
|
||||
_550,
|
||||
/// User not local; please try `<forward-path>` (See Section 3.4)
|
||||
/// User not local; please try <forward-path> (See Section 3.4)
|
||||
_551,
|
||||
/// Requested mail action aborted: exceeded storage allocation
|
||||
_552,
|
||||
|
@ -852,7 +852,7 @@ pub enum ReplyCode {
|
|||
}
|
||||
|
||||
impl ReplyCode {
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
fn as_str(&self) -> &'static str {
|
||||
use ReplyCode::*;
|
||||
match self {
|
||||
_211 => "System status, or system help reply",
|
||||
|
@ -893,7 +893,7 @@ impl ReplyCode {
|
|||
}
|
||||
}
|
||||
|
||||
pub const fn is_err(&self) -> bool {
|
||||
fn is_err(&self) -> bool {
|
||||
use ReplyCode::*;
|
||||
matches!(
|
||||
self,
|
||||
|
@ -920,7 +920,7 @@ impl ReplyCode {
|
|||
|
||||
impl TryFrom<&'_ str> for ReplyCode {
|
||||
type Error = Error;
|
||||
fn try_from(val: &'_ str) -> Result<Self> {
|
||||
fn try_from(val: &'_ str) -> Result<ReplyCode> {
|
||||
if val.len() != 3 {
|
||||
debug!("{}", val);
|
||||
}
|
||||
|
@ -981,12 +981,12 @@ impl<'s> Reply<'s> {
|
|||
/// code, a space or '-' and end with '\r\n'
|
||||
pub fn new(s: &'s str, code: ReplyCode) -> Self {
|
||||
let lines: SmallVec<_> = s.lines().map(|l| &l[4..l.len()]).collect();
|
||||
Self { lines, code }
|
||||
Reply { lines, code }
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_lines<'r>(
|
||||
_self: &mut (impl futures::io::AsyncRead + std::marker::Unpin + Send),
|
||||
_self: &mut (impl futures::io::AsyncRead + std::marker::Unpin),
|
||||
ret: &'r mut String,
|
||||
expected_reply_code: Option<(ReplyCode, &[ReplyCode])>,
|
||||
buffer: &mut String,
|
||||
|
@ -998,8 +998,8 @@ async fn read_lines<'r>(
|
|||
let mut returned_code: Option<ReplyCode> = None;
|
||||
'read_loop: loop {
|
||||
while let Some(pos) = ret[last_line_idx..].find("\r\n") {
|
||||
// "Formally, a reply is defined to be the sequence: a three-digit code, `<SP>`,
|
||||
// one line of text, and `<CRLF>`, or a multiline reply (as defined in the same
|
||||
// "Formally, a reply is defined to be the sequence: a three-digit code, <SP>,
|
||||
// one line of text, and <CRLF>, or a multiline reply (as defined in the same
|
||||
// section)."
|
||||
if ret[last_line_idx..].len() < 4
|
||||
|| !ret[last_line_idx..]
|
||||
|
@ -1069,10 +1069,7 @@ mod test {
|
|||
thread,
|
||||
};
|
||||
|
||||
use mailin_embedded::{
|
||||
response::{INTERNAL_ERROR, OK},
|
||||
Handler, Response, Server, SslConfig,
|
||||
};
|
||||
use mailin_embedded::{Handler, Response, Server, SslConfig};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -1099,13 +1096,12 @@ mod test {
|
|||
},
|
||||
}
|
||||
|
||||
type QueuedMail = ((IpAddr, String), Message);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct MyHandler {
|
||||
mails: Arc<Mutex<Vec<QueuedMail>>>,
|
||||
mails: Arc<Mutex<Vec<((IpAddr, String), Message)>>>,
|
||||
stored: Arc<Mutex<Vec<(String, crate::Envelope)>>>,
|
||||
}
|
||||
use mailin_embedded::response::{INTERNAL_ERROR, OK};
|
||||
|
||||
impl Handler for MyHandler {
|
||||
fn helo(&mut self, ip: IpAddr, domain: &str) -> Response {
|
||||
|
@ -1127,8 +1123,8 @@ mod test {
|
|||
.rev()
|
||||
.find(|((i, d), _)| (i, d.as_str()) == (&ip, domain))
|
||||
{
|
||||
eprintln!("mail is {:?}", &message);
|
||||
if matches!(message, Message::Helo) {
|
||||
std::dbg!(&message);
|
||||
if let Message::Helo = message {
|
||||
*message = Message::Mail {
|
||||
from: from.to_string(),
|
||||
};
|
||||
|
@ -1141,7 +1137,7 @@ mod test {
|
|||
fn rcpt(&mut self, _to: &str) -> Response {
|
||||
eprintln!("rcpt() to {:?}", _to);
|
||||
if let Some((_, message)) = self.mails.lock().unwrap().last_mut() {
|
||||
eprintln!("rcpt mail is {:?}", &message);
|
||||
std::dbg!(&message);
|
||||
if let Message::Mail { from } = message {
|
||||
*message = Message::Rcpt {
|
||||
from: from.clone(),
|
||||
|
@ -1171,7 +1167,7 @@ mod test {
|
|||
if d != _domain {
|
||||
return INTERNAL_ERROR;
|
||||
}
|
||||
eprintln!("data_start mail is {:?}", &message);
|
||||
std::dbg!(&message);
|
||||
if let Message::Rcpt { from, to } = message {
|
||||
*message = Message::DataStart {
|
||||
from: from.to_string(),
|
||||
|
@ -1193,7 +1189,7 @@ mod test {
|
|||
};
|
||||
return Ok(());
|
||||
} else if let Message::Data { buf, .. } = message {
|
||||
buf.extend(_buf.iter());
|
||||
buf.extend(_buf.into_iter().copied());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
@ -1206,8 +1202,8 @@ mod test {
|
|||
for to in to {
|
||||
match crate::Envelope::from_bytes(&buf, None) {
|
||||
Ok(env) => {
|
||||
eprintln!("data_end env is {:?}", &env);
|
||||
eprintln!("data_end env.other_headers is {:?}", env.other_headers());
|
||||
std::dbg!(&env);
|
||||
std::dbg!(env.other_headers());
|
||||
self.stored.lock().unwrap().push((to.clone(), env));
|
||||
}
|
||||
Err(err) => {
|
||||
|
|
|
@ -54,17 +54,19 @@ pub fn open_or_create_db(
|
|||
) -> Result<Connection> {
|
||||
let mut second_try: bool = false;
|
||||
loop {
|
||||
let db_path = identifier.map_or_else(
|
||||
|| db_path(description.name),
|
||||
|id| db_path(&format!("{}_{}", id, description.name)),
|
||||
)?;
|
||||
let set_mode = !db_path.exists();
|
||||
if set_mode {
|
||||
let db_path = if let Some(id) = identifier {
|
||||
db_path(&format!("{}_{}", id, description.name))
|
||||
} else {
|
||||
db_path(description.name)
|
||||
}?;
|
||||
let mut set_mode = false;
|
||||
if !db_path.exists() {
|
||||
log::info!(
|
||||
"Creating {} database in {}",
|
||||
description.name,
|
||||
db_path.display()
|
||||
);
|
||||
set_mode = true;
|
||||
}
|
||||
let conn = Connection::open(&db_path).map_err(|e| Error::new(e.to_string()))?;
|
||||
if set_mode {
|
||||
|
@ -79,8 +81,7 @@ pub fn open_or_create_db(
|
|||
let version: i32 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
|
||||
if version != 0_i32 && version as u32 != description.version {
|
||||
log::info!(
|
||||
"Database version mismatch, is {} but expected {}. Attempting to recreate \
|
||||
database.",
|
||||
"Database version mismatch, is {} but expected {}",
|
||||
version,
|
||||
description.version
|
||||
);
|
||||
|
@ -110,10 +111,11 @@ pub fn open_or_create_db(
|
|||
|
||||
/// Return database to a clean slate.
|
||||
pub fn reset_db(description: &DatabaseDescription, identifier: Option<&str>) -> Result<()> {
|
||||
let db_path = identifier.map_or_else(
|
||||
|| db_path(description.name),
|
||||
|id| db_path(&format!("{}_{}", id, description.name)),
|
||||
)?;
|
||||
let db_path = if let Some(id) = identifier {
|
||||
db_path(&format!("{}_{}", id, description.name))
|
||||
} else {
|
||||
db_path(description.name)
|
||||
}?;
|
||||
if !db_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
@ -128,17 +130,27 @@ pub fn reset_db(description: &DatabaseDescription, identifier: Option<&str>) ->
|
|||
|
||||
impl ToSql for Envelope {
|
||||
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
|
||||
let v: Vec<u8> = serde_json::to_vec(self).map_err(|e| {
|
||||
rusqlite::Error::ToSqlConversionFailure(Box::new(Error::new(e.to_string())))
|
||||
})?;
|
||||
let v: Vec<u8> = bincode::Options::serialize(bincode::config::DefaultOptions::new(), self)
|
||||
.map_err(|e| {
|
||||
rusqlite::Error::ToSqlConversionFailure(Box::new(Error::new(e.to_string())))
|
||||
})?;
|
||||
Ok(ToSqlOutput::from(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql for Envelope {
|
||||
fn column_result(value: rusqlite::types::ValueRef) -> FromSqlResult<Self> {
|
||||
use std::convert::TryFrom;
|
||||
|
||||
let b: Vec<u8> = FromSql::column_result(value)?;
|
||||
|
||||
serde_json::from_slice(&b).map_err(|e| FromSqlError::Other(Box::new(e)))
|
||||
bincode::Options::deserialize(
|
||||
bincode::Options::with_limit(
|
||||
bincode::config::DefaultOptions::new(),
|
||||
2 * u64::try_from(b.len()).map_err(|e| FromSqlError::Other(Box::new(e)))?,
|
||||
),
|
||||
&b,
|
||||
)
|
||||
.map_err(|e| FromSqlError::Other(Box::new(e)))
|
||||
}
|
||||
}
|
|
@ -32,14 +32,19 @@ use super::{
|
|||
types::{LineBreakClass, Reflow},
|
||||
};
|
||||
|
||||
#[derive(Default, Debug, Eq, PartialEq, Copy, Clone)]
|
||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||
pub enum LineBreakCandidate {
|
||||
MandatoryBreak,
|
||||
BreakAllowed,
|
||||
#[default]
|
||||
NoBreak, // Not used.
|
||||
}
|
||||
|
||||
impl Default for LineBreakCandidate {
|
||||
fn default() -> Self {
|
||||
LineBreakCandidate::NoBreak
|
||||
}
|
||||
}
|
||||
|
||||
use LineBreakCandidate::*;
|
||||
|
||||
pub struct LineBreakCandidateIter<'a> {
|
||||
|
@ -1197,7 +1202,8 @@ fn reflow_helper(
|
|||
let paragraph = paragraph
|
||||
.trim_start_matches("es)
|
||||
.replace(&format!("\n{}", "es), "")
|
||||
.replace(['\n', '\r'], "");
|
||||
.replace('\n', "")
|
||||
.replace('\r', "");
|
||||
if in_paragraph {
|
||||
if let Some(width) = width {
|
||||
ret.extend(
|
||||
|
@ -1212,7 +1218,7 @@ fn reflow_helper(
|
|||
ret.push(format!("{}{}", "es, ¶graph));
|
||||
}
|
||||
} else {
|
||||
let paragraph = paragraph.replace(['\n', '\r'], "");
|
||||
let paragraph = paragraph.replace('\n', "").replace('\r', "");
|
||||
|
||||
if in_paragraph {
|
||||
if let Some(width) = width {
|
||||
|
@ -1264,9 +1270,10 @@ easy to take MORE than nothing.'"#;
|
|||
}
|
||||
|
||||
mod segment_tree {
|
||||
//! Simple segment tree implementation for maximum in range queries. This
|
||||
//! is useful if given an array of numbers you want to get the
|
||||
//! maximum value inside an interval quickly.
|
||||
/*! Simple segment tree implementation for maximum in range queries. This
|
||||
* is useful if given an array of numbers you want to get the
|
||||
* maximum value inside an interval quickly.
|
||||
*/
|
||||
use std::{convert::TryFrom, iter::FromIterator};
|
||||
|
||||
use smallvec::SmallVec;
|
||||
|
@ -1278,9 +1285,9 @@ mod segment_tree {
|
|||
}
|
||||
|
||||
impl SegmentTree {
|
||||
pub(super) fn new(val: SmallVec<[usize; 1024]>) -> Self {
|
||||
pub(super) fn new(val: SmallVec<[usize; 1024]>) -> SegmentTree {
|
||||
if val.is_empty() {
|
||||
return Self {
|
||||
return SegmentTree {
|
||||
array: val.clone(),
|
||||
tree: val,
|
||||
};
|
||||
|
@ -1301,7 +1308,7 @@ mod segment_tree {
|
|||
segment_tree[i] = segment_tree[2 * i] + segment_tree[2 * i + 1];
|
||||
}
|
||||
|
||||
Self {
|
||||
SegmentTree {
|
||||
array: val,
|
||||
tree: segment_tree,
|
||||
}
|
||||
|
@ -1376,15 +1383,15 @@ enum ReflowState {
|
|||
}
|
||||
|
||||
impl ReflowState {
|
||||
fn new(reflow: Reflow, width: Option<usize>, cur_index: usize) -> Self {
|
||||
fn new(reflow: Reflow, width: Option<usize>, cur_index: usize) -> ReflowState {
|
||||
match reflow {
|
||||
Reflow::All if width.is_some() => Self::AllWidth {
|
||||
Reflow::All if width.is_some() => ReflowState::AllWidth {
|
||||
width: width.unwrap(),
|
||||
state: LineBreakTextState::AtLine { cur_index },
|
||||
},
|
||||
Reflow::All => Self::All { cur_index },
|
||||
Reflow::FormatFlowed => Self::FormatFlowed { cur_index },
|
||||
Reflow::No => Self::No { cur_index },
|
||||
Reflow::All => ReflowState::All { cur_index },
|
||||
Reflow::FormatFlowed => ReflowState::FormatFlowed { cur_index },
|
||||
Reflow::No => ReflowState::No { cur_index },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1412,7 +1419,7 @@ impl Default for LineBreakText {
|
|||
|
||||
impl LineBreakText {
|
||||
pub fn new(text: String, reflow: Reflow, width: Option<usize>) -> Self {
|
||||
Self {
|
||||
LineBreakText {
|
||||
text,
|
||||
state: ReflowState::new(reflow, width, 0),
|
||||
paragraph: VecDeque::new(),
|
||||
|
@ -1784,7 +1791,8 @@ fn reflow_helper2(
|
|||
let paragraph = paragraph
|
||||
.trim_start_matches("es)
|
||||
.replace(&format!("\n{}", "es), "")
|
||||
.replace(['\n', '\r'], "");
|
||||
.replace('\n', "")
|
||||
.replace('\r', "");
|
||||
if in_paragraph {
|
||||
if let Some(width) = width {
|
||||
ret.extend(
|
||||
|
@ -1799,7 +1807,7 @@ fn reflow_helper2(
|
|||
ret.push_back(format!("{}{}", "es, ¶graph));
|
||||
}
|
||||
} else {
|
||||
let paragraph = paragraph.replace(['\n', '\r'], "");
|
||||
let paragraph = paragraph.replace('\n', "").replace('\r', "");
|
||||
|
||||
if in_paragraph {
|
||||
if let Some(width) = width {
|
||||
|
|
|
@ -110,7 +110,7 @@ impl Truncate for String {
|
|||
.take(new_len)
|
||||
.last()
|
||||
{
|
||||
Self::truncate(self, last);
|
||||
String::truncate(self, last);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -170,11 +170,11 @@ pub trait GlobMatch {
|
|||
|
||||
impl GlobMatch for str {
|
||||
fn matches_glob(&self, _pattern: &str) -> bool {
|
||||
let pattern: Vec<&Self> = _pattern
|
||||
let pattern: Vec<&str> = _pattern
|
||||
.strip_suffix('/')
|
||||
.unwrap_or(_pattern)
|
||||
.split_graphemes();
|
||||
let s: Vec<&Self> = self.strip_suffix('/').unwrap_or(self).split_graphemes();
|
||||
let s: Vec<&str> = self.strip_suffix('/').unwrap_or(self).split_graphemes();
|
||||
|
||||
// Taken from https://research.swtch.com/glob
|
||||
|
||||
|
|
|
@ -109,16 +109,19 @@ fn bisearch(ucs: WChar, table: &'static [Interval]) -> bool {
|
|||
pub fn wcwidth(ucs: WChar) -> Option<usize> {
|
||||
if bisearch(ucs, super::tables::ASCII) {
|
||||
Some(1)
|
||||
} else if bisearch(ucs, super::tables::PRIVATE)
|
||||
|| bisearch(ucs, super::tables::NONPRINT)
|
||||
|| bisearch(ucs, super::tables::COMBINING)
|
||||
{
|
||||
} else if bisearch(ucs, super::tables::PRIVATE) {
|
||||
None
|
||||
} else if bisearch(ucs, super::tables::NONPRINT) {
|
||||
None
|
||||
} else if bisearch(ucs, super::tables::COMBINING) {
|
||||
None
|
||||
} else if bisearch(ucs, super::tables::DOUBLEWIDE) {
|
||||
Some(2)
|
||||
} else if bisearch(ucs, super::tables::AMBIGUOUS) {
|
||||
Some(1)
|
||||
} else if bisearch(ucs, super::tables::UNASSIGNED) || bisearch(ucs, super::tables::WIDENEDIN9) {
|
||||
} else if bisearch(ucs, super::tables::UNASSIGNED) {
|
||||
Some(2)
|
||||
} else if bisearch(ucs, super::tables::WIDENEDIN9) {
|
||||
Some(2)
|
||||
} else {
|
||||
Some(1)
|
||||
|
|
|
@ -19,29 +19,29 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! e-mail threading (conversations)
|
||||
//!
|
||||
//! This module implements Jamie Zawinski's [threading
|
||||
//! algorithm](https://www.jwz.org/doc/threading.html). Quoted comments (/* " .. " */) are taken
|
||||
//! almost verbatim from the algorithm.
|
||||
//!
|
||||
//! The entry point of this module is the [`Threads`] struct and its
|
||||
//! [`new`](Threads::new) method. It contains [`ThreadNode`s](ThreadNode) which
|
||||
//! are the nodes in the thread trees that might have messages associated with
|
||||
//! them. The root nodes (first messages in each thread) are stored in
|
||||
//! [`root_set`](Threads::root_set) and [`thread_nodes`](Threads::thread_nodes)
|
||||
//! vectors. [`Threads`] has inner mutability since we need to sort without the
|
||||
//! user having mutable ownership.
|
||||
/*!
|
||||
* e-mail threading (conversations)
|
||||
*
|
||||
* This module implements Jamie Zawinski's [threading algorithm](https://www.jwz.org/doc/threading.html). Quoted comments (/* " .. " */) are
|
||||
* taken almost verbatim from the algorithm.
|
||||
*
|
||||
* The entry point of this module is the `Threads` struct and its `new`
|
||||
* method. It contains `ThreadNodes` which are the nodes in the thread trees
|
||||
* that might have messages associated with them. The root nodes (first
|
||||
* messages in each thread) are stored in `root_set` and `tree`
|
||||
* vectors. `Threads` has inner mutability since we need to sort without the
|
||||
* user having mutable ownership.
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
datetime::UnixTimestamp,
|
||||
email::{address::StrBuild, parser::BytesExt, *},
|
||||
UnixTimestamp,
|
||||
};
|
||||
|
||||
mod iterators;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
collections::{HashMap, HashSet},
|
||||
fmt,
|
||||
iter::FromIterator,
|
||||
ops::Index,
|
||||
|
@ -88,7 +88,7 @@ macro_rules! uuid_hash_type {
|
|||
$n(Uuid::new_v4())
|
||||
}
|
||||
|
||||
pub const fn null() -> Self {
|
||||
pub fn null() -> Self {
|
||||
$n(Uuid::nil())
|
||||
}
|
||||
}
|
||||
|
@ -332,28 +332,30 @@ impl SubjectPrefix for &[u8] {
|
|||
|| slice.starts_with(b"FW: ")
|
||||
|| slice.starts_with(b"Fw: ")
|
||||
{
|
||||
slice = &slice[b"RE: ".len()..];
|
||||
slice = &slice[3..];
|
||||
continue;
|
||||
}
|
||||
if slice.starts_with(b"FWD: ")
|
||||
|| slice.starts_with(b"Fwd: ")
|
||||
|| slice.starts_with(b"fwd: ")
|
||||
{
|
||||
slice = &slice[b"FWD: ".len()..];
|
||||
slice = &slice[4..];
|
||||
continue;
|
||||
}
|
||||
if slice.starts_with(b" ") || slice.starts_with(b"\t") || slice.starts_with(b"\r") {
|
||||
//FIXME just trim whitespace
|
||||
slice = &slice[b" ".len()..];
|
||||
slice = &slice[1..];
|
||||
continue;
|
||||
}
|
||||
if slice.starts_with(b"[")
|
||||
&& !(slice.starts_with(b"[PATCH") || slice.starts_with(b"[RFC"))
|
||||
{
|
||||
if let Some(pos) = slice.find(b"]") {
|
||||
slice = &slice[pos + 1..];
|
||||
slice = &slice[pos..];
|
||||
continue;
|
||||
}
|
||||
slice = &slice[1..];
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -407,14 +409,14 @@ impl SubjectPrefix for &str {
|
|||
|| slice.starts_with("FW: ")
|
||||
|| slice.starts_with("Fw: ")
|
||||
{
|
||||
slice = &slice["RE: ".len()..];
|
||||
slice = &slice[3..];
|
||||
continue;
|
||||
}
|
||||
if slice.starts_with("FWD: ")
|
||||
|| slice.starts_with("Fwd: ")
|
||||
|| slice.starts_with("fwd: ")
|
||||
{
|
||||
slice = &slice["FWD: ".len()..];
|
||||
slice = &slice[4..];
|
||||
continue;
|
||||
}
|
||||
if slice.starts_with(' ') || slice.starts_with('\t') || slice.starts_with('\r') {
|
||||
|
@ -426,9 +428,11 @@ impl SubjectPrefix for &str {
|
|||
&& !(slice.starts_with("[PATCH") || slice.starts_with("[RFC"))
|
||||
{
|
||||
if let Some(pos) = slice.find(']') {
|
||||
slice = &slice[pos + 1..];
|
||||
slice = &slice[pos..];
|
||||
continue;
|
||||
}
|
||||
slice = &slice[1..];
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -470,26 +474,36 @@ impl SubjectPrefix for &str {
|
|||
|
||||
/* Sorting states. */
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy, Deserialize, Serialize)]
|
||||
pub enum SortOrder {
|
||||
Asc,
|
||||
#[default]
|
||||
Desc,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy, Deserialize, Serialize)]
|
||||
pub enum SortField {
|
||||
Subject,
|
||||
#[default]
|
||||
Date,
|
||||
}
|
||||
|
||||
impl Default for SortField {
|
||||
fn default() -> Self {
|
||||
SortField::Date
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SortOrder {
|
||||
fn default() -> Self {
|
||||
SortOrder::Desc
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SortField {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> StdResult<Self, Self::Err> {
|
||||
match s.trim() {
|
||||
"subject" | "s" | "sub" | "sbj" | "subj" => Ok(Self::Subject),
|
||||
"date" | "d" => Ok(Self::Date),
|
||||
"subject" | "s" | "sub" | "sbj" | "subj" => Ok(SortField::Subject),
|
||||
"date" | "d" => Ok(SortField::Date),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
@ -499,8 +513,8 @@ impl FromStr for SortOrder {
|
|||
type Err = ();
|
||||
fn from_str(s: &str) -> StdResult<Self, Self::Err> {
|
||||
match s.trim() {
|
||||
"asc" => Ok(Self::Asc),
|
||||
"desc" => Ok(Self::Desc),
|
||||
"asc" => Ok(SortOrder::Asc),
|
||||
"desc" => Ok(SortOrder::Desc),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
@ -525,13 +539,13 @@ pub enum ThreadGroup {
|
|||
|
||||
impl Default for ThreadGroup {
|
||||
fn default() -> Self {
|
||||
Self::Root(Thread::default())
|
||||
ThreadGroup::Root(Thread::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl ThreadGroup {
|
||||
pub fn root(&self) -> Option<&Thread> {
|
||||
if let Self::Root(ref root) = self {
|
||||
if let ThreadGroup::Root(ref root) = self {
|
||||
Some(root)
|
||||
} else {
|
||||
None
|
||||
|
@ -580,8 +594,8 @@ pub struct ThreadNode {
|
|||
}
|
||||
|
||||
impl Default for ThreadNode {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
fn default() -> ThreadNode {
|
||||
ThreadNode {
|
||||
message: None,
|
||||
parent: None,
|
||||
other_mailbox: false,
|
||||
|
@ -596,7 +610,7 @@ impl Default for ThreadNode {
|
|||
|
||||
impl ThreadNode {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
ThreadNode::default()
|
||||
}
|
||||
|
||||
pub fn show_subject(&self) -> bool {
|
||||
|
@ -658,7 +672,7 @@ pub struct Threads {
|
|||
}
|
||||
|
||||
impl PartialEq for ThreadNode {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
fn eq(&self, other: &ThreadNode) -> bool {
|
||||
match (self.message, other.message) {
|
||||
(Some(s), Some(o)) => s == o,
|
||||
_ => false,
|
||||
|
@ -703,7 +717,7 @@ impl Threads {
|
|||
parent_group
|
||||
}
|
||||
|
||||
pub fn new(length: usize) -> Self {
|
||||
pub fn new(length: usize) -> Threads {
|
||||
/* To reconstruct thread information from the mails we need: */
|
||||
|
||||
/* a vector to hold thread members */
|
||||
|
@ -725,7 +739,7 @@ impl Threads {
|
|||
let envelope_to_thread: HashMap<EnvelopeHash, ThreadHash> =
|
||||
HashMap::with_capacity_and_hasher(length, Default::default());
|
||||
|
||||
Self {
|
||||
Threads {
|
||||
thread_nodes,
|
||||
message_ids,
|
||||
message_ids_set,
|
||||
|
@ -740,8 +754,11 @@ impl Threads {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn threads_iter(&self, root_tree: SmallVec<[ThreadNodeHash; 1024]>) -> ThreadsIterator {
|
||||
ThreadsIterator {
|
||||
pub fn threads_group_iter(
|
||||
&self,
|
||||
root_tree: SmallVec<[ThreadNodeHash; 1024]>,
|
||||
) -> ThreadsGroupIterator {
|
||||
ThreadsGroupIterator {
|
||||
root_tree,
|
||||
pos: 0,
|
||||
stack: SmallVec::new(),
|
||||
|
@ -749,8 +766,8 @@ impl Threads {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn thread_iter(&self, index: ThreadHash) -> ThreadIterator {
|
||||
ThreadIterator {
|
||||
pub fn thread_group_iter(&self, index: ThreadHash) -> ThreadGroupIterator {
|
||||
ThreadGroupIterator {
|
||||
group: self.thread_ref(index).root(),
|
||||
pos: 0,
|
||||
stack: SmallVec::new(),
|
||||
|
@ -758,7 +775,6 @@ impl Threads {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::result_unit_err)]
|
||||
pub fn update_envelope(
|
||||
&mut self,
|
||||
envelopes: &Envelopes,
|
||||
|
@ -862,62 +878,38 @@ impl Threads {
|
|||
env_hash: EnvelopeHash,
|
||||
envelopes: &Envelopes,
|
||||
) {
|
||||
let mut stack = VecDeque::new();
|
||||
stack.push_back((id, Some(env_hash)));
|
||||
let envelopes = envelopes.read().unwrap();
|
||||
while let Some((id, env_hash)) = stack.pop_front() {
|
||||
if let Some(env_hash) = env_hash {
|
||||
let mut subject = envelopes[&env_hash].subject();
|
||||
let mut subject = subject.to_mut().as_bytes();
|
||||
let stripped_subject = subject.strip_prefixes();
|
||||
{
|
||||
// check immediate parent envelope, skipping thread nodes that have no
|
||||
// corresponding envelope
|
||||
let mut parent_cursor = self.thread_nodes[&id].parent;
|
||||
while let Some(parent_id) = parent_cursor {
|
||||
if let Some(parent_hash) = self.thread_nodes[&parent_id].message {
|
||||
debug_assert!(envelopes.contains_key(&parent_hash));
|
||||
/* decide if the subject should be shown in the UI.
|
||||
* If parent subject is Foobar and reply is `Re: Foobar`
|
||||
* then showing the reply's subject is redundant
|
||||
*/
|
||||
let mut parent_subject = envelopes[&parent_hash].subject();
|
||||
let mut parent_subject = parent_subject.to_mut().as_bytes();
|
||||
parent_subject.strip_prefixes();
|
||||
if stripped_subject == &parent_subject
|
||||
|| stripped_subject.ends_with(parent_subject)
|
||||
{
|
||||
self.thread_nodes.entry(id).and_modify(|e| {
|
||||
e.show_subject = false;
|
||||
});
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
parent_cursor = self.thread_nodes[&parent_id].parent;
|
||||
}
|
||||
}
|
||||
let mut subject = envelopes[&env_hash].subject();
|
||||
let mut subject = subject.to_mut().as_bytes();
|
||||
let stripped_subject = subject.strip_prefixes();
|
||||
if let Some(parent_id) = self.thread_nodes[&id].parent {
|
||||
if let Some(parent_hash) = self.thread_nodes[&parent_id].message {
|
||||
debug_assert!(envelopes.contains_key(&parent_hash));
|
||||
/* decide if the subject should be shown in the UI.
|
||||
* If parent subject is Foobar and reply is `Re: Foobar`
|
||||
* then showing the reply's subject is reduntant
|
||||
*/
|
||||
let mut parent_subject = envelopes[&parent_hash].subject();
|
||||
let mut parent_subject = parent_subject.to_mut().as_bytes();
|
||||
parent_subject.strip_prefixes();
|
||||
if stripped_subject == &parent_subject {
|
||||
self.thread_nodes.entry(id).and_modify(|e| {
|
||||
e.show_subject = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..self.thread_nodes[&id].children.len() {
|
||||
let child_hash = self.thread_nodes[&id].children[i];
|
||||
stack.push_back((child_hash, self.thread_nodes[&child_hash].message()));
|
||||
if let Some(child_env_hash) = self.thread_nodes[&child_hash].message() {
|
||||
let mut child_subject = envelopes[&child_env_hash].subject();
|
||||
let mut child_subject = child_subject.to_mut().as_bytes();
|
||||
child_subject.strip_prefixes();
|
||||
if stripped_subject == &child_subject
|
||||
|| child_subject.ends_with(stripped_subject)
|
||||
{
|
||||
self.thread_nodes.entry(child_hash).and_modify(|e| {
|
||||
e.show_subject = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i in 0..self.thread_nodes[&id].children.len() {
|
||||
let child_hash = self.thread_nodes[&id].children[i];
|
||||
stack.push_back((child_hash, self.thread_nodes[&child_hash].message()));
|
||||
for i in 0..self.thread_nodes[&id].children.len() {
|
||||
let child_hash = self.thread_nodes[&id].children[i];
|
||||
if let Some(child_env_hash) = self.thread_nodes[&child_hash].message() {
|
||||
let mut child_subject = envelopes[&child_env_hash].subject();
|
||||
let mut child_subject = child_subject.to_mut().as_bytes();
|
||||
child_subject.strip_prefixes();
|
||||
if stripped_subject == &child_subject {
|
||||
self.thread_nodes.entry(child_hash).and_modify(|e| {
|
||||
e.show_subject = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -958,12 +950,11 @@ impl Threads {
|
|||
}
|
||||
}
|
||||
let envelopes_lck = envelopes.read().unwrap();
|
||||
let message_id = envelopes_lck[&env_hash].message_id().raw();
|
||||
let reply_to_id: Option<ThreadNodeHash> = envelopes_lck[&env_hash]
|
||||
.in_reply_to()
|
||||
.map(StrBuild::raw)
|
||||
.filter(|irt| irt != &message_id)
|
||||
.and_then(|r| self.message_ids.get(r).cloned());
|
||||
let message_id = envelopes_lck[&env_hash].message_id().raw();
|
||||
|
||||
if other_mailbox
|
||||
&& reply_to_id.is_none()
|
||||
|
@ -1000,23 +991,26 @@ impl Threads {
|
|||
}
|
||||
|
||||
let thread_hash = self.thread_nodes[&new_id].group;
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = self.groups.entry(thread_hash) {
|
||||
e.insert(ThreadGroup::Root(Thread {
|
||||
root: new_id,
|
||||
date: envelopes_lck[&env_hash].date(),
|
||||
len: 1,
|
||||
unseen: if !envelopes_lck[&env_hash].is_seen() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
},
|
||||
attachments: if envelopes_lck[&env_hash].has_attachments() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
},
|
||||
snoozed: false,
|
||||
}));
|
||||
if !self.groups.contains_key(&thread_hash) {
|
||||
self.groups.insert(
|
||||
thread_hash,
|
||||
ThreadGroup::Root(Thread {
|
||||
root: new_id,
|
||||
date: envelopes_lck[&env_hash].date(),
|
||||
len: 1,
|
||||
unseen: if !envelopes_lck[&env_hash].is_seen() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
},
|
||||
attachments: if envelopes_lck[&env_hash].has_attachments() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
},
|
||||
snoozed: false,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
let parent_group = self.thread_ref_mut(thread_hash);
|
||||
parent_group.date = std::cmp::max(parent_group.date, envelopes_lck[&env_hash].date());
|
||||
|
@ -1044,11 +1038,7 @@ impl Threads {
|
|||
*self.envelope_to_thread.entry(env_hash).or_default() = thread_hash;
|
||||
if let Some(reply_to_id) = reply_to_id {
|
||||
make!((reply_to_id) parent of (new_id), self);
|
||||
} else if let Some(r) = envelopes_lck[&env_hash]
|
||||
.in_reply_to()
|
||||
.map(StrBuild::raw)
|
||||
.filter(|irt| irt != &message_id)
|
||||
{
|
||||
} else if let Some(r) = envelopes_lck[&env_hash].in_reply_to().map(StrBuild::raw) {
|
||||
let reply_to_id = ThreadNodeHash::from(r);
|
||||
self.thread_nodes.insert(
|
||||
reply_to_id,
|
||||
|
@ -1078,16 +1068,11 @@ impl Threads {
|
|||
if envelopes_lck[&env_hash].references.is_some() {
|
||||
let mut current_descendant_id = new_id;
|
||||
let mut references = envelopes_lck[&env_hash].references();
|
||||
if references.first().filter(|irt| irt.raw() != message_id)
|
||||
== envelopes_lck[&env_hash].in_reply_to().as_ref()
|
||||
{
|
||||
if references.first() == envelopes_lck[&env_hash].in_reply_to().as_ref() {
|
||||
references.reverse();
|
||||
}
|
||||
|
||||
for reference in references.into_iter().rev() {
|
||||
if reference.raw() == message_id {
|
||||
continue;
|
||||
}
|
||||
if let Some(&id) = self.message_ids.get(reference.raw()) {
|
||||
if self.thread_nodes[&id].date > self.thread_nodes[¤t_descendant_id].date
|
||||
|| self.thread_nodes[¤t_descendant_id].parent.is_some()
|
||||
|
@ -1679,7 +1664,7 @@ fn save_graph(
|
|||
|
||||
let mut file = File::create(format!(
|
||||
"/tmp/meli/threads/threads_{}.json",
|
||||
crate::utils::datetime::now()
|
||||
crate::datetime::now()
|
||||
))
|
||||
.unwrap();
|
||||
file.write_all(s.as_bytes()).unwrap();
|
||||
|
|
|
@ -25,28 +25,28 @@ use smallvec::SmallVec;
|
|||
|
||||
use super::{ThreadNode, ThreadNodeHash};
|
||||
|
||||
/// [`ThreadsIterator`] returns messages according to the sorted order.
|
||||
///
|
||||
/// For example, for the following threads:
|
||||
///
|
||||
/// ```text
|
||||
/// A_
|
||||
/// |_ B
|
||||
/// |_C
|
||||
/// D
|
||||
/// E_
|
||||
/// |_F
|
||||
/// ```
|
||||
///
|
||||
/// the iterator returns them as `A, B, C, D, E, F`.
|
||||
pub struct ThreadsIterator<'a> {
|
||||
/* `ThreadsIterator` returns messages according to the sorted order. For
|
||||
* example, for the following threads:
|
||||
*
|
||||
* ```
|
||||
* A_
|
||||
* |_ B
|
||||
* |_C
|
||||
* D
|
||||
* E_
|
||||
* |_F
|
||||
* ```
|
||||
*
|
||||
* the iterator returns them as `A, B, C, D, E, F`
|
||||
*/
|
||||
|
||||
pub struct ThreadsGroupIterator<'a> {
|
||||
pub(super) root_tree: SmallVec<[ThreadNodeHash; 1024]>,
|
||||
pub(super) pos: usize,
|
||||
pub(super) stack: SmallVec<[usize; 16]>,
|
||||
pub(super) thread_nodes: &'a HashMap<ThreadNodeHash, ThreadNode>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for ThreadsIterator<'a> {
|
||||
impl<'a> Iterator for ThreadsGroupIterator<'a> {
|
||||
type Item = (usize, ThreadNodeHash, bool);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
|
@ -70,36 +70,41 @@ impl<'a> Iterator for ThreadsIterator<'a> {
|
|||
if !self.thread_nodes[&tree[self.pos]].children.is_empty() {
|
||||
self.stack.push(self.pos);
|
||||
self.pos = 0;
|
||||
return Some(ret);
|
||||
if self.thread_nodes[&ret.1].message.is_some() {
|
||||
return Some(ret);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
self.pos += 1;
|
||||
return Some(ret);
|
||||
if self.thread_nodes[&ret.1].message.is_some() {
|
||||
return Some(ret);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* `ThreadIterator` returns messages of a specific thread according to the
|
||||
* sorted order. For example, for the following thread:
|
||||
*
|
||||
* ```
|
||||
* A_
|
||||
* |_ B
|
||||
* |_C
|
||||
* |_D
|
||||
* ```
|
||||
*
|
||||
* the iterator returns them as `A, B, C, D`
|
||||
*/
|
||||
|
||||
/// [`ThreadIterator`] returns messages of a specific thread according to the
|
||||
/// sorted order.
|
||||
///
|
||||
/// For example, for the following thread:
|
||||
///
|
||||
/// ```text
|
||||
/// A_
|
||||
/// |_ B
|
||||
/// |_C
|
||||
/// |_D
|
||||
/// ```
|
||||
///
|
||||
/// the iterator returns them as `A, B, C, D`.
|
||||
pub struct ThreadIterator<'a> {
|
||||
pub struct ThreadGroupIterator<'a> {
|
||||
pub(super) group: ThreadNodeHash,
|
||||
pub(super) pos: usize,
|
||||
pub(super) stack: SmallVec<[usize; 16]>,
|
||||
pub(super) thread_nodes: &'a HashMap<ThreadNodeHash, ThreadNode>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for ThreadIterator<'a> {
|
||||
impl<'a> Iterator for ThreadGroupIterator<'a> {
|
||||
type Item = (usize, ThreadNodeHash);
|
||||
fn next(&mut self) -> Option<(usize, ThreadNodeHash)> {
|
||||
loop {
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
* meli - melib library
|
||||
*
|
||||
* Copyright 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::time::Duration;
|
||||
|
||||
use futures::future::{self, Either, Future};
|
||||
|
||||
pub async fn timeout<O>(
|
||||
dur: Option<Duration>,
|
||||
f: impl Future<Output = O> + Send,
|
||||
) -> crate::Result<O> {
|
||||
futures::pin_mut!(f);
|
||||
if let Some(dur) = dur {
|
||||
match future::select(f, smol::Timer::after(dur)).await {
|
||||
Either::Left((out, _)) => Ok(out),
|
||||
Either::Right(_) => {
|
||||
Err(crate::error::Error::new("Timed out.")
|
||||
.set_kind(crate::error::ErrorKind::Timeout))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(f.await)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sleep(dur: Duration) {
|
||||
smol::Timer::after(dur).await;
|
||||
}
|
|
@ -1,157 +0,0 @@
|
|||
/*
|
||||
* meli - lib.rs
|
||||
*
|
||||
* Copyright 2017 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/>.
|
||||
*/
|
||||
|
||||
//! Utility modules for general use.
|
||||
|
||||
pub mod connections;
|
||||
pub mod datetime;
|
||||
pub mod futures;
|
||||
pub mod random;
|
||||
#[macro_use]
|
||||
pub mod logging;
|
||||
pub mod parsec;
|
||||
pub mod percent_encoding;
|
||||
pub mod shellexpand;
|
||||
#[cfg(feature = "sqlite3")]
|
||||
pub mod sqlite3;
|
||||
|
||||
pub mod html_escape {
|
||||
//! HTML Coded Character Set
|
||||
|
||||
/// Numeric and Special Graphic Entity Set
|
||||
///
|
||||
/// ```text
|
||||
/// GLYPH NAME SYNTAX DESCRIPTION
|
||||
/// < lt < Less than sign
|
||||
/// > gt > Greater than sign
|
||||
/// & amp & Ampersand
|
||||
/// " quot " Double quote sign
|
||||
/// ```
|
||||
///
|
||||
/// Source: <https://www.w3.org/MarkUp/html-spec/html-spec_9.html#SEC9.7.1>
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
pub enum HtmlEntity {
|
||||
/// Less than sign
|
||||
Lt,
|
||||
/// Greater than sign
|
||||
Gt,
|
||||
/// Ampersand
|
||||
Amp,
|
||||
/// Double quote sign
|
||||
Quot,
|
||||
}
|
||||
|
||||
impl HtmlEntity {
|
||||
pub const ALL: [&str; 4] = ["<", ">", "&", """];
|
||||
pub const GLYPHS: [&str; 4] = ["<", ">", "&", "\""];
|
||||
|
||||
pub const fn glyph(self) -> char {
|
||||
match self {
|
||||
Self::Lt => '<',
|
||||
Self::Gt => '>',
|
||||
Self::Amp => '&',
|
||||
Self::Quot => '"',
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Lt => "lt",
|
||||
Self::Gt => "gt",
|
||||
Self::Amp => "amp",
|
||||
Self::Quot => "quot",
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn syntax(self) -> &'static str {
|
||||
match self {
|
||||
Self::Lt => "<",
|
||||
Self::Gt => ">",
|
||||
Self::Amp => "&",
|
||||
Self::Quot => """,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! declare_u64_hash {
|
||||
($type_name:ident) => {
|
||||
#[derive(
|
||||
Hash,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Debug,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Copy,
|
||||
Clone,
|
||||
)]
|
||||
#[repr(transparent)]
|
||||
pub struct $type_name(pub u64);
|
||||
|
||||
impl $type_name {
|
||||
#[inline(always)]
|
||||
pub fn from_bytes(bytes: &[u8]) -> Self {
|
||||
use std::{collections::hash_map::DefaultHasher, hash::Hasher};
|
||||
let mut h = DefaultHasher::new();
|
||||
h.write(bytes);
|
||||
Self(h.finish())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn to_be_bytes(self) -> [u8; 8] {
|
||||
self.0.to_be_bytes()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub const fn is_null(self) -> bool {
|
||||
self.0 == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for $type_name {
|
||||
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(fmt, "{}", self.0)
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "sqlite3")]
|
||||
impl rusqlite::types::ToSql for $type_name {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
Ok(rusqlite::types::ToSqlOutput::from(self.0 as i64))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite3")]
|
||||
impl rusqlite::types::FromSql for $type_name {
|
||||
fn column_result(
|
||||
value: rusqlite::types::ValueRef,
|
||||
) -> rusqlite::types::FromSqlResult<Self> {
|
||||
let b: i64 = rusqlite::types::FromSql::column_result(value)?;
|
||||
|
||||
Ok($type_name(b as u64))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,493 +0,0 @@
|
|||
// Copyright 2013-2016 The rust-url developers.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
|
||||
//! URLs use special characters to indicate the parts of the request.
|
||||
//! For example, a `?` question mark marks the end of a path and the start of a
|
||||
//! query string. In order for that character to exist inside a path, it needs
|
||||
//! to be encoded differently.
|
||||
//!
|
||||
//! Percent encoding replaces reserved characters with the `%` escape character
|
||||
//! followed by a byte value as two hexadecimal digits.
|
||||
//! For example, an ASCII space is replaced with `%20`.
|
||||
//!
|
||||
//! When encoding, the set of characters that can (and should, for readability)
|
||||
//! be left alone depends on the context.
|
||||
//! The `?` question mark mentioned above is not a separator when used literally
|
||||
//! inside of a query string, and therefore does not need to be encoded.
|
||||
//! The [`AsciiSet`] parameter of [`percent_encode`] and [`utf8_percent_encode`]
|
||||
//! lets callers configure this.
|
||||
//!
|
||||
//! This crate deliberately does not provide many different sets.
|
||||
//! Users should consider in what context the encoded string will be used,
|
||||
//! read relevant specifications, and define their own set.
|
||||
//! This is done by using the `add` method of an existing set.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```rust
|
||||
//! use melib::percent_encoding::{percent_decode_str, utf8_percent_encode, AsciiSet, CONTROLS};
|
||||
//!
|
||||
//! /// https://url.spec.whatwg.org/#fragment-percent-encode-set
|
||||
//! const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! utf8_percent_encode("foo <bar>", FRAGMENT).to_string(),
|
||||
//! "foo%20%3Cbar%3E"
|
||||
//! );
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! percent_decode_str("foo%20%3Cbar%3E").decode_utf8().unwrap(),
|
||||
//! "foo <bar>"
|
||||
//! );
|
||||
//! ```
|
||||
|
||||
use std::{borrow::Cow, fmt, mem, slice, str};
|
||||
|
||||
/// Represents a set of characters or bytes in the ASCII range.
|
||||
///
|
||||
/// This is used in [`percent_encode`] and [`utf8_percent_encode`].
|
||||
/// This is similar to [percent-encode sets](https://url.spec.whatwg.org/#percent-encoded-bytes).
|
||||
///
|
||||
/// Use the `add` method of an existing set to define a new set. For example:
|
||||
///
|
||||
/// ```
|
||||
/// use melib::percent_encoding::{AsciiSet, CONTROLS};
|
||||
///
|
||||
/// /// https://url.spec.whatwg.org/#fragment-percent-encode-set
|
||||
/// const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');
|
||||
/// ```
|
||||
pub struct AsciiSet {
|
||||
mask: [Chunk; ASCII_RANGE_LEN / BITS_PER_CHUNK],
|
||||
}
|
||||
|
||||
type Chunk = u32;
|
||||
|
||||
const ASCII_RANGE_LEN: usize = 0x80;
|
||||
|
||||
const BITS_PER_CHUNK: usize = 8 * mem::size_of::<Chunk>();
|
||||
|
||||
impl AsciiSet {
|
||||
/// Called with UTF-8 bytes rather than code points.
|
||||
/// Not used for non-ASCII bytes.
|
||||
const fn contains(&self, byte: u8) -> bool {
|
||||
let chunk = self.mask[byte as usize / BITS_PER_CHUNK];
|
||||
let mask = 1 << (byte as usize % BITS_PER_CHUNK);
|
||||
(chunk & mask) != 0
|
||||
}
|
||||
|
||||
const fn should_percent_encode(&self, byte: u8) -> bool {
|
||||
!byte.is_ascii() || self.contains(byte)
|
||||
}
|
||||
|
||||
pub const fn add(&self, byte: u8) -> Self {
|
||||
let mut mask = self.mask;
|
||||
mask[byte as usize / BITS_PER_CHUNK] |= 1 << (byte as usize % BITS_PER_CHUNK);
|
||||
Self { mask }
|
||||
}
|
||||
|
||||
pub const fn remove(&self, byte: u8) -> Self {
|
||||
let mut mask = self.mask;
|
||||
mask[byte as usize / BITS_PER_CHUNK] &= !(1 << (byte as usize % BITS_PER_CHUNK));
|
||||
Self { mask }
|
||||
}
|
||||
}
|
||||
|
||||
/// The set of 0x00 to 0x1F (C0 controls), and 0x7F (DEL).
|
||||
///
|
||||
/// Note that this includes the newline and tab characters, but not the space
|
||||
/// 0x20.
|
||||
///
|
||||
/// <https://url.spec.whatwg.org/#c0-control-percent-encode-set>
|
||||
pub const CONTROLS: &AsciiSet = &AsciiSet {
|
||||
mask: [
|
||||
!0_u32, // C0: 0x00 to 0x1F (32 bits set)
|
||||
0,
|
||||
0,
|
||||
1 << (0x7F_u32 % 32), // DEL: 0x7F (one bit set)
|
||||
],
|
||||
};
|
||||
|
||||
macro_rules! static_assert {
|
||||
($( $bool: expr, )+) => {
|
||||
const fn _static_assert() {
|
||||
$(
|
||||
let _ = mem::transmute::<[u8; $bool as usize], u8>;
|
||||
)+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static_assert! {
|
||||
CONTROLS.contains(0x00),
|
||||
CONTROLS.contains(0x1F),
|
||||
!CONTROLS.contains(0x20),
|
||||
!CONTROLS.contains(0x7E),
|
||||
CONTROLS.contains(0x7F),
|
||||
}
|
||||
|
||||
/// Everything that is not an ASCII letter or digit.
|
||||
///
|
||||
/// This is probably more eager than necessary in any context.
|
||||
pub const NON_ALPHANUMERIC: &AsciiSet = &CONTROLS
|
||||
.add(b' ')
|
||||
.add(b'!')
|
||||
.add(b'"')
|
||||
.add(b'#')
|
||||
.add(b'$')
|
||||
.add(b'%')
|
||||
.add(b'&')
|
||||
.add(b'\'')
|
||||
.add(b'(')
|
||||
.add(b')')
|
||||
.add(b'*')
|
||||
.add(b'+')
|
||||
.add(b',')
|
||||
.add(b'-')
|
||||
.add(b'.')
|
||||
.add(b'/')
|
||||
.add(b':')
|
||||
.add(b';')
|
||||
.add(b'<')
|
||||
.add(b'=')
|
||||
.add(b'>')
|
||||
.add(b'?')
|
||||
.add(b'@')
|
||||
.add(b'[')
|
||||
.add(b'\\')
|
||||
.add(b']')
|
||||
.add(b'^')
|
||||
.add(b'_')
|
||||
.add(b'`')
|
||||
.add(b'{')
|
||||
.add(b'|')
|
||||
.add(b'}')
|
||||
.add(b'~');
|
||||
|
||||
/// Return the percent-encoding of the given byte.
|
||||
///
|
||||
/// This is unconditional, unlike `percent_encode()` which has an `AsciiSet`
|
||||
/// parameter.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use melib::percent_encoding::percent_encode_byte;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// "foo bar"
|
||||
/// .bytes()
|
||||
/// .map(percent_encode_byte)
|
||||
/// .collect::<String>(),
|
||||
/// "%66%6F%6F%20%62%61%72"
|
||||
/// );
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn percent_encode_byte(byte: u8) -> &'static str {
|
||||
static ENC_TABLE: &[u8; 768] = b"\
|
||||
%00%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F\
|
||||
%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F\
|
||||
%20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F\
|
||||
%30%31%32%33%34%35%36%37%38%39%3A%3B%3C%3D%3E%3F\
|
||||
%40%41%42%43%44%45%46%47%48%49%4A%4B%4C%4D%4E%4F\
|
||||
%50%51%52%53%54%55%56%57%58%59%5A%5B%5C%5D%5E%5F\
|
||||
%60%61%62%63%64%65%66%67%68%69%6A%6B%6C%6D%6E%6F\
|
||||
%70%71%72%73%74%75%76%77%78%79%7A%7B%7C%7D%7E%7F\
|
||||
%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F\
|
||||
%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F\
|
||||
%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF\
|
||||
%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF\
|
||||
%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF\
|
||||
%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF\
|
||||
%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF\
|
||||
%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF\
|
||||
";
|
||||
|
||||
let index = usize::from(byte) * 3;
|
||||
// SAFETY: ENC_TABLE is ascii-only, so any subset if it should be
|
||||
// ascii-only too, which is valid utf8.
|
||||
unsafe { str::from_utf8_unchecked(&ENC_TABLE[index..index + 3]) }
|
||||
}
|
||||
|
||||
/// Percent-encode the given bytes with the given set.
|
||||
///
|
||||
/// Non-ASCII bytes and bytes in `ascii_set` are encoded.
|
||||
///
|
||||
/// The return type:
|
||||
///
|
||||
/// * Implements `Iterator<Item = &str>` and therefore has a
|
||||
/// `.collect::<String>()` method,
|
||||
/// * Implements `Display` and therefore has a `.to_string()` method,
|
||||
/// * Implements `Into<Cow<str>>` borrowing `input` when none of its bytes are
|
||||
/// encoded.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use melib::percent_encoding::{percent_encode, NON_ALPHANUMERIC};
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// percent_encode(b"foo bar?", NON_ALPHANUMERIC).to_string(),
|
||||
/// "foo%20bar%3F"
|
||||
/// );
|
||||
/// ```
|
||||
#[inline]
|
||||
pub const fn percent_encode<'a>(
|
||||
input: &'a [u8],
|
||||
ascii_set: &'static AsciiSet,
|
||||
) -> PercentEncode<'a> {
|
||||
PercentEncode {
|
||||
bytes: input,
|
||||
ascii_set,
|
||||
}
|
||||
}
|
||||
|
||||
/// Percent-encode the UTF-8 encoding of the given string.
|
||||
///
|
||||
/// See [`percent_encode`] regarding the return type.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use melib::percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// utf8_percent_encode("foo bar?", NON_ALPHANUMERIC).to_string(),
|
||||
/// "foo%20bar%3F"
|
||||
/// );
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn utf8_percent_encode<'a>(input: &'a str, ascii_set: &'static AsciiSet) -> PercentEncode<'a> {
|
||||
percent_encode(input.as_bytes(), ascii_set)
|
||||
}
|
||||
|
||||
/// The return type of [`percent_encode`] and [`utf8_percent_encode`].
|
||||
#[derive(Clone)]
|
||||
pub struct PercentEncode<'a> {
|
||||
bytes: &'a [u8],
|
||||
ascii_set: &'static AsciiSet,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for PercentEncode<'a> {
|
||||
type Item = &'a str;
|
||||
|
||||
fn next(&mut self) -> Option<&'a str> {
|
||||
if let Some((&first_byte, remaining)) = self.bytes.split_first() {
|
||||
if self.ascii_set.should_percent_encode(first_byte) {
|
||||
self.bytes = remaining;
|
||||
Some(percent_encode_byte(first_byte))
|
||||
} else {
|
||||
// The unsafe blocks here are appropriate because the bytes are
|
||||
// confirmed as a subset of UTF-8 in should_percent_encode.
|
||||
for (i, &byte) in remaining.iter().enumerate() {
|
||||
if self.ascii_set.should_percent_encode(byte) {
|
||||
// 1 for first_byte + i for previous iterations of this loop
|
||||
let (unchanged_slice, remaining) = self.bytes.split_at(1 + i);
|
||||
self.bytes = remaining;
|
||||
return Some(unsafe { str::from_utf8_unchecked(unchanged_slice) });
|
||||
}
|
||||
}
|
||||
let unchanged_slice = self.bytes;
|
||||
self.bytes = &[][..];
|
||||
Some(unsafe { str::from_utf8_unchecked(unchanged_slice) })
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
if self.bytes.is_empty() {
|
||||
(0, Some(0))
|
||||
} else {
|
||||
(1, Some(self.bytes.len()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for PercentEncode<'a> {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for c in (*self).clone() {
|
||||
formatter.write_str(c)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<PercentEncode<'a>> for Cow<'a, str> {
|
||||
fn from(mut iter: PercentEncode<'a>) -> Self {
|
||||
iter.next().map_or_else(
|
||||
|| "".into(),
|
||||
|first| {
|
||||
iter.next().map_or_else(
|
||||
|| first.into(),
|
||||
|second| {
|
||||
let mut string = first.to_owned();
|
||||
string.push_str(second);
|
||||
string.extend(iter);
|
||||
string.into()
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Percent-decode the given string.
|
||||
///
|
||||
/// <https://url.spec.whatwg.org/#string-percent-decode>
|
||||
///
|
||||
/// See [`percent_decode`] regarding the return type.
|
||||
#[inline]
|
||||
pub fn percent_decode_str(input: &str) -> PercentDecode<'_> {
|
||||
percent_decode(input.as_bytes())
|
||||
}
|
||||
|
||||
/// Percent-decode the given bytes.
|
||||
///
|
||||
/// <https://url.spec.whatwg.org/#percent-decode>
|
||||
///
|
||||
/// Any sequence of `%` followed by two hexadecimal digits is decoded.
|
||||
/// The return type:
|
||||
///
|
||||
/// * Implements `Into<Cow<u8>>` borrowing `input` when it contains no
|
||||
/// percent-encoded sequence,
|
||||
/// * Implements `Iterator<Item = u8>` and therefore has a
|
||||
/// `.collect::<Vec<u8>>()` method,
|
||||
/// * Has `decode_utf8()` and `decode_utf8_lossy()` methods.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use melib::percent_encoding::percent_decode;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// percent_decode(b"foo%20bar%3f").decode_utf8().unwrap(),
|
||||
/// "foo bar?"
|
||||
/// );
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn percent_decode(input: &[u8]) -> PercentDecode<'_> {
|
||||
PercentDecode {
|
||||
bytes: input.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The return type of [`percent_decode`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PercentDecode<'a> {
|
||||
bytes: slice::Iter<'a, u8>,
|
||||
}
|
||||
|
||||
fn after_percent_sign(iter: &mut slice::Iter<'_, u8>) -> Option<u8> {
|
||||
let mut cloned_iter = iter.clone();
|
||||
let h = char::from(*cloned_iter.next()?).to_digit(16)?;
|
||||
let l = char::from(*cloned_iter.next()?).to_digit(16)?;
|
||||
*iter = cloned_iter;
|
||||
Some(h as u8 * 0x10 + l as u8)
|
||||
}
|
||||
|
||||
impl<'a> Iterator for PercentDecode<'a> {
|
||||
type Item = u8;
|
||||
|
||||
fn next(&mut self) -> Option<u8> {
|
||||
self.bytes.next().map(|&byte| {
|
||||
if byte == b'%' {
|
||||
after_percent_sign(&mut self.bytes).unwrap_or(byte)
|
||||
} else {
|
||||
byte
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let bytes = self.bytes.len();
|
||||
((bytes + 2) / 3, Some(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<PercentDecode<'a>> for Cow<'a, [u8]> {
|
||||
fn from(iter: PercentDecode<'a>) -> Self {
|
||||
match iter.if_any() {
|
||||
Some(vec) => Cow::Owned(vec),
|
||||
None => Cow::Borrowed(iter.bytes.as_slice()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PercentDecode<'a> {
|
||||
/// If the percent-decoding is different from the input, return it as a new
|
||||
/// bytes vector.
|
||||
fn if_any(&self) -> Option<Vec<u8>> {
|
||||
let mut bytes_iter = self.bytes.clone();
|
||||
while bytes_iter.any(|&b| b == b'%') {
|
||||
if let Some(decoded_byte) = after_percent_sign(&mut bytes_iter) {
|
||||
let initial_bytes = self.bytes.as_slice();
|
||||
let unchanged_bytes_len = initial_bytes.len() - bytes_iter.len() - 3;
|
||||
let mut decoded = initial_bytes[..unchanged_bytes_len].to_owned();
|
||||
decoded.push(decoded_byte);
|
||||
decoded.extend(PercentDecode { bytes: bytes_iter });
|
||||
return Some(decoded);
|
||||
}
|
||||
}
|
||||
// Nothing to decode
|
||||
None
|
||||
}
|
||||
|
||||
/// Decode the result of percent-decoding as UTF-8.
|
||||
///
|
||||
/// This is return `Err` when the percent-decoded bytes are not well-formed
|
||||
/// in UTF-8.
|
||||
pub fn decode_utf8(self) -> Result<Cow<'a, str>, str::Utf8Error> {
|
||||
match self.clone().into() {
|
||||
Cow::Borrowed(bytes) => match str::from_utf8(bytes) {
|
||||
Ok(s) => Ok(s.into()),
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
Cow::Owned(bytes) => match String::from_utf8(bytes) {
|
||||
Ok(s) => Ok(s.into()),
|
||||
Err(e) => Err(e.utf8_error()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode the result of percent-decoding as UTF-8, lossily.
|
||||
///
|
||||
/// Invalid UTF-8 percent-encoded byte sequences will be replaced <20> U+FFFD,
|
||||
/// the replacement character.
|
||||
pub fn decode_utf8_lossy(self) -> Cow<'a, str> {
|
||||
decode_utf8_lossy(self.clone().into())
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_utf8_lossy(input: Cow<'_, [u8]>) -> Cow<'_, str> {
|
||||
// Note: This function is duplicated in `form_urlencoded/src/query_encoding.rs`.
|
||||
match input {
|
||||
Cow::Borrowed(bytes) => String::from_utf8_lossy(bytes),
|
||||
Cow::Owned(bytes) => {
|
||||
match String::from_utf8_lossy(&bytes) {
|
||||
Cow::Borrowed(utf8) => {
|
||||
// If from_utf8_lossy returns a Cow::Borrowed, then we can
|
||||
// be sure our original bytes were valid UTF-8. This is because
|
||||
// if the bytes were invalid UTF-8 from_utf8_lossy would have
|
||||
// to allocate a new owned string to back the Cow so it could
|
||||
// replace invalid bytes with a placeholder.
|
||||
|
||||
// First we do a debug_assert to confirm our description above.
|
||||
let raw_utf8: *const [u8] = utf8.as_bytes();
|
||||
debug_assert!(raw_utf8 == std::ptr::addr_of!(*bytes));
|
||||
|
||||
// Given we know the original input bytes are valid UTF-8,
|
||||
// and we have ownership of those bytes, we re-use them and
|
||||
// return a Cow::Owned here.
|
||||
Cow::Owned(unsafe { String::from_utf8_unchecked(bytes) })
|
||||
}
|
||||
Cow::Owned(s) => Cow::Owned(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
* meli - melib crate.
|
||||
*
|
||||
* Copyright 2017-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::{fs::File, io::prelude::*, time::SystemTime};
|
||||
|
||||
const EXPECT: &str = "Could not open/read /dev/urandom";
|
||||
|
||||
pub fn random_u64() -> u64 {
|
||||
let mut f = File::open("/dev/urandom").expect(EXPECT);
|
||||
let mut buffer = [0; 8];
|
||||
|
||||
// read exactly 8 bytes
|
||||
f.read_exact(&mut buffer).expect(EXPECT);
|
||||
|
||||
u64::from(buffer[0])
|
||||
| (u64::from(buffer[1]) << 8)
|
||||
| (u64::from(buffer[2]) << 16)
|
||||
| (u64::from(buffer[3]) << 24)
|
||||
| (u64::from(buffer[4]) << 32)
|
||||
| (u64::from(buffer[5]) << 40)
|
||||
| (u64::from(buffer[6]) << 48)
|
||||
| (u64::from(buffer[7]) << 56)
|
||||
}
|
||||
|
||||
pub fn random_u32() -> u32 {
|
||||
let mut f = File::open("/dev/urandom").expect(EXPECT);
|
||||
let mut buffer = [0; 4];
|
||||
|
||||
// read exactly 4 bytes
|
||||
f.read_exact(&mut buffer).expect(EXPECT);
|
||||
|
||||
u32::from(buffer[0])
|
||||
| (u32::from(buffer[1]) << 8)
|
||||
| (u32::from(buffer[2]) << 16)
|
||||
| (u32::from(buffer[3]) << 24)
|
||||
}
|
||||
|
||||
pub fn random_u8() -> u8 {
|
||||
let mut f = File::open("/dev/urandom").expect(EXPECT);
|
||||
let mut buffer = [0; 1];
|
||||
|
||||
// read exactly 1 byte
|
||||
f.read_exact(&mut buffer).expect(EXPECT);
|
||||
|
||||
buffer[0]
|
||||
}
|
||||
|
||||
pub fn clock() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}
|
|
@ -1,242 +0,0 @@
|
|||
/*
|
||||
* meli - lib.rs
|
||||
*
|
||||
* Copyright 2017 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/>.
|
||||
*/
|
||||
|
||||
//! A `ShellExpandTrait` to expand paths like a shell.
|
||||
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
os::unix::ffi::OsStrExt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use smallvec::SmallVec;
|
||||
|
||||
pub trait ShellExpandTrait {
|
||||
fn expand(&self) -> PathBuf;
|
||||
fn complete(&self, force: bool) -> SmallVec<[String; 128]>;
|
||||
}
|
||||
|
||||
impl ShellExpandTrait for Path {
|
||||
fn expand(&self) -> PathBuf {
|
||||
let mut ret = PathBuf::new();
|
||||
for c in self.components() {
|
||||
let c_to_str = c.as_os_str().to_str();
|
||||
match c_to_str {
|
||||
Some("~") => {
|
||||
if let Ok(home_dir) = std::env::var("HOME") {
|
||||
ret.push(home_dir)
|
||||
} else {
|
||||
return PathBuf::new();
|
||||
}
|
||||
}
|
||||
Some(var) if var.starts_with('$') => {
|
||||
let env_name = var.split_at(1).1;
|
||||
if env_name.chars().all(char::is_uppercase) {
|
||||
ret.push(std::env::var(env_name).unwrap_or_default());
|
||||
} else {
|
||||
ret.push(c);
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
ret.push(c);
|
||||
}
|
||||
None => {
|
||||
/* path is invalid */
|
||||
return PathBuf::new();
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn complete(&self, force: bool) -> SmallVec<[String; 128]> {
|
||||
use std::{convert::TryFrom, os::unix::io::AsRawFd};
|
||||
|
||||
use libc::dirent64;
|
||||
use nix::fcntl::OFlag;
|
||||
|
||||
const BUF_SIZE: ::libc::size_t = 8 << 10;
|
||||
|
||||
let (prefix, _match) = if self.as_os_str().as_bytes().ends_with(b"/.") {
|
||||
(self.components().as_path(), OsStr::from_bytes(b"."))
|
||||
} else if self.exists() && (!force || self.as_os_str().as_bytes().ends_with(b"/")) {
|
||||
return SmallVec::new();
|
||||
} else {
|
||||
let last_component = self
|
||||
.components()
|
||||
.last()
|
||||
.map(|c| c.as_os_str())
|
||||
.unwrap_or_else(|| OsStr::from_bytes(b""));
|
||||
let prefix = if let Some(p) = self.parent() {
|
||||
p
|
||||
} else {
|
||||
return SmallVec::new();
|
||||
};
|
||||
(prefix, last_component)
|
||||
};
|
||||
|
||||
let dir = match ::nix::dir::Dir::openat(
|
||||
::libc::AT_FDCWD,
|
||||
prefix,
|
||||
OFlag::O_DIRECTORY | OFlag::O_NOATIME | OFlag::O_RDONLY | OFlag::O_CLOEXEC,
|
||||
::nix::sys::stat::Mode::S_IRUSR | ::nix::sys::stat::Mode::S_IXUSR,
|
||||
)
|
||||
.or_else(|_| {
|
||||
::nix::dir::Dir::openat(
|
||||
::libc::AT_FDCWD,
|
||||
prefix,
|
||||
OFlag::O_DIRECTORY | OFlag::O_RDONLY | OFlag::O_CLOEXEC,
|
||||
::nix::sys::stat::Mode::S_IRUSR | ::nix::sys::stat::Mode::S_IXUSR,
|
||||
)
|
||||
}) {
|
||||
Ok(dir) => dir,
|
||||
Err(err) => {
|
||||
debug!(prefix);
|
||||
debug!(err);
|
||||
return SmallVec::new();
|
||||
}
|
||||
};
|
||||
|
||||
let mut buf: Vec<u8> = Vec::with_capacity(BUF_SIZE);
|
||||
let mut entries = SmallVec::new();
|
||||
loop {
|
||||
let n: i64 = unsafe {
|
||||
::libc::syscall(
|
||||
::libc::SYS_getdents64,
|
||||
dir.as_raw_fd(),
|
||||
buf.as_ptr(),
|
||||
BUF_SIZE - 256,
|
||||
)
|
||||
};
|
||||
|
||||
let Ok(n) = usize::try_from(n) else {
|
||||
return SmallVec::new();
|
||||
};
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
unsafe {
|
||||
buf.set_len(n);
|
||||
}
|
||||
let mut pos = 0;
|
||||
while pos < n {
|
||||
let dir = unsafe { std::mem::transmute::<&[u8], &[dirent64]>(&buf[pos..]) };
|
||||
let entry = unsafe { std::ffi::CStr::from_ptr(dir[0].d_name.as_ptr()) };
|
||||
if entry.to_bytes() != b"."
|
||||
&& entry.to_bytes() != b".."
|
||||
&& entry.to_bytes().starts_with(_match.as_bytes())
|
||||
{
|
||||
if dir[0].d_type == ::libc::DT_DIR && !entry.to_bytes().ends_with(b"/") {
|
||||
let mut s = unsafe {
|
||||
String::from_utf8_unchecked(
|
||||
entry.to_bytes()[_match.as_bytes().len()..].to_vec(),
|
||||
)
|
||||
};
|
||||
s.push('/');
|
||||
entries.push(s);
|
||||
} else {
|
||||
entries.push(unsafe {
|
||||
String::from_utf8_unchecked(
|
||||
entry.to_bytes()[_match.as_bytes().len()..].to_vec(),
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
pos += dir[0].d_reclen as usize;
|
||||
}
|
||||
// https://github.com/romkatv/gitstatus/blob/caf44f7aaf33d0f46e6749e50595323c277e0908/src/dir.cc
|
||||
// "It's tempting to bail here if n + sizeof(linux_dirent64) +
|
||||
// 512 <= n. After all, there was enough space
|
||||
// for another entry but SYS_getdents64 didn't write it, so this
|
||||
// must be the end of the directory listing,
|
||||
// right? Unfortunately, no. SYS_getdents64 is finicky.
|
||||
// It sometimes writes a partial list of entries even if the
|
||||
// full list would fit."
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn complete(&self, force: bool) -> SmallVec<[String; 128]> {
|
||||
let mut entries = SmallVec::new();
|
||||
let (prefix, _match) = {
|
||||
if self.exists() && (!force || self.as_os_str().as_bytes().ends_with(b"/")) {
|
||||
// println!("{} {:?}", self.display(), self.components().last());
|
||||
return entries;
|
||||
} else {
|
||||
let last_component = self
|
||||
.components()
|
||||
.last()
|
||||
.map(|c| c.as_os_str())
|
||||
.unwrap_or_else(|| OsStr::from_bytes(b""));
|
||||
let prefix = if let Some(p) = self.parent() {
|
||||
p
|
||||
} else {
|
||||
return entries;
|
||||
};
|
||||
(prefix, last_component)
|
||||
}
|
||||
};
|
||||
if force && self.is_dir() && !self.as_os_str().as_bytes().ends_with(b"/") {
|
||||
entries.push("/".to_string());
|
||||
}
|
||||
|
||||
if let Ok(iter) = std::fs::read_dir(prefix) {
|
||||
for entry in iter.flatten() {
|
||||
if entry.path().as_os_str().as_bytes() != b"."
|
||||
&& entry.path().as_os_str().as_bytes() != b".."
|
||||
&& entry
|
||||
.path()
|
||||
.as_os_str()
|
||||
.as_bytes()
|
||||
.starts_with(_match.as_bytes())
|
||||
{
|
||||
if entry.path().is_dir() && !entry.path().as_os_str().as_bytes().ends_with(b"/")
|
||||
{
|
||||
let mut s = unsafe {
|
||||
String::from_utf8_unchecked(
|
||||
entry.path().as_os_str().as_bytes()[_match.as_bytes().len()..]
|
||||
.to_vec(),
|
||||
)
|
||||
};
|
||||
s.push('/');
|
||||
entries.push(s);
|
||||
} else {
|
||||
entries.push(unsafe {
|
||||
String::from_utf8_unchecked(
|
||||
entry.path().as_os_str().as_bytes()[_match.as_bytes().len()..]
|
||||
.to_vec(),
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
entries
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shellexpandtrait() {
|
||||
assert!(Path::new("~").expand().complete(false).is_empty());
|
||||
assert!(!Path::new("~").expand().complete(true).is_empty());
|
||||
}
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
//! Command line arguments.
|
||||
|
||||
use super::*;
|
||||
use meli::*;
|
||||
|
||||
#[cfg(feature = "cli-docs")]
|
||||
fn parse_manpage(src: &str) -> Result<ManPages> {
|
||||
|
|
213
src/command.rs
213
src/command.rs
|
@ -19,9 +19,8 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! A parser module for user commands passed through
|
||||
//! [`Command`](crate::types::UIMode::Command) mode.
|
||||
|
||||
/*! A parser module for user commands passed through Command mode.
|
||||
*/
|
||||
pub use melib::thread::{SortField, SortOrder};
|
||||
use melib::{
|
||||
nom::{
|
||||
|
@ -1006,6 +1005,107 @@ pub fn parse_command(input: &[u8]) -> Result<Action, Error> {
|
|||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
let mut input = "sort".to_string();
|
||||
macro_rules! match_input {
|
||||
($input:expr) => {{
|
||||
let mut sugg = Default::default();
|
||||
let mut vec = vec![];
|
||||
//print!("{}", $input);
|
||||
for (_tags, _desc, tokens) in COMMAND_COMPLETION.iter() {
|
||||
//println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
|
||||
let m = tokens.matches(&mut $input.as_str(), &mut sugg);
|
||||
if !m.is_empty() {
|
||||
vec.push(tokens);
|
||||
//print!("{:?} ", desc);
|
||||
//println!(" result = {:#?}\n\n", m);
|
||||
}
|
||||
}
|
||||
//println!("suggestions = {:#?}", sugg);
|
||||
sugg.into_iter()
|
||||
.map(|s| format!("{}{}", $input.as_str(), s.as_str()))
|
||||
.collect::<HashSet<String>>()
|
||||
}};
|
||||
}
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["sort date".to_string(), "sort subject".to_string()]).collect(),
|
||||
);
|
||||
input = "so".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["sort".to_string()]).collect(),
|
||||
);
|
||||
input = "so ".to_string();
|
||||
assert_eq!(&match_input!(input), &HashSet::default(),);
|
||||
input = "to".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["toggle".to_string()]).collect(),
|
||||
);
|
||||
input = "toggle ".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter([
|
||||
"toggle mouse".to_string(),
|
||||
"toggle sign".to_string(),
|
||||
"toggle encrypt".to_string(),
|
||||
"toggle thread_snooze".to_string()
|
||||
])
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_parser_interactive() {
|
||||
use std::io;
|
||||
let mut input = String::new();
|
||||
loop {
|
||||
input.clear();
|
||||
print!("> ");
|
||||
match io::stdin().read_line(&mut input) {
|
||||
Ok(_n) => {
|
||||
println!("Input is {:?}", input.as_str().trim());
|
||||
let mut sugg = Default::default();
|
||||
let mut vec = vec![];
|
||||
//print!("{}", input);
|
||||
for (_tags, _desc, tokens) in COMMAND_COMPLETION.iter() {
|
||||
//println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
|
||||
let m = tokens.matches(&mut input.as_str().trim(), &mut sugg);
|
||||
if !m.is_empty() {
|
||||
vec.push(tokens);
|
||||
//print!("{:?} ", desc);
|
||||
//println!(" result = {:#?}\n\n", m);
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"suggestions = {:#?}",
|
||||
sugg.into_iter()
|
||||
.zip(vec.into_iter())
|
||||
.map(|(s, v)| format!(
|
||||
"{}{} {:?}",
|
||||
input.as_str().trim(),
|
||||
if input.trim().is_empty() {
|
||||
s.trim()
|
||||
} else {
|
||||
s.as_str()
|
||||
},
|
||||
v
|
||||
))
|
||||
.collect::<Vec<String>>()
|
||||
);
|
||||
if input.trim() == "quit" {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(error) => println!("error: {}", error),
|
||||
}
|
||||
}
|
||||
println!("alright");
|
||||
}
|
||||
|
||||
/// Get command suggestions for input
|
||||
pub fn command_completion_suggestions(input: &str) -> Vec<String> {
|
||||
use crate::melib::ShellExpandTrait;
|
||||
|
@ -1024,110 +1124,3 @@ pub fn command_completion_suggestions(input: &str) -> Vec<String> {
|
|||
.map(|s| format!("{}{}", input, s.as_str()))
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_command_parser() {
|
||||
let mut input = "sort".to_string();
|
||||
macro_rules! match_input {
|
||||
($input:expr) => {{
|
||||
let mut sugg = Default::default();
|
||||
let mut vec = vec![];
|
||||
//print!("{}", $input);
|
||||
for (_tags, _desc, tokens) in COMMAND_COMPLETION.iter() {
|
||||
//println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
|
||||
let m = tokens.matches(&mut $input.as_str(), &mut sugg);
|
||||
if !m.is_empty() {
|
||||
vec.push(tokens);
|
||||
//print!("{:?} ", desc);
|
||||
//println!(" result = {:#?}\n\n", m);
|
||||
}
|
||||
}
|
||||
//println!("suggestions = {:#?}", sugg);
|
||||
sugg.into_iter()
|
||||
.map(|s| format!("{}{}", $input.as_str(), s.as_str()))
|
||||
.collect::<HashSet<String>>()
|
||||
}};
|
||||
}
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["sort date".to_string(), "sort subject".to_string()])
|
||||
.collect(),
|
||||
);
|
||||
input = "so".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["sort".to_string()]).collect(),
|
||||
);
|
||||
input = "so ".to_string();
|
||||
assert_eq!(&match_input!(input), &HashSet::default(),);
|
||||
input = "to".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["toggle".to_string()]).collect(),
|
||||
);
|
||||
input = "toggle ".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter([
|
||||
"toggle mouse".to_string(),
|
||||
"toggle sign".to_string(),
|
||||
"toggle encrypt".to_string(),
|
||||
"toggle thread_snooze".to_string()
|
||||
])
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_parser_interactive() {
|
||||
use std::io;
|
||||
let mut input = String::new();
|
||||
loop {
|
||||
input.clear();
|
||||
print!("> ");
|
||||
match io::stdin().read_line(&mut input) {
|
||||
Ok(_n) => {
|
||||
println!("Input is {:?}", input.as_str().trim());
|
||||
let mut sugg = Default::default();
|
||||
let mut vec = vec![];
|
||||
//print!("{}", input);
|
||||
for (_tags, _desc, tokens) in COMMAND_COMPLETION.iter() {
|
||||
//println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
|
||||
let m = tokens.matches(&mut input.as_str().trim(), &mut sugg);
|
||||
if !m.is_empty() {
|
||||
vec.push(tokens);
|
||||
//print!("{:?} ", desc);
|
||||
//println!(" result = {:#?}\n\n", m);
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"suggestions = {:#?}",
|
||||
sugg.into_iter()
|
||||
.zip(vec.into_iter())
|
||||
.map(|(s, v)| format!(
|
||||
"{}{} {:?}",
|
||||
input.as_str().trim(),
|
||||
if input.trim().is_empty() {
|
||||
s.trim()
|
||||
} else {
|
||||
s.as_str()
|
||||
},
|
||||
v
|
||||
))
|
||||
.collect::<Vec<String>>()
|
||||
);
|
||||
if input.trim() == "quit" {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(error) => println!("error: {}", error),
|
||||
}
|
||||
}
|
||||
println!("alright");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,14 +19,16 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! User actions that need to be handled by the UI
|
||||
/*!
|
||||
* User actions that need to be handled by the UI
|
||||
*/
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use melib::email::mailto::Mailto;
|
||||
pub use melib::thread::{SortField, SortOrder};
|
||||
use melib::uuid::Uuid;
|
||||
|
||||
use crate::components::{Component, ComponentId};
|
||||
use crate::components::Component;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TagAction {
|
||||
|
@ -59,7 +61,7 @@ pub enum ListingAction {
|
|||
#[derive(Debug)]
|
||||
pub enum TabAction {
|
||||
Close,
|
||||
Kill(ComponentId),
|
||||
Kill(Uuid),
|
||||
New(Option<Box<dyn Component>>),
|
||||
ManageMailboxes,
|
||||
}
|
||||
|
@ -89,7 +91,7 @@ pub enum ComposeAction {
|
|||
SaveDraft,
|
||||
ToggleSign,
|
||||
ToggleEncrypt,
|
||||
Mailto(Mailto),
|
||||
Mailto(melib::Mailto),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -144,38 +146,3 @@ impl Action {
|
|||
type AccountName = String;
|
||||
type MailboxPath = String;
|
||||
type NewMailboxPath = String;
|
||||
|
||||
macro_rules! impl_into_action {
|
||||
($({$t:ty => $var:tt}),*) => {
|
||||
$(
|
||||
impl From<$t> for Action {
|
||||
fn from(v: $t) -> Self {
|
||||
Self::$var(v)
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
macro_rules! impl_tuple_into_action {
|
||||
($({$a:ty,$b:ty => $var:tt}),*) => {
|
||||
$(
|
||||
impl From<($a,$b)> for Action {
|
||||
fn from((a, b): ($a,$b)) -> Self {
|
||||
Self::$var(a, b)
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
impl_into_action!(
|
||||
{ ListingAction => Listing },
|
||||
{ TabAction => Tab },
|
||||
{ MailingListAction => MailingListAction },
|
||||
{ ViewAction => View },
|
||||
{ ComposeAction => Compose }
|
||||
);
|
||||
impl_tuple_into_action!(
|
||||
{ AccountName, MailboxOperation => Mailbox },
|
||||
{ AccountName, AccountAction => AccountAction }
|
||||
);
|
||||
|
|
|
@ -27,12 +27,10 @@ use std::{
|
|||
|
||||
thread_local!(static CMD_HISTORY_FILE: Arc<Mutex<std::fs::File>> = Arc::new(Mutex::new({
|
||||
let data_dir = xdg::BaseDirectories::with_prefix("meli").unwrap();
|
||||
OpenOptions::new()
|
||||
.append(true) /* writes will append to a file instead of overwriting previous contents */
|
||||
.create(true) /* a new file will be created if the file does not yet already exist. */
|
||||
.read(true)
|
||||
.open(data_dir.place_data_file("cmd_history").unwrap())
|
||||
.unwrap()
|
||||
OpenOptions::new().append(true) /* writes will append to a file instead of overwriting previous contents */
|
||||
.create(true) /* a new file will be created if the file does not yet already exist.*/
|
||||
.read(true)
|
||||
.open(data_dir.place_data_file("cmd_history").unwrap()).unwrap()
|
||||
})));
|
||||
|
||||
pub fn log_cmd(mut cmd: String) {
|
||||
|
|
|
@ -19,13 +19,12 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Components visual and logical separations of application interfaces.
|
||||
///
|
||||
/// They can draw on the terminal and receive events, but also do other stuff
|
||||
/// as well.
|
||||
/// For an example, see the [`notifications`] module.
|
||||
/// See also the [`Component`] trait for more details.
|
||||
use smallvec::SmallVec;
|
||||
/*! Components visual and logical separations of application interfaces.
|
||||
*
|
||||
* They can draw on the terminal and receive events, but also do other stuff
|
||||
* as well. (For example, see the `notifications` module.)
|
||||
* See the `Component` Trait for more details.
|
||||
*/
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
|
@ -58,46 +57,7 @@ use std::{
|
|||
use indexmap::IndexMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Copy, Eq, Deserialize, Hash, Ord, PartialOrd, PartialEq, Serialize)]
|
||||
#[repr(transparent)]
|
||||
pub struct ComponentId(Uuid);
|
||||
|
||||
impl AsRef<Uuid> for ComponentId {
|
||||
fn as_ref(&self) -> &Uuid {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ComponentId {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&self.0.as_simple(), fmt)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ComponentId {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&self.0.as_simple(), fmt)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ComponentId {
|
||||
fn default() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::LowerHex for ComponentId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::LowerHex::fmt(self.0.as_hyphenated(), f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::UpperHex for ComponentId {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::UpperHex::fmt(self.0.as_hyphenated(), f)
|
||||
}
|
||||
}
|
||||
pub type ComponentId = Uuid;
|
||||
|
||||
pub type ShortcutMap = IndexMap<&'static str, Key>;
|
||||
pub type ShortcutMaps = IndexMap<&'static str, ShortcutMap>;
|
||||
|
@ -132,187 +92,28 @@ pub enum ScrollUpdate {
|
|||
|
||||
/// Types implementing this Trait can draw on the terminal and receive events.
|
||||
/// If a type wants to skip drawing if it has not changed anything, it can hold
|
||||
/// some flag in its fields (eg `self.dirty = false`) and act upon that in their
|
||||
/// [`draw`](Component::draw) implementation.
|
||||
/// some flag in its fields (eg self.dirty = false) and act upon that in their
|
||||
/// `draw` implementation.
|
||||
pub trait Component: Display + Debug + Send + Sync {
|
||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context);
|
||||
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool;
|
||||
fn is_dirty(&self) -> bool;
|
||||
/// If the component is meant to be currently visible to the user.
|
||||
fn is_visible(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// If the component can quit right away without any unsaved or ongoing
|
||||
/// operations.
|
||||
fn can_quit_cleanly(&mut self, _context: &Context) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_dirty(&mut self, value: bool);
|
||||
|
||||
fn kill(&mut self, _id: ComponentId, _context: &mut Context) {}
|
||||
|
||||
fn set_id(&mut self, _id: ComponentId) {}
|
||||
fn id(&self) -> ComponentId;
|
||||
|
||||
fn shortcuts(&self, _context: &Context) -> ShortcutMaps {
|
||||
fn get_shortcuts(&self, _context: &Context) -> ShortcutMaps {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Get status message for the status line.
|
||||
fn status(&self, _context: &Context) -> String {
|
||||
fn get_status(&self, _context: &Context) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn attributes(&self) -> &'static ComponentAttr {
|
||||
&ComponentAttr::DEFAULT
|
||||
}
|
||||
|
||||
fn children(&self) -> IndexMap<ComponentId, &dyn Component> {
|
||||
IndexMap::default()
|
||||
}
|
||||
|
||||
fn children_mut(&mut self) -> IndexMap<ComponentId, &mut dyn Component> {
|
||||
IndexMap::default()
|
||||
}
|
||||
|
||||
fn realize(&self, parent: Option<ComponentId>, context: &mut Context) {
|
||||
// log::trace!("Realizing id {} w/ parent {:?}", self.id(), &parent);
|
||||
context.realized.insert(self.id(), parent);
|
||||
}
|
||||
|
||||
fn unrealize(&self, context: &mut Context) {
|
||||
// log::trace!("Unrealizing id {}", self.id());
|
||||
context.unrealized.insert(self.id());
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::ComponentUnrealize(self.id()));
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Box<dyn Component> {
|
||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
(**self).draw(grid, area, context)
|
||||
}
|
||||
|
||||
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
(**self).process_event(event, context)
|
||||
}
|
||||
|
||||
fn is_dirty(&self) -> bool {
|
||||
(**self).is_dirty()
|
||||
}
|
||||
|
||||
fn is_visible(&self) -> bool {
|
||||
(**self).is_visible()
|
||||
}
|
||||
|
||||
fn can_quit_cleanly(&mut self, context: &Context) -> bool {
|
||||
(**self).can_quit_cleanly(context)
|
||||
}
|
||||
|
||||
fn set_dirty(&mut self, value: bool) {
|
||||
(**self).set_dirty(value)
|
||||
}
|
||||
|
||||
fn kill(&mut self, id: ComponentId, context: &mut Context) {
|
||||
(**self).kill(id, context)
|
||||
}
|
||||
|
||||
fn id(&self) -> ComponentId {
|
||||
(**self).id()
|
||||
}
|
||||
|
||||
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
(**self).shortcuts(context)
|
||||
}
|
||||
|
||||
fn status(&self, context: &Context) -> String {
|
||||
(**self).status(context)
|
||||
}
|
||||
|
||||
fn attributes(&self) -> &'static ComponentAttr {
|
||||
(**self).attributes()
|
||||
}
|
||||
|
||||
fn children(&self) -> IndexMap<ComponentId, &dyn Component> {
|
||||
(**self).children()
|
||||
}
|
||||
|
||||
fn children_mut(&mut self) -> IndexMap<ComponentId, &mut dyn Component> {
|
||||
(**self).children_mut()
|
||||
}
|
||||
|
||||
fn realize(&self, parent: Option<ComponentId>, context: &mut Context) {
|
||||
(**self).realize(parent, context)
|
||||
}
|
||||
|
||||
fn unrealize(&self, context: &mut Context) {
|
||||
(**self).unrealize(context)
|
||||
}
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
/// Attributes of a [`Component`] widget.
|
||||
///
|
||||
/// `ComponentAttr::DEFAULT` represents no attribute.
|
||||
pub struct ComponentAttr: u8 {
|
||||
/// Nothing special going on.
|
||||
const DEFAULT = 0;
|
||||
const HAS_ANIMATIONS = 1;
|
||||
const CONTAINER = 1 << 1;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ComponentAttr {
|
||||
fn default() -> Self {
|
||||
Self::DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
||||
pub struct ComponentPath {
|
||||
id: ComponentId,
|
||||
tail: SmallVec<[ComponentId; 8]>,
|
||||
}
|
||||
|
||||
impl ComponentPath {
|
||||
pub fn new(id: ComponentId) -> Self {
|
||||
Self {
|
||||
id,
|
||||
tail: SmallVec::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_front(&mut self, id: ComponentId) {
|
||||
self.tail.insert(0, self.id);
|
||||
self.id = id;
|
||||
}
|
||||
|
||||
pub fn push_back(&mut self, id: ComponentId) {
|
||||
self.tail.push(id);
|
||||
}
|
||||
|
||||
pub fn resolve<'c>(&self, root: &'c dyn Component) -> Option<&'c dyn Component> {
|
||||
let mut cursor = root;
|
||||
for id in self.tail.iter().rev().chain(std::iter::once(&self.id)) {
|
||||
// log::trace!("resolve cursor = {} next id is {}", cursor.id(), &id);
|
||||
if *id == cursor.id() {
|
||||
// log::trace!("continue;");
|
||||
continue;
|
||||
}
|
||||
cursor = cursor.children().remove(id)?;
|
||||
}
|
||||
Some(cursor)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn parent(&self) -> Option<&ComponentId> {
|
||||
self.tail.first()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn root(&self) -> Option<&ComponentId> {
|
||||
self.tail.last()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,8 +21,6 @@
|
|||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use melib::Card;
|
||||
|
||||
use super::*;
|
||||
|
||||
mod contact_list;
|
||||
|
@ -40,7 +38,7 @@ enum ViewMode {
|
|||
#[derive(Debug)]
|
||||
pub struct ContactManager {
|
||||
id: ComponentId,
|
||||
parent_id: Option<ComponentId>,
|
||||
parent_id: ComponentId,
|
||||
pub card: Card,
|
||||
mode: ViewMode,
|
||||
form: FormWidget<bool>,
|
||||
|
@ -63,8 +61,8 @@ impl ContactManager {
|
|||
fn new(context: &Context) -> Self {
|
||||
let theme_default: ThemeAttribute = crate::conf::value(context, "theme_default");
|
||||
ContactManager {
|
||||
id: ComponentId::default(),
|
||||
parent_id: None,
|
||||
id: Uuid::nil(),
|
||||
parent_id: Uuid::nil(),
|
||||
card: Card::new(),
|
||||
mode: ViewMode::Edit,
|
||||
form: FormWidget::default(),
|
||||
|
@ -77,7 +75,7 @@ impl ContactManager {
|
|||
}
|
||||
}
|
||||
|
||||
fn initialize(&mut self, context: &Context) {
|
||||
fn initialize(&mut self) {
|
||||
let (width, _) = self.content.size();
|
||||
|
||||
let (x, _) = write_string_to_grid(
|
||||
|
@ -113,12 +111,7 @@ impl ContactManager {
|
|||
);
|
||||
}
|
||||
|
||||
self.form = FormWidget::new(
|
||||
("Save".into(), true),
|
||||
/* cursor_up_shortcut */ context.settings.shortcuts.general.scroll_up.clone(),
|
||||
/* cursor_down_shortcut */
|
||||
context.settings.shortcuts.general.scroll_down.clone(),
|
||||
);
|
||||
self.form = FormWidget::new(("Save".into(), true));
|
||||
self.form.add_button(("Cancel(Esc)".into(), false));
|
||||
self.form
|
||||
.push(("NAME".into(), self.card.name().to_string()));
|
||||
|
@ -140,14 +133,14 @@ impl ContactManager {
|
|||
}
|
||||
|
||||
pub fn set_parent_id(&mut self, new_val: ComponentId) {
|
||||
self.parent_id = Some(new_val);
|
||||
self.parent_id = new_val;
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ContactManager {
|
||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
if !self.initialized {
|
||||
self.initialize(context);
|
||||
self.initialize();
|
||||
self.initialized = true;
|
||||
}
|
||||
|
||||
|
@ -193,11 +186,11 @@ impl Component for ContactManager {
|
|||
}
|
||||
}
|
||||
ViewMode::Edit => {
|
||||
if let (Some(parent_id), &UIEvent::Input(Key::Esc)) = (self.parent_id, &event) {
|
||||
if let &mut UIEvent::Input(Key::Esc) = event {
|
||||
if self.can_quit_cleanly(context) {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::Action(Tab(Kill(parent_id))));
|
||||
.push_back(UIEvent::Action(Tab(Kill(self.parent_id))));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -212,8 +205,8 @@ impl Component for ContactManager {
|
|||
(
|
||||
s.to_string(),
|
||||
match v {
|
||||
Field::Text(v) => v.as_str().to_string(),
|
||||
Field::Choice(mut v, c, _) => v.remove(c).to_string(),
|
||||
Field::Text(v, _) => v.as_str().to_string(),
|
||||
Field::Choice(mut v, c) => v.remove(c).to_string(),
|
||||
},
|
||||
)
|
||||
})
|
||||
|
@ -226,10 +219,10 @@ impl Component for ContactManager {
|
|||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage("Saved.".into()),
|
||||
));
|
||||
self.unrealize(context);
|
||||
context.replies.push_back(UIEvent::ComponentKill(self.id));
|
||||
}
|
||||
Some(false) => {
|
||||
self.unrealize(context);
|
||||
context.replies.push_back(UIEvent::ComponentKill(self.id));
|
||||
}
|
||||
}
|
||||
self.set_dirty(true);
|
||||
|
@ -242,7 +235,7 @@ impl Component for ContactManager {
|
|||
ViewMode::ReadOnly => {
|
||||
if let &mut UIEvent::Input(Key::Esc) = event {
|
||||
if self.can_quit_cleanly(context) {
|
||||
self.unrealize(context);
|
||||
context.replies.push_back(UIEvent::ComponentKill(self.id));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -273,33 +266,34 @@ impl Component for ContactManager {
|
|||
self.id
|
||||
}
|
||||
|
||||
fn set_id(&mut self, id: ComponentId) {
|
||||
self.id = id;
|
||||
}
|
||||
|
||||
fn can_quit_cleanly(&mut self, context: &Context) -> bool {
|
||||
if !self.has_changes {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(parent_id) = self.parent_id {
|
||||
/* Play it safe and ask user for confirmation */
|
||||
self.mode = ViewMode::Discard(Box::new(UIDialog::new(
|
||||
"this contact has unsaved changes",
|
||||
vec![
|
||||
('x', "quit without saving".to_string()),
|
||||
('y', "save draft and quit".to_string()),
|
||||
('n', "cancel".to_string()),
|
||||
],
|
||||
true,
|
||||
Some(Box::new(move |_, results: &[char]| match results[0] {
|
||||
'x' => Some(UIEvent::Action(Tab(Kill(parent_id)))),
|
||||
'n' => None,
|
||||
'y' => None,
|
||||
_ => None,
|
||||
})),
|
||||
context,
|
||||
)));
|
||||
self.set_dirty(true);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
let parent_id = self.parent_id;
|
||||
/* Play it safe and ask user for confirmation */
|
||||
self.mode = ViewMode::Discard(Box::new(UIDialog::new(
|
||||
"this contact has unsaved changes",
|
||||
vec![
|
||||
('x', "quit without saving".to_string()),
|
||||
('y', "save draft and quit".to_string()),
|
||||
('n', "cancel".to_string()),
|
||||
],
|
||||
true,
|
||||
Some(Box::new(move |_, results: &[char]| match results[0] {
|
||||
'x' => Some(UIEvent::Action(Tab(Kill(parent_id)))),
|
||||
'n' => None,
|
||||
'y' => None,
|
||||
_ => None,
|
||||
})),
|
||||
context,
|
||||
)));
|
||||
self.set_dirty(true);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,12 +18,12 @@
|
|||
* 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::cmp;
|
||||
|
||||
use melib::{backends::AccountHash, text_processing::TextProcessing, CardId, Draft};
|
||||
use melib::{backends::AccountHash, CardId};
|
||||
|
||||
use super::*;
|
||||
use crate::melib::text_processing::TextProcessing;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum ViewMode {
|
||||
|
@ -105,7 +105,7 @@ impl ContactList {
|
|||
sidebar_divider: context.settings.listing.sidebar_divider,
|
||||
sidebar_divider_theme: conf::value(context, "mail.sidebar_divider"),
|
||||
menu_visibility: true,
|
||||
id: ComponentId::default(),
|
||||
id: ComponentId::new_v4(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -256,7 +256,7 @@ impl ContactList {
|
|||
context: &mut Context,
|
||||
) {
|
||||
if !is_valid_area!(area) {
|
||||
log::debug!("BUG: invalid area in print_account");
|
||||
debug!("BUG: invalid area in print_account");
|
||||
}
|
||||
|
||||
let width = width!(area);
|
||||
|
@ -631,7 +631,7 @@ impl Component for ContactList {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
let shortcuts = self.shortcuts(context);
|
||||
let shortcuts = self.get_shortcuts(context);
|
||||
if self.view.is_none() {
|
||||
match *event {
|
||||
UIEvent::Input(ref key)
|
||||
|
@ -686,7 +686,7 @@ impl Component for ContactList {
|
|||
*draft.headers_mut().get_mut("To").unwrap() =
|
||||
format!("{} <{}>", &card.name(), &card.email());
|
||||
let mut composer = Composer::with_account(account_hash, context);
|
||||
composer.set_draft(draft, context);
|
||||
composer.set_draft(draft);
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::Action(Tab(New(Some(Box::new(composer))))));
|
||||
|
@ -724,7 +724,7 @@ impl Component for ContactList {
|
|||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
|
||||
self.status(context),
|
||||
self.get_status(context),
|
||||
)));
|
||||
}
|
||||
|
||||
|
@ -761,7 +761,7 @@ impl Component for ContactList {
|
|||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
|
||||
self.status(context),
|
||||
self.get_status(context),
|
||||
)));
|
||||
}
|
||||
return true;
|
||||
|
@ -899,9 +899,7 @@ impl Component for ContactList {
|
|||
}
|
||||
} else {
|
||||
match event {
|
||||
UIEvent::ComponentUnrealize(ref kill_id)
|
||||
if self.mode == ViewMode::View(*kill_id) =>
|
||||
{
|
||||
UIEvent::ComponentKill(ref kill_id) if self.mode == ViewMode::View(*kill_id) => {
|
||||
self.mode = ViewMode::List;
|
||||
self.view.take();
|
||||
self.set_dirty(true);
|
||||
|
@ -930,15 +928,15 @@ impl Component for ContactList {
|
|||
self.dirty = value;
|
||||
}
|
||||
|
||||
fn kill(&mut self, uuid: ComponentId, context: &mut Context) {
|
||||
fn kill(&mut self, uuid: Uuid, context: &mut Context) {
|
||||
debug_assert!(uuid == self.id);
|
||||
context.replies.push_back(UIEvent::Action(Tab(Kill(uuid))));
|
||||
}
|
||||
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
let mut map = self
|
||||
.view
|
||||
.as_ref()
|
||||
.map(|p| p.shortcuts(context))
|
||||
.map(|p| p.get_shortcuts(context))
|
||||
.unwrap_or_default();
|
||||
|
||||
map.insert(
|
||||
|
@ -957,6 +955,10 @@ impl Component for ContactList {
|
|||
self.id
|
||||
}
|
||||
|
||||
fn set_id(&mut self, id: ComponentId) {
|
||||
self.id = id;
|
||||
}
|
||||
|
||||
fn can_quit_cleanly(&mut self, context: &Context) -> bool {
|
||||
self.view
|
||||
.as_mut()
|
||||
|
@ -964,7 +966,7 @@ impl Component for ContactList {
|
|||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
fn status(&self, context: &Context) -> String {
|
||||
fn get_status(&self, context: &Context) -> String {
|
||||
format!(
|
||||
"{} entries",
|
||||
context.accounts[self.account_pos].address_book.len()
|
||||
|
|
|
@ -33,11 +33,11 @@ pub mod listing;
|
|||
pub use crate::listing::*;
|
||||
pub mod view;
|
||||
pub use crate::view::*;
|
||||
pub mod compose;
|
||||
mod compose;
|
||||
pub use self::compose::*;
|
||||
|
||||
#[cfg(feature = "gpgme")]
|
||||
pub mod pgp;
|
||||
|
||||
pub mod status;
|
||||
mod status;
|
||||
pub use self::status::*;
|
||||
|
|
|
@ -31,8 +31,7 @@ use std::{
|
|||
use indexmap::IndexSet;
|
||||
use melib::{
|
||||
email::attachment_types::{ContentType, MultipartType},
|
||||
list_management, Address, AddressBook, Draft, HeaderName, SpecialUsageMailbox, SubjectPrefix,
|
||||
UnixTimestamp,
|
||||
list_management, Draft,
|
||||
};
|
||||
use nix::sys::wait::WaitStatus;
|
||||
|
||||
|
@ -113,7 +112,7 @@ pub struct Composer {
|
|||
|
||||
#[derive(Debug)]
|
||||
enum ViewMode {
|
||||
Discard(ComponentId, UIDialog<char>),
|
||||
Discard(Uuid, UIDialog<char>),
|
||||
EditAttachments {
|
||||
widget: EditAttachments,
|
||||
},
|
||||
|
@ -142,7 +141,7 @@ impl fmt::Display for Composer {
|
|||
write!(
|
||||
f,
|
||||
"reply: {}",
|
||||
(&self.draft.headers()[HeaderName::SUBJECT]).trim_at_boundary(8)
|
||||
(&self.draft.headers()["Subject"]).trim_at_boundary(8)
|
||||
)
|
||||
} else {
|
||||
write!(f, "composing")
|
||||
|
@ -175,7 +174,7 @@ impl Composer {
|
|||
embed_area: ((0, 0), (0, 0)),
|
||||
embed: None,
|
||||
initialized: false,
|
||||
id: ComponentId::default(),
|
||||
id: ComponentId::new_v4(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -205,11 +204,11 @@ impl Composer {
|
|||
if v.is_empty() {
|
||||
continue;
|
||||
}
|
||||
ret.draft.set_header(h.into(), v.into());
|
||||
ret.draft.set_header(h, v.into());
|
||||
}
|
||||
if *account_settings!(context[account_hash].composing.insert_user_agent) {
|
||||
ret.draft.set_header(
|
||||
HeaderName::USER_AGENT,
|
||||
"User-Agent",
|
||||
format!("meli {}", option_env!("CARGO_PKG_VERSION").unwrap_or("0.0")),
|
||||
);
|
||||
}
|
||||
|
@ -297,9 +296,9 @@ impl Composer {
|
|||
subject.to_string()
|
||||
}
|
||||
};
|
||||
ret.draft.set_header(HeaderName::SUBJECT, subject);
|
||||
ret.draft.set_header("Subject", subject);
|
||||
ret.draft.set_header(
|
||||
HeaderName::REFERENCES,
|
||||
"References",
|
||||
format!(
|
||||
"{} {}",
|
||||
envelope
|
||||
|
@ -315,19 +314,17 @@ impl Composer {
|
|||
envelope.message_id_display()
|
||||
),
|
||||
);
|
||||
ret.draft.set_header(
|
||||
HeaderName::IN_REPLY_TO,
|
||||
envelope.message_id_display().into(),
|
||||
);
|
||||
ret.draft
|
||||
.set_header("In-Reply-To", envelope.message_id_display().into());
|
||||
|
||||
if let Some(reply_to) = envelope.other_headers().get(HeaderName::TO) {
|
||||
if let Some(reply_to) = envelope.other_headers().get("To") {
|
||||
let to: &str = reply_to;
|
||||
let extra_identities = &account.settings.account.extra_identities;
|
||||
if let Some(extra) = extra_identities
|
||||
.iter()
|
||||
.find(|extra| to.contains(extra.as_str()))
|
||||
{
|
||||
ret.draft.set_header(HeaderName::FROM, extra.into());
|
||||
ret.draft.set_header("From", extra.into());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -344,20 +341,20 @@ impl Composer {
|
|||
melib::email::parser::generic::mailto(list_post_addr)
|
||||
.map(|(_, m)| m.address)
|
||||
{
|
||||
to.extend(list_address.into_iter());
|
||||
to.insert(list_address);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(reply_to) = envelope
|
||||
.other_headers()
|
||||
.get(HeaderName::MAIL_FOLLOWUP_TO)
|
||||
.get("Mail-Followup-To")
|
||||
.and_then(|v| v.try_into().ok())
|
||||
{
|
||||
to.insert(reply_to);
|
||||
} else if let Some(reply_to) = envelope
|
||||
.other_headers()
|
||||
.get(HeaderName::REPLY_TO)
|
||||
.get("Reply-To")
|
||||
.and_then(|v| v.try_into().ok())
|
||||
{
|
||||
to.insert(reply_to);
|
||||
|
@ -374,7 +371,7 @@ impl Composer {
|
|||
) {
|
||||
to.remove(&ours);
|
||||
}
|
||||
ret.draft.set_header(HeaderName::TO, {
|
||||
ret.draft.set_header("To", {
|
||||
let mut ret: String =
|
||||
to.into_iter()
|
||||
.fold(String::new(), |mut s: String, n: Address| {
|
||||
|
@ -386,15 +383,13 @@ impl Composer {
|
|||
ret.pop();
|
||||
ret
|
||||
});
|
||||
ret.draft
|
||||
.set_header(HeaderName::CC, envelope.field_cc_to_string());
|
||||
} else if let Some(reply_to) = envelope.other_headers().get(HeaderName::MAIL_REPLY_TO) {
|
||||
ret.draft.set_header(HeaderName::TO, reply_to.to_string());
|
||||
} else if let Some(reply_to) = envelope.other_headers().get(HeaderName::REPLY_TO) {
|
||||
ret.draft.set_header(HeaderName::TO, reply_to.to_string());
|
||||
ret.draft.set_header("Cc", envelope.field_cc_to_string());
|
||||
} else if let Some(reply_to) = envelope.other_headers().get("Mail-Reply-To") {
|
||||
ret.draft.set_header("To", reply_to.to_string());
|
||||
} else if let Some(reply_to) = envelope.other_headers().get("Reply-To") {
|
||||
ret.draft.set_header("To", reply_to.to_string());
|
||||
} else {
|
||||
ret.draft
|
||||
.set_header(HeaderName::TO, envelope.field_from_to_string());
|
||||
ret.draft.set_header("To", envelope.field_from_to_string());
|
||||
}
|
||||
ret.draft.body = {
|
||||
let mut ret = attribution_string(
|
||||
|
@ -440,18 +435,19 @@ impl Composer {
|
|||
if let Some(actions) = list_management::ListActions::detect(&parent_message) {
|
||||
if let Some(post) = actions.post {
|
||||
if let list_management::ListAction::Email(list_post_addr) = post[0] {
|
||||
if let Ok((_, mailto)) = melib::email::parser::generic::mailto(list_post_addr) {
|
||||
let mut addresses = vec![(
|
||||
parent_message.from()[0].clone(),
|
||||
parent_message.field_from_to_string(),
|
||||
)];
|
||||
for add in mailto.address {
|
||||
let add_s = add.to_string();
|
||||
addresses.push((add, add_s));
|
||||
}
|
||||
if let Ok(list_address) = melib::email::parser::generic::mailto(list_post_addr)
|
||||
.map(|(_, m)| m.address)
|
||||
{
|
||||
let list_address_string = list_address.to_string();
|
||||
ret.mode = ViewMode::SelectRecipients(UIDialog::new(
|
||||
"select recipients",
|
||||
addresses,
|
||||
vec![
|
||||
(
|
||||
parent_message.from()[0].clone(),
|
||||
parent_message.field_from_to_string(),
|
||||
),
|
||||
(list_address, list_address_string),
|
||||
],
|
||||
false,
|
||||
Some(Box::new(move |id: ComponentId, results: &[Address]| {
|
||||
Some(UIEvent::FinishedUIDialog(
|
||||
|
@ -499,7 +495,7 @@ impl Composer {
|
|||
) -> Self {
|
||||
let mut composer = Composer::with_account(coordinates.0, context);
|
||||
let mut draft: Draft = Draft::default();
|
||||
draft.set_header(HeaderName::SUBJECT, format!("Fwd: {}", env.subject()));
|
||||
draft.set_header("Subject", format!("Fwd: {}", env.subject()));
|
||||
let preamble = format!(
|
||||
r#"
|
||||
---------- Forwarded message ---------
|
||||
|
@ -535,13 +531,13 @@ To: {}
|
|||
draft.attachments.push(preamble);
|
||||
draft.attachments.push(env.body_bytes(bytes).into());
|
||||
}
|
||||
composer.set_draft(draft, context);
|
||||
composer.set_draft(draft);
|
||||
composer
|
||||
}
|
||||
|
||||
pub fn set_draft(&mut self, draft: Draft, context: &Context) {
|
||||
pub fn set_draft(&mut self, draft: Draft) {
|
||||
self.draft = draft;
|
||||
self.update_form(context);
|
||||
self.update_form();
|
||||
}
|
||||
|
||||
fn update_draft(&mut self) {
|
||||
|
@ -554,35 +550,15 @@ To: {}
|
|||
}
|
||||
}
|
||||
|
||||
fn update_form(&mut self, context: &Context) {
|
||||
fn update_form(&mut self) {
|
||||
let old_cursor = self.form.cursor();
|
||||
let shortcuts = self.shortcuts(context);
|
||||
self.form = FormWidget::new(
|
||||
("Save".into(), true),
|
||||
/* cursor_up_shortcut */
|
||||
shortcuts
|
||||
.get(Shortcuts::COMPOSING)
|
||||
.and_then(|c| c.get("scroll_up").cloned())
|
||||
.unwrap_or_else(|| context.settings.shortcuts.composing.scroll_up.clone()),
|
||||
/* cursor_down_shortcut */
|
||||
shortcuts
|
||||
.get(Shortcuts::COMPOSING)
|
||||
.and_then(|c| c.get("scroll_down").cloned())
|
||||
.unwrap_or_else(|| context.settings.shortcuts.composing.scroll_down.clone()),
|
||||
);
|
||||
self.form = FormWidget::new(("Save".into(), true));
|
||||
self.form.hide_buttons();
|
||||
self.form.set_cursor(old_cursor);
|
||||
let headers = self.draft.headers();
|
||||
let account_hash = self.account_hash;
|
||||
for k in &[
|
||||
HeaderName::DATE,
|
||||
HeaderName::FROM,
|
||||
HeaderName::TO,
|
||||
HeaderName::CC,
|
||||
HeaderName::BCC,
|
||||
HeaderName::SUBJECT,
|
||||
] {
|
||||
if k == HeaderName::TO || k == HeaderName::CC || k == HeaderName::BCC {
|
||||
for &k in &["Date", "From", "To", "Cc", "Bcc", "Subject"] {
|
||||
if k == "To" || k == "Cc" || k == "Bcc" {
|
||||
self.form.push_cl((
|
||||
k.into(),
|
||||
headers[k].to_string(),
|
||||
|
@ -595,7 +571,7 @@ To: {}
|
|||
.collect::<Vec<AutoCompleteEntry>>()
|
||||
}),
|
||||
));
|
||||
} else if k == HeaderName::FROM {
|
||||
} else if k == "From" {
|
||||
self.form.push_cl((
|
||||
k.into(),
|
||||
headers[k].to_string(),
|
||||
|
@ -768,7 +744,7 @@ To: {}
|
|||
i,
|
||||
name,
|
||||
a.content_type(),
|
||||
melib::BytesDisplay(a.raw.len())
|
||||
melib::Bytes(a.raw.len())
|
||||
),
|
||||
grid,
|
||||
theme_default.fg,
|
||||
|
@ -779,12 +755,7 @@ To: {}
|
|||
);
|
||||
} else {
|
||||
write_string_to_grid(
|
||||
&format!(
|
||||
"[{}] {} {}",
|
||||
i,
|
||||
a.content_type(),
|
||||
melib::BytesDisplay(a.raw.len())
|
||||
),
|
||||
&format!("[{}] {} {}", i, a.content_type(), melib::Bytes(a.raw.len())),
|
||||
grid,
|
||||
theme_default.fg,
|
||||
theme_default.bg,
|
||||
|
@ -832,6 +803,8 @@ impl Component for Composer {
|
|||
return;
|
||||
}
|
||||
|
||||
let width = width!(area);
|
||||
|
||||
if !self.initialized {
|
||||
#[cfg(feature = "gpgme")]
|
||||
if self.gpg_state.sign_mail.is_unset() {
|
||||
|
@ -839,11 +812,10 @@ impl Component for Composer {
|
|||
context[self.account_hash].pgp.auto_sign
|
||||
));
|
||||
}
|
||||
if !self.draft.headers().contains_key(HeaderName::FROM)
|
||||
|| self.draft.headers()[HeaderName::FROM].is_empty()
|
||||
if !self.draft.headers().contains_key("From") || self.draft.headers()["From"].is_empty()
|
||||
{
|
||||
self.draft.set_header(
|
||||
HeaderName::FROM,
|
||||
"From",
|
||||
context.accounts[&self.account_hash]
|
||||
.settings
|
||||
.account()
|
||||
|
@ -851,14 +823,12 @@ impl Component for Composer {
|
|||
);
|
||||
}
|
||||
self.pager.update_from_str(self.draft.body(), Some(77));
|
||||
self.update_form(context);
|
||||
self.update_form();
|
||||
self.initialized = true;
|
||||
}
|
||||
let header_height = self.form.len();
|
||||
let theme_default = crate::conf::value(context, "theme_default");
|
||||
|
||||
let mid = 0;
|
||||
/*
|
||||
let mid = if width > 80 {
|
||||
let width = width - 80;
|
||||
let mid = width / 2;
|
||||
|
@ -879,7 +849,6 @@ impl Component for Composer {
|
|||
} else {
|
||||
0
|
||||
};
|
||||
*/
|
||||
|
||||
let header_area = (
|
||||
set_x(upper_left, mid + 1),
|
||||
|
@ -990,8 +959,8 @@ impl Component for Composer {
|
|||
let stopped_message: String =
|
||||
format!("Process with PID {} has stopped.", guard.child_pid);
|
||||
let stopped_message_2: String = format!(
|
||||
"-press '{}' (edit shortcut) to re-activate.",
|
||||
shortcuts[Shortcuts::COMPOSING]["edit"]
|
||||
"-press '{}' (edit_mail shortcut) to re-activate.",
|
||||
shortcuts[Shortcuts::COMPOSING]["edit_mail"]
|
||||
);
|
||||
const STOPPED_MESSAGE_3: &str =
|
||||
"-press Ctrl-C to forcefully kill it and return to editor.";
|
||||
|
@ -1043,14 +1012,13 @@ impl Component for Composer {
|
|||
self.embed_area = (upper_left!(header_area), bottom_right!(body_area));
|
||||
}
|
||||
|
||||
if self.pager.size().0 > width!(body_area) {
|
||||
self.pager.set_initialised(false);
|
||||
if !self.mode.is_edit_attachments() {
|
||||
self.pager.set_dirty(true);
|
||||
if self.pager.size().0 > width!(body_area) {
|
||||
self.pager.set_initialised(false);
|
||||
}
|
||||
self.pager.draw(grid, body_area, context);
|
||||
}
|
||||
// Force clean pager area, because if body height is less than body_area it will
|
||||
// might leave draw artifacts in the remaining area.
|
||||
clear_area(grid, body_area, theme_default);
|
||||
self.set_dirty(true);
|
||||
self.pager.draw(grid, body_area, context);
|
||||
|
||||
match self.cursor {
|
||||
Cursor::Headers => {
|
||||
|
@ -1142,59 +1110,7 @@ impl Component for Composer {
|
|||
if let UIEvent::VisibilityChange(_) = event {
|
||||
self.pager.process_event(event, context);
|
||||
}
|
||||
let shortcuts = self.shortcuts(context);
|
||||
// Process scrolling first, since in my infinite wisdom I made this so
|
||||
// unnecessarily complex
|
||||
match &event {
|
||||
UIEvent::Input(ref key)
|
||||
if self.mode.is_edit()
|
||||
&& shortcut!(key == shortcuts[Shortcuts::COMPOSING]["scroll_up"]) =>
|
||||
{
|
||||
self.set_dirty(true);
|
||||
self.cursor = match self.cursor {
|
||||
// match order is evaluation order, so it matters here because of the if guard
|
||||
// process_event side effects
|
||||
Cursor::Attachments => Cursor::Encrypt,
|
||||
Cursor::Encrypt => Cursor::Sign,
|
||||
Cursor::Sign => Cursor::Body,
|
||||
Cursor::Body if !self.pager.process_event(event, context) => {
|
||||
self.form.process_event(event, context);
|
||||
Cursor::Headers
|
||||
}
|
||||
Cursor::Body => Cursor::Body,
|
||||
Cursor::Headers if self.form.process_event(event, context) => Cursor::Headers,
|
||||
Cursor::Headers => Cursor::Headers,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if self.mode.is_edit()
|
||||
&& shortcut!(key == shortcuts[Shortcuts::COMPOSING]["scroll_down"]) =>
|
||||
{
|
||||
self.set_dirty(true);
|
||||
self.cursor = match self.cursor {
|
||||
Cursor::Headers if self.form.process_event(event, context) => Cursor::Headers,
|
||||
Cursor::Headers => Cursor::Body,
|
||||
Cursor::Body if self.pager.process_event(event, context) => Cursor::Body,
|
||||
Cursor::Body => Cursor::Sign,
|
||||
Cursor::Sign => Cursor::Encrypt,
|
||||
Cursor::Encrypt => Cursor::Attachments,
|
||||
Cursor::Attachments => Cursor::Attachments,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if self.cursor == Cursor::Headers
|
||||
&& self.mode.is_edit()
|
||||
&& self.form.process_event(event, context)
|
||||
{
|
||||
if let UIEvent::InsertInput(_) = event {
|
||||
self.has_changes = true;
|
||||
}
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
let shortcuts = self.get_shortcuts(context);
|
||||
match (&mut self.mode, &mut event) {
|
||||
(ViewMode::Edit, _) => {
|
||||
if self.pager.process_event(event, context) {
|
||||
|
@ -1208,13 +1124,10 @@ impl Component for Composer {
|
|||
})
|
||||
.process_event(event, context)
|
||||
{
|
||||
if matches!(
|
||||
widget.buttons.result(),
|
||||
Some(FormButtonActions::Cancel | FormButtonActions::Accept)
|
||||
) {
|
||||
if widget.buttons.result() == Some(FormButtonActions::Cancel) {
|
||||
self.mode = ViewMode::Edit;
|
||||
self.set_dirty(true);
|
||||
}
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1233,7 +1146,7 @@ impl Component for Composer {
|
|||
Flag::SEEN,
|
||||
) {
|
||||
Ok(job) => {
|
||||
let handle = context.main_loop_handler.job_executor.spawn_blocking(job);
|
||||
let handle = context.job_executor.spawn_blocking(job);
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::NewJob(
|
||||
|
@ -1283,36 +1196,32 @@ impl Component for Composer {
|
|||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
(ViewMode::Send(ref dialog), UIEvent::ComponentUnrealize(ref id))
|
||||
(ViewMode::Send(ref dialog), UIEvent::ComponentKill(ref id)) if *id == dialog.id() => {
|
||||
self.mode = ViewMode::Edit;
|
||||
self.set_dirty(true);
|
||||
}
|
||||
(ViewMode::SelectRecipients(ref dialog), UIEvent::ComponentKill(ref id))
|
||||
if *id == dialog.id() =>
|
||||
{
|
||||
self.mode = ViewMode::Edit;
|
||||
self.set_dirty(true);
|
||||
}
|
||||
(ViewMode::SelectRecipients(ref dialog), UIEvent::ComponentUnrealize(ref id))
|
||||
if *id == dialog.id() =>
|
||||
{
|
||||
self.mode = ViewMode::Edit;
|
||||
self.set_dirty(true);
|
||||
}
|
||||
(ViewMode::Discard(_, ref dialog), UIEvent::ComponentUnrealize(ref id))
|
||||
(ViewMode::Discard(_, ref dialog), UIEvent::ComponentKill(ref id))
|
||||
if *id == dialog.id() =>
|
||||
{
|
||||
self.mode = ViewMode::Edit;
|
||||
self.set_dirty(true);
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
(
|
||||
ViewMode::SelectEncryptKey(_, ref mut selector),
|
||||
UIEvent::ComponentUnrealize(ref id),
|
||||
) if *id == selector.id() => {
|
||||
(ViewMode::SelectEncryptKey(_, ref mut selector), UIEvent::ComponentKill(ref id))
|
||||
if *id == selector.id() =>
|
||||
{
|
||||
self.mode = ViewMode::Edit;
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
(ViewMode::Send(ref mut selector), _) => {
|
||||
if selector.process_event(event, context) {
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1321,17 +1230,14 @@ impl Component for Composer {
|
|||
UIEvent::FinishedUIDialog(id, ref mut result),
|
||||
) if selector.id() == *id => {
|
||||
if let Some(to_val) = result.downcast_mut::<String>() {
|
||||
self.draft
|
||||
.set_header(HeaderName::TO, std::mem::take(to_val));
|
||||
self.update_form(context);
|
||||
self.draft.set_header("To", std::mem::take(to_val));
|
||||
self.update_form();
|
||||
}
|
||||
self.mode = ViewMode::Edit;
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
(ViewMode::SelectRecipients(ref mut selector), _) => {
|
||||
if selector.process_event(event, context) {
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1359,13 +1265,12 @@ impl Component for Composer {
|
|||
_ => {}
|
||||
}
|
||||
}
|
||||
self.mode = ViewMode::Edit;
|
||||
self.set_dirty(true);
|
||||
self.mode = ViewMode::Edit;
|
||||
return true;
|
||||
}
|
||||
(ViewMode::Discard(_, ref mut selector), _) => {
|
||||
if selector.process_event(event, context) {
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1379,7 +1284,6 @@ impl Component for Composer {
|
|||
context
|
||||
.replies
|
||||
.push_back(UIEvent::Action(Tab(Kill(self.id))));
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
'n' => {
|
||||
|
@ -1427,7 +1331,6 @@ impl Component for Composer {
|
|||
}
|
||||
(ViewMode::WaitingForSendResult(ref mut selector, _), _) => {
|
||||
if selector.process_event(event, context) {
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1436,13 +1339,17 @@ impl Component for Composer {
|
|||
ViewMode::SelectEncryptKey(is_encrypt, ref mut selector),
|
||||
UIEvent::FinishedUIDialog(id, result),
|
||||
) if *id == selector.id() => {
|
||||
if let Some(Some(key)) = result.downcast_mut::<Option<melib::gpgme::Key>>() {
|
||||
if *is_encrypt {
|
||||
self.gpg_state.encrypt_keys.clear();
|
||||
self.gpg_state.encrypt_keys.push(key.clone());
|
||||
} else {
|
||||
self.gpg_state.sign_keys.clear();
|
||||
self.gpg_state.sign_keys.push(key.clone());
|
||||
debug!(&result);
|
||||
if let Some(key) = result.downcast_mut::<Option<melib::gpgme::Key>>() {
|
||||
debug!("got key {:?}", key);
|
||||
if let Some(key) = key {
|
||||
if *is_encrypt {
|
||||
self.gpg_state.encrypt_keys.clear();
|
||||
self.gpg_state.encrypt_keys.push(key.clone());
|
||||
} else {
|
||||
self.gpg_state.sign_keys.clear();
|
||||
self.gpg_state.sign_keys.push(key.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
self.mode = ViewMode::Edit;
|
||||
|
@ -1452,12 +1359,20 @@ impl Component for Composer {
|
|||
#[cfg(feature = "gpgme")]
|
||||
(ViewMode::SelectEncryptKey(_, ref mut selector), _) => {
|
||||
if selector.process_event(event, context) {
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if self.cursor == Cursor::Headers
|
||||
&& self.mode.is_edit()
|
||||
&& self.form.process_event(event, context)
|
||||
{
|
||||
if let UIEvent::InsertInput(_) = event {
|
||||
self.has_changes = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
match *event {
|
||||
UIEvent::ConfigReload { old_settings: _ } => {
|
||||
|
@ -1466,11 +1381,32 @@ impl Component for Composer {
|
|||
UIEvent::Resize => {
|
||||
self.set_dirty(true);
|
||||
}
|
||||
/*
|
||||
/* Switch e-mail From: field to the `left` configured account. */
|
||||
UIEvent::Input(Key::Left) if self.cursor == Cursor::From => {
|
||||
self.draft.headers_mut().insert(
|
||||
"From".into(),
|
||||
get_display_name(context, self.account_hash),
|
||||
);
|
||||
self.dirty = true;
|
||||
return true;
|
||||
}
|
||||
/* Switch e-mail From: field to the `right` configured account. */
|
||||
UIEvent::Input(Key::Right) if self.cursor == Cursor::From => {
|
||||
if self.account_cursor + 1 < context.accounts.len() {
|
||||
self.account_cursor += 1;
|
||||
self.draft.headers_mut().insert(
|
||||
"From".into(),
|
||||
get_display_name(context, self.account_cursor),
|
||||
);
|
||||
self.dirty = true;
|
||||
}
|
||||
return true;
|
||||
}*/
|
||||
UIEvent::Input(ref key)
|
||||
if self.mode.is_edit()
|
||||
&& shortcut!(key == shortcuts[Shortcuts::COMPOSING]["scroll_up"]) =>
|
||||
{
|
||||
self.set_dirty(true);
|
||||
self.cursor = match self.cursor {
|
||||
Cursor::Headers => return true,
|
||||
Cursor::Body => {
|
||||
|
@ -1481,12 +1417,12 @@ impl Component for Composer {
|
|||
Cursor::Encrypt => Cursor::Sign,
|
||||
Cursor::Attachments => Cursor::Encrypt,
|
||||
};
|
||||
self.dirty = true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if self.mode.is_edit()
|
||||
&& shortcut!(key == shortcuts[Shortcuts::COMPOSING]["scroll_down"]) =>
|
||||
{
|
||||
self.set_dirty(true);
|
||||
self.cursor = match self.cursor {
|
||||
Cursor::Headers => Cursor::Body,
|
||||
Cursor::Body => Cursor::Sign,
|
||||
|
@ -1494,6 +1430,7 @@ impl Component for Composer {
|
|||
Cursor::Encrypt => Cursor::Attachments,
|
||||
Cursor::Attachments => return true,
|
||||
};
|
||||
self.dirty = true;
|
||||
}
|
||||
UIEvent::Input(Key::Char('\n'))
|
||||
if self.mode.is_edit()
|
||||
|
@ -1511,7 +1448,7 @@ impl Component for Composer {
|
|||
}
|
||||
_ => {}
|
||||
};
|
||||
self.set_dirty(true);
|
||||
self.dirty = true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::COMPOSING]["send_mail"])
|
||||
|
@ -1636,8 +1573,6 @@ impl Component for Composer {
|
|||
context
|
||||
.replies
|
||||
.push_back(UIEvent::EmbedInput((k.clone(), b.to_vec())));
|
||||
drop(embed_guard);
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
Ok(WaitStatus::Signaled(_, signal, _)) => {
|
||||
|
@ -1681,7 +1616,7 @@ impl Component for Composer {
|
|||
UIEvent::Input(ref key)
|
||||
if self.mode.is_edit()
|
||||
&& self.cursor == Cursor::Sign
|
||||
&& shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit"]) =>
|
||||
&& shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit_mail"]) =>
|
||||
{
|
||||
#[cfg(feature = "gpgme")]
|
||||
match melib::email::parser::address::rfc2822address_list(
|
||||
|
@ -1721,7 +1656,7 @@ impl Component for Composer {
|
|||
UIEvent::Input(ref key)
|
||||
if self.mode.is_edit()
|
||||
&& self.cursor == Cursor::Encrypt
|
||||
&& shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit"]) =>
|
||||
&& shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit_mail"]) =>
|
||||
{
|
||||
#[cfg(feature = "gpgme")]
|
||||
match melib::email::parser::address::rfc2822address_list(
|
||||
|
@ -1761,10 +1696,10 @@ impl Component for Composer {
|
|||
UIEvent::Input(ref key)
|
||||
if self.mode.is_edit()
|
||||
&& self.cursor == Cursor::Attachments
|
||||
&& shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit"]) =>
|
||||
&& shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit_mail"]) =>
|
||||
{
|
||||
self.mode = ViewMode::EditAttachments {
|
||||
widget: EditAttachments::new(Some(self.account_hash)),
|
||||
widget: EditAttachments::new(),
|
||||
};
|
||||
self.set_dirty(true);
|
||||
|
||||
|
@ -1772,7 +1707,7 @@ impl Component for Composer {
|
|||
}
|
||||
UIEvent::Input(ref key)
|
||||
if self.embed.is_some()
|
||||
&& shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit"]) =>
|
||||
&& shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit_mail"]) =>
|
||||
{
|
||||
self.embed.as_ref().unwrap().lock().unwrap().wake_up();
|
||||
match self.embed.take() {
|
||||
|
@ -1816,7 +1751,7 @@ impl Component for Composer {
|
|||
}
|
||||
UIEvent::Input(ref key)
|
||||
if self.mode.is_edit()
|
||||
&& shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit"]) =>
|
||||
&& shortcut!(key == shortcuts[Shortcuts::COMPOSING]["edit_mail"]) =>
|
||||
{
|
||||
/* Edit draft in $EDITOR */
|
||||
let editor = if let Some(editor_command) =
|
||||
|
@ -1833,7 +1768,6 @@ impl Component for Composer {
|
|||
.to_string(),
|
||||
Some(NotificationType::Error(melib::error::ErrorKind::None)),
|
||||
));
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
Ok(v) => v,
|
||||
|
@ -1850,7 +1784,6 @@ impl Component for Composer {
|
|||
self.draft.to_edit_string().as_str().as_bytes(),
|
||||
None,
|
||||
None,
|
||||
Some("eml"),
|
||||
true,
|
||||
);
|
||||
|
||||
|
@ -1879,7 +1812,6 @@ impl Component for Composer {
|
|||
));
|
||||
}
|
||||
}
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
/* Kill input thread so that spawned command can be sole receiver of stdin */
|
||||
|
@ -1888,7 +1820,7 @@ impl Component for Composer {
|
|||
}
|
||||
|
||||
let editor_command = format!("{} {}", editor, f.path().display());
|
||||
log::trace!(
|
||||
log::debug!(
|
||||
"Executing: sh -c \"{}\"",
|
||||
editor_command.replace('"', "\\\"")
|
||||
);
|
||||
|
@ -1909,7 +1841,6 @@ impl Component for Composer {
|
|||
));
|
||||
context.replies.push_back(UIEvent::Fork(ForkType::Finished));
|
||||
context.restore_input();
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1946,7 +1877,7 @@ impl Component for Composer {
|
|||
));
|
||||
return false;
|
||||
}
|
||||
let f = create_temp_file(&[], None, None, None, true);
|
||||
let f = create_temp_file(&[], None, None, true);
|
||||
match Command::new("sh")
|
||||
.args(["-c", command])
|
||||
.stdin(Stdio::null())
|
||||
|
@ -1989,7 +1920,6 @@ impl Component for Composer {
|
|||
format!("could not execute pipe command {}: {}", command, &err),
|
||||
Some(NotificationType::Error(melib::error::ErrorKind::External)),
|
||||
));
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -2022,12 +1952,9 @@ impl Component for Composer {
|
|||
} else {
|
||||
context.replies.push_back(UIEvent::Notification(
|
||||
None,
|
||||
"You haven't defined any command to launch in \
|
||||
[terminal.file_picker_command]."
|
||||
.into(),
|
||||
"You haven't defined any command to launch.".into(),
|
||||
Some(NotificationType::Error(melib::error::ErrorKind::None)),
|
||||
));
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
};
|
||||
/* Kill input thread so that spawned command can be sole receiver of stdin */
|
||||
|
@ -2035,7 +1962,7 @@ impl Component for Composer {
|
|||
context.input_kill();
|
||||
}
|
||||
|
||||
log::trace!("Executing: sh -c \"{}\"", command.replace('"', "\\\""));
|
||||
log::debug!("Executing: sh -c \"{}\"", command.replace('"', "\\\""));
|
||||
match Command::new("sh")
|
||||
.args(["-c", command])
|
||||
.stdin(Stdio::inherit())
|
||||
|
@ -2045,7 +1972,7 @@ impl Component for Composer {
|
|||
.and_then(|child| Ok(child.wait_with_output()?.stderr))
|
||||
{
|
||||
Ok(stderr) => {
|
||||
log::trace!("stderr: {}", &String::from_utf8_lossy(&stderr));
|
||||
debug!(&String::from_utf8_lossy(&stderr));
|
||||
for path in stderr.split(|c| [b'\0', b'\t', b'\n'].contains(c)) {
|
||||
match melib::email::compose::attachment_from_file(
|
||||
&String::from_utf8_lossy(path).as_ref(),
|
||||
|
@ -2077,7 +2004,6 @@ impl Component for Composer {
|
|||
Some(NotificationType::Error(melib::error::ErrorKind::External)),
|
||||
));
|
||||
context.restore_input();
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -2112,7 +2038,6 @@ impl Component for Composer {
|
|||
Flag::SEEN | Flag::DRAFT,
|
||||
self.account_hash,
|
||||
);
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
|
@ -2139,13 +2064,7 @@ impl Component for Composer {
|
|||
fn is_dirty(&self) -> bool {
|
||||
match self.mode {
|
||||
ViewMode::Embed => true,
|
||||
ViewMode::EditAttachments { ref widget } => {
|
||||
widget.dirty
|
||||
|| widget.buttons.is_dirty()
|
||||
|| self.dirty
|
||||
|| self.pager.is_dirty()
|
||||
|| self.form.is_dirty()
|
||||
}
|
||||
ViewMode::EditAttachments { ref widget } => widget.dirty || widget.buttons.is_dirty(),
|
||||
ViewMode::Edit => self.dirty || self.pager.is_dirty() || self.form.is_dirty(),
|
||||
ViewMode::Discard(_, ref widget) => {
|
||||
widget.is_dirty() || self.pager.is_dirty() || self.form.is_dirty()
|
||||
|
@ -2179,7 +2098,7 @@ impl Component for Composer {
|
|||
}
|
||||
}
|
||||
|
||||
fn kill(&mut self, uuid: ComponentId, context: &mut Context) {
|
||||
fn kill(&mut self, uuid: Uuid, context: &mut Context) {
|
||||
if self.id != uuid {
|
||||
return;
|
||||
}
|
||||
|
@ -2210,9 +2129,9 @@ impl Component for Composer {
|
|||
);
|
||||
}
|
||||
|
||||
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
let mut map = if self.mode.is_edit() {
|
||||
self.pager.shortcuts(context)
|
||||
self.pager.get_shortcuts(context)
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
@ -2227,6 +2146,9 @@ impl Component for Composer {
|
|||
fn id(&self) -> ComponentId {
|
||||
self.id
|
||||
}
|
||||
fn set_id(&mut self, id: ComponentId) {
|
||||
self.id = id;
|
||||
}
|
||||
|
||||
fn can_quit_cleanly(&mut self, context: &Context) -> bool {
|
||||
if !self.has_changes {
|
||||
|
@ -2313,6 +2235,7 @@ pub fn send_draft(
|
|||
);
|
||||
match output {
|
||||
Err(err) => {
|
||||
debug!("{:?} could not sign draft msg", err);
|
||||
log::error!(
|
||||
"Could not sign draft in account `{}`: {err}.",
|
||||
context.accounts[&account_hash].name(),
|
||||
|
@ -2403,7 +2326,7 @@ pub fn send_draft_async(
|
|||
) -> Result<Pin<Box<dyn Future<Output = Result<()>> + Send>>> {
|
||||
let store_sent_mail = *account_settings!(context[account_hash].composing.store_sent_mail);
|
||||
let format_flowed = *account_settings!(context[account_hash].composing.format_flowed);
|
||||
let event_sender = context.main_loop_handler.sender.clone();
|
||||
let event_sender = context.sender.clone();
|
||||
#[cfg(feature = "gpgme")]
|
||||
#[allow(clippy::type_complexity)]
|
||||
let mut filters_stack: Vec<
|
||||
|
@ -2496,7 +2419,7 @@ pub fn send_draft_async(
|
|||
))))
|
||||
.unwrap();
|
||||
} else if !store_sent_mail && is_ok {
|
||||
let f = create_temp_file(message.as_bytes(), None, None, Some("eml"), false);
|
||||
let f = create_temp_file(message.as_bytes(), None, None, false);
|
||||
log::info!(
|
||||
"store_sent_mail is false; stored sent mail to {}",
|
||||
f.path().display()
|
||||
|
@ -2536,7 +2459,7 @@ fn attribution_string(
|
|||
.map(|addr| addr.get_email())
|
||||
.unwrap_or_else(|| "\"\"".to_string()),
|
||||
);
|
||||
melib::utils::datetime::timestamp_to_string(date, Some(fmt.as_str()), posix)
|
||||
melib::datetime::timestamp_to_string(date, Some(fmt.as_str()), posix)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -2571,12 +2494,9 @@ hello world.
|
|||
&mut context,
|
||||
false,
|
||||
);
|
||||
assert_eq!(&composer.draft.headers()["Subject"], "RE: your e-mail");
|
||||
assert_eq!(
|
||||
&composer.draft.headers()[HeaderName::SUBJECT],
|
||||
"RE: your e-mail"
|
||||
);
|
||||
assert_eq!(
|
||||
&composer.draft.headers()[HeaderName::TO],
|
||||
&composer.draft.headers()["To"],
|
||||
r#"some name <some@example.com>"#
|
||||
);
|
||||
let raw_mail = r#"From: "some name" <some@example.com>
|
||||
|
@ -2600,12 +2520,9 @@ hello world.
|
|||
&mut context,
|
||||
false,
|
||||
);
|
||||
assert_eq!(&composer.draft.headers()["Subject"], "Re: your e-mail");
|
||||
assert_eq!(
|
||||
&composer.draft.headers()[HeaderName::SUBJECT],
|
||||
"Re: your e-mail"
|
||||
);
|
||||
assert_eq!(
|
||||
&composer.draft.headers()[HeaderName::TO],
|
||||
&composer.draft.headers()["To"],
|
||||
r#"some name <some@example.com>"#
|
||||
);
|
||||
}
|
||||
|
|
|
@ -39,8 +39,6 @@ pub enum EditAttachmentMode {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct EditAttachments {
|
||||
/// For shortcut setting retrieval.
|
||||
pub account_hash: Option<AccountHash>,
|
||||
pub mode: EditAttachmentMode,
|
||||
pub buttons: ButtonWidget<FormButtonActions>,
|
||||
pub cursor: EditAttachmentCursor,
|
||||
|
@ -49,48 +47,29 @@ pub struct EditAttachments {
|
|||
}
|
||||
|
||||
impl EditAttachments {
|
||||
pub fn new(account_hash: Option<AccountHash>) -> Self {
|
||||
//ButtonWidget::new(("Add".into(), FormButtonActions::Other("add")));
|
||||
let mut buttons = ButtonWidget::new(("Go Back".into(), FormButtonActions::Cancel));
|
||||
pub fn new() -> Self {
|
||||
let mut buttons = ButtonWidget::new(("Add".into(), FormButtonActions::Other("add")));
|
||||
buttons.push(("Go Back".into(), FormButtonActions::Cancel));
|
||||
buttons.set_focus(true);
|
||||
buttons.set_cursor(1);
|
||||
EditAttachments {
|
||||
account_hash,
|
||||
mode: EditAttachmentMode::Overview,
|
||||
buttons,
|
||||
cursor: EditAttachmentCursor::Buttons,
|
||||
dirty: true,
|
||||
id: ComponentId::default(),
|
||||
id: ComponentId::new_v4(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EditAttachmentsRefMut<'_, '_> {
|
||||
fn new_edit_widget(
|
||||
&self,
|
||||
no: usize,
|
||||
context: &Context,
|
||||
) -> Option<Box<FormWidget<FormButtonActions>>> {
|
||||
fn new_edit_widget(&self, no: usize) -> Option<Box<FormWidget<FormButtonActions>>> {
|
||||
if no >= self.draft.attachments().len() {
|
||||
return None;
|
||||
}
|
||||
let filename = self.draft.attachments()[no].content_type().name();
|
||||
let mime_type = self.draft.attachments()[no].content_type();
|
||||
let shortcuts = self.shortcuts(context);
|
||||
|
||||
let mut ret = FormWidget::new(
|
||||
("Save".into(), FormButtonActions::Accept),
|
||||
/* cursor_up_shortcut */
|
||||
shortcuts
|
||||
.get(Shortcuts::COMPOSING)
|
||||
.and_then(|c| c.get("scroll_up").cloned())
|
||||
.unwrap_or_else(|| context.settings.shortcuts.composing.scroll_up.clone()),
|
||||
/* cursor_down_shortcut */
|
||||
shortcuts
|
||||
.get(Shortcuts::COMPOSING)
|
||||
.and_then(|c| c.get("scroll_down").cloned())
|
||||
.unwrap_or_else(|| context.settings.shortcuts.composing.scroll_down.clone()),
|
||||
);
|
||||
let mut ret = FormWidget::new(("Save".into(), FormButtonActions::Accept));
|
||||
|
||||
ret.add_button(("Reset".into(), FormButtonActions::Reset));
|
||||
ret.add_button(("Cancel".into(), FormButtonActions::Cancel));
|
||||
|
@ -161,7 +140,7 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
|
|||
i,
|
||||
name,
|
||||
a.content_type(),
|
||||
melib::BytesDisplay(a.raw.len())
|
||||
melib::Bytes(a.raw.len())
|
||||
),
|
||||
grid,
|
||||
theme_default.fg,
|
||||
|
@ -172,12 +151,7 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
|
|||
);
|
||||
} else {
|
||||
write_string_to_grid(
|
||||
&format!(
|
||||
"[{}] {} {}",
|
||||
i,
|
||||
a.content_type(),
|
||||
melib::BytesDisplay(a.raw.len())
|
||||
),
|
||||
&format!("[{}] {} {}", i, a.content_type(), melib::Bytes(a.raw.len())),
|
||||
grid,
|
||||
theme_default.fg,
|
||||
bg,
|
||||
|
@ -214,7 +188,7 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
|
|||
}
|
||||
Some(FormButtonActions::Reset) => {
|
||||
let no = *no;
|
||||
if let Some(inner) = self.new_edit_widget(no, context) {
|
||||
if let Some(inner) = self.new_edit_widget(no) {
|
||||
self.inner.mode = EditAttachmentMode::Edit { inner, no };
|
||||
}
|
||||
}
|
||||
|
@ -223,12 +197,8 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
|
|||
return true;
|
||||
}
|
||||
} else {
|
||||
let shortcuts = self.shortcuts(context);
|
||||
|
||||
match event {
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::COMPOSING]["scroll_up"]) =>
|
||||
{
|
||||
UIEvent::Input(Key::Up) => {
|
||||
self.set_dirty(true);
|
||||
match self.inner.cursor {
|
||||
EditAttachmentCursor::AttachmentNo(ref mut n) => {
|
||||
|
@ -254,9 +224,7 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[Shortcuts::COMPOSING]["scroll_down"]) =>
|
||||
{
|
||||
UIEvent::Input(Key::Down) => {
|
||||
self.set_dirty(true);
|
||||
match self.inner.cursor {
|
||||
EditAttachmentCursor::AttachmentNo(ref mut n) => {
|
||||
|
@ -278,7 +246,7 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
|
|||
UIEvent::Input(Key::Char('\n')) => {
|
||||
match self.inner.cursor {
|
||||
EditAttachmentCursor::AttachmentNo(ref no) => {
|
||||
if let Some(inner) = self.new_edit_widget(*no, context) {
|
||||
if let Some(inner) = self.new_edit_widget(*no) {
|
||||
self.inner.mode = EditAttachmentMode::Edit { inner, no: *no };
|
||||
}
|
||||
self.set_dirty(true);
|
||||
|
@ -323,22 +291,17 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
|
|||
}
|
||||
}
|
||||
|
||||
fn kill(&mut self, _uuid: ComponentId, _context: &mut Context) {}
|
||||
fn kill(&mut self, _uuid: Uuid, _context: &mut Context) {}
|
||||
|
||||
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
let mut map = ShortcutMaps::default();
|
||||
|
||||
let our_map: ShortcutMap = self
|
||||
.inner
|
||||
.account_hash
|
||||
.map(|acc| account_settings!(context[acc].shortcuts.composing).key_values())
|
||||
.unwrap_or_else(|| context.settings.shortcuts.composing.key_values());
|
||||
map.insert(Shortcuts::COMPOSING, our_map);
|
||||
|
||||
map
|
||||
fn get_shortcuts(&self, _context: &Context) -> ShortcutMaps {
|
||||
ShortcutMaps::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> ComponentId {
|
||||
self.inner.id
|
||||
}
|
||||
|
||||
fn set_id(&mut self, new_id: ComponentId) {
|
||||
self.inner.id = new_id;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,11 @@ impl KeySelection {
|
|||
context: &mut Context,
|
||||
) -> Result<Self> {
|
||||
use melib::gpgme::*;
|
||||
debug!("KeySelection::new");
|
||||
debug!(&secret);
|
||||
debug!(&local);
|
||||
debug!(&pattern);
|
||||
debug!(&allow_remote_lookup);
|
||||
let mut ctx = Context::new()?;
|
||||
if local {
|
||||
ctx.set_auto_key_locate(LocateKey::LOCAL)?;
|
||||
|
@ -63,10 +68,7 @@ impl KeySelection {
|
|||
ctx.set_auto_key_locate(LocateKey::WKD | LocateKey::LOCAL)?;
|
||||
}
|
||||
let job = ctx.keylist(secret, Some(pattern.clone()))?;
|
||||
let handle = context
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized(job);
|
||||
let handle = context.job_executor.spawn_specialized(job);
|
||||
let mut progress_spinner = ProgressSpinner::new(8, context);
|
||||
progress_spinner.start();
|
||||
Ok(KeySelection::LoadingKeys {
|
||||
|
@ -104,6 +106,8 @@ impl Component for KeySelection {
|
|||
}
|
||||
|
||||
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
debug!(&self);
|
||||
debug!(&event);
|
||||
match self {
|
||||
KeySelection::LoadingKeys {
|
||||
ref mut progress_spinner,
|
||||
|
@ -190,7 +194,7 @@ impl Component for KeySelection {
|
|||
Ok(Some(Err(err))) => {
|
||||
*self = KeySelection::Error {
|
||||
err,
|
||||
id: ComponentId::default(),
|
||||
id: ComponentId::new_v4(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -225,14 +229,14 @@ impl Component for KeySelection {
|
|||
}
|
||||
}
|
||||
|
||||
fn kill(&mut self, _uuid: ComponentId, _context: &mut Context) {}
|
||||
fn kill(&mut self, _uuid: Uuid, _context: &mut Context) {}
|
||||
|
||||
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
match self {
|
||||
KeySelection::LoadingKeys { .. } | KeySelection::Error { .. } => {
|
||||
ShortcutMaps::default()
|
||||
}
|
||||
KeySelection::Loaded { ref widget, .. } => widget.shortcuts(context),
|
||||
KeySelection::Loaded { ref widget, .. } => widget.get_shortcuts(context),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -246,6 +250,17 @@ impl Component for KeySelection {
|
|||
KeySelection::Loaded { ref widget, .. } => widget.id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_id(&mut self, new_id: ComponentId) {
|
||||
match self {
|
||||
KeySelection::LoadingKeys {
|
||||
ref mut progress_spinner,
|
||||
..
|
||||
} => progress_spinner.set_id(new_id),
|
||||
KeySelection::Error { ref mut id, .. } => *id = new_id,
|
||||
KeySelection::Loaded { ref mut widget, .. } => widget.set_id(new_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
@ -22,11 +22,8 @@
|
|||
//! Pre-submission hooks for draft validation and/or transformations.
|
||||
pub use std::borrow::Cow;
|
||||
|
||||
use melib::email::headers::HeaderName;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub enum HookFn {
|
||||
/// Stateful hook.
|
||||
Closure(Box<dyn FnMut(&mut Context, &mut Draft) -> Result<()> + Send + Sync>),
|
||||
|
@ -74,7 +71,7 @@ impl std::ops::DerefMut for HookFn {
|
|||
pub struct Hook {
|
||||
/// Hook name for enabling/disabling it from configuration.
|
||||
///
|
||||
/// See [`crate::conf::ComposingSettings::disabled_compose_hooks`].
|
||||
/// See [`ComposingSettings::disabled_compose_hooks`].
|
||||
name: Cow<'static, str>,
|
||||
hook_fn: HookFn,
|
||||
}
|
||||
|
@ -82,7 +79,7 @@ pub struct Hook {
|
|||
impl Hook {
|
||||
/// Hook name for enabling/disabling it from configuration.
|
||||
///
|
||||
/// See [`crate::conf::ComposingSettings::disabled_compose_hooks`].
|
||||
/// See [`ComposingSettings::disabled_compose_hooks`].
|
||||
pub fn name(&self) -> &str {
|
||||
self.name.as_ref()
|
||||
}
|
||||
|
@ -156,10 +153,10 @@ impl std::ops::DerefMut for Hook {
|
|||
}
|
||||
|
||||
fn past_date_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> {
|
||||
use melib::utils::datetime::*;
|
||||
use melib::datetime::*;
|
||||
if let Some(v) = draft
|
||||
.headers
|
||||
.get(HeaderName::DATE)
|
||||
.get("Date")
|
||||
.map(rfc822_to_timestamp)
|
||||
.and_then(Result::ok)
|
||||
{
|
||||
|
@ -184,8 +181,8 @@ pub const PASTDATEWARN: Hook = Hook {
|
|||
};
|
||||
|
||||
fn important_header_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> {
|
||||
for hdr in [HeaderName::FROM, HeaderName::TO] {
|
||||
match draft.headers.get(&hdr).map(melib::Address::list_try_from) {
|
||||
for hdr in ["From", "To"] {
|
||||
match draft.headers.get(hdr).map(melib::Address::list_try_from) {
|
||||
Some(Ok(_)) => {}
|
||||
Some(Err(err)) => return Err(format!("{hdr} header value is invalid ({err}).").into()),
|
||||
None => return Err(format!("{hdr} header is missing and should be present.").into()),
|
||||
|
@ -195,8 +192,8 @@ fn important_header_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> {
|
|||
{
|
||||
match draft
|
||||
.headers
|
||||
.get(HeaderName::DATE)
|
||||
.map(melib::utils::datetime::rfc822_to_timestamp)
|
||||
.get("Date")
|
||||
.map(melib::datetime::rfc822_to_timestamp)
|
||||
{
|
||||
Some(Err(err)) => return Err(format!("Date header value is invalid ({err}).").into()),
|
||||
Some(Ok(0)) => return Err("Date header value is invalid.".into()),
|
||||
|
@ -204,10 +201,10 @@ fn important_header_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
for hdr in [HeaderName::CC, HeaderName::BCC] {
|
||||
for hdr in ["Cc", "Bcc"] {
|
||||
if let Some(Err(err)) = draft
|
||||
.headers
|
||||
.get(&hdr)
|
||||
.get(hdr)
|
||||
.filter(|v| !v.trim().is_empty())
|
||||
.map(melib::Address::list_try_from)
|
||||
{
|
||||
|
@ -226,7 +223,7 @@ pub const HEADERWARN: Hook = Hook {
|
|||
fn missing_attachment_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> {
|
||||
if draft
|
||||
.headers
|
||||
.get(HeaderName::SUBJECT)
|
||||
.get("Subject")
|
||||
.map(|s| s.to_lowercase().contains("attach"))
|
||||
.unwrap_or(false)
|
||||
&& draft.attachments.is_empty()
|
||||
|
@ -250,7 +247,7 @@ pub const MISSINGATTACHMENTWARN: Hook = Hook {
|
|||
fn empty_draft_warn(_ctx: &mut Context, draft: &mut Draft) -> Result<()> {
|
||||
if draft
|
||||
.headers
|
||||
.get(HeaderName::SUBJECT)
|
||||
.get("Subject")
|
||||
.filter(|v| !v.trim().is_empty())
|
||||
.is_none()
|
||||
&& draft.body.trim().is_empty()
|
||||
|
@ -278,8 +275,8 @@ mod tests {
|
|||
let mut draft = Draft::default();
|
||||
draft
|
||||
.set_body("αδφαφσαφασ".to_string())
|
||||
.set_header(HeaderName::SUBJECT, "test_update()".into())
|
||||
.set_header(HeaderName::DATE, "Sun, 16 Jun 2013 17:56:45 +0200".into());
|
||||
.set_header("Subject", "test_update()".into())
|
||||
.set_header("Date", "Sun, 16 Jun 2013 17:56:45 +0200".into());
|
||||
println!("Check that past Date header value produces a warning…");
|
||||
#[allow(const_item_mutation)]
|
||||
let err_msg = PASTDATEWARN(&mut ctx, &mut draft).unwrap_err().to_string();
|
||||
|
@ -302,8 +299,8 @@ mod tests {
|
|||
let mut draft = Draft::default();
|
||||
draft
|
||||
.set_body("αδφαφσαφασ".to_string())
|
||||
.set_header(HeaderName::SUBJECT, "test_update()".into())
|
||||
.set_header(HeaderName::DATE, "Sun sds16 Jun 2013 17:56:45 +0200".into());
|
||||
.set_header("Subject", "test_update()".into())
|
||||
.set_header("Date", "Sun sds16 Jun 2013 17:56:45 +0200".into());
|
||||
let mut hook = HEADERWARN;
|
||||
|
||||
println!("Check for missing/empty From header value…");
|
||||
|
@ -315,7 +312,7 @@ mod tests {
|
|||
"HEADERWARN should complain about From value being empty: {}",
|
||||
err_msg
|
||||
);
|
||||
draft.set_header(HeaderName::FROM, "user <user@example.com>".into());
|
||||
draft.set_header("From", "user <user@example.com>".into());
|
||||
|
||||
println!("Check for missing/empty To header value…");
|
||||
let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string();
|
||||
|
@ -326,7 +323,7 @@ mod tests {
|
|||
"HEADERWARN should complain about To value being empty: {}",
|
||||
err_msg
|
||||
);
|
||||
draft.set_header(HeaderName::TO, "other user <user@example.com>".into());
|
||||
draft.set_header("To", "other user <user@example.com>".into());
|
||||
|
||||
println!("Check for invalid Date header value…");
|
||||
let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string();
|
||||
|
@ -340,11 +337,11 @@ mod tests {
|
|||
draft = Draft::default();
|
||||
draft
|
||||
.set_body("αδφαφσαφασ".to_string())
|
||||
.set_header(HeaderName::FROM, "user <user@example.com>".into())
|
||||
.set_header(HeaderName::TO, "other user <user@example.com>".into())
|
||||
.set_header(HeaderName::SUBJECT, "test_update()".into());
|
||||
.set_header("From", "user <user@example.com>".into())
|
||||
.set_header("To", "other user <user@example.com>".into())
|
||||
.set_header("Subject", "test_update()".into());
|
||||
hook(&mut ctx, &mut draft).unwrap();
|
||||
draft.set_header(HeaderName::FROM, "user user@example.com>".into());
|
||||
draft.set_header("From", "user user@example.com>".into());
|
||||
|
||||
println!("Check for invalid From header value…");
|
||||
let err_msg = hook(&mut ctx, &mut draft).unwrap_err().to_string();
|
||||
|
@ -364,8 +361,8 @@ mod tests {
|
|||
let mut draft = Draft::default();
|
||||
draft
|
||||
.set_body("αδφαφσαφασ".to_string())
|
||||
.set_header(HeaderName::SUBJECT, "Attachments included".into())
|
||||
.set_header(HeaderName::DATE, "Sun, 16 Jun 2013 17:56:45 +0200".into());
|
||||
.set_header("Subject", "Attachments included".into())
|
||||
.set_header("Date", "Sun, 16 Jun 2013 17:56:45 +0200".into());
|
||||
|
||||
let mut hook = MISSINGATTACHMENTWARN;
|
||||
|
||||
|
@ -381,7 +378,7 @@ mod tests {
|
|||
);
|
||||
|
||||
draft
|
||||
.set_header(HeaderName::SUBJECT, "Hello.".into())
|
||||
.set_header("Subject", "Hello.".into())
|
||||
.set_body("Attachments included".to_string());
|
||||
println!(
|
||||
"Check that mentioning attachments in body produces a warning if draft has no \
|
||||
|
@ -397,7 +394,7 @@ mod tests {
|
|||
println!(
|
||||
"Check that mentioning attachments produces no warnings if draft has attachments…"
|
||||
);
|
||||
draft.set_header(HeaderName::SUBJECT, "Attachments included".into());
|
||||
draft.set_header("Subject", "Attachments included".into());
|
||||
let mut attachment = AttachmentBuilder::new(b"");
|
||||
attachment
|
||||
.set_raw(b"foobar".to_vec())
|
||||
|
@ -417,7 +414,7 @@ mod tests {
|
|||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let mut ctx = Context::new_mock(&tempdir);
|
||||
let mut draft = Draft::default();
|
||||
draft.set_header(HeaderName::DATE, "Sun, 16 Jun 2013 17:56:45 +0200".into());
|
||||
draft.set_header("Date", "Sun, 16 Jun 2013 17:56:45 +0200".into());
|
||||
|
||||
let mut hook = EMPTYDRAFTWARN;
|
||||
|
||||
|
@ -430,7 +427,7 @@ mod tests {
|
|||
);
|
||||
|
||||
println!("Check that non-empty draft produces no warning…");
|
||||
draft.set_header(HeaderName::SUBJECT, "Ping".into());
|
||||
draft.set_header("Subject", "Ping".into());
|
||||
hook(&mut ctx, &mut draft).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ use std::{
|
|||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
use melib::{backends::EnvelopeHashBatch, Address};
|
||||
use melib::backends::EnvelopeHashBatch;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::*;
|
||||
|
@ -55,7 +55,6 @@ pub const DEFAULT_SNOOZED_FLAG: &str = "💤";
|
|||
pub struct RowsState<T> {
|
||||
pub selection: HashMap<EnvelopeHash, bool>,
|
||||
pub row_updates: SmallVec<[EnvelopeHash; 8]>,
|
||||
/// FIXME: env vec should have at least one element guaranteed
|
||||
pub thread_to_env: HashMap<ThreadHash, SmallVec<[EnvelopeHash; 8]>>,
|
||||
pub env_to_thread: HashMap<EnvelopeHash, ThreadHash>,
|
||||
pub thread_order: HashMap<ThreadHash, usize>,
|
||||
|
@ -103,27 +102,16 @@ impl<T> RowsState<T> {
|
|||
entry_strings: EntryStrings,
|
||||
) {
|
||||
env_hashes.dedup();
|
||||
env_hashes.retain(|h| !self.all_envelopes.contains(h));
|
||||
if env_hashes.is_empty() {
|
||||
return;
|
||||
}
|
||||
let index = self.entries.len();
|
||||
self.thread_order.insert(thread, index);
|
||||
self.all_threads.insert(thread);
|
||||
for &env_hash in &env_hashes {
|
||||
self.selection.insert(env_hash, false);
|
||||
self.env_to_thread.insert(env_hash, thread);
|
||||
self.env_order.insert(env_hash, index);
|
||||
self.all_envelopes.insert(env_hash);
|
||||
}
|
||||
if !self.all_threads.contains(&thread) {
|
||||
self.thread_order.insert(thread, index);
|
||||
self.all_threads.insert(thread);
|
||||
self.thread_to_env.insert(thread, env_hashes);
|
||||
} else {
|
||||
self.thread_to_env
|
||||
.entry(thread)
|
||||
.or_default()
|
||||
.extend_from_slice(&env_hashes);
|
||||
}
|
||||
self.thread_to_env.insert(thread, env_hashes);
|
||||
self.entries.push((metadata, entry_strings));
|
||||
}
|
||||
|
||||
|
@ -424,20 +412,6 @@ struct AccountMenuEntry {
|
|||
}
|
||||
|
||||
pub trait MailListingTrait: ListingTrait {
|
||||
fn as_component(&self) -> &dyn Component
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self
|
||||
}
|
||||
|
||||
fn as_component_mut(&mut self) -> &mut dyn Component
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self
|
||||
}
|
||||
|
||||
fn perform_action(
|
||||
&mut self,
|
||||
context: &mut Context,
|
||||
|
@ -450,7 +424,7 @@ pub trait MailListingTrait: ListingTrait {
|
|||
/*{
|
||||
let threads_lck = account.collection.get_threads(mailbox_hash);
|
||||
for thread_hash in thread_hashes {
|
||||
for (_, h) in threads_lck.thread_iter(thread_hash) {
|
||||
for (_, h) in threads_lck.thread_group_iter(thread_hash) {
|
||||
envs_to_set.push(threads_lck.thread_nodes()[&h].message().unwrap());
|
||||
}
|
||||
self.row_updates().push(thread_hash);
|
||||
|
@ -476,10 +450,7 @@ pub trait MailListingTrait: ListingTrait {
|
|||
));
|
||||
}
|
||||
Ok(fut) => {
|
||||
let handle = account
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized(fut);
|
||||
let handle = account.job_executor.spawn_specialized(fut);
|
||||
account
|
||||
.insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle });
|
||||
}
|
||||
|
@ -498,10 +469,7 @@ pub trait MailListingTrait: ListingTrait {
|
|||
));
|
||||
}
|
||||
Ok(fut) => {
|
||||
let handle = account
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized(fut);
|
||||
let handle = account.job_executor.spawn_specialized(fut);
|
||||
account
|
||||
.insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle });
|
||||
}
|
||||
|
@ -520,10 +488,7 @@ pub trait MailListingTrait: ListingTrait {
|
|||
));
|
||||
}
|
||||
Ok(fut) => {
|
||||
let handle = account
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized(fut);
|
||||
let handle = account.job_executor.spawn_specialized(fut);
|
||||
account
|
||||
.insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle });
|
||||
}
|
||||
|
@ -542,10 +507,7 @@ pub trait MailListingTrait: ListingTrait {
|
|||
));
|
||||
}
|
||||
Ok(fut) => {
|
||||
let handle = account
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized(fut);
|
||||
let handle = account.job_executor.spawn_specialized(fut);
|
||||
account
|
||||
.insert_job(handle.job_id, JobRequest::SetFlags { env_hashes, handle });
|
||||
}
|
||||
|
@ -564,10 +526,7 @@ pub trait MailListingTrait: ListingTrait {
|
|||
));
|
||||
}
|
||||
Ok(fut) => {
|
||||
let handle = account
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized(fut);
|
||||
let handle = account.job_executor.spawn_specialized(fut);
|
||||
account.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::DeleteMessages { env_hashes, handle },
|
||||
|
@ -592,10 +551,7 @@ pub trait MailListingTrait: ListingTrait {
|
|||
));
|
||||
}
|
||||
Ok(fut) => {
|
||||
let handle = account
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized(fut);
|
||||
let handle = account.job_executor.spawn_specialized(fut);
|
||||
account.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Generic {
|
||||
|
@ -632,10 +588,7 @@ pub trait MailListingTrait: ListingTrait {
|
|||
));
|
||||
}
|
||||
Ok(fut) => {
|
||||
let handle = account
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized(fut);
|
||||
let handle = account.job_executor.spawn_specialized(fut);
|
||||
account.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Generic {
|
||||
|
@ -714,7 +667,7 @@ pub trait MailListingTrait: ListingTrait {
|
|||
let _ = sender.send(r);
|
||||
Ok(())
|
||||
});
|
||||
let handle = account.main_loop_handler.job_executor.spawn_blocking(fut);
|
||||
let handle = account.job_executor.spawn_blocking(fut);
|
||||
let path = path.to_path_buf();
|
||||
account.insert_job(
|
||||
handle.job_id,
|
||||
|
@ -780,8 +733,6 @@ pub trait MailListingTrait: ListingTrait {
|
|||
pub trait ListingTrait: Component {
|
||||
fn coordinates(&self) -> (AccountHash, MailboxHash);
|
||||
fn set_coordinates(&mut self, _: (AccountHash, MailboxHash));
|
||||
fn next_entry(&mut self, context: &mut Context);
|
||||
fn prev_entry(&mut self, context: &mut Context);
|
||||
fn draw_list(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context);
|
||||
fn highlight_line(&mut self, grid: &mut CellBuffer, area: Area, idx: usize, context: &Context);
|
||||
fn filter(
|
||||
|
@ -792,27 +743,12 @@ pub trait ListingTrait: Component {
|
|||
) {
|
||||
}
|
||||
fn unfocused(&self) -> bool;
|
||||
fn view_area(&self) -> Option<Area>;
|
||||
fn set_modifier_active(&mut self, _new_val: bool);
|
||||
fn set_modifier_command(&mut self, _new_val: Option<Modifier>);
|
||||
fn modifier_command(&self) -> Option<Modifier>;
|
||||
fn set_movement(&mut self, mvm: PageMovement);
|
||||
fn focus(&self) -> Focus;
|
||||
fn set_focus(&mut self, new_value: Focus, context: &mut Context);
|
||||
|
||||
fn kick_parent(&self, parent: ComponentId, msg: ListingMessage, context: &mut Context) {
|
||||
log::trace!(
|
||||
"kick_parent self is {} parent is {} msg is {:?}",
|
||||
self.id(),
|
||||
parent,
|
||||
&msg
|
||||
);
|
||||
context.replies.push_back(UIEvent::IntraComm {
|
||||
from: self.id(),
|
||||
to: parent,
|
||||
content: Box::new(msg),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -852,13 +788,32 @@ impl core::ops::DerefMut for ListingComponent {
|
|||
}
|
||||
|
||||
impl ListingComponent {
|
||||
fn id(&self) -> ComponentId {
|
||||
match self {
|
||||
Compact(l) => l.as_component().id(),
|
||||
Plain(l) => l.as_component().id(),
|
||||
Threaded(l) => l.as_component().id(),
|
||||
Conversations(l) => l.as_component().id(),
|
||||
Offline(l) => l.as_component().id(),
|
||||
fn set_style(&mut self, new_style: IndexStyle) {
|
||||
match new_style {
|
||||
IndexStyle::Plain => {
|
||||
if let Plain(_) = self {
|
||||
return;
|
||||
}
|
||||
*self = Plain(PlainListing::new(self.coordinates()));
|
||||
}
|
||||
IndexStyle::Threaded => {
|
||||
if let Threaded(_) = self {
|
||||
return;
|
||||
}
|
||||
*self = Threaded(ThreadListing::new(self.coordinates()));
|
||||
}
|
||||
IndexStyle::Compact => {
|
||||
if let Compact(_) = self {
|
||||
return;
|
||||
}
|
||||
*self = Compact(CompactListing::new(self.coordinates()));
|
||||
}
|
||||
IndexStyle::Conversations => {
|
||||
if let Conversations(_) = self {
|
||||
return;
|
||||
}
|
||||
*self = Conversations(ConversationsListing::new(self.coordinates()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -907,7 +862,6 @@ pub struct Listing {
|
|||
prev_ratio: usize,
|
||||
menu_width: WidgetWidth,
|
||||
focus: ListingFocus,
|
||||
view: Box<ThreadView>,
|
||||
}
|
||||
|
||||
impl fmt::Display for Listing {
|
||||
|
@ -976,20 +930,14 @@ impl Component for Listing {
|
|||
if context.is_online(account_hash).is_err()
|
||||
&& !matches!(self.component, ListingComponent::Offline(_))
|
||||
{
|
||||
self.component.unrealize(context);
|
||||
self.component =
|
||||
Offline(OfflineListing::new((account_hash, MailboxHash::default())));
|
||||
self.component.realize(self.id().into(), context);
|
||||
}
|
||||
|
||||
if let Some(s) = self.status.as_mut() {
|
||||
s.draw(grid, area, context);
|
||||
} else {
|
||||
self.component.draw(grid, area, context);
|
||||
if self.component.unfocused() {
|
||||
self.view
|
||||
.draw(grid, self.component.view_area().unwrap_or(area), context);
|
||||
}
|
||||
}
|
||||
} else if right_component_width == 0 {
|
||||
self.draw_menu(grid, area, context);
|
||||
|
@ -1002,20 +950,14 @@ impl Component for Listing {
|
|||
if context.is_online(account_hash).is_err()
|
||||
&& !matches!(self.component, ListingComponent::Offline(_))
|
||||
{
|
||||
self.component.unrealize(context);
|
||||
self.component =
|
||||
Offline(OfflineListing::new((account_hash, MailboxHash::default())));
|
||||
self.component.realize(self.id().into(), context);
|
||||
}
|
||||
if let Some(s) = self.status.as_mut() {
|
||||
s.draw(grid, (set_x(upper_left, mid + 1), bottom_right), context);
|
||||
} else {
|
||||
let area = (set_x(upper_left, mid + 1), bottom_right);
|
||||
self.component.draw(grid, area, context);
|
||||
if self.component.unfocused() {
|
||||
self.view
|
||||
.draw(grid, self.component.view_area().unwrap_or(area), context);
|
||||
}
|
||||
self.component
|
||||
.draw(grid, (set_x(upper_left, mid + 1), bottom_right), context);
|
||||
}
|
||||
}
|
||||
self.dirty = false;
|
||||
|
@ -1099,8 +1041,8 @@ impl Component for Listing {
|
|||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(match msg {
|
||||
Some(msg) => format!("{} {}", self.status(context), msg),
|
||||
None => self.status(context),
|
||||
Some(msg) => format!("{} {}", self.get_status(context), msg),
|
||||
None => self.get_status(context),
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
@ -1163,7 +1105,7 @@ impl Component for Listing {
|
|||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
|
||||
self.status(context),
|
||||
self.get_status(context),
|
||||
)));
|
||||
self.set_dirty(true);
|
||||
return true;
|
||||
|
@ -1190,73 +1132,9 @@ impl Component for Listing {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
UIEvent::IntraComm {
|
||||
from,
|
||||
to,
|
||||
ref content,
|
||||
} if (*from, *to) == (self.component.id(), self.id()) => {
|
||||
match content.downcast_ref::<ListingMessage>().copied() {
|
||||
None => {}
|
||||
Some(ListingMessage::FocusUpdate { new_value }) => {
|
||||
self.view.process_event(
|
||||
&mut UIEvent::VisibilityChange(!matches!(new_value, Focus::None)),
|
||||
context,
|
||||
);
|
||||
if matches!(new_value, Focus::Entry) {
|
||||
// Need to clear gap between sidebar and listing component, if any.
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
Some(ListingMessage::UpdateView) => {
|
||||
log::trace!("UpdateView");
|
||||
}
|
||||
Some(ListingMessage::OpenEntryUnderCursor {
|
||||
env_hash,
|
||||
thread_hash,
|
||||
show_thread,
|
||||
}) => {
|
||||
let (a, m) = self.component.coordinates();
|
||||
self.view.unrealize(context);
|
||||
self.view = Box::new(ThreadView::new(
|
||||
(a, m, env_hash),
|
||||
thread_hash,
|
||||
Some(env_hash),
|
||||
if show_thread {
|
||||
None
|
||||
} else {
|
||||
Some(ThreadViewFocus::MailView)
|
||||
},
|
||||
context,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "debug-tracing")]
|
||||
UIEvent::IntraComm {
|
||||
from,
|
||||
to,
|
||||
ref content,
|
||||
} => {
|
||||
if *from == self.component.id() || *to == self.id() {
|
||||
log::debug!(
|
||||
"BUG intracomm event: {:?} downcast content {:?}",
|
||||
event,
|
||||
content.downcast_ref::<ListingMessage>().copied()
|
||||
);
|
||||
log::debug!(
|
||||
"BUG component is {} and self id is {}",
|
||||
self.component.id(),
|
||||
self.id()
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if self.component.unfocused() && self.view.process_event(event, context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.focus == ListingFocus::Mailbox && self.status.is_some() {
|
||||
if let Some(s) = self.status.as_mut() {
|
||||
if s.process_event(event, context) {
|
||||
|
@ -1264,14 +1142,14 @@ impl Component for Listing {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (self.focus == ListingFocus::Mailbox && self.status.is_none())
|
||||
&& ((self.component.unfocused() && self.view.process_event(event, context))
|
||||
|| self.component.process_event(event, context))
|
||||
if self.focus == ListingFocus::Mailbox
|
||||
&& self.status.is_none()
|
||||
&& self.component.process_event(event, context)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
let shortcuts = self.shortcuts(context);
|
||||
let shortcuts = self.get_shortcuts(context);
|
||||
if self.focus == ListingFocus::Mailbox {
|
||||
match *event {
|
||||
UIEvent::Input(Key::Mouse(MouseEvent::Press(MouseButton::Left, x, _y)))
|
||||
|
@ -1458,19 +1336,19 @@ impl Component for Listing {
|
|||
match event {
|
||||
UIEvent::Action(ref action) => match action {
|
||||
Action::Listing(ListingAction::SetPlain) => {
|
||||
self.set_style(IndexStyle::Plain, context);
|
||||
self.component.set_style(IndexStyle::Plain);
|
||||
return true;
|
||||
}
|
||||
Action::Listing(ListingAction::SetThreaded) => {
|
||||
self.set_style(IndexStyle::Threaded, context);
|
||||
self.component.set_style(IndexStyle::Threaded);
|
||||
return true;
|
||||
}
|
||||
Action::Listing(ListingAction::SetCompact) => {
|
||||
self.set_style(IndexStyle::Compact, context);
|
||||
self.component.set_style(IndexStyle::Compact);
|
||||
return true;
|
||||
}
|
||||
Action::Listing(ListingAction::SetConversations) => {
|
||||
self.set_style(IndexStyle::Conversations, context);
|
||||
self.component.set_style(IndexStyle::Conversations);
|
||||
return true;
|
||||
}
|
||||
Action::Listing(ListingAction::Import(file_path, mailbox_path)) => {
|
||||
|
@ -1680,20 +1558,6 @@ impl Component for Listing {
|
|||
self.component
|
||||
.set_modifier_command(Some(Modifier::Intersection));
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if self.component.unfocused()
|
||||
&& shortcut!(key == shortcuts[Shortcuts::LISTING]["next_entry"]) =>
|
||||
{
|
||||
self.component.next_entry(context);
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if self.component.unfocused()
|
||||
&& shortcut!(
|
||||
key == shortcuts[Shortcuts::LISTING]["previous_entry"]
|
||||
) =>
|
||||
{
|
||||
self.component.prev_entry(context);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -1770,7 +1634,7 @@ impl Component for Listing {
|
|||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
|
||||
self.status(context),
|
||||
self.get_status(context),
|
||||
)));
|
||||
return true;
|
||||
}
|
||||
|
@ -2029,7 +1893,7 @@ impl Component for Listing {
|
|||
UIEvent::Action(Action::Compose(ComposeAction::Mailto(ref mailto))) => {
|
||||
let account_hash = context.accounts[self.cursor_pos.0].hash();
|
||||
let mut composer = Composer::with_account(account_hash, context);
|
||||
composer.set_draft(mailto.into(), context);
|
||||
composer.set_draft(mailto.into());
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::Action(Tab(New(Some(Box::new(composer))))));
|
||||
|
@ -2046,7 +1910,7 @@ impl Component for Listing {
|
|||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
|
||||
self.status(context),
|
||||
self.get_status(context),
|
||||
)));
|
||||
}
|
||||
UIEvent::Input(Key::Backspace) if !self.cmd_buf.is_empty() => {
|
||||
|
@ -2088,11 +1952,6 @@ impl Component for Listing {
|
|||
.as_ref()
|
||||
.map(Component::is_dirty)
|
||||
.unwrap_or_else(|| self.component.is_dirty())
|
||||
|| if self.component.unfocused() {
|
||||
self.view.is_dirty()
|
||||
} else {
|
||||
self.component.is_dirty()
|
||||
}
|
||||
}
|
||||
|
||||
fn set_dirty(&mut self, value: bool) {
|
||||
|
@ -2101,24 +1960,18 @@ impl Component for Listing {
|
|||
s.set_dirty(value);
|
||||
} else {
|
||||
self.component.set_dirty(value);
|
||||
if self.component.unfocused() {
|
||||
self.view.set_dirty(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
let mut map = if let Some(s) = self.status.as_ref() {
|
||||
s.shortcuts(context)
|
||||
s.get_shortcuts(context)
|
||||
} else {
|
||||
self.component.shortcuts(context)
|
||||
self.component.get_shortcuts(context)
|
||||
};
|
||||
let mut config_map = context.settings.shortcuts.listing.key_values();
|
||||
if self.focus != ListingFocus::Menu {
|
||||
config_map.remove("open_mailbox");
|
||||
if self.component.unfocused() {
|
||||
map.extend(self.view.shortcuts(context).into_iter());
|
||||
}
|
||||
}
|
||||
map.insert(Shortcuts::LISTING, config_map);
|
||||
|
||||
|
@ -2126,10 +1979,13 @@ impl Component for Listing {
|
|||
}
|
||||
|
||||
fn id(&self) -> ComponentId {
|
||||
self.id
|
||||
self.component.id()
|
||||
}
|
||||
fn set_id(&mut self, id: ComponentId) {
|
||||
self.component.set_id(id);
|
||||
}
|
||||
|
||||
fn status(&self, context: &Context) -> String {
|
||||
fn get_status(&self, context: &Context) -> String {
|
||||
let mailbox_hash = match self.cursor_pos.1 {
|
||||
MenuEntryCursor::Mailbox(idx) => {
|
||||
if let Some(MailboxMenuEntry { mailbox_hash, .. }) =
|
||||
|
@ -2169,38 +2025,6 @@ impl Component for Listing {
|
|||
MailboxStatus::Failed(_) | MailboxStatus::None => account[&mailbox_hash].status(),
|
||||
}
|
||||
}
|
||||
|
||||
fn children(&self) -> IndexMap<ComponentId, &dyn Component> {
|
||||
let mut ret = IndexMap::default();
|
||||
ret.insert(
|
||||
self.component.id(),
|
||||
match &self.component {
|
||||
Compact(l) => l.as_component(),
|
||||
Plain(l) => l.as_component(),
|
||||
Threaded(l) => l.as_component(),
|
||||
Conversations(l) => l.as_component(),
|
||||
Offline(l) => l.as_component(),
|
||||
},
|
||||
);
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
fn children_mut(&mut self) -> IndexMap<ComponentId, &mut dyn Component> {
|
||||
let mut ret = IndexMap::default();
|
||||
ret.insert(
|
||||
self.component.id(),
|
||||
match &mut self.component {
|
||||
Compact(l) => l.as_component_mut(),
|
||||
Plain(l) => l.as_component_mut(),
|
||||
Threaded(l) => l.as_component_mut(),
|
||||
Conversations(l) => l.as_component_mut(),
|
||||
Offline(l) => l.as_component_mut(),
|
||||
},
|
||||
);
|
||||
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
impl Listing {
|
||||
|
@ -2238,25 +2062,20 @@ impl Listing {
|
|||
first_account_hash,
|
||||
MailboxHash::default(),
|
||||
))),
|
||||
view: Box::<ThreadView>::default(),
|
||||
accounts: account_entries,
|
||||
status: None,
|
||||
dirty: true,
|
||||
cursor_pos: (0, MenuEntryCursor::Mailbox(0)),
|
||||
menu_cursor_pos: (0, MenuEntryCursor::Mailbox(0)),
|
||||
menu_content: CellBuffer::new_with_context(0, 0, None, context),
|
||||
menu_scrollbar_show_timer: context.main_loop_handler.job_executor.clone().create_timer(
|
||||
menu_scrollbar_show_timer: context.job_executor.clone().create_timer(
|
||||
std::time::Duration::from_secs(0),
|
||||
std::time::Duration::from_millis(1200),
|
||||
),
|
||||
show_menu_scrollbar: ShowMenuScrollbar::Never,
|
||||
startup_checks_rate: RateLimit::new(
|
||||
2,
|
||||
1000,
|
||||
context.main_loop_handler.job_executor.clone(),
|
||||
),
|
||||
startup_checks_rate: RateLimit::new(2, 1000, context.job_executor.clone()),
|
||||
theme_default: conf::value(context, "theme_default"),
|
||||
id: ComponentId::default(),
|
||||
id: ComponentId::new_v4(),
|
||||
sidebar_divider: *account_settings!(
|
||||
context[first_account_hash].listing.sidebar_divider
|
||||
),
|
||||
|
@ -2268,7 +2087,6 @@ impl Listing {
|
|||
focus: ListingFocus::Mailbox,
|
||||
cmd_buf: String::with_capacity(4),
|
||||
};
|
||||
ret.component.realize(ret.id().into(), context);
|
||||
ret.change_account(context);
|
||||
ret
|
||||
}
|
||||
|
@ -2765,18 +2583,16 @@ impl Listing {
|
|||
|
||||
let index_style =
|
||||
mailbox_settings!(context[account_hash][mailbox_hash].listing.index_style);
|
||||
self.set_style(*index_style, context);
|
||||
self.component.set_style(*index_style);
|
||||
} else if !matches!(self.component, ListingComponent::Offline(_)) {
|
||||
self.component.unrealize(context);
|
||||
self.component =
|
||||
Offline(OfflineListing::new((account_hash, MailboxHash::default())));
|
||||
self.component.realize(self.id().into(), context);
|
||||
}
|
||||
self.status = None;
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
|
||||
self.status(context),
|
||||
self.get_status(context),
|
||||
)));
|
||||
}
|
||||
MenuEntryCursor::Status => {
|
||||
|
@ -2802,71 +2618,11 @@ impl Listing {
|
|||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
|
||||
self.status(context),
|
||||
self.get_status(context),
|
||||
)));
|
||||
}
|
||||
|
||||
fn is_menu_visible(&self) -> bool {
|
||||
!matches!(self.component.focus(), Focus::EntryFullscreen) && self.menu_visibility
|
||||
}
|
||||
|
||||
fn set_style(&mut self, new_style: IndexStyle, context: &mut Context) {
|
||||
let old = match new_style {
|
||||
IndexStyle::Plain => {
|
||||
if matches!(self.component, Plain(_)) {
|
||||
return;
|
||||
}
|
||||
let coordinates = self.component.coordinates();
|
||||
std::mem::replace(
|
||||
&mut self.component,
|
||||
Plain(PlainListing::new(self.id, coordinates)),
|
||||
)
|
||||
}
|
||||
IndexStyle::Threaded => {
|
||||
if matches!(self.component, Threaded(_)) {
|
||||
return;
|
||||
}
|
||||
let coordinates = self.component.coordinates();
|
||||
std::mem::replace(
|
||||
&mut self.component,
|
||||
Threaded(ThreadListing::new(self.id, coordinates, context)),
|
||||
)
|
||||
}
|
||||
IndexStyle::Compact => {
|
||||
if matches!(self.component, Compact(_)) {
|
||||
return;
|
||||
}
|
||||
let coordinates = self.component.coordinates();
|
||||
std::mem::replace(
|
||||
&mut self.component,
|
||||
Compact(CompactListing::new(self.id, coordinates)),
|
||||
)
|
||||
}
|
||||
IndexStyle::Conversations => {
|
||||
if matches!(self.component, Conversations(_)) {
|
||||
return;
|
||||
}
|
||||
let coordinates = self.component.coordinates();
|
||||
std::mem::replace(
|
||||
&mut self.component,
|
||||
Conversations(ConversationsListing::new(self.id, coordinates)),
|
||||
)
|
||||
}
|
||||
};
|
||||
old.unrealize(context);
|
||||
self.component.realize(self.id.into(), context);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ListingMessage {
|
||||
FocusUpdate {
|
||||
new_value: Focus,
|
||||
},
|
||||
OpenEntryUnderCursor {
|
||||
env_hash: EnvelopeHash,
|
||||
thread_hash: ThreadHash,
|
||||
show_thread: bool,
|
||||
},
|
||||
UpdateView,
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
use std::{cmp, collections::BTreeMap, convert::TryInto, iter::FromIterator};
|
||||
|
||||
use indexmap::IndexSet;
|
||||
use melib::{TagHash, Threads};
|
||||
|
||||
use super::*;
|
||||
use crate::{components::PageMovement, jobs::JoinHandle};
|
||||
|
@ -185,13 +184,12 @@ pub struct CompactListing {
|
|||
force_draw: bool,
|
||||
/// If `self.view` exists or not.
|
||||
focus: Focus,
|
||||
view: Box<ThreadView>,
|
||||
color_cache: ColorCache,
|
||||
|
||||
movement: Option<PageMovement>,
|
||||
modifier_active: bool,
|
||||
modifier_command: Option<Modifier>,
|
||||
view_area: Option<Area>,
|
||||
parent: ComponentId,
|
||||
id: ComponentId,
|
||||
}
|
||||
|
||||
|
@ -288,7 +286,6 @@ impl MailListingTrait for CompactListing {
|
|||
self.sort,
|
||||
&context.accounts[&self.cursor_pos.0].collection.envelopes,
|
||||
);
|
||||
drop(threads);
|
||||
|
||||
self.redraw_threads_list(
|
||||
context,
|
||||
|
@ -296,22 +293,10 @@ impl MailListingTrait for CompactListing {
|
|||
);
|
||||
|
||||
if !force && old_cursor_pos == self.new_cursor_pos {
|
||||
self.kick_parent(self.parent, ListingMessage::UpdateView, context);
|
||||
self.view.update(context);
|
||||
} else if self.unfocused() {
|
||||
if let Some((thread_hash, env_hash)) = self
|
||||
.get_thread_under_cursor(self.cursor_pos.2)
|
||||
.and_then(|thread| self.rows.thread_to_env.get(&thread).map(|e| (thread, e[0])))
|
||||
{
|
||||
self.kick_parent(
|
||||
self.parent,
|
||||
ListingMessage::OpenEntryUnderCursor {
|
||||
thread_hash,
|
||||
env_hash,
|
||||
show_thread: true,
|
||||
},
|
||||
context,
|
||||
);
|
||||
self.set_focus(Focus::Entry, context);
|
||||
if let Some(thread) = self.get_thread_under_cursor(self.cursor_pos.2) {
|
||||
self.view = Box::new(ThreadView::new(self.new_cursor_pos, thread, None, context));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -371,13 +356,13 @@ impl MailListingTrait for CompactListing {
|
|||
continue 'items_for_loop;
|
||||
};
|
||||
if !context.accounts[&self.cursor_pos.0].contains_key(root_env_hash) {
|
||||
log::debug!("key = {}", root_env_hash);
|
||||
log::debug!(
|
||||
debug!("key = {}", root_env_hash);
|
||||
debug!(
|
||||
"name = {} {}",
|
||||
account[&self.cursor_pos.1].name(),
|
||||
context.accounts[&self.cursor_pos.0].name()
|
||||
);
|
||||
log::debug!("{:#?}", context.accounts);
|
||||
debug!("{:#?}", context.accounts);
|
||||
|
||||
panic!();
|
||||
}
|
||||
|
@ -401,7 +386,7 @@ impl MailListingTrait for CompactListing {
|
|||
from_address_list.clear();
|
||||
from_address_set.clear();
|
||||
for (envelope, show_subject) in threads
|
||||
.thread_iter(thread)
|
||||
.thread_group_iter(thread)
|
||||
.filter_map(|(_, h)| {
|
||||
Some((
|
||||
threads.thread_nodes()[&h].message()?,
|
||||
|
@ -578,49 +563,13 @@ impl ListingTrait for CompactListing {
|
|||
fn set_coordinates(&mut self, coordinates: (AccountHash, MailboxHash)) {
|
||||
self.new_cursor_pos = (coordinates.0, coordinates.1, 0);
|
||||
self.focus = Focus::None;
|
||||
self.view = Box::<ThreadView>::default();
|
||||
self.filtered_selection.clear();
|
||||
self.filtered_order.clear();
|
||||
self.filter_term.clear();
|
||||
self.rows.row_updates.clear();
|
||||
}
|
||||
|
||||
fn next_entry(&mut self, context: &mut Context) {
|
||||
if self
|
||||
.get_thread_under_cursor(self.cursor_pos.2 + 1)
|
||||
.is_some()
|
||||
{
|
||||
// TODO: makes this less ugly.
|
||||
self.movement = Some(PageMovement::Down(1));
|
||||
self.force_draw = true;
|
||||
self.dirty = true;
|
||||
self.cursor_pos.2 += 1;
|
||||
self.new_cursor_pos.2 += 1;
|
||||
self.set_focus(Focus::Entry, context);
|
||||
self.cursor_pos.2 -= 1;
|
||||
self.new_cursor_pos.2 -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn prev_entry(&mut self, context: &mut Context) {
|
||||
if self.cursor_pos.2 == 0 {
|
||||
return;
|
||||
}
|
||||
if self
|
||||
.get_thread_under_cursor(self.cursor_pos.2 - 1)
|
||||
.is_some()
|
||||
{
|
||||
// TODO: makes this less ugly.
|
||||
self.movement = Some(PageMovement::Up(1));
|
||||
self.force_draw = true;
|
||||
self.dirty = true;
|
||||
self.cursor_pos.2 -= 1;
|
||||
self.new_cursor_pos.2 -= 1;
|
||||
self.set_focus(Focus::Entry, context);
|
||||
self.cursor_pos.2 += 1;
|
||||
self.new_cursor_pos.2 += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn highlight_line(&mut self, grid: &mut CellBuffer, area: Area, idx: usize, context: &Context) {
|
||||
let thread_hash = if let Some(h) = self.get_thread_under_cursor(idx) {
|
||||
h
|
||||
|
@ -862,10 +811,6 @@ impl ListingTrait for CompactListing {
|
|||
);
|
||||
}
|
||||
|
||||
fn view_area(&self) -> Option<Area> {
|
||||
self.view_area
|
||||
}
|
||||
|
||||
fn unfocused(&self) -> bool {
|
||||
!matches!(self.focus, Focus::None)
|
||||
}
|
||||
|
@ -890,6 +835,8 @@ impl ListingTrait for CompactListing {
|
|||
fn set_focus(&mut self, new_value: Focus, context: &mut Context) {
|
||||
match new_value {
|
||||
Focus::None => {
|
||||
self.view
|
||||
.process_event(&mut UIEvent::VisibilityChange(false), context);
|
||||
self.dirty = true;
|
||||
/* If self.rows.row_updates is not empty and we exit a thread, the row_update
|
||||
* events will be performed but the list will not be drawn.
|
||||
|
@ -898,35 +845,15 @@ impl ListingTrait for CompactListing {
|
|||
self.force_draw = true;
|
||||
}
|
||||
Focus::Entry => {
|
||||
if let Some((thread_hash, env_hash)) = self
|
||||
.get_thread_under_cursor(self.cursor_pos.2)
|
||||
.and_then(|thread| self.rows.thread_to_env.get(&thread).map(|e| (thread, e[0])))
|
||||
{
|
||||
self.force_draw = true;
|
||||
self.dirty = true;
|
||||
self.kick_parent(
|
||||
self.parent,
|
||||
ListingMessage::OpenEntryUnderCursor {
|
||||
thread_hash,
|
||||
env_hash,
|
||||
show_thread: true,
|
||||
},
|
||||
context,
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
self.force_draw = true;
|
||||
self.dirty = true;
|
||||
self.view.set_dirty(true);
|
||||
}
|
||||
Focus::EntryFullscreen => {
|
||||
self.dirty = true;
|
||||
self.view.set_dirty(true);
|
||||
}
|
||||
}
|
||||
self.focus = new_value;
|
||||
self.kick_parent(
|
||||
self.parent,
|
||||
ListingMessage::FocusUpdate { new_value },
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
fn focus(&self) -> Focus {
|
||||
|
@ -942,7 +869,7 @@ impl fmt::Display for CompactListing {
|
|||
|
||||
impl CompactListing {
|
||||
pub const DESCRIPTION: &'static str = "compact listing";
|
||||
pub fn new(parent: ComponentId, coordinates: (AccountHash, MailboxHash)) -> Box<Self> {
|
||||
pub fn new(coordinates: (AccountHash, MailboxHash)) -> Box<Self> {
|
||||
Box::new(CompactListing {
|
||||
cursor_pos: (coordinates.0, MailboxHash::default(), 0),
|
||||
new_cursor_pos: (coordinates.0, coordinates.1, 0),
|
||||
|
@ -961,17 +888,15 @@ impl CompactListing {
|
|||
rows: RowsState::default(),
|
||||
dirty: true,
|
||||
force_draw: true,
|
||||
view: Box::<ThreadView>::default(),
|
||||
color_cache: ColorCache::default(),
|
||||
movement: None,
|
||||
modifier_active: false,
|
||||
modifier_command: None,
|
||||
view_area: None,
|
||||
parent,
|
||||
id: ComponentId::default(),
|
||||
id: ComponentId::new_v4(),
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn make_entry_string(
|
||||
&self,
|
||||
root_envelope: &Envelope,
|
||||
|
@ -1020,7 +945,7 @@ impl CompactListing {
|
|||
tags_string.pop();
|
||||
}
|
||||
}
|
||||
let subject = if *mailbox_settings!(
|
||||
let mut subject = if *mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.thread_subject_pack
|
||||
|
@ -1028,18 +953,16 @@ impl CompactListing {
|
|||
other_subjects
|
||||
.into_iter()
|
||||
.fold(String::new(), |mut acc, s| {
|
||||
if s.trim().is_empty() {
|
||||
return acc;
|
||||
}
|
||||
if !acc.is_empty() {
|
||||
acc.push_str(", ");
|
||||
}
|
||||
acc.push_str(s.trim());
|
||||
acc.push_str(s);
|
||||
acc
|
||||
})
|
||||
} else {
|
||||
root_envelope.subject().trim().to_string()
|
||||
root_envelope.subject().to_string()
|
||||
};
|
||||
subject.truncate_at_boundary(150);
|
||||
EntryStrings {
|
||||
date: DateString(ConversationsListing::format_date(context, thread.date())),
|
||||
subject: if thread.len() > 1 {
|
||||
|
@ -1165,7 +1088,7 @@ impl CompactListing {
|
|||
let mut from_address_set: std::collections::HashSet<Vec<u8>> =
|
||||
std::collections::HashSet::new();
|
||||
for (envelope, show_subject) in threads
|
||||
.thread_iter(thread_hash)
|
||||
.thread_group_iter(thread_hash)
|
||||
.filter_map(|(_, h)| {
|
||||
threads.thread_nodes()[&h]
|
||||
.message()
|
||||
|
@ -1316,8 +1239,10 @@ impl CompactListing {
|
|||
}
|
||||
debug_assert!(end >= start);
|
||||
if self.rows_drawn.get_max(start, end) == 0 {
|
||||
//debug!("not drawing {}-{}", start, end);
|
||||
return;
|
||||
}
|
||||
//debug!("drawing {}-{}", start, end);
|
||||
for i in start..=end {
|
||||
self.rows_drawn.update(i, 0);
|
||||
}
|
||||
|
@ -1479,6 +1404,7 @@ impl CompactListing {
|
|||
if !account.collection.contains_key(&env_hash) {
|
||||
continue;
|
||||
}
|
||||
debug!(account.collection.get_env(env_hash).subject());
|
||||
let env_thread_node_hash = account.collection.get_env(env_hash).thread();
|
||||
if !threads.thread_nodes.contains_key(&env_thread_node_hash) {
|
||||
continue;
|
||||
|
@ -1513,13 +1439,12 @@ impl CompactListing {
|
|||
|
||||
impl Component for CompactListing {
|
||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
if matches!(self.focus, Focus::EntryFullscreen) {
|
||||
self.view_area = area.into();
|
||||
if !self.is_dirty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if !self.is_dirty() {
|
||||
return;
|
||||
if matches!(self.focus, Focus::EntryFullscreen) {
|
||||
return self.view.draw(grid, area, context);
|
||||
}
|
||||
|
||||
if !self.unfocused() {
|
||||
|
@ -1755,13 +1680,13 @@ impl Component for CompactListing {
|
|||
return;
|
||||
}
|
||||
|
||||
self.view_area = area.into();
|
||||
self.view.draw(grid, area, context);
|
||||
}
|
||||
self.dirty = false;
|
||||
}
|
||||
|
||||
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
let shortcuts = self.shortcuts(context);
|
||||
let shortcuts = self.get_shortcuts(context);
|
||||
|
||||
match (&event, self.focus) {
|
||||
(UIEvent::Input(ref k), Focus::Entry)
|
||||
|
@ -1785,6 +1710,10 @@ impl Component for CompactListing {
|
|||
_ => {}
|
||||
}
|
||||
|
||||
if self.unfocused() && self.view.process_event(event, context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.length > 0 {
|
||||
match *event {
|
||||
UIEvent::Input(ref k)
|
||||
|
@ -1792,8 +1721,11 @@ impl Component for CompactListing {
|
|||
&& (shortcut!(k == shortcuts[Shortcuts::LISTING]["open_entry"])
|
||||
|| shortcut!(k == shortcuts[Shortcuts::LISTING]["focus_right"])) =>
|
||||
{
|
||||
self.set_focus(Focus::Entry, context);
|
||||
|
||||
if let Some(thread) = self.get_thread_under_cursor(self.cursor_pos.2) {
|
||||
self.view =
|
||||
Box::new(ThreadView::new(self.cursor_pos, thread, None, context));
|
||||
self.set_focus(Focus::Entry, context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref k)
|
||||
|
@ -1845,6 +1777,7 @@ impl Component for CompactListing {
|
|||
UIEvent::Action(ref action) => {
|
||||
match action {
|
||||
Action::Sort(field, order) if !self.unfocused() => {
|
||||
debug!("Sort {:?} , {:?}", field, order);
|
||||
self.sort = (*field, *order);
|
||||
self.sortcmd = true;
|
||||
if !self.filtered_selection.is_empty() {
|
||||
|
@ -1856,6 +1789,7 @@ impl Component for CompactListing {
|
|||
return true;
|
||||
}
|
||||
Action::SubSort(field, order) if !self.unfocused() => {
|
||||
debug!("SubSort {:?} , {:?}", field, order);
|
||||
self.subsort = (*field, *order);
|
||||
// FIXME: perform subsort.
|
||||
return true;
|
||||
|
@ -1902,7 +1836,7 @@ impl Component for CompactListing {
|
|||
self.refresh_mailbox(context, false);
|
||||
self.set_dirty(true);
|
||||
}
|
||||
UIEvent::EnvelopeRename(_, ref new_hash) => {
|
||||
UIEvent::EnvelopeRename(ref old_hash, ref new_hash) => {
|
||||
let account = &context.accounts[&self.cursor_pos.0];
|
||||
let threads = account.collection.get_threads(self.cursor_pos.1);
|
||||
if !account.collection.contains_key(new_hash) {
|
||||
|
@ -1920,8 +1854,13 @@ impl Component for CompactListing {
|
|||
}
|
||||
|
||||
self.set_dirty(true);
|
||||
|
||||
if self.unfocused() {
|
||||
self.view
|
||||
.process_event(&mut UIEvent::EnvelopeRename(*old_hash, *new_hash), context);
|
||||
}
|
||||
}
|
||||
UIEvent::EnvelopeRemove(_, ref thread_hash) => {
|
||||
UIEvent::EnvelopeRemove(ref _env_hash, ref thread_hash) => {
|
||||
if self.rows.thread_order.contains_key(thread_hash) {
|
||||
self.refresh_mailbox(context, false);
|
||||
self.set_dirty(true);
|
||||
|
@ -1945,6 +1884,11 @@ impl Component for CompactListing {
|
|||
}
|
||||
|
||||
self.set_dirty(true);
|
||||
|
||||
if self.unfocused() {
|
||||
self.view
|
||||
.process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context);
|
||||
}
|
||||
}
|
||||
UIEvent::ChangeMode(UIMode::Normal) => {
|
||||
self.set_dirty(true);
|
||||
|
@ -1981,7 +1925,6 @@ impl Component for CompactListing {
|
|||
) {
|
||||
Ok(job) => {
|
||||
let handle = context.accounts[&self.cursor_pos.0]
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized(job);
|
||||
self.search_job = Some((filter_term.to_string(), handle));
|
||||
|
@ -2004,7 +1947,6 @@ impl Component for CompactListing {
|
|||
) {
|
||||
Ok(job) => {
|
||||
let mut handle = context.accounts[&self.cursor_pos.0]
|
||||
.main_loop_handler
|
||||
.job_executor
|
||||
.spawn_specialized(job);
|
||||
if let Ok(Some(search_result)) = try_recv_timeout!(&mut handle.chan) {
|
||||
|
@ -2068,17 +2010,24 @@ impl Component for CompactListing {
|
|||
fn is_dirty(&self) -> bool {
|
||||
match self.focus {
|
||||
Focus::None => self.dirty,
|
||||
Focus::Entry => self.dirty,
|
||||
Focus::EntryFullscreen => false,
|
||||
Focus::Entry => self.dirty || self.view.is_dirty(),
|
||||
Focus::EntryFullscreen => self.view.is_dirty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_dirty(&mut self, value: bool) {
|
||||
self.dirty = value;
|
||||
if self.unfocused() {
|
||||
self.view.set_dirty(value);
|
||||
}
|
||||
}
|
||||
|
||||
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
let mut map = ShortcutMaps::default();
|
||||
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
let mut map = if self.unfocused() {
|
||||
self.view.get_shortcuts(context)
|
||||
} else {
|
||||
ShortcutMaps::default()
|
||||
};
|
||||
|
||||
map.insert(
|
||||
Shortcuts::LISTING,
|
||||
|
@ -2091,4 +2040,7 @@ impl Component for CompactListing {
|
|||
fn id(&self) -> ComponentId {
|
||||
self.id
|
||||
}
|
||||
fn set_id(&mut self, id: ComponentId) {
|
||||
self.id = id;
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue