mirror of
https://github.com/FliegendeWurst/inboxid.git
synced 2024-11-08 10:20:39 +00:00
browse: basic config system
This commit is contained in:
parent
7d2e6de827
commit
508934c50c
66
Cargo.lock
generated
66
Cargo.lock
generated
@ -275,6 +275,16 @@ dependencies = [
|
|||||||
"syn 0.11.11",
|
"syn 0.11.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "directories-next"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"dirs-sys-next",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs-next"
|
name = "dirs-next"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@ -450,18 +460,25 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"cursive",
|
"cursive",
|
||||||
"cursive_tree_view",
|
"cursive_tree_view",
|
||||||
|
"directories-next",
|
||||||
"imap",
|
"imap",
|
||||||
"itertools",
|
"itertools",
|
||||||
|
"log",
|
||||||
"maildir",
|
"maildir",
|
||||||
"mailparse",
|
"mailparse",
|
||||||
"mailproc",
|
"mailproc",
|
||||||
"mime2ext",
|
"mime2ext",
|
||||||
"moins",
|
"moins",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot",
|
||||||
"petgraph",
|
"petgraph",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"rustls-connector",
|
"rustls-connector",
|
||||||
"rustyline",
|
"rustyline",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
"subprocess",
|
"subprocess",
|
||||||
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -474,6 +491,15 @@ dependencies = [
|
|||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "instant"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@ -526,13 +552,23 @@ checksum = "56d855069fafbb9b344c0f962150cd2c1187975cb1c22c1522c240d8c4986714"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "libsqlite3-sys"
|
name = "libsqlite3-sys"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
source = "git+https://github.com/rusqlite/rusqlite?branch=master#ddf69f749a67fdb673837ebb3c881a522fd7f638"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f6332d94daa84478d55a6aa9dbb3b305ed6500fb0cb9400cb9e1525d0e0e188"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a3c91c24eae6777794bb1997ad98bbb87daf92890acab859f7eaa4320333176"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@ -733,6 +769,31 @@ dependencies = [
|
|||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
|
||||||
|
dependencies = [
|
||||||
|
"instant",
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"instant",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "petgraph"
|
name = "petgraph"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@ -852,7 +913,8 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "rusqlite"
|
name = "rusqlite"
|
||||||
version = "0.25.0"
|
version = "0.25.0"
|
||||||
source = "git+https://github.com/rusqlite/rusqlite?branch=master#ddf69f749a67fdb673837ebb3c881a522fd7f638"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48381bf52627e7b0e02c4c0e4c0c88fc1cf2228a2eb7461d9499b1372399f1da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"fallible-iterator",
|
"fallible-iterator",
|
||||||
|
12
Cargo.toml
12
Cargo.toml
@ -15,8 +15,7 @@ mailparse = "0.13.2"
|
|||||||
rustls-connector = "0.13.1"
|
rustls-connector = "0.13.1"
|
||||||
ascii_table = { git = "https://gitlab.com/arnekeller/ascii-table.git", branch = "master" }
|
ascii_table = { git = "https://gitlab.com/arnekeller/ascii-table.git", branch = "master" }
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
# remove when 0.24.3 is released
|
rusqlite = { version = "0.25.0", features = ["bundled"] }
|
||||||
rusqlite = { git = "https://github.com/rusqlite/rusqlite", branch = "master", features = ["bundled"] }
|
|
||||||
rustyline = "8.0.0"
|
rustyline = "8.0.0"
|
||||||
moins = { git = "https://github.com/FliegendeWurst/moins", branch = "master" }
|
moins = { git = "https://github.com/FliegendeWurst/moins", branch = "master" }
|
||||||
anyhow = "1.0.40"
|
anyhow = "1.0.40"
|
||||||
@ -26,6 +25,13 @@ 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 = { git = "https://github.com/FliegendeWurst/cursive_tree_view.git", branch = "master" }
|
cursive_tree_view = { git = "https://github.com/FliegendeWurst/cursive_tree_view.git", branch = "master" }
|
||||||
|
directories-next = "2.0.0"
|
||||||
|
serde_derive = "1.0.25"
|
||||||
|
serde = "1.0.25"
|
||||||
|
toml = "0.5.8"
|
||||||
|
once_cell = "1.7.2"
|
||||||
|
parking_lot = "0.11.1"
|
||||||
|
log = "0.4.14"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
overflow-checks = true
|
overflow-checks = true # useful when debugging
|
||||||
|
@ -3,17 +3,21 @@
|
|||||||
use std::{array::IntoIter, cell::RefCell, cmp, collections::{HashMap, HashSet}, env, fmt::Display, io, sync::{Arc, Mutex}};
|
use std::{array::IntoIter, cell::RefCell, cmp, collections::{HashMap, HashSet}, env, fmt::Display, io, sync::{Arc, Mutex}};
|
||||||
|
|
||||||
use cursive::{Cursive, CursiveExt};
|
use cursive::{Cursive, CursiveExt};
|
||||||
|
use cursive::event::{Event, Key};
|
||||||
use cursive::traits::Identifiable;
|
use cursive::traits::Identifiable;
|
||||||
use cursive::view::{Scrollable, SizeConstraint};
|
use cursive::view::{Scrollable, SizeConstraint, View};
|
||||||
use cursive::views::{LinearLayout, ResizedView, TextView};
|
use cursive::views::{Button, Checkbox, LinearLayout, OnEventView, ResizedView, TextView};
|
||||||
use cursive_tree_view::{Placement, TreeEntry, TreeView};
|
use cursive_tree_view::{Placement, TreeEntry, TreeView};
|
||||||
use inboxid::*;
|
use inboxid::*;
|
||||||
use io::Write;
|
use io::Write;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use log::error;
|
||||||
use mailparse::ParsedMail;
|
use mailparse::ParsedMail;
|
||||||
|
use parking_lot::RwLock;
|
||||||
use petgraph::{EdgeDirection, graph::{DiGraph, NodeIndex}, visit::{Dfs, IntoNodeReferences}};
|
use petgraph::{EdgeDirection, graph::{DiGraph, NodeIndex}, visit::{Dfs, IntoNodeReferences}};
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
|
load_config();
|
||||||
let sink = Arc::new(Mutex::new(Vec::new()));
|
let sink = Arc::new(Mutex::new(Vec::new()));
|
||||||
std::io::set_output_capture(Some(sink.clone()));
|
std::io::set_output_capture(Some(sink.clone()));
|
||||||
let result = std::panic::catch_unwind(|| {
|
let result = std::panic::catch_unwind(|| {
|
||||||
@ -238,6 +242,40 @@ fn show_listing(mailbox: &str) -> Result<()> {
|
|||||||
.child(mail_content_resized);
|
.child(mail_content_resized);
|
||||||
siv.add_fullscreen_layer(ResizedView::with_full_screen(main));
|
siv.add_fullscreen_layer(ResizedView::with_full_screen(main));
|
||||||
|
|
||||||
|
let mut setup = LinearLayout::vertical();
|
||||||
|
{
|
||||||
|
let config = CONFIG.get().unwrap().read();
|
||||||
|
let show_email_addresses = Checkbox::new()
|
||||||
|
.with_checked(config.browse.show_email_addresses)
|
||||||
|
.on_change(|_siv, checked| {
|
||||||
|
CONFIG.get().unwrap().write().browse.show_email_addresses = checked;
|
||||||
|
});
|
||||||
|
setup.add_child(
|
||||||
|
LinearLayout::horizontal()
|
||||||
|
.child(show_email_addresses)
|
||||||
|
.child(TextView::new(" Show email addresses"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// most horrible hack
|
||||||
|
let setup: Arc<RwLock<Option<Box<dyn View>>>> = Arc::new(RwLock::new(Some(Box::new(ResizedView::new(SizeConstraint::Full, SizeConstraint::Full, setup)))));
|
||||||
|
let setup2 = Arc::clone(&setup);
|
||||||
|
let setup_view: ResizedView<LinearLayout> = *setup.write().take().unwrap().as_boxed_any().downcast().unwrap();
|
||||||
|
let setup_view = OnEventView::new(setup_view)
|
||||||
|
.on_event(Event::Key(Key::F10), move |s| {
|
||||||
|
let setup = s.pop_layer().unwrap();
|
||||||
|
*setup2.write() = Some(setup);
|
||||||
|
if let Err(e) = CONFIG.get().unwrap().read().save() {
|
||||||
|
error!("failed to save config {:?}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
*setup.write() = Some(Box::new(setup_view));
|
||||||
|
|
||||||
|
let setup2 = Arc::clone(&setup);
|
||||||
|
siv.add_global_callback(Event::Key(Key::F2), move |s| {
|
||||||
|
let setup = setup2.write().take().unwrap();
|
||||||
|
s.add_fullscreen_layer(setup);
|
||||||
|
});
|
||||||
|
|
||||||
siv.add_global_callback('q', |s| s.quit());
|
siv.add_global_callback('q', |s| s.quit());
|
||||||
|
|
||||||
siv.run();
|
siv.run();
|
||||||
|
120
src/lib.rs
120
src/lib.rs
@ -1,15 +1,20 @@
|
|||||||
use std::{borrow::Cow, cmp, 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, path::PathBuf};
|
||||||
|
|
||||||
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::{theme::Style, utils::span::{IndexedCow, IndexedSpan, SpannedString}};
|
||||||
use cursive_tree_view::TreeEntry;
|
use cursive_tree_view::TreeEntry;
|
||||||
|
use directories_next::ProjectDirs;
|
||||||
use imap::{Session, types::Flag};
|
use imap::{Session, types::Flag};
|
||||||
|
use log::info;
|
||||||
use maildir::{MailEntry, Maildir};
|
use maildir::{MailEntry, Maildir};
|
||||||
use mailparse::{MailHeaderMap, ParsedMail, SingleInfo, addrparse, dateparse};
|
use mailparse::{MailHeaderMap, ParsedMail, SingleInfo, addrparse, dateparse};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use parking_lot::RwLock;
|
||||||
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}};
|
||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||||
pub type ImapSession = Session<StreamOwned<ClientSession, TcpStream>>;
|
pub type ImapSession = Session<StreamOwned<ClientSession, TcpStream>>;
|
||||||
@ -107,14 +112,14 @@ pub struct EasyMail<'a> {
|
|||||||
impl EasyMail<'_> {
|
impl EasyMail<'_> {
|
||||||
pub fn new_pseudo(subject: String) -> Self {
|
pub fn new_pseudo(subject: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
mail: None,
|
mail: None,
|
||||||
id: MaildirID::new(0, 0),
|
id: MaildirID::new(0, 0),
|
||||||
flags: "S".to_owned(),
|
flags: "S".to_owned(),
|
||||||
from: SingleInfo {
|
from: SingleInfo {
|
||||||
display_name: None,
|
display_name: None,
|
||||||
addr: String::new()
|
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()
|
||||||
}
|
}
|
||||||
@ -125,7 +130,13 @@ impl EasyMail<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn from(&self) -> String {
|
pub fn from(&self) -> String {
|
||||||
self.from.display_name.as_deref().unwrap_or_default().to_owned()
|
let name = self.from.display_name.as_deref().unwrap_or_default();
|
||||||
|
if let Some(config) = CONFIG.get() {
|
||||||
|
if config.read().browse.show_email_addresses {
|
||||||
|
return format!("{} <{}>", name, self.from.addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name.to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_header(&self, header: &str) -> String {
|
pub fn get_header(&self, header: &str) -> String {
|
||||||
@ -138,32 +149,32 @@ impl EasyMail<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for EasyMail<'_> {
|
impl Debug for EasyMail<'_> {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "Mail[ID={},Subject={:?}]", self.id.uid, self.subject)
|
write!(f, "Mail[ID={},Subject={:?}]", self.id.uid, self.subject)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for EasyMail<'_> {
|
impl Display for EasyMail<'_> {
|
||||||
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.subject)
|
write!(f, "{}", self.subject)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for EasyMail<'_> {
|
impl PartialEq for EasyMail<'_> {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.id == other.id && self.from == other.from && self.subject == other.subject
|
self.id == other.id && self.from == other.from && self.subject == other.subject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Eq for EasyMail<'_> {}
|
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.display_name.hash(state);
|
self.from.display_name.hash(state);
|
||||||
self.from.addr.hash(state);
|
self.from.addr.hash(state);
|
||||||
self.subject.hash(state);
|
self.subject.hash(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Deref for EasyMail<'a> {
|
impl<'a> Deref for EasyMail<'a> {
|
||||||
@ -387,3 +398,80 @@ pub fn get_imap_session() -> Result<ImapSession> {
|
|||||||
let port = 993;
|
let port = 993;
|
||||||
connect(&host, port, &user, &password)
|
connect(&host, port, &user, &password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn load_config() {
|
||||||
|
CONFIG.get_or_init(|| {
|
||||||
|
let config = Config::load_from_fs();
|
||||||
|
let cfg = match config {
|
||||||
|
Ok(config) => if let Some(config) = config {
|
||||||
|
config.into()
|
||||||
|
} else {
|
||||||
|
Config::default().into()
|
||||||
|
},
|
||||||
|
Err(e) => panic!("failed to load configuration: {:?}", e)
|
||||||
|
};
|
||||||
|
info!("config {:?}", cfg);
|
||||||
|
cfg
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static CONFIG: OnceCell<RwLock<Config>> = OnceCell::new();
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
pub struct Config {
|
||||||
|
#[serde(default)]
|
||||||
|
pub browse: Browse
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_paths() -> Result<ProjectDirs> {
|
||||||
|
Ok(directories_next::ProjectDirs::from("", "", "Inboxid").context("unable to determine configuration directory")?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_config_path() -> Result<PathBuf> {
|
||||||
|
let paths = get_paths()?;
|
||||||
|
Ok(paths.config_dir().join("config.toml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
fn load_from_fs() -> Result<Option<Self>> {
|
||||||
|
let config = get_config_path()?;
|
||||||
|
if config.exists() {
|
||||||
|
let content = fs::read_to_string(&config)?;
|
||||||
|
Ok(Some(toml::from_str(&content)?))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self) -> Result<()> {
|
||||||
|
let config = get_config_path()?;
|
||||||
|
fs::create_dir_all(config.parent().unwrap())?;
|
||||||
|
fs::write(config, toml::to_string(&self)?)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Config {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
browse: Browse {
|
||||||
|
show_email_addresses: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct Browse {
|
||||||
|
#[serde(default)]
|
||||||
|
pub show_email_addresses: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Browse {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
show_email_addresses: Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user