/* 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 std::ptr; use thin_vec::thin_vec; use nserror::nsresult; use nsstring::nsString; use xpcom::interfaces::{nsIMsgIncomingServer, nsIPrompt, nsIPromptService, nsMsgAuthMethod}; use xpcom::{RefCounted, RefPtr, get_service}; use crate::user_interactive_server::UserInteractiveServer; use crate::{ IMAP_MSG_STRING_BUNDLE, MESSENGER_STRING_BUNDLE, PasswordPromptResult, get_formatted_string, get_string, get_string_bundle, register_alert, }; /// The outcome of the handling of an authentication error, and the action that /// should be taken next. #[repr(C)] pub enum AuthErrorOutcome { /// The authentication problem might have been resolved (e.g. the user has /// set a new password), so the request should be retried. RETRY, /// The authentication error could not be recovered from, so the request /// should be aborted. ABORT, } /// Handle an authentication failure that came from the given /// [`nsIMsgIncomingServer`]. /// /// Note the actual error is not included here, because all we need to know here /// is that we failed to authenticate against the remote server. /// /// # Safety /// /// The arguments must point to valid objects or be the null pointer. In the /// latter case, this function will return [`nserror::NS_ERROR_NULL_POINTER`]. #[unsafe(no_mangle)] pub unsafe extern "C" fn handle_auth_failure_from_incoming_server( incoming_server: *const nsIMsgIncomingServer, action: *mut AuthErrorOutcome, ) -> nsresult { if incoming_server.is_null() || action.is_null() { return nserror::NS_ERROR_NULL_POINTER; } // SAFETY: We have already ensured the provided pointer isn't null, and the // function's call contract implies consumers should ensure it's valid. // `RefPtr::from_raw` only returns `None` if the pointer is null, and we // have already ensured all of our pointers are non-null, so unwrapping // shouldn't panic here. let incoming_server = unsafe { RefPtr::from_raw(incoming_server).unwrap() }; match handle_auth_failure(incoming_server) { Ok(outcome) => { // SAFETY: We have already ensured the provided pointer is not null, // and the function's call contract implies consumers should ensure // it's valid. unsafe { *action = outcome; } nserror::NS_OK } Err(err) => err, } } /// Handle an authentication error that came from the given server, as per its /// preferred authentication method. /// /// Note the actual error is not included here, because all we need to know here /// is that we failed to authenticate against the remote server. pub fn handle_auth_failure(server: RefPtr) -> Result where ServerT: UserInteractiveServer + RefCounted, { match server.auth_method()? { nsMsgAuthMethod::OAuth2 => notify_oauth_failure(server), nsMsgAuthMethod::passwordCleartext | nsMsgAuthMethod::NTLM => { notify_password_failure(server) } _ => Err(nserror::NS_ERROR_NOT_IMPLEMENTED), } } /// Notify the user about a failure to authenticate against the given server /// using OAuth2. /// /// This function always returns [`AuthErrorOutcome::ABORT`] (unless an error /// occurs while notifying). fn notify_oauth_failure(server: RefPtr) -> Result where ServerT: UserInteractiveServer + RefCounted, { let host_name = server.host_name()?; let uri = server.uri()?; let bundle = get_string_bundle(IMAP_MSG_STRING_BUNDLE)?; let message = get_formatted_string(&bundle, c"imapOAuth2Error", thin_vec![host_name])?; register_alert(message, uri)?; Ok(AuthErrorOutcome::ABORT) } /// Notify the user about a failure to authenticate against the given server /// using a password. /// /// The user is shown a modal with three options which define what further /// action needs to be taken: /// * retry: [`AuthErrorOutcome::RETRY`] is returned immediately. /// * enter a new password: the user is shown a prompt to enter a new password, /// after validating which [`AuthErrorOutcome::RETRY`] is returned to let the /// server it should try again with the new password. /// * cancel: [`AuthErrorOutcome::ABORT`] is returned immediately. pub fn notify_password_failure( server: RefPtr, ) -> Result where ServerT: UserInteractiveServer + RefCounted, { // If there isn't a password set on the server, there's no point asking the // user if they want to retry, so we skip straight to prompting for a new // one. let password = server.password()?; if password.is_empty() { let outcome = match prompt_for_password(server, String::new())? { // The prompt has been cancelled by the user, we should stop trying // to re-auth now. PasswordPromptResult::Cancelled => AuthErrorOutcome::ABORT, // The user has provided a password, let's try again using it. The // password might stil be empty, in which case it will likely fail // again, but if we're reaching this branch then the user has // explicitly asked us to try again. PasswordPromptResult::NewPassword => AuthErrorOutcome::RETRY, }; return Ok(outcome); } let host_name = server.host_name()?; let username = server.username()?; let display_name = server.display_name()?; let bundle = get_string_bundle(MESSENGER_STRING_BUNDLE)?; let message = get_formatted_string( &bundle, c"mailServerLoginFailed2", thin_vec![host_name, username], )?; // We need to make this (and all related strings) `nsString`s rather than // `CString`s because `ConfirmEx` expects them to be UTF-16 (`*const u16`, // rather than `*const c_char`). let message = nsString::from(&message); let title = get_formatted_string( &bundle, c"mailServerLoginFailedTitleWithAccount", thin_vec![display_name], )?; let title = nsString::from(&title); let retry_button = get_string(&bundle, c"mailServerLoginFailedRetryButton")?; let retry_button = nsString::from(&retry_button); let new_password_button = get_string(&bundle, c"mailServerLoginFailedEnterNewPasswordButton")?; let new_password_button = nsString::from(&new_password_button); let prompt_service = get_service::(c"@mozilla.org/prompter;1") .ok_or(nserror::NS_ERROR_UNEXPECTED)?; let button_flags = (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_0) + (nsIPrompt::BUTTON_TITLE_CANCEL * nsIPrompt::BUTTON_POS_1) + (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_2); let mut pressed_button_index: i32 = 0; let mut checkbox_check_state = false; // SAFETY: `aParent` is always allowed to be null, `aButton1Title` is unused // because `BUTTON_TITLE_IS_STRING` was not set for button 1, `aCheckMsg` is // null to avoid a checkbox, and the remaining values were safely // constructed above. unsafe { prompt_service.ConfirmEx( ptr::null(), // aParent title.as_ptr(), // aDialogTitle message.as_ptr(), // aText button_flags, // aButtonFlags retry_button.as_ptr(), // aButton0Title ptr::null(), // aButton1Title new_password_button.as_ptr(), // aButton2Title ptr::null(), // aCheckMsg &raw mut checkbox_check_state, // aCheckState &raw mut pressed_button_index, // retval ) } .to_result()?; // Figure out what to do next based on the user's input. let action = match pressed_button_index { // Retry button. 0 => AuthErrorOutcome::RETRY, // New password button. 2 => { let password = server.password()?; server.forget_password()?; match prompt_for_password(server, password)? { PasswordPromptResult::Cancelled => AuthErrorOutcome::ABORT, PasswordPromptResult::NewPassword => AuthErrorOutcome::RETRY, }; AuthErrorOutcome::RETRY } // Anything else, including the Cancel button. _ => AuthErrorOutcome::ABORT, }; Ok(action) } /// Prompt the user to enter a new password and return it. /// /// This ends up calling `GetPasswordWithUI` on the relevant `nsIMsg[...]Server` /// interface, which is expected to handle the storage of the password once the /// user validates the prompt. fn prompt_for_password( server: RefPtr, old_password: String, ) -> Result where ServerT: UserInteractiveServer + RefCounted, { let host_name = server.host_name()?; let username = server.username()?; let bundle = get_string_bundle(IMAP_MSG_STRING_BUNDLE)?; let title = get_formatted_string( &bundle, c"imapEnterPasswordPromptTitleWithUsername", thin_vec![username.clone()], )?; let message = get_formatted_string( &bundle, c"imapEnterServerPasswordPrompt", thin_vec![username, host_name], )?; server.prompt_for_password(message, title, old_password) }