use anyhow::Result;
use crate::gmail::auth;
use crate::gmail::client::GmailClient;
use crate::gmail::error::GmailError;
pub fn create_client() -> Result<GmailClient> {
create_client_for(None)
}
pub fn create_client_for(account: Option<&str>) -> Result<GmailClient> {
create_client_from(auth::load_credentials_for(account)?)
}
pub fn create_client_from(credentials: auth::GmailCredentials) -> Result<GmailClient> {
GmailClient::from_credentials(&credentials)
}
pub fn print_shadowing_notice() {
eprintln!(
"note: legacy Gmail credentials are now shadowed for invocations without --account. \
Run `gmail account import-legacy` to migrate any other legacy account, or \
`gmail auth logout` to remove the old credentials once every mailbox you use is \
migrated."
);
}
pub(crate) fn with_modify_scope_hint(err: anyhow::Error) -> anyhow::Error {
if is_insufficient_scope(&err) {
err.context(
"This Gmail account is authorised read-only, and this command needs the \
`gmail.modify` scope. Re-run `omni-dev gmail auth login --modify` (adding \
`--account NAME` for a named account) to grant it.",
)
} else {
err
}
}
pub(crate) fn is_insufficient_scope(err: &anyhow::Error) -> bool {
err.downcast_ref::<GmailError>().is_some_and(|gmail| {
matches!(gmail, GmailError::ApiRequestFailed { status: 403, .. })
&& gmail.reason() == Some("insufficientPermissions")
})
}
pub(crate) fn label_ids_contain_any<T: AsRef<str>>(label_ids: &[String], targets: &[T]) -> bool {
targets
.iter()
.any(|target| label_ids.iter().any(|label| label == target.as_ref()))
}
#[cfg(test)]
mod label_membership_tests {
use super::label_ids_contain_any;
#[test]
fn label_ids_contain_any_matches_str_targets() {
let label_ids = vec!["INBOX".to_string(), "UNREAD".to_string()];
assert!(label_ids_contain_any(&label_ids, &["UNREAD", "STARRED"]));
assert!(!label_ids_contain_any(&label_ids, &["SPAM", "TRASH"]));
}
#[test]
fn label_ids_contain_any_matches_string_targets() {
let label_ids = vec!["SPAM".to_string()];
let targets = vec!["SPAM".to_string(), "TRASH".to_string()];
assert!(label_ids_contain_any(&label_ids, &targets));
}
#[test]
fn label_ids_contain_any_is_false_for_empty_targets() {
let label_ids = vec!["INBOX".to_string()];
let targets: Vec<String> = Vec::new();
assert!(!label_ids_contain_any(&label_ids, &targets));
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::gmail::auth::{GmailCredentials, GmailScope};
use crate::utils::secret::Secret;
#[test]
fn create_client_from_uses_gmail_api_host() {
let creds = GmailCredentials {
client_id: "client".to_string(),
client_secret: Secret::new("secret"),
refresh_token: Secret::new("refresh"),
scope: GmailScope::ReadOnly,
};
let client = create_client_from(creds).unwrap();
assert_eq!(client.base_url(), "https://gmail.googleapis.com");
}
#[test]
fn create_client_for_named_account_uses_that_accounts_credentials() {
let guard = crate::gmail::test_support::EnvGuard::take();
let dir = guard.clear_credentials();
let settings_path = dir.path().join(".omni-dev").join("settings.json");
crate::utils::settings::Settings::upsert_gmail_account(
&settings_path,
"work",
&[
(
"client_id",
serde_json::Value::String("work-id".to_string()),
),
(
"client_secret",
serde_json::Value::String("work-secret".to_string()),
),
(
"refresh_token",
serde_json::Value::String("work-refresh".to_string()),
),
],
)
.unwrap();
let client = create_client_for(Some("work")).unwrap();
assert_eq!(client.base_url(), "https://gmail.googleapis.com");
}
fn api_error(status: u16, reason: Option<&str>) -> anyhow::Error {
GmailError::ApiRequestFailed {
status,
body: "Insufficient Permission".to_string(),
reason: reason.map(str::to_string),
}
.into()
}
#[test]
fn with_modify_scope_hint_explains_an_insufficient_scope_403() {
let err = with_modify_scope_hint(api_error(403, Some("insufficientPermissions")));
assert!(
err.to_string().contains("gmail auth login --modify"),
"{err}"
);
let cause = err.source().unwrap().to_string();
assert!(cause.contains("HTTP 403"), "{cause}");
}
#[test]
fn with_modify_scope_hint_leaves_other_errors_alone() {
for err in [
api_error(403, Some("rateLimitExceeded")),
api_error(403, None),
api_error(400, Some("insufficientPermissions")),
anyhow::anyhow!("network down"),
] {
let before = err.to_string();
let after = with_modify_scope_hint(err);
assert_eq!(after.to_string(), before);
assert!(after.source().is_none());
}
}
#[test]
fn is_insufficient_scope_matches_only_the_scope_403() {
assert!(is_insufficient_scope(&api_error(
403,
Some("insufficientPermissions")
)));
assert!(is_insufficient_scope(
&api_error(403, Some("insufficientPermissions")).context("while inserting")
));
for err in [
api_error(403, Some("rateLimitExceeded")),
api_error(403, None),
api_error(400, Some("insufficientPermissions")),
anyhow::anyhow!("network down"),
] {
assert!(!is_insufficient_scope(&err), "{err}");
}
}
#[test]
fn create_client_for_unknown_account_errors() {
let guard = crate::gmail::test_support::EnvGuard::take();
let _dir = guard.clear_credentials();
let err = create_client_for(Some("bogus")).unwrap_err();
assert!(err.to_string().contains("unknown Gmail account 'bogus'"));
}
}