mirror of
https://github.com/FliegendeWurst/inboxid.git
synced 2024-11-25 10:25:08 +00:00
browse: show mail author and timestamp
This commit is contained in:
parent
a171abfdae
commit
7d2e6de827
40
Cargo.lock
generated
40
Cargo.lock
generated
@ -213,7 +213,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"num",
|
"num",
|
||||||
"owning_ref",
|
"owning_ref",
|
||||||
"syn 1.0.67",
|
"syn 1.0.68",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
"wasmer_enumset",
|
"wasmer_enumset",
|
||||||
@ -223,11 +223,11 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cursive_tree_view"
|
name = "cursive_tree_view"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/FliegendeWurst/cursive_tree_view.git?branch=master#f3a0470f229a5ab57da600cf552b916ad33c6390"
|
||||||
checksum = "cd59affc6d600a69df27972fb5fe7f38a743aeb0710299e3324022c7d80717a0"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cursive_core",
|
"cursive_core",
|
||||||
"debug_stub_derive",
|
"debug_stub_derive",
|
||||||
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -251,7 +251,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.9",
|
"quote 1.0.9",
|
||||||
"strsim",
|
"strsim",
|
||||||
"syn 1.0.67",
|
"syn 1.0.68",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -262,7 +262,7 @@ checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core",
|
||||||
"quote 1.0.9",
|
"quote 1.0.9",
|
||||||
"syn 1.0.67",
|
"syn 1.0.68",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -335,7 +335,7 @@ checksum = "e5c450cf304c9e18d45db562025a14fb1ca0f5c769b6f609309f81d4c31de455"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.9",
|
"quote 1.0.9",
|
||||||
"syn 1.0.67",
|
"syn 1.0.68",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -525,8 +525,8 @@ checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libsqlite3-sys"
|
name = "libsqlite3-sys"
|
||||||
version = "0.21.0"
|
version = "0.22.0"
|
||||||
source = "git+https://github.com/rusqlite/rusqlite?branch=master#ed3bfbdf9d9e577e8d4cff937b053a0e429a4cd7"
|
source = "git+https://github.com/rusqlite/rusqlite?branch=master#ddf69f749a67fdb673837ebb3c881a522fd7f638"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
@ -595,9 +595,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mime2ext"
|
name = "mime2ext"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b88947611258697e12f8602a44003b0885ca5fe30f27132d63c8f47fe98f2f2e"
|
checksum = "d8b337a0b7c1d5d8d3c08096823831a5d7a3f6aadea1150b7fe622c38c14e283"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -751,9 +751,9 @@ checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.24"
|
version = "1.0.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
|
checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-xid 0.2.1",
|
"unicode-xid 0.2.1",
|
||||||
]
|
]
|
||||||
@ -851,8 +851,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rusqlite"
|
name = "rusqlite"
|
||||||
version = "0.24.2"
|
version = "0.25.0"
|
||||||
source = "git+https://github.com/rusqlite/rusqlite?branch=master#ed3bfbdf9d9e577e8d4cff937b053a0e429a4cd7"
|
source = "git+https://github.com/rusqlite/rusqlite?branch=master#ddf69f749a67fdb673837ebb3c881a522fd7f638"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"fallible-iterator",
|
"fallible-iterator",
|
||||||
@ -995,7 +995,7 @@ checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.9",
|
"quote 1.0.9",
|
||||||
"syn 1.0.67",
|
"syn 1.0.68",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1081,9 +1081,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.67"
|
version = "1.0.68"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6498a9efc342871f91cc2d0d694c674368b4ceb40f62b65a7a08c3792935e702"
|
checksum = "3ce15dd3ed8aa2f8eeac4716d6ef5ab58b6b9256db41d7e1a0224c2788e8fd87"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.9",
|
"quote 1.0.9",
|
||||||
@ -1206,7 +1206,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.9",
|
"quote 1.0.9",
|
||||||
"syn 1.0.67",
|
"syn 1.0.68",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1228,7 +1228,7 @@ checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.9",
|
"quote 1.0.9",
|
||||||
"syn 1.0.67",
|
"syn 1.0.68",
|
||||||
"wasm-bindgen-backend",
|
"wasm-bindgen-backend",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
@ -1258,7 +1258,7 @@ dependencies = [
|
|||||||
"darling",
|
"darling",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote 1.0.9",
|
"quote 1.0.9",
|
||||||
"syn 1.0.67",
|
"syn 1.0.68",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -25,4 +25,7 @@ subprocess = "0.2.6"
|
|||||||
mime2ext = "0.1.2"
|
mime2ext = "0.1.2"
|
||||||
petgraph = "0.5.1"
|
petgraph = "0.5.1"
|
||||||
cursive = { version = "0.16.3", default-features = false, features = ["termion-backend"] }
|
cursive = { version = "0.16.3", default-features = false, features = ["termion-backend"] }
|
||||||
cursive_tree_view = "0.7.0"
|
cursive_tree_view = { git = "https://github.com/FliegendeWurst/cursive_tree_view.git", branch = "master" }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
overflow-checks = true
|
||||||
|
@ -6,7 +6,7 @@ use cursive::{Cursive, CursiveExt};
|
|||||||
use cursive::traits::Identifiable;
|
use cursive::traits::Identifiable;
|
||||||
use cursive::view::{Scrollable, SizeConstraint};
|
use cursive::view::{Scrollable, SizeConstraint};
|
||||||
use cursive::views::{LinearLayout, ResizedView, TextView};
|
use cursive::views::{LinearLayout, ResizedView, TextView};
|
||||||
use cursive_tree_view::{Placement, TreeView};
|
use cursive_tree_view::{Placement, TreeEntry, TreeView};
|
||||||
use inboxid::*;
|
use inboxid::*;
|
||||||
use io::Write;
|
use io::Write;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
@ -25,8 +25,8 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
match result {
|
match result {
|
||||||
Ok(res) => res,
|
Ok(res) => res,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
if let Err(e) = io::stderr().lock().write_all(&sink.lock().unwrap()) {
|
if let Err(e) = io::stderr().lock().write_all(&sink.lock().unwrap()) {
|
||||||
println!("{:?}", e);
|
println!("{:?}", e);
|
||||||
}
|
}
|
||||||
@ -62,7 +62,7 @@ fn show_listing(mailbox: &str) -> Result<()> {
|
|||||||
} else {
|
} else {
|
||||||
flags_display.push('*');
|
flags_display.push('*');
|
||||||
}
|
}
|
||||||
rows.push(IntoIter::new([(mails.len() - i).to_string(), flags_display, mail.from.clone(), mail.subject.clone(), mail.date_iso.clone()]));
|
rows.push(IntoIter::new([(mails.len() - i).to_string(), flags_display, mail.from(), mail.subject.clone(), mail.date_iso.clone()]));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut mails_by_id = HashMap::new();
|
let mut mails_by_id = HashMap::new();
|
||||||
@ -251,15 +251,17 @@ struct MailPart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Display for MailPart {
|
impl Display for MailPart {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{}", self.part.ctype.mimetype)
|
write!(f, "{}", self.part.ctype.mimetype)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&'static ParsedMail<'static>> for MailPart {
|
impl From<&'static ParsedMail<'static>> for MailPart {
|
||||||
fn from(part: &'static ParsedMail<'static>) -> Self {
|
fn from(part: &'static ParsedMail<'static>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
part
|
part
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TreeEntry for MailPart {}
|
||||||
|
@ -40,7 +40,7 @@ fn show_listing(mailbox: &str) -> Result<()> {
|
|||||||
} else {
|
} else {
|
||||||
flags_display.push('*');
|
flags_display.push('*');
|
||||||
}
|
}
|
||||||
rows.push(IntoIter::new([(mails.len() - i).to_string(), flags_display, mail.from.clone(), mail.subject.clone(), mail.date_iso.clone()]));
|
rows.push(IntoIter::new([(mails.len() - i).to_string(), flags_display, mail.from(), mail.subject.clone(), mail.date_iso.clone()]));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut ascii_table = AsciiTable::default();
|
let mut ascii_table = AsciiTable::default();
|
||||||
|
@ -26,7 +26,7 @@ fn show_listing(mailbox: &str) -> Result<()> {
|
|||||||
|
|
||||||
let mut rows = Vec::new();
|
let mut rows = Vec::new();
|
||||||
for mail in &mails {
|
for mail in &mails {
|
||||||
rows.push(IntoIter::new([mail.from.clone(), mail.subject.clone(), mail.date_iso.clone()]));
|
rows.push(IntoIter::new([mail.from(), mail.subject.clone(), mail.date_iso.clone()]));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut ascii_table = AsciiTable::default();
|
let mut ascii_table = AsciiTable::default();
|
||||||
|
94
src/lib.rs
94
src/lib.rs
@ -1,10 +1,12 @@
|
|||||||
use std::{borrow::Cow, convert::{TryFrom, TryInto}, env, fmt::{Debug, Display}, fs, hash::Hash, io, net::TcpStream, ops::Deref};
|
use std::{borrow::Cow, cmp, convert::{TryFrom, TryInto}, env, fmt::{Debug, Display}, fs, hash::Hash, io, net::TcpStream, ops::Deref};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use chrono::{DateTime, Local, NaiveDateTime, TimeZone};
|
use chrono::{DateTime, Local, NaiveDateTime, TimeZone};
|
||||||
|
use cursive::{theme::Style, utils::span::{IndexedCow, IndexedSpan, SpannedString}};
|
||||||
|
use cursive_tree_view::TreeEntry;
|
||||||
use imap::{Session, types::Flag};
|
use imap::{Session, types::Flag};
|
||||||
use maildir::{MailEntry, Maildir};
|
use maildir::{MailEntry, Maildir};
|
||||||
use mailparse::{MailHeaderMap, ParsedMail, dateparse};
|
use mailparse::{MailHeaderMap, ParsedMail, SingleInfo, addrparse, dateparse};
|
||||||
use petgraph::{Graph, graph::NodeIndex};
|
use petgraph::{Graph, graph::NodeIndex};
|
||||||
use rusqlite::{Connection, params};
|
use rusqlite::{Connection, params};
|
||||||
use rustls_connector::{RustlsConnector, rustls::{ClientSession, StreamOwned}};
|
use rustls_connector::{RustlsConnector, rustls::{ClientSession, StreamOwned}};
|
||||||
@ -96,7 +98,7 @@ pub struct EasyMail<'a> {
|
|||||||
mail: Option<ParsedMail<'a>>,
|
mail: Option<ParsedMail<'a>>,
|
||||||
pub id: MaildirID,
|
pub id: MaildirID,
|
||||||
pub flags: String,
|
pub flags: String,
|
||||||
pub from: String,
|
from: SingleInfo,
|
||||||
pub subject: String,
|
pub subject: String,
|
||||||
pub date: DateTime<Local>,
|
pub date: DateTime<Local>,
|
||||||
pub date_iso: String,
|
pub date_iso: String,
|
||||||
@ -108,7 +110,10 @@ impl EasyMail<'_> {
|
|||||||
mail: None,
|
mail: None,
|
||||||
id: MaildirID::new(0, 0),
|
id: MaildirID::new(0, 0),
|
||||||
flags: "S".to_owned(),
|
flags: "S".to_owned(),
|
||||||
from: String::new(),
|
from: SingleInfo {
|
||||||
|
display_name: None,
|
||||||
|
addr: String::new()
|
||||||
|
},
|
||||||
subject,
|
subject,
|
||||||
date: Local.from_utc_datetime(&NaiveDateTime::from_timestamp(0, 0)),
|
date: Local.from_utc_datetime(&NaiveDateTime::from_timestamp(0, 0)),
|
||||||
date_iso: "????-??-??".to_owned()
|
date_iso: "????-??-??".to_owned()
|
||||||
@ -119,6 +124,10 @@ impl EasyMail<'_> {
|
|||||||
self.mail.is_none()
|
self.mail.is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from(&self) -> String {
|
||||||
|
self.from.display_name.as_deref().unwrap_or_default().to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_header(&self, header: &str) -> String {
|
pub fn get_header(&self, header: &str) -> String {
|
||||||
self.get_headers().get_all_values(header).join(" ")
|
self.get_headers().get_all_values(header).join(" ")
|
||||||
}
|
}
|
||||||
@ -151,7 +160,8 @@ impl Eq for EasyMail<'_> {}
|
|||||||
impl Hash for EasyMail<'_> {
|
impl Hash for EasyMail<'_> {
|
||||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||||
self.id.hash(state);
|
self.id.hash(state);
|
||||||
self.from.hash(state);
|
self.from.display_name.hash(state);
|
||||||
|
self.from.addr.hash(state);
|
||||||
self.subject.hash(state);
|
self.subject.hash(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -164,6 +174,76 @@ impl<'a> Deref for EasyMail<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TreeEntry for &EasyMail<'_> {
|
||||||
|
fn display(&self, width: usize) -> SpannedString<Style> {
|
||||||
|
if self.is_pseudo() {
|
||||||
|
return self.subject.clone().into();
|
||||||
|
}
|
||||||
|
let from = self.from();
|
||||||
|
let mut line = self.subject.clone();
|
||||||
|
let mut i = width.saturating_sub(1 + from.len() + 1 + self.date_iso.len());
|
||||||
|
while i <= line.len() && !line.is_char_boundary(i) {
|
||||||
|
if i == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i -= 1;
|
||||||
|
}
|
||||||
|
line.truncate(i);
|
||||||
|
let subj_len = line.len();
|
||||||
|
while line.len() < i {
|
||||||
|
line.push(' ');
|
||||||
|
}
|
||||||
|
line.push(' ');
|
||||||
|
line += &from;
|
||||||
|
line.push(' ');
|
||||||
|
line += &self.date_iso;
|
||||||
|
|
||||||
|
let spans = vec![
|
||||||
|
IndexedSpan {
|
||||||
|
content: IndexedCow::Borrowed {
|
||||||
|
start: 0,
|
||||||
|
end: subj_len
|
||||||
|
},
|
||||||
|
attr: Style::default(),
|
||||||
|
width: subj_len
|
||||||
|
},
|
||||||
|
IndexedSpan {
|
||||||
|
content: IndexedCow::Borrowed {
|
||||||
|
start: 0,
|
||||||
|
end: 0
|
||||||
|
},
|
||||||
|
attr: Style::default(),
|
||||||
|
width: line.len() - subj_len - from.len() - self.date_iso.len() - 1
|
||||||
|
},
|
||||||
|
IndexedSpan {
|
||||||
|
content: IndexedCow::Borrowed {
|
||||||
|
start: line.len() - self.date_iso.len() - 1 - from.len(),
|
||||||
|
end: line.len() - self.date_iso.len() - 1
|
||||||
|
},
|
||||||
|
attr: Style::default(),
|
||||||
|
width: from.len()
|
||||||
|
},
|
||||||
|
IndexedSpan {
|
||||||
|
content: IndexedCow::Borrowed {
|
||||||
|
start: 0,
|
||||||
|
end: 0
|
||||||
|
},
|
||||||
|
attr: Style::default(),
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
IndexedSpan {
|
||||||
|
content: IndexedCow::Borrowed {
|
||||||
|
start: line.len() - self.date_iso.len(),
|
||||||
|
end: line.len()
|
||||||
|
},
|
||||||
|
attr: Style::default(),
|
||||||
|
width: self.date_iso.len()
|
||||||
|
},
|
||||||
|
];
|
||||||
|
SpannedString::with_spans(&line, spans)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait MailExtension {
|
pub trait MailExtension {
|
||||||
fn get_tree_structure<'a>(&'a self, graph: &mut Graph<&'a ParsedMail<'a>, ()>, parent: Option<NodeIndex>);
|
fn get_tree_structure<'a>(&'a self, graph: &mut Graph<&'a ParsedMail<'a>, ()>, parent: Option<NodeIndex>);
|
||||||
fn print_tree_structure(&self, depth: usize, counter: &mut usize);
|
fn print_tree_structure(&self, depth: usize, counter: &mut usize);
|
||||||
@ -232,7 +312,7 @@ impl MaildirExtension for Maildir {
|
|||||||
let flags = maile.flags().to_owned();
|
let flags = maile.flags().to_owned();
|
||||||
let mail = maile.parsed()?;
|
let mail = maile.parsed()?;
|
||||||
let headers = mail.get_headers();
|
let headers = mail.get_headers();
|
||||||
let from = headers.get_all_values("From").join(" ");
|
let from = addrparse(&headers.get_all_values("From").join(" "))?.extract_single_info().context("failed to extract from")?;
|
||||||
let subject = headers.get_all_values("Subject").join(" ");
|
let subject = headers.get_all_values("Subject").join(" ");
|
||||||
let date = headers.get_all_values("Date").join(" ");
|
let date = headers.get_all_values("Date").join(" ");
|
||||||
let date = dateparse(&date).map(|x|
|
let date = dateparse(&date).map(|x|
|
||||||
@ -259,7 +339,7 @@ impl MaildirExtension for Maildir {
|
|||||||
let flags = maile.flags().to_owned();
|
let flags = maile.flags().to_owned();
|
||||||
let mail = maile.parsed()?;
|
let mail = maile.parsed()?;
|
||||||
let headers = mail.get_headers();
|
let headers = mail.get_headers();
|
||||||
let from = headers.get_all_values("From").join(" ");
|
let from = addrparse(&headers.get_all_values("From").join(" "))?.extract_single_info().context("failed to extract from")?;
|
||||||
let subject = headers.get_all_values("Subject").join(" ");
|
let subject = headers.get_all_values("Subject").join(" ");
|
||||||
let date = headers.get_all_values("Date").join(" ");
|
let date = headers.get_all_values("Date").join(" ");
|
||||||
let date = dateparse(&date).map(|x|
|
let date = dateparse(&date).map(|x|
|
||||||
|
Loading…
Reference in New Issue
Block a user