Drop clap
This commit is contained in:
parent
f72c075516
commit
931e59cab9
5 changed files with 150 additions and 259 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -1,5 +1,16 @@
|
|||
# SpamAssassin Milter changelog
|
||||
|
||||
## 0.3.2 (unreleased)
|
||||
|
||||
### Changed
|
||||
|
||||
* The command-line user interface has been reimplemented using a lighter-weight,
|
||||
no-dependencies approach.
|
||||
|
||||
While there are some differences in appearance and error reporting, there are
|
||||
no functional changes in using the program. Make sure that you are not relying
|
||||
on undocumented (and therefore unsupported) behaviour of the old CLI.
|
||||
|
||||
## 0.3.1 (2022-03-14)
|
||||
|
||||
### Fixed
|
||||
|
|
88
Cargo.lock
generated
88
Cargo.lock
generated
|
@ -13,17 +13,6 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
|
@ -81,30 +70,6 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
"clap_lex",
|
||||
"indexmap",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213"
|
||||
dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.21"
|
||||
|
@ -194,12 +159,6 @@ dependencies = [
|
|||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
|
@ -209,16 +168,6 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indymilter"
|
||||
version = "0.1.1"
|
||||
|
@ -312,12 +261,6 @@ version = "1.11.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b10983b38c53aebdf33f542c6275b0f58a238129d00c4ae0e6fb59738d783ca"
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.9"
|
||||
|
@ -402,7 +345,6 @@ dependencies = [
|
|||
"async-trait",
|
||||
"byte-strings",
|
||||
"chrono",
|
||||
"clap",
|
||||
"futures",
|
||||
"indymilter",
|
||||
"ipnet",
|
||||
|
@ -412,12 +354,6 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.95"
|
||||
|
@ -429,21 +365,6 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.44"
|
||||
|
@ -539,15 +460,6 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
|
|
|
@ -14,7 +14,6 @@ exclude = ["/.gitignore", "/.gitlab-ci.yml"]
|
|||
async-trait = "0.1.52"
|
||||
byte-strings = "0.2.2"
|
||||
chrono = "0.4.19"
|
||||
clap = "3.1.0"
|
||||
futures = "0.3.19"
|
||||
indymilter = "0.1.1"
|
||||
ipnet = "2.3.1"
|
||||
|
|
|
@ -15,8 +15,8 @@ spamassassin-milter \- milter for spam filtering with SpamAssassin
|
|||
.OP \-t NETS
|
||||
.OP \-v
|
||||
.I SOCKET
|
||||
.RB [ \-\-
|
||||
.IR SPAMC_ARGS ...]
|
||||
.RB [ \-\- ]
|
||||
.RI [ SPAMC_ARGS ...]
|
||||
.YS
|
||||
.SH DESCRIPTION
|
||||
.B spamassassin-milter
|
||||
|
@ -42,10 +42,10 @@ or a UNIX domain socket in the form
|
|||
.BI unix: PATH
|
||||
(for example,
|
||||
.BR unix:/run/spamassassin-milter.sock ).
|
||||
After the options and argument, additional arguments
|
||||
After the options and argument, the remaining arguments
|
||||
.I SPAMC_ARGS
|
||||
listed after
|
||||
.B \-\-
|
||||
(optionally preceded by
|
||||
.BR \-\- )
|
||||
are gathered as arguments to pass to the
|
||||
.B spamc
|
||||
invocation.
|
||||
|
@ -73,7 +73,7 @@ messages are not processed with SpamAssassin.
|
|||
Process messages normally, but do not take action or apply any modifications.
|
||||
Combined with
|
||||
.BR \-\-verbose ,
|
||||
this gives accurate insight into what would happen if run without
|
||||
this gives insight into what would happen if run without
|
||||
.BR \-\-dry-run .
|
||||
.TP
|
||||
.BR \-h ", " \-\-help
|
||||
|
@ -154,8 +154,11 @@ Trust connections coming from the IP networks or addresses
|
|||
Connections from IP addresses contained in trusted networks are not processed
|
||||
with SpamAssassin.
|
||||
.I NETS
|
||||
is a comma-separated list of IP network addresses, for example,
|
||||
must be a comma-separated list of IP network addresses, for example,
|
||||
.BR ::1/128,127.0.0.0/8,192.168.1.39 .
|
||||
If
|
||||
.I NETS
|
||||
is empty, no IP addresses are trusted.
|
||||
If this option is not used, all connections from loopback addresses are trusted.
|
||||
.TP
|
||||
.BR \-v ", " \-\-verbose
|
||||
|
|
292
src/main.rs
292
src/main.rs
|
@ -1,10 +1,11 @@
|
|||
use clap::{Arg, Command, ErrorKind};
|
||||
use futures::stream::StreamExt;
|
||||
use indymilter::Listener;
|
||||
use signal_hook::consts::{SIGINT, SIGTERM};
|
||||
use signal_hook_tokio::{Handle, Signals};
|
||||
use spamassassin_milter::{Config, MILTER_NAME, VERSION};
|
||||
use std::{net::IpAddr, os::unix::fs::FileTypeExt, path::Path, process, str::FromStr};
|
||||
use std::{
|
||||
env, error::Error, net::IpAddr, os::unix::fs::FileTypeExt, path::Path, process, str::FromStr,
|
||||
};
|
||||
use tokio::{
|
||||
fs,
|
||||
net::{TcpListener, UnixListener},
|
||||
|
@ -12,86 +13,13 @@ use tokio::{
|
|||
task::JoinHandle,
|
||||
};
|
||||
|
||||
const ARG_AUTH_UNTRUSTED: &str = "AUTH_UNTRUSTED";
|
||||
const ARG_DRY_RUN: &str = "DRY_RUN";
|
||||
const ARG_MAX_MESSAGE_SIZE: &str = "MAX_MESSAGE_SIZE";
|
||||
const ARG_PRESERVE_BODY: &str = "PRESERVE_BODY";
|
||||
const ARG_PRESERVE_HEADERS: &str = "PRESERVE_HEADERS";
|
||||
const ARG_REJECT_SPAM: &str = "REJECT_SPAM";
|
||||
const ARG_REPLY_CODE: &str = "REPLY_CODE";
|
||||
const ARG_REPLY_STATUS_CODE: &str = "REPLY_STATUS_CODE";
|
||||
const ARG_REPLY_TEXT: &str = "REPLY_TEXT";
|
||||
const ARG_TRUSTED_NETWORKS: &str = "TRUSTED_NETWORKS";
|
||||
const ARG_VERBOSE: &str = "VERBOSE";
|
||||
const ARG_SOCKET: &str = "SOCKET";
|
||||
const ARG_SPAMC_ARGS: &str = "SPAMC_ARGS";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let command = Command::new(MILTER_NAME)
|
||||
.version(VERSION)
|
||||
.arg(Arg::new(ARG_AUTH_UNTRUSTED)
|
||||
.short('a')
|
||||
.long("auth-untrusted")
|
||||
.help("Treat authenticated senders as untrusted"))
|
||||
.arg(Arg::new(ARG_DRY_RUN)
|
||||
.short('n')
|
||||
.long("dry-run")
|
||||
.help("Process messages without applying changes"))
|
||||
.arg(Arg::new(ARG_MAX_MESSAGE_SIZE)
|
||||
.short('s')
|
||||
.long("max-message-size")
|
||||
.value_name("BYTES")
|
||||
.help("Maximum message size to process"))
|
||||
.arg(Arg::new(ARG_PRESERVE_BODY)
|
||||
.short('B')
|
||||
.long("preserve-body")
|
||||
.help("Suppress rewriting of message body"))
|
||||
.arg(Arg::new(ARG_PRESERVE_HEADERS)
|
||||
.short('H')
|
||||
.long("preserve-headers")
|
||||
.help("Suppress rewriting of Subject/From/To headers"))
|
||||
.arg(Arg::new(ARG_REJECT_SPAM)
|
||||
.short('r')
|
||||
.long("reject-spam")
|
||||
.help("Reject messages flagged as spam"))
|
||||
.arg(Arg::new(ARG_REPLY_CODE)
|
||||
.short('C')
|
||||
.long("reply-code")
|
||||
.value_name("CODE")
|
||||
.help("Reply code when rejecting messages"))
|
||||
.arg(Arg::new(ARG_REPLY_STATUS_CODE)
|
||||
.short('S')
|
||||
.long("reply-status-code")
|
||||
.value_name("CODE")
|
||||
.help("Status code when rejecting messages"))
|
||||
.arg(Arg::new(ARG_REPLY_TEXT)
|
||||
.short('R')
|
||||
.long("reply-text")
|
||||
.value_name("MSG")
|
||||
.help("Reply text when rejecting messages"))
|
||||
.arg(Arg::new(ARG_TRUSTED_NETWORKS)
|
||||
.short('t')
|
||||
.long("trusted-networks")
|
||||
.value_name("NETS")
|
||||
.use_value_delimiter(true)
|
||||
.help("Trust connections from these networks"))
|
||||
.arg(Arg::new(ARG_VERBOSE)
|
||||
.short('v')
|
||||
.long("verbose")
|
||||
.help("Enable verbose operation logging"))
|
||||
.arg(Arg::new(ARG_SOCKET)
|
||||
.required(true)
|
||||
.help("Listening socket of the milter"))
|
||||
.arg(Arg::new(ARG_SPAMC_ARGS)
|
||||
.last(true)
|
||||
.multiple_occurrences(true)
|
||||
.help("Additional arguments to pass to spamc"));
|
||||
|
||||
let (socket, config) = match build_config(command) {
|
||||
let (socket, config) = match parse_args() {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
e.exit();
|
||||
eprintln!("error: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -160,7 +88,7 @@ enum Socket {
|
|||
}
|
||||
|
||||
impl FromStr for Socket {
|
||||
type Err = ();
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if let Some(s) = s.strip_prefix("inet:") {
|
||||
|
@ -168,126 +96,164 @@ impl FromStr for Socket {
|
|||
} else if let Some(s) = s.strip_prefix("unix:") {
|
||||
Ok(Self::Unix(s.into()))
|
||||
} else {
|
||||
Err(())
|
||||
Err(format!("invalid value for socket: \"{}\"", s))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_config(mut command: Command) -> clap::Result<(Socket, Config)> {
|
||||
let matches = command.get_matches_mut();
|
||||
const USAGE_TEXT: &str = "\
|
||||
USAGE:
|
||||
spamassassin-milter [OPTIONS] <SOCKET> [--] [<SPAMC_ARGS>...]
|
||||
|
||||
let socket = matches.value_of(ARG_SOCKET).unwrap();
|
||||
let socket = match socket.parse() {
|
||||
Ok(socket) => socket,
|
||||
Err(()) => {
|
||||
return Err(command.error(
|
||||
ErrorKind::InvalidValue,
|
||||
format!("Invalid value for socket: \"{}\"", socket),
|
||||
));
|
||||
}
|
||||
};
|
||||
ARGUMENTS:
|
||||
<SOCKET> Listening socket of the milter
|
||||
<SPAMC_ARGS>... Additional arguments to pass to spamc
|
||||
|
||||
OPTIONS:
|
||||
-a, --auth-untrusted Treat authenticated senders as untrusted
|
||||
-n, --dry-run Process messages without applying changes
|
||||
-h, --help Print usage information
|
||||
-s, --max-message-size <BYTES> Maximum message size to process
|
||||
-B, --preserve-body Suppress rewriting of message body
|
||||
-H, --preserve-headers Suppress rewriting of Subject/From/To headers
|
||||
-r, --reject-spam Reject messages flagged as spam
|
||||
-C, --reply-code <CODE> Reply code when rejecting messages
|
||||
-S, --reply-status-code <CODE> Status code when rejecting messages
|
||||
-R, --reply-text <MSG> Reply text when rejecting messages
|
||||
-t, --trusted-networks <NETS> Trust connections from these networks
|
||||
-v, --verbose Enable verbose operation logging
|
||||
-V, --version Print version information
|
||||
";
|
||||
|
||||
fn parse_args() -> Result<(Socket, Config), Box<dyn Error>> {
|
||||
let mut args = env::args_os()
|
||||
.skip(1)
|
||||
.map(|s| s.into_string().map_err(|_| "invalid UTF-8 bytes in argument"));
|
||||
|
||||
let mut config = Config::builder();
|
||||
|
||||
if let Some(bytes) = matches.value_of(ARG_MAX_MESSAGE_SIZE) {
|
||||
match bytes.parse() {
|
||||
Ok(bytes) => {
|
||||
let mut reply_code = None;
|
||||
let mut reply_status_code = None;
|
||||
|
||||
let socket = loop {
|
||||
let arg = args.next().ok_or("required argument <SOCKET> missing")??;
|
||||
|
||||
let missing_value = || format!("missing value for option {}", arg);
|
||||
|
||||
match arg.as_str() {
|
||||
"-h" | "--help" => {
|
||||
println!("{} {}", MILTER_NAME, VERSION);
|
||||
println!();
|
||||
print!("{}", USAGE_TEXT);
|
||||
process::exit(0);
|
||||
}
|
||||
"-V" | "--version" => {
|
||||
println!("{} {}", MILTER_NAME, VERSION);
|
||||
process::exit(0);
|
||||
}
|
||||
"-a" | "--auth-untrusted" => {
|
||||
config = config.auth_untrusted(true);
|
||||
}
|
||||
"-n" | "--dry-run" => {
|
||||
config = config.dry_run(true);
|
||||
}
|
||||
"-B" | "--preserve-body" => {
|
||||
config = config.preserve_body(true);
|
||||
}
|
||||
"-H" | "--preserve-headers" => {
|
||||
config = config.preserve_headers(true);
|
||||
}
|
||||
"-r" | "--reject-spam" => {
|
||||
config = config.reject_spam(true);
|
||||
}
|
||||
"-v" | "--verbose" => {
|
||||
config = config.verbose(true);
|
||||
}
|
||||
"-s" | "--max-message-size" => {
|
||||
let arg = args.next().ok_or_else(missing_value)??;
|
||||
let bytes = arg.parse()
|
||||
.map_err(|_| format!("invalid value for max message size: \"{}\"", arg))?;
|
||||
|
||||
config = config.max_message_size(bytes);
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(command.error(
|
||||
ErrorKind::InvalidValue,
|
||||
format!("Invalid value for max message size: \"{}\"", bytes),
|
||||
));
|
||||
"-C" | "--reply-code" => {
|
||||
let code = args.next().ok_or_else(missing_value)??;
|
||||
|
||||
reply_code = Some(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
"-S" | "--reply-status-code" => {
|
||||
let code = args.next().ok_or_else(missing_value)??;
|
||||
|
||||
if let Some(nets) = matches.values_of(ARG_TRUSTED_NETWORKS) {
|
||||
config = config.use_trusted_networks(true);
|
||||
reply_status_code = Some(code);
|
||||
}
|
||||
"-R" | "--reply-text" => {
|
||||
let msg = args.next().ok_or_else(missing_value)??;
|
||||
|
||||
config = config.reply_text(msg);
|
||||
}
|
||||
"-t" | "--trusted-networks" => {
|
||||
let arg = args.next().ok_or_else(missing_value)??;
|
||||
|
||||
config = config.use_trusted_networks(true);
|
||||
|
||||
for net in arg.split(',').filter(|n| !n.is_empty()) {
|
||||
// Both `ipnet::IpNet` and `std::net::IpAddr` inputs are
|
||||
// supported.
|
||||
let net = net.parse()
|
||||
.or_else(|_| net.parse::<IpAddr>().map(From::from))
|
||||
.map_err(|_| format!("invalid value for trusted network address: \"{}\"", net))?;
|
||||
|
||||
for net in nets.filter(|n| !n.is_empty()) {
|
||||
// Both `ipnet::IpNet` and `std::net::IpAddr` inputs are supported.
|
||||
match net
|
||||
.parse()
|
||||
.or_else(|_| net.parse::<IpAddr>().map(From::from))
|
||||
{
|
||||
Ok(net) => {
|
||||
config = config.trusted_network(net);
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(command.error(
|
||||
ErrorKind::InvalidValue,
|
||||
format!("Invalid value for trusted network address: \"{}\"", net),
|
||||
));
|
||||
}
|
||||
}
|
||||
arg => break arg.parse()?,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let reply_code = matches.value_of(ARG_REPLY_CODE);
|
||||
let reply_status_code = matches.value_of(ARG_REPLY_STATUS_CODE);
|
||||
validate_reply_codes(&mut command, reply_code, reply_status_code)?;
|
||||
validate_reply_codes(reply_code.as_deref(), reply_status_code.as_deref())?;
|
||||
|
||||
if matches.is_present(ARG_AUTH_UNTRUSTED) {
|
||||
config = config.auth_untrusted(true);
|
||||
}
|
||||
if matches.is_present(ARG_DRY_RUN) {
|
||||
config = config.dry_run(true);
|
||||
}
|
||||
if matches.is_present(ARG_PRESERVE_BODY) {
|
||||
config = config.preserve_body(true);
|
||||
}
|
||||
if matches.is_present(ARG_PRESERVE_HEADERS) {
|
||||
config = config.preserve_headers(true);
|
||||
}
|
||||
if matches.is_present(ARG_REJECT_SPAM) {
|
||||
config = config.reject_spam(true);
|
||||
}
|
||||
if matches.is_present(ARG_VERBOSE) {
|
||||
config = config.verbose(true);
|
||||
}
|
||||
if let Some(code) = reply_code {
|
||||
config = config.reply_code(code.to_owned());
|
||||
config = config.reply_code(code);
|
||||
}
|
||||
if let Some(code) = reply_status_code {
|
||||
config = config.reply_status_code(code.to_owned());
|
||||
config = config.reply_status_code(code);
|
||||
}
|
||||
if let Some(msg) = matches.value_of(ARG_REPLY_TEXT) {
|
||||
config = config.reply_text(msg.to_owned());
|
||||
}
|
||||
if let Some(spamc_args) = matches.values_of(ARG_SPAMC_ARGS) {
|
||||
|
||||
if let Some(arg) = args.next() {
|
||||
let arg = arg?;
|
||||
|
||||
let mut spamc_args = if arg == "--" { vec![] } else { vec![arg] };
|
||||
|
||||
for arg in args {
|
||||
spamc_args.push(arg?);
|
||||
}
|
||||
|
||||
config = config.spamc_args(spamc_args);
|
||||
};
|
||||
}
|
||||
|
||||
Ok((socket, config.build()))
|
||||
}
|
||||
|
||||
fn validate_reply_codes(
|
||||
command: &mut Command,
|
||||
reply_code: Option<&str>,
|
||||
reply_status_code: Option<&str>,
|
||||
) -> clap::Result<()> {
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
match (reply_code, reply_status_code) {
|
||||
(Some(c1), Some(c2))
|
||||
if !((c1.starts_with('4') || c1.starts_with('5')) && c2.starts_with(&c1[..1])) =>
|
||||
{
|
||||
Err(command.error(
|
||||
ErrorKind::InvalidValue,
|
||||
format!(
|
||||
"Invalid or incompatible values for reply code and status code: \"{}\", \"{}\"",
|
||||
c1, c2
|
||||
),
|
||||
))
|
||||
Err(format!(
|
||||
"invalid or incompatible values for reply code and status code: \"{}\", \"{}\"",
|
||||
c1, c2
|
||||
)
|
||||
.into())
|
||||
}
|
||||
(Some(c), None) if !c.starts_with('5') => {
|
||||
Err(format!("invalid value for reply code (5XX): \"{}\"", c).into())
|
||||
}
|
||||
(None, Some(c)) if !c.starts_with('5') => {
|
||||
Err(format!("invalid value for reply status code (5.X.X): \"{}\"", c).into())
|
||||
}
|
||||
(Some(c), None) if !c.starts_with('5') => Err(command.error(
|
||||
ErrorKind::InvalidValue,
|
||||
format!("Invalid value for reply code (5XX): \"{}\"", c),
|
||||
)),
|
||||
(None, Some(c)) if !c.starts_with('5') => Err(command.error(
|
||||
ErrorKind::InvalidValue,
|
||||
format!("Invalid value for reply status code (5.X.X): \"{}\"", c),
|
||||
)),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue