authentication works for the most part
This commit is contained in:
parent
2b97aca995
commit
8be5d65435
12 changed files with 165 additions and 109 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -781,6 +781,7 @@ version = "0.0.1"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_matches",
|
||||
"async-trait",
|
||||
"derive_builder",
|
||||
"futures",
|
||||
"log",
|
||||
|
|
|
@ -13,6 +13,7 @@ maintenance = { status = "passively-maintained" }
|
|||
|
||||
[dependencies]
|
||||
anyhow = "1.0.38"
|
||||
async-trait = "0.1.42"
|
||||
derive_builder = "0.9.0"
|
||||
futures = "0.3.12"
|
||||
log = "0.4.14"
|
||||
|
|
48
imap/src/client/auth.rs
Normal file
48
imap/src/client/auth.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use anyhow::Result;
|
||||
|
||||
use crate::command::Command;
|
||||
use crate::response::{Response, Status};
|
||||
|
||||
use super::{ClientAuthenticated, ClientUnauthenticated};
|
||||
|
||||
#[async_trait]
|
||||
pub trait Auth {
|
||||
/// Performs authentication, consuming the client
|
||||
// TODO: return the unauthed client if failed?
|
||||
async fn perform_auth(self, client: ClientUnauthenticated) -> Result<ClientAuthenticated>;
|
||||
|
||||
fn convert_client(client: ClientUnauthenticated) -> ClientAuthenticated {
|
||||
match client {
|
||||
ClientUnauthenticated::Encrypted(e) => ClientAuthenticated::Encrypted(e),
|
||||
ClientUnauthenticated::Unencrypted(e) => ClientAuthenticated::Unencrypted(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Plain {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Auth for Plain {
|
||||
async fn perform_auth(self, mut client: ClientUnauthenticated) -> Result<ClientAuthenticated> {
|
||||
let command = Command::Login {
|
||||
username: self.username,
|
||||
password: self.password,
|
||||
};
|
||||
let (result, _) = client.execute(command).await?;
|
||||
|
||||
if !matches!(
|
||||
result,
|
||||
Response::Done {
|
||||
status: Status::Ok,
|
||||
..
|
||||
}
|
||||
) {
|
||||
bail!("unable to login: {:?}", result);
|
||||
}
|
||||
|
||||
Ok(<Self as Auth>::convert_client(client))
|
||||
}
|
||||
}
|
|
@ -116,7 +116,7 @@ where
|
|||
/// Executes the CAPABILITY command
|
||||
pub async fn capabilities(&mut self, force: bool) -> Result<()> {
|
||||
{
|
||||
let caps = &*self.caps.read();
|
||||
let caps = self.caps.read();
|
||||
if caps.is_some() && !force {
|
||||
return Ok(());
|
||||
}
|
||||
|
@ -134,13 +134,10 @@ where
|
|||
.iter()
|
||||
.find(|resp| matches!(resp, Response::Capabilities(_)))
|
||||
{
|
||||
let caps = &mut *self.caps.write();
|
||||
let mut caps = self.caps.write();
|
||||
*caps = Some(new_caps.iter().cloned().collect());
|
||||
}
|
||||
|
||||
// if let Response::Capabilities(caps) = resp {
|
||||
// debug!("capabilities: {:?}", caps);
|
||||
// }
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -183,7 +180,7 @@ where
|
|||
let cap = parse_capability(cap)?;
|
||||
|
||||
self.capabilities(false).await?;
|
||||
let caps = &*self.caps.read();
|
||||
let caps = self.caps.read();
|
||||
// TODO: refresh caps
|
||||
|
||||
let caps = caps.as_ref().unwrap();
|
||||
|
@ -276,6 +273,7 @@ where
|
|||
debug!("got a new line {:?}", next_line);
|
||||
let resp = parse_response(next_line)?;
|
||||
|
||||
// if this is the very first message, treat it as a greeting
|
||||
if let Some(greeting) = greeting.take() {
|
||||
let (greeting, waker) = &mut *greeting.write();
|
||||
debug!("received greeting!");
|
||||
|
@ -285,19 +283,31 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
match &resp {
|
||||
// capabilities list
|
||||
Response::Capabilities(new_caps)
|
||||
// update capabilities list
|
||||
// TODO: probably not really necessary here (done somewhere else)?
|
||||
if let Response::Capabilities(new_caps)
|
||||
| Response::Data {
|
||||
status: Status::Ok,
|
||||
code: Some(ResponseCode::Capabilities(new_caps)),
|
||||
..
|
||||
} => {
|
||||
} = &resp
|
||||
{
|
||||
let caps = &mut *caps.write();
|
||||
*caps = Some(new_caps.iter().cloned().collect());
|
||||
debug!("new caps: {:?}", caps);
|
||||
}
|
||||
|
||||
match &resp {
|
||||
Response::Data {
|
||||
status: Status::Ok, ..
|
||||
} => {
|
||||
let mut results = results.write();
|
||||
if let Some((_, _, intermediate, _)) = results.iter_mut().next() {
|
||||
debug!("pushed to intermediate: {:?}", resp);
|
||||
intermediate.push(resp);
|
||||
}
|
||||
}
|
||||
|
||||
// bye
|
||||
Response::Data {
|
||||
status: Status::Bye,
|
||||
|
@ -308,7 +318,7 @@ where
|
|||
|
||||
Response::Done { tag, .. } => {
|
||||
if tag.starts_with(TAG_PREFIX) {
|
||||
let id = tag.trim_start_matches(TAG_PREFIX).parse::<usize>()?;
|
||||
// let id = tag.trim_start_matches(TAG_PREFIX).parse::<usize>()?;
|
||||
let mut results = results.write();
|
||||
if let Some((_, opt, _, waker)) = results.iter_mut().next() {
|
||||
*opt = Some(resp);
|
||||
|
@ -321,38 +331,6 @@ where
|
|||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// debug!("parsed as: {:?}", resp);
|
||||
// let next_line = next_line.trim_end_matches('\n').trim_end_matches('\r');
|
||||
|
||||
// let mut parts = next_line.split(" ");
|
||||
// let tag = parts.next().unwrap();
|
||||
// let rest = parts.collect::<Vec<_>>().join(" ");
|
||||
|
||||
// if tag == "*" {
|
||||
// debug!("UNTAGGED {:?}", rest);
|
||||
|
||||
// // TODO: verify that the greeting is actually an OK
|
||||
// if let Some(greeting) = greeting.take() {
|
||||
// let (greeting, waker) = &mut *greeting.write();
|
||||
// debug!("got greeting");
|
||||
// *greeting = true;
|
||||
// if let Some(waker) = waker.take() {
|
||||
// waker.wake();
|
||||
// }
|
||||
// }
|
||||
// } else if tag.starts_with(TAG_PREFIX) {
|
||||
// let id = tag.trim_start_matches(TAG_PREFIX).parse::<usize>()?;
|
||||
// debug!("set {} to {:?}", id, rest);
|
||||
// let mut results = results.write();
|
||||
// if let Some((c, w)) = results.get_mut(&id) {
|
||||
// // *c = Some(rest.to_string());
|
||||
// *c = Some(resp);
|
||||
// if let Some(waker) = w.take() {
|
||||
// waker.wake();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
Either::Right((_, _)) => {
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
//! # }
|
||||
//! ```
|
||||
|
||||
pub mod auth;
|
||||
mod inner;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
@ -89,22 +90,18 @@ impl ClientConfig {
|
|||
|
||||
let inner = Client::new(conn, self);
|
||||
inner.wait_for_greeting().await;
|
||||
return Ok(ClientUnauthenticated::Encrypted(
|
||||
ClientUnauthenticatedEncrypted { inner },
|
||||
));
|
||||
return Ok(ClientUnauthenticated::Encrypted(inner));
|
||||
} else {
|
||||
let inner = Client::new(conn, self);
|
||||
inner.wait_for_greeting().await;
|
||||
return Ok(ClientUnauthenticated::Unencrypted(
|
||||
ClientUnauthenticatedUnencrypted { inner },
|
||||
));
|
||||
return Ok(ClientUnauthenticated::Unencrypted(inner));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ClientUnauthenticated {
|
||||
Encrypted(ClientUnauthenticatedEncrypted),
|
||||
Unencrypted(ClientUnauthenticatedUnencrypted),
|
||||
Encrypted(Client<TlsStream<TcpStream>>),
|
||||
Unencrypted(Client<TcpStream>),
|
||||
}
|
||||
|
||||
impl ClientUnauthenticated {
|
||||
|
@ -113,44 +110,46 @@ impl ClientUnauthenticated {
|
|||
// this is a no-op, we don't need to upgrade
|
||||
ClientUnauthenticated::Encrypted(_) => Ok(self),
|
||||
ClientUnauthenticated::Unencrypted(e) => {
|
||||
let client = ClientUnauthenticatedEncrypted {
|
||||
inner: e.inner.upgrade().await?,
|
||||
};
|
||||
Ok(ClientUnauthenticated::Encrypted(client))
|
||||
Ok(ClientUnauthenticated::Encrypted(e.upgrade().await?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// TODO: Exposing low-level execute , shoudl remove later
|
||||
pub async fn execute(&mut self, cmd: Command) -> Result<(Response, Vec<Response>)> {
|
||||
/// Exposing low-level execute
|
||||
async fn execute(&mut self, cmd: Command) -> Result<(Response, Vec<Response>)> {
|
||||
match self {
|
||||
ClientUnauthenticated::Encrypted(e) => e.inner.execute(cmd).await,
|
||||
ClientUnauthenticated::Unencrypted(e) => e.inner.execute(cmd).await,
|
||||
ClientUnauthenticated::Encrypted(e) => e.execute(cmd).await,
|
||||
ClientUnauthenticated::Unencrypted(e) => e.execute(cmd).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the server that the client is talking to has support for the given capability.
|
||||
pub async fn has_capability(&mut self, cap: impl AsRef<str>) -> Result<bool> {
|
||||
match self {
|
||||
ClientUnauthenticated::Encrypted(e) => e.inner.has_capability(cap).await,
|
||||
ClientUnauthenticated::Unencrypted(e) => e.inner.has_capability(cap).await,
|
||||
ClientUnauthenticated::Encrypted(e) => e.has_capability(cap).await,
|
||||
ClientUnauthenticated::Unencrypted(e) => e.has_capability(cap).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClientUnauthenticatedUnencrypted {
|
||||
/// Connection to the remote server
|
||||
inner: Client<TcpStream>,
|
||||
pub enum ClientAuthenticated {
|
||||
Encrypted(Client<TlsStream<TcpStream>>),
|
||||
Unencrypted(Client<TcpStream>),
|
||||
}
|
||||
|
||||
impl ClientUnauthenticatedUnencrypted {
|
||||
pub async fn upgrade(&self) {}
|
||||
impl ClientAuthenticated {
|
||||
/// Exposing low-level execute
|
||||
async fn execute(&mut self, cmd: Command) -> Result<(Response, Vec<Response>)> {
|
||||
match self {
|
||||
ClientAuthenticated::Encrypted(e) => e.execute(cmd).await,
|
||||
ClientAuthenticated::Unencrypted(e) => e.execute(cmd).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// An IMAP client that isn't authenticated.
|
||||
pub struct ClientUnauthenticatedEncrypted {
|
||||
/// Connection to the remote server
|
||||
inner: Client<TlsStream<TcpStream>>,
|
||||
pub async fn list(&mut self) -> Result<()> {
|
||||
let cmd = Command::List { reference: "".to_owned(), mailbox: "*".to_owned() };
|
||||
let resp = self.execute(cmd).await?;
|
||||
debug!("list response: {:?}", resp);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientUnauthenticatedEncrypted {}
|
||||
|
|
|
@ -6,14 +6,19 @@ pub enum Command {
|
|||
Capability,
|
||||
Starttls,
|
||||
Login { username: String, password: String },
|
||||
Select { mailbox: String },
|
||||
List { reference: String, mailbox: String },
|
||||
}
|
||||
|
||||
impl fmt::Display for Command {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use Command::*;
|
||||
match self {
|
||||
Command::Capability => write!(f, "CAPABILITY"),
|
||||
Command::Starttls => write!(f, "STARTTLS"),
|
||||
Command::Login { username, password } => write!(f, "LOGIN {} {}", username, password),
|
||||
Capability => write!(f, "CAPABILITY"),
|
||||
Starttls => write!(f, "STARTTLS"),
|
||||
Login { username, password } => write!(f, "LOGIN {} {}", username, password),
|
||||
Select { mailbox } => write!(f, "SELECT {}", mailbox),
|
||||
List { reference, mailbox } => write!(f, "LIST {:?} {:?}", reference, mailbox),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
#[macro_use]
|
||||
extern crate anyhow;
|
||||
#[macro_use]
|
||||
extern crate async_trait;
|
||||
#[macro_use]
|
||||
extern crate derive_builder;
|
||||
#[macro_use]
|
||||
extern crate futures;
|
||||
|
|
|
@ -104,9 +104,6 @@ fn build_resp_code(pair: Pair<Rule>) -> Option<ResponseCode> {
|
|||
unreachable!("{:#?}", pair);
|
||||
}
|
||||
|
||||
// panic!("pair: {:#?}", pair);
|
||||
debug!("pair: {:#?}", pair);
|
||||
|
||||
let mut pairs = pair.into_inner();
|
||||
let pair = pairs.next()?;
|
||||
Some(match pair.as_rule() {
|
||||
|
@ -155,7 +152,7 @@ fn build_status(pair: Pair<Rule>) -> Status {
|
|||
}
|
||||
}
|
||||
|
||||
fn build_flag_list(pair: Pair<Rule>) -> Vec<Flag> {
|
||||
fn build_flag_list(pair: Pair<Rule>) -> Vec<MailboxFlag> {
|
||||
if !matches!(pair.as_rule(), Rule::flag_list) {
|
||||
unreachable!("{:#?}", pair);
|
||||
}
|
||||
|
@ -163,18 +160,18 @@ fn build_flag_list(pair: Pair<Rule>) -> Vec<Flag> {
|
|||
pair.into_inner().map(build_flag).collect()
|
||||
}
|
||||
|
||||
fn build_flag(pair: Pair<Rule>) -> Flag {
|
||||
fn build_flag(pair: Pair<Rule>) -> MailboxFlag {
|
||||
if !matches!(pair.as_rule(), Rule::flag) {
|
||||
unreachable!("{:#?}", pair);
|
||||
}
|
||||
|
||||
match pair.as_str() {
|
||||
"\\Answered" => Flag::Answered,
|
||||
"\\Flagged" => Flag::Flagged,
|
||||
"\\Deleted" => Flag::Deleted,
|
||||
"\\Seen" => Flag::Seen,
|
||||
"\\Draft" => Flag::Draft,
|
||||
s if s.starts_with("\\") => Flag::Ext(s.to_owned()),
|
||||
"\\Answered" => MailboxFlag::Answered,
|
||||
"\\Flagged" => MailboxFlag::Flagged,
|
||||
"\\Deleted" => MailboxFlag::Deleted,
|
||||
"\\Seen" => MailboxFlag::Seen,
|
||||
"\\Draft" => MailboxFlag::Draft,
|
||||
s if s.starts_with("\\") => MailboxFlag::Ext(s.to_owned()),
|
||||
_ => unreachable!("{:#?}", pair.as_str()),
|
||||
}
|
||||
}
|
||||
|
@ -195,10 +192,28 @@ fn build_mailbox_data(pair: Pair<Rule>) -> MailboxData {
|
|||
MailboxData::Flags(flags)
|
||||
}
|
||||
Rule::mailbox_data_recent => MailboxData::Recent(build_number(pair)),
|
||||
Rule::mailbox_data_list => {
|
||||
let mut pairs = pair.into_inner();
|
||||
let pair = pairs.next().unwrap();
|
||||
let (flags, delimiter, name) = build_mailbox_list(pair);
|
||||
MailboxData::List { flags, delimiter, name }
|
||||
},
|
||||
_ => unreachable!("{:#?}", pair),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_mailbox_list(pair: Pair<Rule>) -> (Vec<String>, Option<String>, String) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn build_mbx_list_flags(pair: Pair<Rule>) -> Vec<String> {
|
||||
if !matches!(pair.as_rule(), Rule::mbx_list_flags) {
|
||||
unreachable!("{:#?}", pair);
|
||||
}
|
||||
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn build_number<T>(pair: Pair<Rule>) -> T
|
||||
where
|
||||
T: FromStr,
|
||||
|
@ -266,11 +281,11 @@ mod tests {
|
|||
assert_eq!(
|
||||
parse_response("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n"),
|
||||
Ok(Response::MailboxData(MailboxData::Flags(vec![
|
||||
Flag::Answered,
|
||||
Flag::Flagged,
|
||||
Flag::Deleted,
|
||||
Flag::Seen,
|
||||
Flag::Draft,
|
||||
MailboxFlag::Answered,
|
||||
MailboxFlag::Flagged,
|
||||
MailboxFlag::Deleted,
|
||||
MailboxFlag::Seen,
|
||||
MailboxFlag::Draft,
|
||||
])))
|
||||
);
|
||||
|
||||
|
|
|
@ -64,11 +64,13 @@ header_list = { "(" ~ header_fld_name ~ (sp ~ header_fld_name)* ~ ")" }
|
|||
list_wildcards = @{ "%" | "*" }
|
||||
literal = @{ "{" ~ number ~ "}" ~ crlf ~ char8* }
|
||||
mailbox = { ^"INBOX" | astring }
|
||||
mailbox_data = { mailbox_data_flags | (^"LIST" ~ sp ~ mailbox_list) | (^"LSUB" ~ sp ~ mailbox_list) | (^"SEARCH" ~ (sp ~ nz_number)*) | (^"STATUS" ~ sp ~ mailbox ~ sp ~ ^"(" ~ status_att_list? ~ ^")") | mailbox_data_exists | mailbox_data_recent }
|
||||
mailbox_data = { mailbox_data_flags | mailbox_data_list | (^"LSUB" ~ sp ~ mailbox_list) | (^"SEARCH" ~ (sp ~ nz_number)*) | (^"STATUS" ~ sp ~ mailbox ~ sp ~ ^"(" ~ status_att_list? ~ ^")") | mailbox_data_exists | mailbox_data_recent }
|
||||
mailbox_data_exists = { number ~ sp ~ ^"EXISTS" }
|
||||
mailbox_data_flags = { ^"FLAGS" ~ sp ~ flag_list }
|
||||
mailbox_data_recent = { number ~ sp ~ ^"RECENT" }
|
||||
mailbox_list = { "(" ~ mbx_list_flags* ~ ")" ~ sp ~ (dquote ~ quoted_char ~ dquote | nil) ~ sp ~ mailbox }
|
||||
mailbox_data_list = { ^"LIST" ~ sp ~ mailbox_list }
|
||||
mailbox_list = { "(" ~ mbx_list_flags* ~ ")" ~ sp ~ mailbox_list_string ~ sp ~ mailbox }
|
||||
mailbox_list_string = { dquote ~ quoted_char ~ dquote | nil }
|
||||
mbx_list_flags = { (mbx_list_oflag ~ sp)* ~ mbx_list_sflag ~ (sp ~ mbx_list_oflag)* | mbx_list_oflag ~ (sp ~ mbx_list_oflag)* }
|
||||
mbx_list_oflag = { "\\NoInferiors" | flag_extension }
|
||||
mbx_list_sflag = { "\\NoSelect" | "\\Marked" | "\\Unmarked" }
|
||||
|
|
|
@ -65,7 +65,7 @@ pub enum AttributeValue {}
|
|||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum MailboxData {
|
||||
Exists(u32),
|
||||
Flags(Vec<Flag>),
|
||||
Flags(Vec<MailboxFlag>),
|
||||
List {
|
||||
flags: Vec<String>,
|
||||
delimiter: Option<String>,
|
||||
|
@ -88,7 +88,7 @@ pub enum MailboxData {
|
|||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub enum Flag {
|
||||
pub enum MailboxFlag {
|
||||
Answered,
|
||||
Flagged,
|
||||
Deleted,
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
use anyhow::Result;
|
||||
use futures::{future::FutureExt, stream::StreamExt};
|
||||
use panorama_imap::{
|
||||
client::{ClientBuilder, ClientConfig},
|
||||
client::{ClientBuilder, ClientConfig, auth::{self, Auth}},
|
||||
command::Command as ImapCommand,
|
||||
};
|
||||
use tokio::{sync::mpsc::UnboundedReceiver, task::JoinHandle};
|
||||
|
@ -78,7 +78,7 @@ async fn imap_main(acct: MailAccountConfig) -> Result<()> {
|
|||
debug!("connecting to {}:{}", &acct.imap.server, acct.imap.port);
|
||||
let unauth = builder.open().await?;
|
||||
|
||||
let mut unauth = if matches!(acct.imap.tls, TlsMethod::Starttls) {
|
||||
let unauth = if matches!(acct.imap.tls, TlsMethod::Starttls) {
|
||||
debug!("attempting to upgrade");
|
||||
let client = unauth.upgrade().await?;
|
||||
debug!("upgrade successful");
|
||||
|
@ -89,18 +89,21 @@ async fn imap_main(acct: MailAccountConfig) -> Result<()> {
|
|||
|
||||
debug!("preparing to auth");
|
||||
// check if the authentication method is supported
|
||||
let authed = match acct.imap.auth {
|
||||
let mut authed = match acct.imap.auth {
|
||||
ImapAuth::Plain { username, password } => {
|
||||
let ok = unauth.has_capability("AUTH=PLAIN").await?;
|
||||
let res = unauth.execute(ImapCommand::Login { username, password }).await?;
|
||||
debug!("res: {:?}", res);
|
||||
let auth = auth::Plain {username, password};
|
||||
auth.perform_auth(unauth).await?
|
||||
}
|
||||
};
|
||||
|
||||
debug!("authentication successful!");
|
||||
|
||||
// debug!("sending CAPABILITY");
|
||||
// let result = unauth.capabilities().await?;
|
||||
|
||||
loop {
|
||||
debug!("listing all emails...");
|
||||
authed.list().await?;
|
||||
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
|
||||
debug!("heartbeat");
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ struct Opt {
|
|||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() -> Result<()> {
|
||||
let now = chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]");
|
||||
|
||||
// parse command line arguments into options struct
|
||||
let opt = Opt::from_args();
|
||||
|
||||
|
@ -33,8 +35,8 @@ async fn main() -> Result<()> {
|
|||
let mut logger = fern::Dispatch::new()
|
||||
.format(move |out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"[{}][{}] {}",
|
||||
// chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
|
||||
"{}[{}][{}] {}",
|
||||
now,
|
||||
record.target(),
|
||||
colors.color(record.level()),
|
||||
message
|
||||
|
|
Loading…
Reference in a new issue