Bug 1896171 - Part 4: Send messages via EWS. r=leftmostcat
Differential Revision: https://phabricator.services.mozilla.com/D211575 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
7dcc534d1f
Коммит
8368cfcc02
|
@ -171,7 +171,7 @@ async function createAccountInBackend(config) {
|
|||
smtpServer.clientid = newOutgoingClientid;
|
||||
} else if (config.outgoing.type == "ews") {
|
||||
const ewsServer = outServer.QueryInterface(Ci.nsIEwsServer);
|
||||
ewsServer.ewsURL = config.outgoing.ewsURL;
|
||||
ewsServer.initialize(config.outgoing.ewsURL);
|
||||
} else {
|
||||
// Note: createServer should already have thrown if given a type we don't
|
||||
// support, so if we're able to reach this then something has gone very
|
||||
|
|
|
@ -977,7 +977,7 @@ export var MsgUtils = {
|
|||
formatStringWithSMTPHostName(userIdentity, composeBundle, errorName) {
|
||||
const smtpServer =
|
||||
MailServices.outgoingServer.getServerByIdentity(userIdentity);
|
||||
const smtpHostname = smtpServer.hostname;
|
||||
const smtpHostname = smtpServer.serverURI.host;
|
||||
return composeBundle.formatStringFromName(errorName, [smtpHostname]);
|
||||
},
|
||||
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use nserror::{nsresult, NS_OK};
|
||||
use nsstring::nsACString;
|
||||
use xpcom::{
|
||||
interfaces::{nsILoadGroup, nsLoadFlags},
|
||||
xpcom_method, RefPtr,
|
||||
};
|
||||
|
||||
/// A stub [`nsIRequest`] that only implementes the `Cancel` method. Currently
|
||||
/// only used for sending.
|
||||
///
|
||||
/// This struct is to be expanded (to actually cancel outgoing requests) once
|
||||
/// the code architecture for creating and sending request with backoff allows
|
||||
/// for idiosyncratic semantics.
|
||||
///
|
||||
/// [`nsIRequest`]: xpcom::interfaces::nsIRequest
|
||||
#[xpcom::xpcom(implement(nsIRequest), atomic)]
|
||||
pub(crate) struct CancellableRequest {}
|
||||
|
||||
impl CancellableRequest {
|
||||
pub fn new() -> RefPtr<Self> {
|
||||
CancellableRequest::allocate(InitCancellableRequest {})
|
||||
}
|
||||
|
||||
xpcom_method!(cancel => Cancel(aStatus: nsresult));
|
||||
fn cancel(&self, _status: nsresult) -> Result<(), nsresult> {
|
||||
log::error!("request cancellation is not currently fully implemented, only stubbed out");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///////////////////////////////////
|
||||
/// Rest of the nsIRequest impl ///
|
||||
///////////////////////////////////
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn CancelWithReason(&self, _aStatus: nsresult, _aReason: *const nsACString) -> nsresult {
|
||||
return nserror::NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn GetCanceledReason(&self, _aCanceledReason: *mut nsACString) -> nsresult {
|
||||
return nserror::NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn GetLoadFlags(&self, _aLoadFlags: *mut nsLoadFlags) -> nsresult {
|
||||
return nserror::NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn GetLoadGroup(&self, _aLoadGroup: *mut *const nsILoadGroup) -> nsresult {
|
||||
return nserror::NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn GetName(&self, _aName: *mut nsACString) -> nsresult {
|
||||
return nserror::NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn GetStatus(&self, _aStatus: *mut nsresult) -> nsresult {
|
||||
return nserror::NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn GetTRRMode(&self, _retval: *mut u32) -> nsresult {
|
||||
return nserror::NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn IsPending(&self, _retval: *mut bool) -> nsresult {
|
||||
return nserror::NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn Resume(&self) -> nsresult {
|
||||
return nserror::NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn SetCanceledReason(&self, _aCanceledReason: *const nsACString) -> nsresult {
|
||||
return nserror::NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn SetLoadFlags(&self, _aLoadFlags: nsLoadFlags) -> nsresult {
|
||||
return nserror::NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn SetLoadGroup(&self, _aLoadGroup: *const nsILoadGroup) -> nsresult {
|
||||
return nserror::NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn SetTRRMode(&self, _mode: u32) -> nsresult {
|
||||
return nserror::NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn Suspend(&self) -> nsresult {
|
||||
return nserror::NS_ERROR_NOT_IMPLEMENTED;
|
||||
}
|
||||
}
|
|
@ -4,14 +4,17 @@
|
|||
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
|
||||
use base64::prelude::*;
|
||||
use ews::{
|
||||
create_item::{self, CreateItem, MessageDisposition},
|
||||
get_folder::GetFolder,
|
||||
get_item::GetItem,
|
||||
soap,
|
||||
sync_folder_hierarchy::{self, SyncFolderHierarchy},
|
||||
sync_folder_items::{self, SyncFolderItems},
|
||||
ArrayOfRecipients, BaseFolderId, BaseItemId, BaseShape, Folder, FolderShape, Importance,
|
||||
ItemShape, Message, Operation, PathToElement, RealItem, ResponseClass,
|
||||
ItemShape, Message, MimeContent, Operation, PathToElement, RealItem, ResponseClass,
|
||||
ResponseCode,
|
||||
};
|
||||
use fxhash::FxHashMap;
|
||||
use moz_http::StatusCode;
|
||||
|
@ -23,13 +26,13 @@ use uuid::Uuid;
|
|||
use xpcom::{
|
||||
getter_addrefs,
|
||||
interfaces::{
|
||||
nsIMsgDBHdr, nsMsgFolderFlagType, nsMsgFolderFlags, nsMsgMessageFlags, nsMsgPriority,
|
||||
IEwsClient, IEwsFolderCallbacks, IEwsMessageCallbacks,
|
||||
nsIMsgDBHdr, nsIRequestObserver, nsMsgFolderFlagType, nsMsgFolderFlags, nsMsgMessageFlags,
|
||||
nsMsgPriority, IEwsClient, IEwsFolderCallbacks, IEwsMessageCallbacks,
|
||||
},
|
||||
RefPtr,
|
||||
};
|
||||
|
||||
use crate::authentication::credentials::Credentials;
|
||||
use crate::{authentication::credentials::Credentials, cancellable_request::CancellableRequest};
|
||||
|
||||
pub(crate) struct XpComEwsClient {
|
||||
pub endpoint: Url,
|
||||
|
@ -625,41 +628,12 @@ impl XpComEwsClient {
|
|||
|
||||
let response = self.make_operation_request(op).await?;
|
||||
for response_message in response.response_messages.get_item_response_message {
|
||||
match response_message.response_class {
|
||||
ResponseClass::Success => (),
|
||||
|
||||
ResponseClass::Warning => {
|
||||
let message = if let Some(code) = response_message.response_code {
|
||||
if let Some(text) = response_message.message_text {
|
||||
format!("GetItem operation encountered `{code:?}' warning: {text}")
|
||||
} else {
|
||||
format!("GetItem operation encountered `{code:?}' warning")
|
||||
}
|
||||
} else if let Some(text) = response_message.message_text {
|
||||
format!("GetItem operation encountered warning: {text}")
|
||||
} else {
|
||||
format!("GetItem operation encountered unknown warning")
|
||||
};
|
||||
|
||||
log::warn!("{message}");
|
||||
}
|
||||
|
||||
ResponseClass::Error => {
|
||||
let message = if let Some(code) = response_message.response_code {
|
||||
if let Some(text) = response_message.message_text {
|
||||
format!("GetItem operation encountered `{code:?}' error: {text}")
|
||||
} else {
|
||||
format!("GetItem operation encountered `{code:?}' error")
|
||||
}
|
||||
} else if let Some(text) = response_message.message_text {
|
||||
format!("GetItem operation encountered error: {text}")
|
||||
} else {
|
||||
format!("GetItem operation encountered unknown error")
|
||||
};
|
||||
|
||||
return Err(XpComEwsError::Processing { message });
|
||||
}
|
||||
}
|
||||
process_response_message_class(
|
||||
"GetItem",
|
||||
response_message.response_class,
|
||||
response_message.response_code,
|
||||
response_message.message_text,
|
||||
)?;
|
||||
|
||||
// The expected shape of the list of response messages is
|
||||
// underspecified, but EWS always seems to return one message
|
||||
|
@ -681,6 +655,108 @@ impl XpComEwsClient {
|
|||
Ok(items)
|
||||
}
|
||||
|
||||
/// Send a message by performing a [`CreateItem`] operation via EWS.
|
||||
///
|
||||
/// All headers except for Bcc are expected to be included in the provided
|
||||
/// MIME content.
|
||||
///
|
||||
/// [`CreateItem`] https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/createitem-operation-email-message
|
||||
pub async fn send_message(
|
||||
self,
|
||||
mime_content: String,
|
||||
message_id: String,
|
||||
should_request_dsn: bool,
|
||||
observer: RefPtr<nsIRequestObserver>,
|
||||
) {
|
||||
let cancellable_request = CancellableRequest::new();
|
||||
|
||||
// Notify that the request has started.
|
||||
if let Err(err) =
|
||||
unsafe { observer.OnStartRequest(cancellable_request.coerce()) }.to_result()
|
||||
{
|
||||
log::error!("aborting sending: an error occurred while starting the observer: {err}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Send the request, using an inner method to more easily handle errors.
|
||||
// Use the return value to determine which status we should use when
|
||||
// notifying the end of the request.
|
||||
let status = match self
|
||||
.send_message_inner(mime_content, message_id, should_request_dsn)
|
||||
.await
|
||||
{
|
||||
Ok(_) => nserror::NS_OK,
|
||||
Err(err) => {
|
||||
log::error!("an error occurred while attempting to send the message: {err:?}");
|
||||
|
||||
match err {
|
||||
XpComEwsError::XpCom(status) => status,
|
||||
XpComEwsError::Http(err) => err.into(),
|
||||
|
||||
_ => nserror::NS_ERROR_FAILURE,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Notify that the request has finished.
|
||||
if let Err(err) =
|
||||
unsafe { observer.OnStopRequest(cancellable_request.coerce(), status) }.to_result()
|
||||
{
|
||||
log::error!("an error occurred while stopping the observer: {err}")
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_message_inner(
|
||||
&self,
|
||||
mime_content: String,
|
||||
message_id: String,
|
||||
should_request_dsn: bool,
|
||||
) -> Result<(), XpComEwsError> {
|
||||
// Create a new message using the default values, and set the ones we
|
||||
// need.
|
||||
let message = create_item::Message {
|
||||
mime_content: Some(MimeContent {
|
||||
character_set: None,
|
||||
content: BASE64_STANDARD.encode(mime_content),
|
||||
}),
|
||||
is_delivery_receipt_requested: Some(should_request_dsn),
|
||||
internet_message_id: Some(message_id),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let create_item = CreateItem {
|
||||
items: vec![create_item::Item::Message(message)],
|
||||
|
||||
// We don't need EWS to copy messages to the Sent folder after
|
||||
// they've been sent, because the internal MessageSend module
|
||||
// already takes care of it, and will include additional headers we
|
||||
// don't send to EWS (such as Bcc).
|
||||
message_disposition: Some(MessageDisposition::SendOnly),
|
||||
saved_item_folder_id: None,
|
||||
};
|
||||
|
||||
let response = self.make_operation_request(create_item).await?;
|
||||
|
||||
// We have only sent one message, therefore the response should only
|
||||
// contain one response message.
|
||||
let response_messages = response.response_messages.create_item_response_message;
|
||||
if response_messages.len() != 1 {
|
||||
return Err(XpComEwsError::Processing {
|
||||
message: String::from("expected only one message in CreateItem response"),
|
||||
});
|
||||
}
|
||||
|
||||
// Get the first (and only) response message, and check if there's a
|
||||
// warning or an error we should handle.
|
||||
let response_message = response_messages.into_iter().next().unwrap();
|
||||
process_response_message_class(
|
||||
"CreateItem",
|
||||
response_message.response_class,
|
||||
response_message.response_code,
|
||||
response_message.message_text,
|
||||
)
|
||||
}
|
||||
|
||||
/// Makes a request to the EWS endpoint to perform an operation.
|
||||
///
|
||||
/// If the request is throttled, it will be retried after the delay given in
|
||||
|
@ -981,3 +1057,50 @@ where
|
|||
handler(client_err, desc);
|
||||
}
|
||||
}
|
||||
|
||||
/// Look at the response class of a response message, and do nothing, warn or
|
||||
/// return an error accordingly.
|
||||
fn process_response_message_class(
|
||||
op_name: &str,
|
||||
response_class: ResponseClass,
|
||||
response_code: Option<ResponseCode>,
|
||||
message_text: Option<String>,
|
||||
) -> Result<(), XpComEwsError> {
|
||||
match response_class {
|
||||
ResponseClass::Success => Ok(()),
|
||||
|
||||
ResponseClass::Warning => {
|
||||
let message = if let Some(code) = response_code {
|
||||
if let Some(text) = message_text {
|
||||
format!("{op_name} operation encountered `{code:?}' warning: {text}")
|
||||
} else {
|
||||
format!("{op_name} operation encountered `{code:?}' warning")
|
||||
}
|
||||
} else if let Some(text) = message_text {
|
||||
format!("{op_name} operation encountered warning: {text}")
|
||||
} else {
|
||||
format!("{op_name} operation encountered unknown warning")
|
||||
};
|
||||
|
||||
log::warn!("{message}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
ResponseClass::Error => {
|
||||
let message = if let Some(code) = response_code {
|
||||
if let Some(text) = message_text {
|
||||
format!("{op_name} operation encountered `{code:?}' error: {text}")
|
||||
} else {
|
||||
format!("{op_name} operation encountered `{code:?}' error")
|
||||
}
|
||||
} else if let Some(text) = message_text {
|
||||
format!("{op_name} operation encountered error: {text}")
|
||||
} else {
|
||||
format!("{op_name} operation encountered unknown error")
|
||||
};
|
||||
|
||||
Err(XpComEwsError::Processing { message })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ use xpcom::{
|
|||
};
|
||||
|
||||
mod authentication;
|
||||
mod cancellable_request;
|
||||
mod client;
|
||||
mod outgoing;
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ use std::cell::{OnceCell, RefCell};
|
|||
use std::os::raw::{c_char, c_void};
|
||||
use std::ptr;
|
||||
|
||||
use cstr::cstr;
|
||||
use nserror::nsresult;
|
||||
use nserror::NS_OK;
|
||||
use nsstring::{nsACString, nsCString};
|
||||
|
@ -13,13 +14,16 @@ use url::Url;
|
|||
use xpcom::{create_instance, getter_addrefs, nsIID};
|
||||
use xpcom::{
|
||||
interfaces::{
|
||||
nsIFile, nsIFileInputStream, nsIIOService, nsIMsgIdentity, nsIMsgStatusFeedback,
|
||||
nsIMsgWindow, nsIRequestObserver, nsIURI, nsIUrlListener, nsMsgAuthMethodValue,
|
||||
nsMsgSocketTypeValue,
|
||||
msgIOAuth2Module, nsIFile, nsIFileInputStream, nsIIOService, nsIMsgIdentity,
|
||||
nsIMsgStatusFeedback, nsIMsgWindow, nsIRequestObserver, nsIURI, nsIUrlListener,
|
||||
nsMsgAuthMethodValue, nsMsgSocketTypeValue,
|
||||
},
|
||||
xpcom_method, RefPtr,
|
||||
};
|
||||
|
||||
use crate::authentication::credentials::AuthenticationProvider;
|
||||
use crate::client::XpComEwsClient;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn nsEwsOutgoingServerConstructor(
|
||||
iid: &nsIID,
|
||||
|
@ -185,7 +189,7 @@ impl EwsOutgoingServer {
|
|||
let url = nsCString::from(ews_url.as_str());
|
||||
|
||||
let io_service =
|
||||
xpcom::get_service::<nsIIOService>(cstr::cstr!("@mozilla.org/network/io-service;1"))
|
||||
xpcom::get_service::<nsIIOService>(cstr!("@mozilla.org/network/io-service;1"))
|
||||
.ok_or(nserror::NS_ERROR_FAILURE)?;
|
||||
|
||||
getter_addrefs(|p| unsafe { io_service.NewURI(&*url, ptr::null(), ptr::null(), p) })
|
||||
|
@ -231,28 +235,44 @@ impl EwsOutgoingServer {
|
|||
_sender: &nsACString,
|
||||
_password: &nsACString,
|
||||
_status_listener: &nsIMsgStatusFeedback,
|
||||
_request_dsn: bool,
|
||||
_message_id: &nsACString,
|
||||
should_request_dsn: bool,
|
||||
message_id: &nsACString,
|
||||
observer: &nsIRequestObserver,
|
||||
) -> Result<(), nsresult> {
|
||||
unsafe {
|
||||
// For now we don't pass in a request. Null/empty requests are
|
||||
// supported by the MessageSend module, so it won't cause an issue
|
||||
// if the user tries to abort before the operation completes. Once
|
||||
// sending to EWS is implemented, we will retrieve the nsIChannel
|
||||
// from the moz_http request and turn it into an nsIRequest, to let
|
||||
// Necko deal with cancellations.
|
||||
observer.OnStartRequest(ptr::null());
|
||||
}
|
||||
|
||||
let message_content = read_file(file_path)?;
|
||||
let message_content =
|
||||
String::from_utf8(message_content).or(Err(nserror::NS_ERROR_FAILURE))?;
|
||||
println!("{}", message_content);
|
||||
|
||||
unsafe {
|
||||
observer.OnStopRequest(ptr::null(), NS_OK);
|
||||
}
|
||||
// Ensure the URL is properly set.
|
||||
let url = self
|
||||
.ews_url
|
||||
.get()
|
||||
.ok_or_else(|| {
|
||||
log::error!("EwsOutgoingServer::SendMailMessage: EWS URL not set");
|
||||
nserror::NS_ERROR_NOT_INITIALIZED
|
||||
})?
|
||||
.clone();
|
||||
|
||||
let credentials = self.get_credentials()?;
|
||||
|
||||
// Set up the client to build and send the request.
|
||||
let client = XpComEwsClient {
|
||||
endpoint: url,
|
||||
credentials,
|
||||
client: moz_http::Client::new(),
|
||||
};
|
||||
|
||||
// Send the request asynchronously.
|
||||
moz_task::spawn_local(
|
||||
"send_mail",
|
||||
client.send_message(
|
||||
message_content,
|
||||
message_id.to_utf8().into(),
|
||||
should_request_dsn,
|
||||
RefPtr::new(observer),
|
||||
),
|
||||
)
|
||||
.detach();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -306,12 +326,39 @@ impl EwsOutgoingServer {
|
|||
}
|
||||
}
|
||||
|
||||
// Make it possible to create an Auth from this server's attributes.
|
||||
impl AuthenticationProvider for &EwsOutgoingServer {
|
||||
fn username(&self) -> Result<nsCString, nsresult> {
|
||||
Ok(self.username.borrow().clone())
|
||||
}
|
||||
|
||||
fn password(&self) -> Result<nsCString, nsresult> {
|
||||
Ok(self.password.borrow().clone())
|
||||
}
|
||||
|
||||
fn auth_method(&self) -> Result<nsMsgAuthMethodValue, nsresult> {
|
||||
Ok(self.auth_method.borrow().clone())
|
||||
}
|
||||
|
||||
fn oauth2_module(&self) -> Result<Option<RefPtr<msgIOAuth2Module>>, nsresult> {
|
||||
let oauth2_module =
|
||||
create_instance::<msgIOAuth2Module>(c"@mozilla.org/mail/oauth2-module;1").ok_or(
|
||||
Err::<RefPtr<msgIOAuth2Module>, _>(nserror::NS_ERROR_FAILURE),
|
||||
)?;
|
||||
|
||||
let mut oauth2_supported = false;
|
||||
unsafe { oauth2_module.InitFromOutgoing(self.coerce(), &mut oauth2_supported) }
|
||||
.to_result()?;
|
||||
|
||||
Ok(oauth2_supported.then_some(oauth2_module))
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the file provided and read its content into a vector of bytes.
|
||||
fn read_file(file: &nsIFile) -> Result<Vec<u8>, nsresult> {
|
||||
let file_stream = create_instance::<nsIFileInputStream>(cstr::cstr!(
|
||||
"@mozilla.org/network/file-input-stream;1"
|
||||
))
|
||||
.ok_or(nserror::NS_ERROR_FAILURE)?;
|
||||
let file_stream =
|
||||
create_instance::<nsIFileInputStream>(cstr!("@mozilla.org/network/file-input-stream;1"))
|
||||
.ok_or(nserror::NS_ERROR_FAILURE)?;
|
||||
|
||||
// Open a stream from the file, and figure out how many bytes can be read
|
||||
// from it.
|
||||
|
|
|
@ -83,5 +83,20 @@ impl From<nsresult> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Error> for nsresult {
|
||||
fn from(value: Error) -> Self {
|
||||
match value {
|
||||
Error::UnsupportedScheme(_) => nserror::NS_ERROR_UNKNOWN_PROTOCOL,
|
||||
Error::TimedOut => nserror::NS_ERROR_NET_TIMEOUT,
|
||||
Error::UnknownHost => nserror::NS_ERROR_UNKNOWN_HOST,
|
||||
Error::UnknownNetworkError(result) => result,
|
||||
Error::RedirectLoop => nserror::NS_ERROR_REDIRECT_LOOP,
|
||||
Error::Unknown(result) => result,
|
||||
|
||||
_ => nserror::NS_ERROR_FAILURE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A result which error type is always an [`enum@Error`].
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
|
Загрузка…
Ссылка в новой задаче