#![allow(clippy::unwrap_used, clippy::expect_used)]
mod helpers;
use std::time::Duration;
use daaki_imap::types::{FetchAttr, SequenceSet};
use daaki_imap::{IdleEvent, ImapConnection, SessionState, TlsMode};
use helpers::{skip_unless, IMAPS_PORT, TIMEOUT};
use tokio_util::sync::CancellationToken;
const GMAIL_HOST: &str = "imap.gmail.com";
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_connect_and_login() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = ImapConnection::connect(GMAIL_HOST, IMAPS_PORT, TlsMode::Implicit, TIMEOUT)
.await
.unwrap();
assert_eq!(conn.session_state(), SessionState::NotAuthenticated);
conn.login(&gmail.username, &gmail.password, TIMEOUT)
.await
.unwrap();
assert_eq!(conn.session_state(), SessionState::Authenticated);
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_capabilities() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
let caps = conn.capabilities();
assert!(
caps.iter()
.any(|c| matches!(c, daaki_imap::types::Capability::Imap4Rev1)),
"expected IMAP4rev1 capability, got: {caps:?}"
);
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_list_mailboxes() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
let mailboxes = conn.list("", "*", TIMEOUT).await.unwrap();
assert!(
mailboxes
.iter()
.any(|m| m.name.as_str().eq_ignore_ascii_case("INBOX")),
"expected INBOX in LIST response"
);
let has_gmail_hierarchy = mailboxes
.iter()
.any(|m| m.name.as_str().starts_with("[Gmail]"));
assert!(
has_gmail_hierarchy,
"expected [Gmail]/ hierarchy in LIST response: {:?}",
mailboxes.iter().map(|m| &m.name).collect::<Vec<_>>()
);
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_select_inbox() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
let selected = conn.select("INBOX", TIMEOUT).await.unwrap();
assert!(
selected.uid_validity.unwrap_or(0) > 0,
"UIDVALIDITY must be > 0"
);
assert_eq!(conn.session_state(), SessionState::Selected);
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_fetch_recent_message() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
let selected = conn.select("INBOX", TIMEOUT).await.unwrap();
if selected.exists == 0 {
eprintln!("skipping fetch test: INBOX is empty");
conn.logout().await.unwrap();
return;
}
let fetches = conn
.fetch(
&SequenceSet::new("1").unwrap(),
&[
FetchAttr::Uid,
FetchAttr::Flags,
FetchAttr::Envelope,
FetchAttr::Rfc822Size,
FetchAttr::InternalDate,
],
TIMEOUT,
)
.await
.unwrap();
assert!(!fetches.is_empty(), "FETCH should return at least one item");
let msg = &fetches[0];
assert!(msg.uid.is_some(), "UID should be present");
assert!(msg.envelope.is_some(), "ENVELOPE should be present");
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_search_all() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
conn.select("INBOX", TIMEOUT).await.unwrap();
let result = conn.search("ALL", TIMEOUT).await.unwrap();
let _ = result;
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_uid_search() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
conn.select("INBOX", TIMEOUT).await.unwrap();
let result = conn.uid_search("ALL", TIMEOUT).await.unwrap();
let _ = result;
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_namespace() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
let ns = conn.namespace(TIMEOUT).await.unwrap();
assert!(
!ns.personal.is_empty(),
"expected at least one personal namespace"
);
let ns = &ns.personal[0];
assert_eq!(
ns.delimiter,
Some('/'),
"expected '/' delimiter for Gmail personal namespace"
);
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_noop() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
conn.noop(TIMEOUT).await.unwrap();
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_idle_and_cancel() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
conn.select("INBOX", TIMEOUT).await.unwrap();
let cancel = CancellationToken::new();
let cancel_clone = cancel.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(500)).await;
cancel_clone.cancel();
});
let event = conn.idle(TIMEOUT, cancel).await.unwrap();
assert_eq!(event, IdleEvent::Cancelled);
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_examine_inbox() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
let selected = conn.examine("INBOX", TIMEOUT).await.unwrap();
assert!(
selected.uid_validity.unwrap_or(0) > 0,
"UIDVALIDITY must be > 0"
);
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_compress_deflate() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
match conn.compress(TIMEOUT).await {
Ok(()) => {
let mailboxes = conn.list("", "*", TIMEOUT).await.unwrap();
assert!(
mailboxes
.iter()
.any(|m| m.name.as_str().eq_ignore_ascii_case("INBOX")),
"LIST over compressed connection should still find INBOX"
);
let selected = conn.select("INBOX", TIMEOUT).await.unwrap();
assert!(selected.uid_validity.unwrap_or(0) > 0);
conn.noop(TIMEOUT).await.unwrap();
}
Err(daaki_imap::Error::MissingCapability(_)) => {
eprintln!("skipping: Gmail does not advertise COMPRESS=DEFLATE");
}
Err(e) => panic!("unexpected error from COMPRESS: {e:?}"),
}
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_uid_fetch() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
let selected = conn.select("INBOX", TIMEOUT).await.unwrap();
if selected.exists == 0 {
eprintln!("skipping uid_fetch test: INBOX is empty");
conn.logout().await.unwrap();
return;
}
let uids = conn.uid_search("ALL", TIMEOUT).await.unwrap();
if uids.ids.is_empty() {
eprintln!("skipping uid_fetch test: no UIDs from search");
conn.logout().await.unwrap();
return;
}
let uid = uids.ids[0];
let uid_set = uid.to_string();
let fetches = conn
.uid_fetch(
&SequenceSet::new(&uid_set).unwrap(),
&[FetchAttr::Uid, FetchAttr::Flags, FetchAttr::Envelope],
TIMEOUT,
)
.await
.unwrap();
assert!(
!fetches.is_empty(),
"UID FETCH should return at least one item"
);
for msg in &fetches {
assert!(msg.uid.is_some(), "every UID FETCH result must have a UID");
}
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_fetch_bodystructure() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
let selected = conn.select("INBOX", TIMEOUT).await.unwrap();
if selected.exists == 0 {
eprintln!("skipping bodystructure test: INBOX is empty");
conn.logout().await.unwrap();
return;
}
let fetches = conn
.fetch(
&SequenceSet::new("1").unwrap(),
&[FetchAttr::Uid, FetchAttr::BodyStructure],
TIMEOUT,
)
.await
.unwrap();
assert!(!fetches.is_empty());
let msg = &fetches[0];
assert!(
msg.body_structure.is_some(),
"BODYSTRUCTURE should be present"
);
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_status_inbox() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
let items = conn
.status("INBOX", "(MESSAGES UNSEEN UIDNEXT UIDVALIDITY)", TIMEOUT)
.await
.unwrap()
.items;
assert!(!items.is_empty(), "STATUS should return at least one item");
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_id() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
match conn
.id(
&[("name", Some("daaki-test")), ("version", Some("0.1.0"))],
TIMEOUT,
)
.await
{
Ok(server_id) => {
assert!(
!server_id.is_empty(),
"expected server to return ID information"
);
}
Err(daaki_imap::Error::MissingCapability(_)) => {
eprintln!("skipping: Gmail does not advertise ID capability");
}
Err(e) => panic!("unexpected error from ID: {e:?}"),
}
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_enable_condstore() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
match conn.enable(&["CONDSTORE"], TIMEOUT).await {
Ok(enabled) => {
assert!(
enabled.iter().any(|s| s.eq_ignore_ascii_case("CONDSTORE")),
"expected CONDSTORE to be enabled: {enabled:?}"
);
}
Err(daaki_imap::Error::MissingCapability(_)) => {
eprintln!("skipping: Gmail does not advertise ENABLE capability");
}
Err(e) => panic!("unexpected error from ENABLE: {e:?}"),
}
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_lsub() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
let subscribed = conn.lsub("", "*", TIMEOUT).await.unwrap();
assert!(
subscribed
.iter()
.any(|m| m.name.as_str().eq_ignore_ascii_case("INBOX")),
"expected INBOX in LSUB response: {:?}",
subscribed.iter().map(|m| &m.name).collect::<Vec<_>>()
);
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_unselect() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
conn.select("INBOX", TIMEOUT).await.unwrap();
assert_eq!(conn.session_state(), SessionState::Selected);
match conn.unselect(TIMEOUT).await {
Ok(()) => {
assert_eq!(conn.session_state(), SessionState::Authenticated);
}
Err(daaki_imap::Error::MissingCapability(_)) => {
eprintln!("skipping: Gmail does not advertise UNSELECT capability");
}
Err(e) => panic!("unexpected error from UNSELECT: {e:?}"),
}
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_sort_by_date() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
conn.select("INBOX", TIMEOUT).await.unwrap();
match conn.sort("DATE", "UTF-8", "ALL", TIMEOUT).await {
Ok(result) => {
let _ = result;
}
Err(daaki_imap::Error::MissingCapability(_)) => {
eprintln!("skipping: Gmail does not advertise SORT capability");
}
Err(e) => panic!("unexpected error from SORT: {e:?}"),
}
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_thread_references() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
conn.select("INBOX", TIMEOUT).await.unwrap();
match conn.thread("REFERENCES", "UTF-8", "ALL", TIMEOUT).await {
Ok(threads) => {
let _ = threads;
}
Err(daaki_imap::Error::MissingCapability(_)) => {
eprintln!("skipping: Gmail does not advertise THREAD=REFERENCES capability");
}
Err(e) => panic!("unexpected error from THREAD: {e:?}"),
}
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_select_condstore() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
match conn
.select_with(
"INBOX",
&daaki_imap::types::SelectOptions::condstore(),
TIMEOUT,
)
.await
{
Ok(selected) => {
assert!(
selected.uid_validity.unwrap_or(0) > 0,
"UIDVALIDITY must be > 0"
);
assert!(
selected.highest_mod_seq.is_some(),
"expected HIGHESTMODSEQ in CONDSTORE SELECT response"
);
}
Err(daaki_imap::Error::MissingCapability(_)) => {
eprintln!("skipping: Gmail does not advertise CONDSTORE capability");
}
Err(e) => panic!("unexpected error from SELECT CONDSTORE: {e:?}"),
}
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_search_unseen() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
conn.select("INBOX", TIMEOUT).await.unwrap();
let result = conn.search("UNSEEN", TIMEOUT).await.unwrap();
let _ = result;
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_search_since_date() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
conn.select("INBOX", TIMEOUT).await.unwrap();
let result = conn.search("SINCE 01-Jan-2020", TIMEOUT).await.unwrap();
let _ = result;
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_fetch_body_header() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
let selected = conn.select("INBOX", TIMEOUT).await.unwrap();
if selected.exists == 0 {
eprintln!("skipping fetch body header test: INBOX is empty");
conn.logout().await.unwrap();
return;
}
let fetches = conn
.fetch(
&SequenceSet::new("1").unwrap(),
&[FetchAttr::BodySection {
peek: true,
section: Some("HEADER".into()),
partial: None,
}],
TIMEOUT,
)
.await
.unwrap();
assert!(!fetches.is_empty());
let msg = &fetches[0];
assert!(
!msg.body_sections.is_empty(),
"expected at least one body section"
);
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_idle_timeout() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
conn.select("INBOX", TIMEOUT).await.unwrap();
let cancel = CancellationToken::new();
let event = conn.idle(Duration::from_secs(2), cancel).await.unwrap();
assert_eq!(event, IdleEvent::Timeout);
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_select_multiple_mailboxes() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
conn.select("INBOX", TIMEOUT).await.unwrap();
assert_eq!(conn.session_state(), SessionState::Selected);
let selected = conn.select("[Gmail]/All Mail", TIMEOUT).await.unwrap();
assert!(selected.uid_validity.unwrap_or(0) > 0);
assert_eq!(conn.session_state(), SessionState::Selected);
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_check() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
conn.select("INBOX", TIMEOUT).await.unwrap();
conn.check(TIMEOUT).await.unwrap();
conn.logout().await.unwrap();
}
#[tokio::test]
#[ignore = "requires Gmail credentials"]
async fn gmail_list_inbox_only() {
let creds = helpers::load_credentials();
let gmail = skip_unless!(creds.gmail, "Gmail");
let conn = helpers::connect_provider(GMAIL_HOST, gmail).await;
let mailboxes = conn.list("", "INBOX", TIMEOUT).await.unwrap();
assert_eq!(
mailboxes.len(),
1,
"LIST for 'INBOX' should return exactly one result: {mailboxes:?}"
);
assert!(mailboxes[0].name.as_str().eq_ignore_ascii_case("INBOX"));
conn.logout().await.unwrap();
}