imap: add managesieve connection
So far only the connection is implemented, and using the testing/manage_sieve binary you can get a shell to a managesieve server. The managesieve interface will be used in the UI from a plugin, but the plugin's interface isn't implemented yet.memfd
parent
63467a3c45
commit
6079909f9c
|
@ -32,6 +32,7 @@ mod connection;
|
||||||
pub use connection::*;
|
pub use connection::*;
|
||||||
mod watch;
|
mod watch;
|
||||||
pub use watch::*;
|
pub use watch::*;
|
||||||
|
pub mod managesieve;
|
||||||
|
|
||||||
use crate::async_workers::{Async, AsyncBuilder, AsyncStatus, WorkContext};
|
use crate::async_workers::{Async, AsyncBuilder, AsyncStatus, WorkContext};
|
||||||
use crate::backends::BackendOp;
|
use crate::backends::BackendOp;
|
||||||
|
@ -69,6 +70,7 @@ pub struct ImapServerConf {
|
||||||
pub server_port: u16,
|
pub server_port: u16,
|
||||||
pub use_starttls: bool,
|
pub use_starttls: bool,
|
||||||
pub danger_accept_invalid_certs: bool,
|
pub danger_accept_invalid_certs: bool,
|
||||||
|
pub protocol: ImapProtocol,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct IsSubscribedFn(Box<dyn Fn(&str) -> bool + Send + Sync>);
|
struct IsSubscribedFn(Box<dyn Fn(&str) -> bool + Send + Sync>);
|
||||||
|
@ -87,6 +89,7 @@ impl std::ops::Deref for IsSubscribedFn {
|
||||||
}
|
}
|
||||||
type Capabilities = FnvHashSet<Vec<u8>>;
|
type Capabilities = FnvHashSet<Vec<u8>>;
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
macro_rules! get_conf_val {
|
macro_rules! get_conf_val {
|
||||||
($s:ident[$var:literal]) => {
|
($s:ident[$var:literal]) => {
|
||||||
$s.extra.get($var).ok_or_else(|| {
|
$s.extra.get($var).ok_or_else(|| {
|
||||||
|
@ -697,6 +700,7 @@ impl ImapType {
|
||||||
server_port,
|
server_port,
|
||||||
use_starttls,
|
use_starttls,
|
||||||
danger_accept_invalid_certs,
|
danger_accept_invalid_certs,
|
||||||
|
protocol: ImapProtocol::IMAP,
|
||||||
};
|
};
|
||||||
let online = Arc::new(Mutex::new((
|
let online = Arc::new(Mutex::new((
|
||||||
Instant::now(),
|
Instant::now(),
|
||||||
|
|
|
@ -35,10 +35,17 @@ use std::time::Instant;
|
||||||
use super::protocol_parser;
|
use super::protocol_parser;
|
||||||
use super::{Capabilities, ImapServerConf};
|
use super::{Capabilities, ImapServerConf};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum ImapProtocol {
|
||||||
|
IMAP,
|
||||||
|
ManageSieve,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ImapStream {
|
pub struct ImapStream {
|
||||||
cmd_id: usize,
|
cmd_id: usize,
|
||||||
stream: native_tls::TlsStream<std::net::TcpStream>,
|
stream: native_tls::TlsStream<std::net::TcpStream>,
|
||||||
|
protocol: ImapProtocol,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -79,11 +86,19 @@ impl ImapStream {
|
||||||
let mut socket = TcpStream::connect(&addr)?;
|
let mut socket = TcpStream::connect(&addr)?;
|
||||||
socket.set_read_timeout(Some(std::time::Duration::new(4, 0)))?;
|
socket.set_read_timeout(Some(std::time::Duration::new(4, 0)))?;
|
||||||
socket.set_write_timeout(Some(std::time::Duration::new(4, 0)))?;
|
socket.set_write_timeout(Some(std::time::Duration::new(4, 0)))?;
|
||||||
let cmd_id = 0;
|
let cmd_id = 1;
|
||||||
if server_conf.use_starttls {
|
if server_conf.use_starttls {
|
||||||
socket.write_all(format!("M{} STARTTLS\r\n", cmd_id).as_bytes())?;
|
|
||||||
|
|
||||||
let mut buf = vec![0; 1024];
|
let mut buf = vec![0; 1024];
|
||||||
|
match server_conf.protocol {
|
||||||
|
ImapProtocol::IMAP => {
|
||||||
|
socket.write_all(format!("M{} STARTTLS\r\n", cmd_id).as_bytes())?
|
||||||
|
}
|
||||||
|
ImapProtocol::ManageSieve => {
|
||||||
|
let len = socket.read(&mut buf)?;
|
||||||
|
debug!(unsafe { std::str::from_utf8_unchecked(&buf[0..len]) });
|
||||||
|
debug!(socket.write_all(b"STARTTLS\r\n")?);
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut response = String::with_capacity(1024);
|
let mut response = String::with_capacity(1024);
|
||||||
let mut broken = false;
|
let mut broken = false;
|
||||||
let now = std::time::Instant::now();
|
let now = std::time::Instant::now();
|
||||||
|
@ -91,12 +106,23 @@ impl ImapStream {
|
||||||
while now.elapsed().as_secs() < 3 {
|
while now.elapsed().as_secs() < 3 {
|
||||||
let len = socket.read(&mut buf)?;
|
let len = socket.read(&mut buf)?;
|
||||||
response.push_str(unsafe { std::str::from_utf8_unchecked(&buf[0..len]) });
|
response.push_str(unsafe { std::str::from_utf8_unchecked(&buf[0..len]) });
|
||||||
if response.starts_with("* OK ") && response.find("\r\n").is_some() {
|
match server_conf.protocol {
|
||||||
if let Some(pos) = response.as_bytes().find(b"\r\n") {
|
ImapProtocol::IMAP => {
|
||||||
response.drain(0..pos + 2);
|
if response.starts_with("* OK ") && response.find("\r\n").is_some() {
|
||||||
|
if let Some(pos) = response.as_bytes().find(b"\r\n") {
|
||||||
|
response.drain(0..pos + 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImapProtocol::ManageSieve => {
|
||||||
|
if response.starts_with("OK ") && response.find("\r\n").is_some() {
|
||||||
|
response.clear();
|
||||||
|
broken = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if response.starts_with("M0 OK") {
|
if response.starts_with("M1 OK") {
|
||||||
broken = true;
|
broken = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -134,7 +160,31 @@ impl ImapStream {
|
||||||
conn_result?
|
conn_result?
|
||||||
};
|
};
|
||||||
let mut res = String::with_capacity(8 * 1024);
|
let mut res = String::with_capacity(8 * 1024);
|
||||||
let mut ret = ImapStream { cmd_id, stream };
|
let mut ret = ImapStream {
|
||||||
|
cmd_id,
|
||||||
|
stream,
|
||||||
|
protocol: server_conf.protocol,
|
||||||
|
};
|
||||||
|
if let ImapProtocol::ManageSieve = server_conf.protocol {
|
||||||
|
use data_encoding::BASE64;
|
||||||
|
ret.read_response(&mut res)?;
|
||||||
|
ret.send_command(
|
||||||
|
format!(
|
||||||
|
"AUTHENTICATE \"PLAIN\" \"{}\"",
|
||||||
|
BASE64.encode(
|
||||||
|
format!(
|
||||||
|
"\0{}\0{}",
|
||||||
|
&server_conf.server_username, &server_conf.server_password
|
||||||
|
)
|
||||||
|
.as_bytes()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.as_bytes(),
|
||||||
|
)?;
|
||||||
|
ret.read_response(&mut res)?;
|
||||||
|
return Ok((Default::default(), ret));
|
||||||
|
}
|
||||||
|
|
||||||
ret.send_command(b"CAPABILITY")?;
|
ret.send_command(b"CAPABILITY")?;
|
||||||
ret.read_response(&mut res)?;
|
ret.read_response(&mut res)?;
|
||||||
let capabilities: std::result::Result<Vec<&[u8]>, _> = res
|
let capabilities: std::result::Result<Vec<&[u8]>, _> = res
|
||||||
|
@ -229,7 +279,10 @@ impl ImapStream {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_response(&mut self, ret: &mut String) -> Result<()> {
|
pub fn read_response(&mut self, ret: &mut String) -> Result<()> {
|
||||||
let id = format!("M{} ", self.cmd_id - 1);
|
let id = match self.protocol {
|
||||||
|
ImapProtocol::IMAP => format!("M{} ", self.cmd_id - 1),
|
||||||
|
ImapProtocol::ManageSieve => String::new(),
|
||||||
|
};
|
||||||
self.read_lines(ret, &id, true)
|
self.read_lines(ret, &id, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,15 +345,26 @@ impl ImapStream {
|
||||||
|
|
||||||
pub fn send_command(&mut self, command: &[u8]) -> Result<()> {
|
pub fn send_command(&mut self, command: &[u8]) -> Result<()> {
|
||||||
let command = command.trim();
|
let command = command.trim();
|
||||||
self.stream.write_all(b"M")?;
|
match self.protocol {
|
||||||
self.stream.write_all(self.cmd_id.to_string().as_bytes())?;
|
ImapProtocol::IMAP => {
|
||||||
self.stream.write_all(b" ")?;
|
self.stream.write_all(b"M")?;
|
||||||
self.cmd_id += 1;
|
self.stream.write_all(self.cmd_id.to_string().as_bytes())?;
|
||||||
|
self.stream.write_all(b" ")?;
|
||||||
|
self.cmd_id += 1;
|
||||||
|
}
|
||||||
|
ImapProtocol::ManageSieve => {}
|
||||||
|
}
|
||||||
|
|
||||||
self.stream.write_all(command)?;
|
self.stream.write_all(command)?;
|
||||||
self.stream.write_all(b"\r\n")?;
|
self.stream.write_all(b"\r\n")?;
|
||||||
debug!("sent: M{} {}", self.cmd_id - 1, unsafe {
|
match self.protocol {
|
||||||
std::str::from_utf8_unchecked(command)
|
ImapProtocol::IMAP => {
|
||||||
});
|
debug!("sent: M{} {}", self.cmd_id - 1, unsafe {
|
||||||
|
std::str::from_utf8_unchecked(command)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ImapProtocol::ManageSieve => {}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -365,14 +429,20 @@ impl ImapConnection {
|
||||||
|
|
||||||
pub fn read_response(&mut self, ret: &mut String) -> Result<()> {
|
pub fn read_response(&mut self, ret: &mut String) -> Result<()> {
|
||||||
self.try_send(|s| s.read_response(ret))?;
|
self.try_send(|s| s.read_response(ret))?;
|
||||||
let r: ImapResponse = ImapResponse::from(&ret);
|
|
||||||
if let ImapResponse::Bye(ref response_code) = r {
|
match self.server_conf.protocol {
|
||||||
self.stream = Err(MeliError::new(format!(
|
ImapProtocol::IMAP => {
|
||||||
"Offline: received BYE: {:?}",
|
let r: ImapResponse = ImapResponse::from(&ret);
|
||||||
response_code
|
if let ImapResponse::Bye(ref response_code) = r {
|
||||||
)));
|
self.stream = Err(MeliError::new(format!(
|
||||||
|
"Offline: received BYE: {:?}",
|
||||||
|
response_code
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
r.into()
|
||||||
|
}
|
||||||
|
ImapProtocol::ManageSieve => Ok(()),
|
||||||
}
|
}
|
||||||
r.into()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_lines(&mut self, ret: &mut String, termination_string: String) -> Result<()> {
|
pub fn read_lines(&mut self, ret: &mut String, termination_string: String) -> Result<()> {
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
* meli - managesieve
|
||||||
|
*
|
||||||
|
* 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 super::{ImapConnection, ImapProtocol, ImapServerConf};
|
||||||
|
use crate::conf::AccountSettings;
|
||||||
|
use crate::error::{MeliError, Result};
|
||||||
|
use crate::get_conf_val;
|
||||||
|
use nom::IResult;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
named!(
|
||||||
|
pub managesieve_capabilities<Vec<(&[u8], &[u8])>>,
|
||||||
|
do_parse!(
|
||||||
|
ret: separated_nonempty_list_complete!(tag!(b"\r\n"), alt_complete!(separated_pair!(quoted_raw, tag!(b" "), quoted_raw) | map!(quoted_raw, |q| (q, &b""[..]))))
|
||||||
|
>> opt!(tag!("\r\n"))
|
||||||
|
>> ({ ret })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_managesieve_capabilities() {
|
||||||
|
assert_eq!(managesieve_capabilities(b"\"IMPLEMENTATION\" \"Dovecot Pigeonhole\"\r\n\"SIEVE\" \"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext\"\r\n\"NOTIFY\" \"mailto\"\r\n\"SASL\" \"PLAIN\"\r\n\"STARTTLS\"\r\n\"VERSION\" \"1.0\"\r\n").to_full_result(), Ok(vec![
|
||||||
|
(&b"IMPLEMENTATION"[..],&b"Dovecot Pigeonhole"[..]),
|
||||||
|
(&b"SIEVE"[..],&b"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext"[..]),
|
||||||
|
(&b"NOTIFY"[..],&b"mailto"[..]),
|
||||||
|
(&b"SASL"[..],&b"PLAIN"[..]),
|
||||||
|
(&b"STARTTLS"[..], &b""[..]),
|
||||||
|
(&b"VERSION"[..],&b"1.0"[..])])
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a byte sequence surrounded by "s and decoded if necessary
|
||||||
|
pub fn quoted_raw(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||||
|
if input.is_empty() || input[0] != b'"' {
|
||||||
|
return IResult::Error(nom::ErrorKind::Custom(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut i = 1;
|
||||||
|
while i < input.len() {
|
||||||
|
if input[i] == b'\"' && input[i - 1] != b'\\' {
|
||||||
|
return IResult::Done(&input[i + 1..], &input[1..i]);
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IResult::Error(nom::ErrorKind::Custom(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ManageSieve {
|
||||||
|
fn havespace(&mut self) -> Result<()>;
|
||||||
|
fn putscript(&mut self) -> Result<()>;
|
||||||
|
|
||||||
|
fn listscripts(&mut self) -> Result<()>;
|
||||||
|
fn setactive(&mut self) -> Result<()>;
|
||||||
|
|
||||||
|
fn getscript(&mut self) -> Result<()>;
|
||||||
|
|
||||||
|
fn deletescript(&mut self) -> Result<()>;
|
||||||
|
fn renamescript(&mut self) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_managesieve_connection(s: &AccountSettings) -> Result<ImapConnection> {
|
||||||
|
let server_hostname = get_conf_val!(s["server_hostname"])?;
|
||||||
|
let server_username = get_conf_val!(s["server_username"])?;
|
||||||
|
let server_password = get_conf_val!(s["server_password"])?;
|
||||||
|
let server_port = get_conf_val!(s["server_port"], 4190)?;
|
||||||
|
let danger_accept_invalid_certs: bool = get_conf_val!(s["danger_accept_invalid_certs"], false)?;
|
||||||
|
let server_conf = ImapServerConf {
|
||||||
|
server_hostname: server_hostname.to_string(),
|
||||||
|
server_username: server_username.to_string(),
|
||||||
|
server_password: server_password.to_string(),
|
||||||
|
server_port,
|
||||||
|
use_starttls: true,
|
||||||
|
danger_accept_invalid_certs,
|
||||||
|
protocol: ImapProtocol::ManageSieve,
|
||||||
|
};
|
||||||
|
let online = Arc::new(Mutex::new((
|
||||||
|
Instant::now(),
|
||||||
|
Err(MeliError::new("Account is uninitialised.")),
|
||||||
|
)));
|
||||||
|
Ok(ImapConnection::new_connection(&server_conf, online))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ManageSieve for ImapConnection {
|
||||||
|
fn havespace(&mut self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn putscript(&mut self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn listscripts(&mut self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn setactive(&mut self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getscript(&mut self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deletescript(&mut self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn renamescript(&mut self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -988,7 +988,7 @@ pub fn quoted(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
|
||||||
|
|
||||||
let mut i = 1;
|
let mut i = 1;
|
||||||
while i < input.len() {
|
while i < input.len() {
|
||||||
if input[i] == b'\"' && (i == 0 || (input[i - 1] != b'\\')) {
|
if input[i] == b'\"' && input[i - 1] != b'\\' {
|
||||||
return match crate::email::parser::phrase(&input[1..i], false) {
|
return match crate::email::parser::phrase(&input[1..i], false) {
|
||||||
IResult::Done(_, out) => IResult::Done(&input[i + 1..], out),
|
IResult::Done(_, out) => IResult::Done(&input[i + 1..], out),
|
||||||
e => e,
|
e => e,
|
||||||
|
|
|
@ -13,6 +13,10 @@ path = "src/email_parse.rs"
|
||||||
name = "imapconn"
|
name = "imapconn"
|
||||||
path = "src/imap_conn.rs"
|
path = "src/imap_conn.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "managesieve_conn"
|
||||||
|
path = "src/managesieve.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
melib = { path = "../melib", version = "*", features = ["debug-tracing", "unicode_algorithms"] }
|
melib = { path = "../melib", version = "*", features = ["debug-tracing", "unicode_algorithms"] }
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
extern crate melib;
|
||||||
|
|
||||||
|
use melib::backends::imap::managesieve::new_managesieve_connection;
|
||||||
|
use melib::AccountSettings;
|
||||||
|
use melib::Result;
|
||||||
|
|
||||||
|
/// Opens an interactive shell on a managesieve server. Suggested use is with rlwrap(1)
|
||||||
|
///
|
||||||
|
/// # Example invocation:
|
||||||
|
/// ```sh
|
||||||
|
/// ./manage_sieve server_hostname server_username server_password server_port");
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// `danger_accept_invalid_certs` is turned on by default, so no certificate validation is performed.
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let mut args = std::env::args().skip(1).collect::<Vec<String>>();
|
||||||
|
if args.len() != 4 {
|
||||||
|
eprintln!(
|
||||||
|
"Usage: manage_sieve server_hostname server_username server_password server_port"
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (a, b, c, d) = (
|
||||||
|
std::mem::replace(&mut args[0], String::new()),
|
||||||
|
std::mem::replace(&mut args[1], String::new()),
|
||||||
|
std::mem::replace(&mut args[2], String::new()),
|
||||||
|
std::mem::replace(&mut args[3], String::new()),
|
||||||
|
);
|
||||||
|
let set = AccountSettings {
|
||||||
|
extra: [
|
||||||
|
("server_hostname".to_string(), a),
|
||||||
|
("server_username".to_string(), b),
|
||||||
|
("server_password".to_string(), c),
|
||||||
|
("server_port".to_string(), d),
|
||||||
|
(
|
||||||
|
"danger_accept_invalid_certs".to_string(),
|
||||||
|
"true".to_string(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut conn = new_managesieve_connection(&set)?;
|
||||||
|
conn.connect()?;
|
||||||
|
let mut res = String::with_capacity(8 * 1024);
|
||||||
|
|
||||||
|
let mut input = String::new();
|
||||||
|
loop {
|
||||||
|
use std::io;
|
||||||
|
input.clear();
|
||||||
|
match io::stdin().read_line(&mut input) {
|
||||||
|
Ok(_) => {
|
||||||
|
if input.trim().eq_ignore_ascii_case("logout") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
conn.send_command(input.as_bytes()).unwrap();
|
||||||
|
conn.read_lines(&mut res, String::new()).unwrap();
|
||||||
|
println!("out: {}", &res);
|
||||||
|
}
|
||||||
|
Err(error) => println!("error: {}", error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue