use std::collections::BTreeSet;
use anyhow::{Context, Result};
use chrono::{DateTime, FixedOffset};
use io_msgraph::v1::{
rest::{
batch::MsgraphBatchRequest,
users::{
mail_folders::{MsgraphMailFolder, list::MsgraphMailFoldersListParams},
messages::{
MsgraphFlagStatus, MsgraphFollowupFlag, MsgraphImportance, MsgraphMessage,
MsgraphRecipient,
list::{MsgraphMessagesListParams, MsgraphMessagesListResponse},
},
},
},
send::{MsgraphSend, user_path},
};
use url::Url;
use crate::{
email::{
address::Address,
envelope::{Envelope, normalize_message_id},
flag::{Flag, FlagOp, IanaFlag},
mailbox::{Mailbox, MailboxRole},
search::{eval::sort_envelopes, query::SearchEmailsQuery},
},
msgraph::{client::MsgraphClient, search},
};
const ENVELOPE_SELECT: &str = "id,subject,from,toRecipients,receivedDateTime,isRead,isDraft,hasAttachments,internetMessageId,importance,flag,categories";
const WELL_KNOWN_FOLDERS: [(&str, MailboxRole); 6] = [
("inbox", MailboxRole::Inbox),
("sentitems", MailboxRole::Sent),
("drafts", MailboxRole::Drafts),
("deleteditems", MailboxRole::Trash),
("junkemail", MailboxRole::Junk),
("archive", MailboxRole::Archive),
];
impl MsgraphClient {
pub fn list_mailboxes(&mut self, _with_counts: bool) -> Result<Vec<Mailbox>> {
let params = MsgraphMailFoldersListParams {
top: Some(100),
..Default::default()
};
let folders = self.mail_folders_list(¶ms)?.response.value;
let roles = self.folder_roles()?;
Ok(folders
.into_iter()
.map(|folder| {
let role = roles
.iter()
.find(|(id, _)| *id == folder.id)
.map(|(_, role)| role.clone());
mailbox_from(folder, role)
})
.collect())
}
fn folder_roles(&mut self) -> Result<Vec<(String, MailboxRole)>> {
let user = user_path(&self.user_id);
let requests: Vec<_> = WELL_KNOWN_FOLDERS
.iter()
.map(|(name, _)| MsgraphBatchRequest {
id: name.to_string(),
method: "GET".into(),
url: format!("/{user}/mailFolders/{name}?$select=id"),
..Default::default()
})
.collect();
let mut roles = Vec::new();
for response in self.batch(&requests)?.response.responses {
let Some((_, role)) = WELL_KNOWN_FOLDERS
.iter()
.find(|(name, _)| *name == response.id)
else {
continue;
};
if let Ok(folder) = response.parse::<MsgraphMailFolder>() {
roles.push((folder.id, role.clone()));
}
}
Ok(roles)
}
pub fn list_envelopes(
&mut self,
mailbox: &str,
page: Option<u32>,
page_size: Option<u32>,
_with_attachment: bool,
) -> Result<Vec<Envelope>> {
let page = page.unwrap_or(1).max(1);
let (top, skip) = match page_size {
Some(size) => (Some(size), Some((page - 1) * size)),
None => (None, None),
};
let params = MsgraphMessagesListParams {
top,
skip,
select: Some(ENVELOPE_SELECT),
..Default::default()
};
let messages = self.messages_list(Some(mailbox), ¶ms)?.response.value;
Ok(messages.into_iter().map(envelope_from).collect())
}
pub fn search_envelopes(
&mut self,
mailbox: &str,
query: Option<&SearchEmailsQuery>,
page: Option<u32>,
page_size: Option<u32>,
with_attachment: bool,
) -> Result<Vec<Envelope>> {
let sort = query.and_then(|query| query.sort.as_deref());
let Some(filter) = query.and_then(|query| query.filter.as_ref()) else {
let mut envelopes = self.list_envelopes(mailbox, page, page_size, with_attachment)?;
sort_envelopes(&mut envelopes, sort);
return Ok(envelopes);
};
let search = search::filter_to_search(filter)?;
let params = MsgraphMessagesListParams {
top: page_size,
select: Some(ENVELOPE_SELECT),
search: Some(&search),
..Default::default()
};
let mut response = self.messages_list(Some(mailbox), ¶ms)?.response;
for _ in 1..page.unwrap_or(1).max(1) {
let Some(next) = response.next_link else {
return Ok(Vec::new());
};
let url = Url::parse(&next).context("Cannot parse the message paging link")?;
let coroutine = MsgraphSend::<MsgraphMessagesListResponse>::get(&self.auth, url);
response = self.run(coroutine)?.response;
}
let mut envelopes: Vec<Envelope> = response.value.into_iter().map(envelope_from).collect();
sort_envelopes(&mut envelopes, sort);
Ok(envelopes)
}
pub fn store_flags(
&mut self,
_mailbox: &str,
ids: &[&str],
flags: &[Flag],
op: FlagOp,
) -> Result<()> {
let patch = flag_patch(flags, op);
for id in ids {
self.message_update(id, &patch)?;
}
Ok(())
}
pub fn get_message(&mut self, mailbox: &str, id: &str, seen: bool) -> Result<Vec<u8>> {
let raw = self.message_get_raw(id)?.response;
if seen {
let seen = Flag::from_iana(IanaFlag::Seen);
self.store_flags(mailbox, &[id], &[seen], FlagOp::Add)?;
}
Ok(raw)
}
pub fn copy_messages(&mut self, _from: &str, to: &str, ids: &[&str]) -> Result<usize> {
for id in ids {
self.message_copy(id, to)?;
}
Ok(ids.len())
}
pub fn move_messages(&mut self, _from: &str, to: &str, ids: &[&str]) -> Result<usize> {
for id in ids {
self.message_move(id, to)?;
}
Ok(ids.len())
}
pub fn delete_messages(&mut self, ids: &[&str]) -> Result<()> {
for id in ids {
self.message_delete(id)?;
}
Ok(())
}
pub fn send_message(&mut self, raw: Vec<u8>) -> Result<()> {
self.mail_send_mime(&raw)?;
Ok(())
}
}
fn mailbox_from(folder: MsgraphMailFolder, role: Option<MailboxRole>) -> Mailbox {
Mailbox {
id: folder.id,
name: folder.display_name,
role,
total: folder.total_item_count,
unread: folder.unread_item_count,
}
}
fn envelope_from(message: MsgraphMessage) -> Envelope {
let flags = flags_from_message(&message);
let message_id = message
.internet_message_id
.as_deref()
.and_then(normalize_message_id);
let date = message
.received_date_time
.as_deref()
.and_then(parse_rfc3339);
let from = message.from.map(address_from).into_iter().collect();
let to = message
.to_recipients
.into_iter()
.map(address_from)
.collect();
Envelope {
id: message.id,
message_id,
in_reply_to: Vec::new(),
flags,
subject: message.subject.unwrap_or_default(),
from,
to,
date,
size: 0,
has_attachment: message.has_attachments,
}
}
fn flag_patch(flags: &[Flag], op: FlagOp) -> MsgraphMessage {
let mut patch = MsgraphMessage::default();
match op {
FlagOp::Set => {
patch.is_read = Some(flags.iter().any(Flag::is_seen));
patch.flag = Some(followup(flags.iter().any(Flag::is_flagged)));
patch.importance = Some(importance(flags.iter().any(Flag::is_important)));
patch.categories = flags
.iter()
.filter(|flag| flag.iana().is_none())
.map(|flag| flag.raw().to_string())
.collect();
}
FlagOp::Add => {
for flag in flags {
if flag.is_seen() {
patch.is_read = Some(true);
}
if flag.is_flagged() {
patch.flag = Some(followup(true));
}
if flag.is_important() {
patch.importance = Some(MsgraphImportance::High);
}
}
}
FlagOp::Remove => {
for flag in flags {
if flag.is_seen() {
patch.is_read = Some(false);
}
if flag.is_flagged() {
patch.flag = Some(followup(false));
}
if flag.is_important() {
patch.importance = Some(MsgraphImportance::Normal);
}
}
}
}
patch
}
fn flags_from_message(message: &MsgraphMessage) -> BTreeSet<Flag> {
let mut flags = BTreeSet::new();
if message.is_read == Some(true) {
flags.insert(Flag::from_iana(IanaFlag::Seen));
}
if matches!(
message.flag.as_ref().and_then(|flag| flag.flag_status),
Some(MsgraphFlagStatus::Flagged)
) {
flags.insert(Flag::from_iana(IanaFlag::Flagged));
}
if message.importance == Some(MsgraphImportance::High) {
flags.insert(Flag::from_iana(IanaFlag::Important));
}
if message.is_draft == Some(true) {
flags.insert(Flag::from_iana(IanaFlag::Draft));
}
for category in &message.categories {
flags.insert(Flag::from_raw(category.clone()));
}
flags
}
fn address_from(recipient: MsgraphRecipient) -> Address {
Address {
name: recipient.email_address.name,
email: recipient.email_address.address.unwrap_or_default(),
}
}
fn followup(flagged: bool) -> MsgraphFollowupFlag {
let flag_status = if flagged {
MsgraphFlagStatus::Flagged
} else {
MsgraphFlagStatus::NotFlagged
};
MsgraphFollowupFlag {
flag_status: Some(flag_status),
}
}
fn importance(important: bool) -> MsgraphImportance {
if important {
MsgraphImportance::High
} else {
MsgraphImportance::Normal
}
}
fn parse_rfc3339(raw: &str) -> Option<DateTime<FixedOffset>> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
DateTime::parse_from_rfc3339(trimmed).ok()
}