Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "imap"
|
||||
version = "0.16.22"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imap_proto = { path = "../imap-proto" }
|
||||
types = { path = "../types" }
|
||||
directory = { path = "../directory" }
|
||||
trc = { path = "../trc" }
|
||||
store = { path = "../store" }
|
||||
common = { path = "../common" }
|
||||
email = { path = "../email" }
|
||||
nlp = { path = "../nlp" }
|
||||
utils = { path = "../utils" }
|
||||
registry = { path = "../registry" }
|
||||
mail-parser = { version = "0.11", features = ["full_encoding"] }
|
||||
tokio = { version = "1.53", features = ["full"] }
|
||||
tokio-rustls = { version = "0.26", default-features = false, features = ["aws_lc_rs", "tls12"] }
|
||||
parking_lot = "0.12"
|
||||
ahash = { version = "0.8" }
|
||||
md5 = "0.8.1"
|
||||
rand = "0.10.2"
|
||||
compact_str = "0.10.0"
|
||||
|
||||
[features]
|
||||
test_mode = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,519 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{iter::Peekable, sync::Arc, vec::IntoIter};
|
||||
|
||||
use common::{
|
||||
KV_RATE_LIMIT_IMAP,
|
||||
network::{SessionResult, SessionStream},
|
||||
};
|
||||
use imap_proto::{
|
||||
Command, ResponseCode, ResponseType, StatusResponse,
|
||||
receiver::{self, Request},
|
||||
};
|
||||
use trc::SecurityEvent;
|
||||
|
||||
use super::{SelectedMailbox, Session, SessionData, State};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn ingest(&mut self, bytes: &[u8]) -> SessionResult {
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::RawInput),
|
||||
SpanId = self.session_id,
|
||||
Size = bytes.len(),
|
||||
Contents = trc::Value::from_maybe_string(bytes),
|
||||
);
|
||||
|
||||
let mut bytes = bytes.iter();
|
||||
let mut requests = Vec::with_capacity(2);
|
||||
let mut needs_literal = None;
|
||||
let mut has_expunge = false;
|
||||
|
||||
loop {
|
||||
match self.receiver.parse(&mut bytes) {
|
||||
Ok(request) => match self.is_allowed(request).await {
|
||||
Ok(request) => {
|
||||
has_expunge |=
|
||||
matches!(request.command, Command::Expunge(_) | Command::Close);
|
||||
requests.push(request);
|
||||
}
|
||||
Err(err) => {
|
||||
if !self.write_error(err).await {
|
||||
return SessionResult::Close;
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(receiver::Error::NeedsMoreData) => {
|
||||
break;
|
||||
}
|
||||
Err(receiver::Error::NeedsLiteral { size }) => {
|
||||
needs_literal = size.into();
|
||||
break;
|
||||
}
|
||||
Err(receiver::Error::Error { response }) => {
|
||||
// Check for port scanners
|
||||
if matches!(
|
||||
(&self.state, response.key(trc::Key::Code)),
|
||||
(
|
||||
State::NotAuthenticated { .. },
|
||||
Some(trc::Value::String(v))
|
||||
) if v == "PARSE"
|
||||
) {
|
||||
match self.server.is_scanner_fail2banned(self.remote_addr).await {
|
||||
Ok(true) => {
|
||||
trc::event!(
|
||||
Security(SecurityEvent::ScanBan),
|
||||
SpanId = self.session_id,
|
||||
RemoteIp = self.remote_addr,
|
||||
Reason = "Invalid IMAP command",
|
||||
);
|
||||
|
||||
return SessionResult::Close;
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(err) => {
|
||||
trc::error!(
|
||||
err.span_id(self.session_id)
|
||||
.details("Failed to check for fail2ban")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !self.write_error(response).await {
|
||||
return SessionResult::Close;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut requests = requests.into_iter().peekable();
|
||||
while let Some(request) = requests.next() {
|
||||
let result = match request.command {
|
||||
Command::List | Command::Lsub => self
|
||||
.handle_list(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Select | Command::Examine => self
|
||||
.handle_select(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Create => self
|
||||
.handle_create(group_requests(&mut requests, vec![request]))
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Delete => self
|
||||
.handle_delete(group_requests(&mut requests, vec![request]))
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Rename => self
|
||||
.handle_rename(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Status => self
|
||||
.handle_status(group_requests(&mut requests, vec![request]))
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Append => self
|
||||
.handle_append(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Close => self
|
||||
.handle_close(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Unselect => self
|
||||
.handle_unselect(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Expunge(is_uid) => self
|
||||
.handle_expunge(request, is_uid)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Search(is_uid) => self
|
||||
.handle_search(request, false, is_uid)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Fetch(_) => self
|
||||
.handle_fetch(group_requests(&mut requests, vec![request]))
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Store(is_uid) => self
|
||||
.handle_store(request, is_uid, !has_expunge)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Copy(is_uid) => self
|
||||
.handle_copy_move(request, false, is_uid)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Move(is_uid) => self
|
||||
.handle_copy_move(request, true, is_uid)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Sort(is_uid) => self
|
||||
.handle_search(request, true, is_uid)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Thread(is_uid) => self
|
||||
.handle_thread(request, is_uid)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Idle => self
|
||||
.handle_idle(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Subscribe => self
|
||||
.handle_subscribe(request, true)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Unsubscribe => self
|
||||
.handle_subscribe(request, false)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Namespace => self
|
||||
.handle_namespace(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Authenticate => Box::pin(self.handle_authenticate(request))
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Login => Box::pin(self.handle_login(request))
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Capability => self
|
||||
.handle_capability(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Enable => self
|
||||
.handle_enable(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::StartTls => self
|
||||
.write_bytes(
|
||||
StatusResponse::ok("Begin TLS negotiation now")
|
||||
.with_tag(request.tag)
|
||||
.into_bytes(),
|
||||
)
|
||||
.await
|
||||
.map(|_| SessionResult::UpgradeTls),
|
||||
Command::Noop => self
|
||||
.handle_noop(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Check => self
|
||||
.handle_noop(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Logout => self
|
||||
.handle_logout(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Close),
|
||||
Command::SetAcl => self
|
||||
.handle_set_acl(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::DeleteAcl => self
|
||||
.handle_set_acl(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::GetAcl => self
|
||||
.handle_get_acl(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::ListRights => self
|
||||
.handle_list_rights(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::MyRights => self
|
||||
.handle_my_rights(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::GetQuota => self
|
||||
.handle_get_quota(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::GetQuotaRoot => self
|
||||
.handle_get_quota_root(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Unauthenticate => self
|
||||
.handle_unauthenticate(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::Id => self
|
||||
.handle_id(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::GetJmapAccess => self
|
||||
.handle_jmap_access(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
Command::UidBatches => self
|
||||
.handle_uidbatches(request)
|
||||
.await
|
||||
.map(|_| SessionResult::Continue),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(SessionResult::Continue) => (),
|
||||
Ok(result) => return result,
|
||||
Err(err) => {
|
||||
if !self.write_error(err).await {
|
||||
return SessionResult::Close;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(needs_literal) = needs_literal
|
||||
&& let Err(err) = self
|
||||
.write_bytes(format!("+ Ready for {} bytes.\r\n", needs_literal).into_bytes())
|
||||
.await
|
||||
{
|
||||
self.write_error(err).await;
|
||||
return SessionResult::Close;
|
||||
}
|
||||
|
||||
SessionResult::Continue
|
||||
}
|
||||
}
|
||||
|
||||
pub fn group_requests(
|
||||
requests: &mut Peekable<IntoIter<Request<Command>>>,
|
||||
mut grouped_requests: Vec<Request<Command>>,
|
||||
) -> Vec<Request<Command>> {
|
||||
let last_command = grouped_requests.last().unwrap().command;
|
||||
loop {
|
||||
match requests.peek() {
|
||||
Some(request) if request.command == last_command => {
|
||||
grouped_requests.push(requests.next().unwrap());
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
grouped_requests
|
||||
}
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
async fn is_allowed(&self, request: Request<Command>) -> trc::Result<Request<Command>> {
|
||||
let state = &self.state;
|
||||
// Rate limit request
|
||||
if let State::Authenticated { data } | State::Selected { data, .. } = state
|
||||
&& let Some(rate) = &self.server.core.imap.rate_requests
|
||||
&& data
|
||||
.server
|
||||
.in_memory_store()
|
||||
.is_rate_allowed(
|
||||
KV_RATE_LIMIT_IMAP,
|
||||
&data.account_id.to_be_bytes(),
|
||||
rate,
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Err(trc::LimitEvent::TooManyRequests.into_err());
|
||||
}
|
||||
|
||||
match &request.command {
|
||||
Command::Capability | Command::Noop | Command::Logout | Command::Id => Ok(request),
|
||||
Command::StartTls => {
|
||||
if !self.is_tls {
|
||||
if self.instance.acceptor.is_tls() {
|
||||
Ok(request)
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("TLS is not available.")
|
||||
.id(request.tag))
|
||||
}
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Already in TLS mode.")
|
||||
.id(request.tag))
|
||||
}
|
||||
}
|
||||
Command::Authenticate => {
|
||||
if let State::NotAuthenticated { .. } = state {
|
||||
if self.is_tls || self.server.core.imap.allow_plain_auth {
|
||||
Ok(request)
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Cannot authenticate over plain-text.")
|
||||
.code(ResponseCode::PrivacyRequired)
|
||||
.id(request.tag))
|
||||
}
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Already authenticated.")
|
||||
.id(request.tag))
|
||||
}
|
||||
}
|
||||
Command::Login => {
|
||||
if let State::NotAuthenticated { .. } = state {
|
||||
if self.is_tls || self.server.core.imap.allow_plain_auth {
|
||||
Ok(request)
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("LOGIN is disabled on the clear-text port.")
|
||||
.id(request.tag))
|
||||
}
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Already authenticated.")
|
||||
.id(request.tag))
|
||||
}
|
||||
}
|
||||
Command::Enable
|
||||
| Command::Select
|
||||
| Command::Examine
|
||||
| Command::Create
|
||||
| Command::Delete
|
||||
| Command::Rename
|
||||
| Command::Subscribe
|
||||
| Command::Unsubscribe
|
||||
| Command::List
|
||||
| Command::Lsub
|
||||
| Command::Namespace
|
||||
| Command::Status
|
||||
| Command::Append
|
||||
| Command::Idle
|
||||
| Command::SetAcl
|
||||
| Command::DeleteAcl
|
||||
| Command::GetAcl
|
||||
| Command::ListRights
|
||||
| Command::MyRights
|
||||
| Command::Unauthenticate
|
||||
| Command::GetQuota
|
||||
| Command::GetQuotaRoot
|
||||
| Command::GetJmapAccess => {
|
||||
if let State::Authenticated { .. } | State::Selected { .. } = state {
|
||||
Ok(request)
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Not authenticated.")
|
||||
.id(request.tag))
|
||||
}
|
||||
}
|
||||
Command::Close
|
||||
| Command::Unselect
|
||||
| Command::Expunge(_)
|
||||
| Command::Search(_)
|
||||
| Command::Fetch(_)
|
||||
| Command::Store(_)
|
||||
| Command::Copy(_)
|
||||
| Command::Move(_)
|
||||
| Command::Check
|
||||
| Command::Sort(_)
|
||||
| Command::Thread(_)
|
||||
| Command::UidBatches => match state {
|
||||
State::Selected { mailbox, .. } => {
|
||||
// RFC 9586 forbids message numbers once UIDONLY is enabled
|
||||
if self.is_uidonly && request.command.requires_uid() {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Message numbers are not allowed once UIDONLY is enabled.")
|
||||
.code(ResponseCode::UidRequired)
|
||||
.ctx(trc::Key::Type, ResponseType::Bad)
|
||||
.id(request.tag))
|
||||
} else if mailbox.is_select
|
||||
|| !matches!(
|
||||
request.command,
|
||||
Command::Store(_) | Command::Expunge(_) | Command::Move(_),
|
||||
)
|
||||
{
|
||||
Ok(request)
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Not permitted in EXAMINE state.")
|
||||
.id(request.tag))
|
||||
}
|
||||
}
|
||||
State::Authenticated { .. } => Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("No mailbox is selected.")
|
||||
.ctx(trc::Key::Type, ResponseType::Bad)
|
||||
.id(request.tag)),
|
||||
State::NotAuthenticated { .. } => Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Not authenticated.")
|
||||
.id(request.tag)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> State<T> {
|
||||
pub fn auth_failures(&self) -> u32 {
|
||||
match self {
|
||||
State::NotAuthenticated { auth_failures, .. } => *auth_failures,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_data(&self) -> Arc<SessionData<T>> {
|
||||
match self {
|
||||
State::Authenticated { data } => data.clone(),
|
||||
State::Selected { data, .. } => data.clone(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mailbox_state(&self) -> (Arc<SessionData<T>>, Arc<SelectedMailbox>) {
|
||||
match self {
|
||||
State::Selected { data, mailbox, .. } => (data.clone(), mailbox.clone()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn session_mailbox_state(&self) -> (Arc<SessionData<T>>, Option<Arc<SelectedMailbox>>) {
|
||||
match self {
|
||||
State::Authenticated { data } => (data.clone(), None),
|
||||
State::Selected { data, mailbox, .. } => (data.clone(), mailbox.clone().into()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_data(&self) -> (Arc<SessionData<T>>, Arc<SelectedMailbox>) {
|
||||
match self {
|
||||
State::Selected { data, mailbox } => (data.clone(), mailbox.clone()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_task<F, R, P>(&self, params: P, fnc: F) -> trc::Result<()>
|
||||
where
|
||||
F: FnOnce(P, &super::SessionData<T>) -> R + Send + 'static,
|
||||
P: Send + Sync + 'static,
|
||||
R: std::future::Future<Output = trc::Result<()>> + Send + 'static,
|
||||
{
|
||||
let data = self.session_data();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = fnc(params, &data).await {
|
||||
let _ = data.write_error(err).await;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
matches!(self, State::Authenticated { .. } | State::Selected { .. })
|
||||
}
|
||||
|
||||
pub fn close_mailbox(&self) -> bool {
|
||||
matches!(self, State::Selected { .. })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{Account, MailboxId, MailboxSync, Session, SessionData};
|
||||
use crate::core::Mailbox;
|
||||
use ahash::AHashMap;
|
||||
use common::{
|
||||
auth::AccessToken,
|
||||
network::{SessionStream, limiter::InFlight},
|
||||
sharing::EffectiveAcl,
|
||||
};
|
||||
use email::{
|
||||
cache::{MessageCacheFetch, email::MessageCacheAccess, mailbox::MailboxCacheAccess},
|
||||
mailbox::INBOX_ID,
|
||||
};
|
||||
use imap_proto::protocol::list::Attribute;
|
||||
use parking_lot::Mutex;
|
||||
use std::{collections::BTreeMap, sync::atomic::Ordering};
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{acl::Acl, collection::Collection, keyword::Keyword, special_use::SpecialUse};
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
pub async fn new(
|
||||
session: &Session<T>,
|
||||
access_token: AccessToken,
|
||||
in_flight: Option<InFlight>,
|
||||
) -> trc::Result<Self> {
|
||||
let mut session = SessionData {
|
||||
stream_tx: session.stream_tx.clone(),
|
||||
server: session.server.clone(),
|
||||
account_id: access_token.account_id(),
|
||||
session_id: session.session_id,
|
||||
mailboxes: Mutex::new(vec![]),
|
||||
state: access_token.state().into(),
|
||||
remote_addr: session.remote_addr,
|
||||
access_token,
|
||||
in_flight,
|
||||
};
|
||||
|
||||
// Fetch mailboxes for the main account
|
||||
let mut mailboxes = vec![
|
||||
session
|
||||
.fetch_account_mailboxes(session.account_id, None, &session.access_token, None)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.unwrap(),
|
||||
];
|
||||
|
||||
// Fetch shared mailboxes
|
||||
for &account_id in session.access_token.shared_accounts(Collection::Mailbox) {
|
||||
let prefix: String = format!(
|
||||
"{}/{}",
|
||||
session.server.core.email.shared_folder,
|
||||
session
|
||||
.server
|
||||
.account(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.name()
|
||||
);
|
||||
mailboxes.push(
|
||||
session
|
||||
.fetch_account_mailboxes(account_id, prefix.into(), &session.access_token, None)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
session.mailboxes = Mutex::new(mailboxes);
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
async fn fetch_account_mailboxes(
|
||||
&self,
|
||||
account_id: u32,
|
||||
mailbox_prefix: Option<String>,
|
||||
access_token: &AccessToken,
|
||||
current_state: Option<u64>,
|
||||
) -> trc::Result<Option<Account>> {
|
||||
let cache = self
|
||||
.server
|
||||
.get_cached_messages(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if current_state.is_some_and(|state| state == cache.last_change_id) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let shared_mailbox_ids = if access_token.is_member(account_id) {
|
||||
None
|
||||
} else {
|
||||
cache.shared_mailboxes(access_token, Acl::Read).into()
|
||||
};
|
||||
|
||||
// Build special uses
|
||||
let mut special_uses = AHashMap::new();
|
||||
for mailbox in &cache.mailboxes.items {
|
||||
if shared_mailbox_ids
|
||||
.as_ref()
|
||||
.is_none_or(|ids| ids.contains(mailbox.document_id))
|
||||
&& !matches!(mailbox.role, SpecialUse::None)
|
||||
{
|
||||
special_uses.insert(mailbox.role, mailbox.document_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Build account
|
||||
let mut account = Account {
|
||||
account_id,
|
||||
prefix: mailbox_prefix,
|
||||
mailbox_names: BTreeMap::new(),
|
||||
mailbox_state: AHashMap::with_capacity(cache.mailboxes.items.len()),
|
||||
last_change_id: cache.last_change_id,
|
||||
};
|
||||
|
||||
for mailbox in &cache.mailboxes.items {
|
||||
if shared_mailbox_ids
|
||||
.as_ref()
|
||||
.is_some_and(|ids| !ids.contains(mailbox.document_id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build mailbox path and map it to its effective id
|
||||
let mailbox_name = if let Some(prefix) = &account.prefix {
|
||||
let mut name = String::with_capacity(prefix.len() + mailbox.path.len() + 1);
|
||||
name.push_str(prefix.as_str());
|
||||
name.push('/');
|
||||
name.push_str(mailbox.path.as_str());
|
||||
name
|
||||
} else {
|
||||
mailbox.path.clone()
|
||||
};
|
||||
let effective_mailbox_id = self
|
||||
.server
|
||||
.core
|
||||
.email
|
||||
.default_folders
|
||||
.iter()
|
||||
.find(|f| f.name == mailbox_name || f.aliases.iter().any(|a| a == &mailbox_name))
|
||||
.and_then(|f| special_uses.get(&f.special_use))
|
||||
.copied()
|
||||
.unwrap_or(mailbox.document_id);
|
||||
account
|
||||
.mailbox_names
|
||||
.insert(mailbox_name, effective_mailbox_id);
|
||||
account.mailbox_state.insert(
|
||||
mailbox.document_id,
|
||||
Mailbox {
|
||||
has_children: cache
|
||||
.mailboxes
|
||||
.items
|
||||
.iter()
|
||||
.any(|child| child.parent_id == mailbox.document_id),
|
||||
is_subscribed: mailbox.subscribers.contains(&access_token.account_id()),
|
||||
special_use: match mailbox.role {
|
||||
SpecialUse::Trash => Some(Attribute::Trash),
|
||||
SpecialUse::Junk => Some(Attribute::Junk),
|
||||
SpecialUse::Drafts => Some(Attribute::Drafts),
|
||||
SpecialUse::Archive => Some(Attribute::Archive),
|
||||
SpecialUse::Sent => Some(Attribute::Sent),
|
||||
SpecialUse::Important => Some(Attribute::Important),
|
||||
SpecialUse::Memos => Some(Attribute::Memos),
|
||||
SpecialUse::Scheduled => Some(Attribute::Scheduled),
|
||||
SpecialUse::Snoozed => Some(Attribute::Snoozed),
|
||||
_ => None,
|
||||
},
|
||||
total_messages: cache.in_mailbox(mailbox.document_id).count() as u64,
|
||||
total_unseen: cache
|
||||
.in_mailbox_without_keyword(mailbox.document_id, &Keyword::Seen)
|
||||
.count() as u64,
|
||||
total_deleted: cache
|
||||
.in_mailbox_with_keyword(mailbox.document_id, &Keyword::Deleted)
|
||||
.count() as u64,
|
||||
uid_validity: mailbox.uid_validity as u64,
|
||||
uid_next: self
|
||||
.get_uid_next(&MailboxId {
|
||||
account_id,
|
||||
mailbox_id: mailbox.document_id,
|
||||
})
|
||||
.await
|
||||
.caused_by(trc::location!())? as u64,
|
||||
total_deleted_storage: None,
|
||||
size: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(account.into())
|
||||
}
|
||||
|
||||
pub async fn synchronize_mailboxes(
|
||||
&self,
|
||||
return_changes: bool,
|
||||
) -> trc::Result<Option<MailboxSync>> {
|
||||
let mut changes = if return_changes {
|
||||
MailboxSync::default().into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Obtain access token
|
||||
let access_token = self
|
||||
.refresh_access_token()
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let state = access_token.state();
|
||||
|
||||
// Shared mailboxes might have changed
|
||||
let mut added_accounts = Vec::new();
|
||||
if self.state.load(Ordering::Relaxed) != state {
|
||||
// Remove unlinked shared accounts
|
||||
let mut added_account_ids = Vec::new();
|
||||
{
|
||||
let mut mailboxes = self.mailboxes.lock();
|
||||
let mut new_accounts = Vec::with_capacity(mailboxes.len());
|
||||
let has_access_to = access_token
|
||||
.shared_accounts(Collection::Mailbox)
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
for account in mailboxes.drain(..) {
|
||||
if access_token.is_account_id(account.account_id)
|
||||
|| has_access_to.contains(&account.account_id)
|
||||
{
|
||||
new_accounts.push(account);
|
||||
} else {
|
||||
// Add unshared mailboxes to deleted list
|
||||
if let Some(changes) = &mut changes {
|
||||
for (mailbox_name, _) in account.mailbox_names {
|
||||
changes.deleted.push(mailbox_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add new shared account ids
|
||||
for account_id in has_access_to {
|
||||
if !new_accounts
|
||||
.iter()
|
||||
.skip(1)
|
||||
.any(|m| m.account_id == account_id)
|
||||
{
|
||||
added_account_ids.push(account_id);
|
||||
}
|
||||
}
|
||||
*mailboxes = new_accounts;
|
||||
}
|
||||
|
||||
// Fetch mailboxes for each new shared account
|
||||
for account_id in added_account_ids {
|
||||
let prefix: String = format!(
|
||||
"{}/{}",
|
||||
self.server.core.email.shared_folder,
|
||||
self.server
|
||||
.account(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.name()
|
||||
);
|
||||
added_accounts.push(
|
||||
self.fetch_account_mailboxes(account_id, prefix.into(), &access_token, None)
|
||||
.await?
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
// Update state
|
||||
self.state.store(state, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
// Fetch mailbox changes for all accounts
|
||||
let mut changed_accounts = Vec::new();
|
||||
let account_states = self
|
||||
.mailboxes
|
||||
.lock()
|
||||
.iter()
|
||||
.map(|m| (m.account_id, m.prefix.clone(), m.last_change_id))
|
||||
.collect::<Vec<_>>();
|
||||
for (account_id, prefix, last_state) in account_states {
|
||||
if let Some(changed_account) = self
|
||||
.fetch_account_mailboxes(account_id, prefix, &access_token, last_state.into())
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
changed_accounts.push(changed_account);
|
||||
}
|
||||
}
|
||||
|
||||
// Update mailboxes
|
||||
if !changed_accounts.is_empty() || !added_accounts.is_empty() {
|
||||
let mut mailboxes = self.mailboxes.lock();
|
||||
|
||||
for changed_account in changed_accounts {
|
||||
if let Some(pos) = mailboxes
|
||||
.iter()
|
||||
.position(|a| a.account_id == changed_account.account_id)
|
||||
{
|
||||
// Add changes and deletions
|
||||
if let Some(changes) = &mut changes {
|
||||
let old_account = &mailboxes[pos];
|
||||
let new_account = &changed_account;
|
||||
|
||||
// Add new mailboxes
|
||||
for (mailbox_name, mailbox_id) in new_account.mailbox_names.iter() {
|
||||
if let Some(old_mailbox) = old_account.mailbox_state.get(mailbox_id) {
|
||||
if let Some(mailbox) = new_account.mailbox_state.get(mailbox_id)
|
||||
&& (mailbox.total_messages != old_mailbox.total_messages
|
||||
|| mailbox.total_unseen != old_mailbox.total_unseen)
|
||||
{
|
||||
changes.changed.push(mailbox_name.clone());
|
||||
}
|
||||
} else {
|
||||
changes.added.push(mailbox_name.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Add deleted mailboxes
|
||||
for (mailbox_name, mailbox_id) in &old_account.mailbox_names {
|
||||
if !new_account.mailbox_state.contains_key(mailbox_id) {
|
||||
changes.deleted.push(mailbox_name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mailboxes[pos] = changed_account;
|
||||
} else {
|
||||
// Add newly shared accounts
|
||||
if let Some(changes) = &mut changes {
|
||||
changes
|
||||
.added
|
||||
.extend(changed_account.mailbox_names.keys().cloned());
|
||||
}
|
||||
|
||||
mailboxes.push(changed_account);
|
||||
}
|
||||
}
|
||||
|
||||
if !added_accounts.is_empty() {
|
||||
// Add newly shared accounts
|
||||
if let Some(changes) = &mut changes {
|
||||
for added_account in &added_accounts {
|
||||
changes
|
||||
.added
|
||||
.extend(added_account.mailbox_names.keys().cloned());
|
||||
}
|
||||
}
|
||||
mailboxes.extend(added_accounts);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
pub fn get_mailbox_by_name(&self, mailbox_name: &str) -> Option<MailboxId> {
|
||||
let is_inbox = mailbox_name.eq_ignore_ascii_case("inbox");
|
||||
for account in self.mailboxes.lock().iter() {
|
||||
if account
|
||||
.prefix
|
||||
.as_ref()
|
||||
.is_none_or(|p| mailbox_name.starts_with(p.as_str()))
|
||||
{
|
||||
for (mailbox_name_, mailbox_id_) in account.mailbox_names.iter() {
|
||||
if (!is_inbox && mailbox_name_ == mailbox_name)
|
||||
|| (is_inbox && *mailbox_id_ == INBOX_ID)
|
||||
{
|
||||
return MailboxId {
|
||||
account_id: account.account_id,
|
||||
mailbox_id: *mailbox_id_,
|
||||
}
|
||||
.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_mailbox_by_id(&self, account_id: u32, mailbox_id: u32) -> Option<MailboxId> {
|
||||
for account in self.mailboxes.lock().iter() {
|
||||
if account.account_id == account_id
|
||||
&& account.mailbox_names.values().any(|id| *id == mailbox_id)
|
||||
{
|
||||
return MailboxId {
|
||||
account_id,
|
||||
mailbox_id,
|
||||
}
|
||||
.into();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn check_mailbox_acl(
|
||||
&self,
|
||||
account_id: u32,
|
||||
document_id: u32,
|
||||
item: Acl,
|
||||
) -> trc::Result<bool> {
|
||||
let access_token = self.refresh_access_token().await?;
|
||||
Ok(access_token.is_member(account_id)
|
||||
|| self
|
||||
.server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::Mailbox,
|
||||
document_id,
|
||||
))
|
||||
.await
|
||||
.and_then(|mailbox| {
|
||||
if let Some(mailbox) = mailbox {
|
||||
Ok(Some(
|
||||
mailbox
|
||||
.unarchive::<email::mailbox::Mailbox>()?
|
||||
.acls
|
||||
.effective_acl(&access_token)
|
||||
.contains(item),
|
||||
))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
trc::ImapEvent::Error
|
||||
.caused_by(trc::location!())
|
||||
.details("Mailbox no longer exists.")
|
||||
})?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{
|
||||
ImapUidToId, Mailbox, MailboxId, MailboxState, NextMailboxState, SelectedMailbox, SessionData,
|
||||
};
|
||||
use crate::core::ImapId;
|
||||
use ahash::AHashMap;
|
||||
use common::network::SessionStream;
|
||||
use email::cache::MessageCacheFetch;
|
||||
use imap_proto::protocol::{Sequence, expunge, select::Exists};
|
||||
use std::collections::BTreeMap;
|
||||
use store::{ValueKey, roaring::RoaringBitmap, write::ValueClass};
|
||||
use trc::AddContext;
|
||||
use types::{collection::Collection, field::MailboxField};
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
pub async fn fetch_messages(
|
||||
&self,
|
||||
mailbox: &MailboxId,
|
||||
current_state: Option<u64>,
|
||||
) -> trc::Result<Option<MailboxState>> {
|
||||
let cached_messages = self
|
||||
.server
|
||||
.get_cached_messages(mailbox.account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if current_state.is_some_and(|state| state == cached_messages.emails.change_id) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Obtain UID next and assign UIDs
|
||||
let uid_map = cached_messages
|
||||
.emails
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
item.mailboxes.iter().find_map(|m| {
|
||||
if m.mailbox_id == mailbox.mailbox_id {
|
||||
Some((m.uid, item.document_id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<BTreeMap<u32, u32>>();
|
||||
let mut uid_max = 0;
|
||||
let mut id_to_imap = AHashMap::with_capacity(uid_map.len());
|
||||
let mut uid_to_id = AHashMap::with_capacity(uid_map.len());
|
||||
|
||||
for (seqnum, (uid, message_id)) in uid_map.into_iter().enumerate() {
|
||||
if uid > uid_max {
|
||||
uid_max = uid;
|
||||
}
|
||||
id_to_imap.insert(
|
||||
message_id,
|
||||
ImapId {
|
||||
uid,
|
||||
seqnum: seqnum as u32 + 1,
|
||||
},
|
||||
);
|
||||
uid_to_id.insert(uid, message_id);
|
||||
}
|
||||
|
||||
Ok(Some(MailboxState {
|
||||
total_messages: id_to_imap.len(),
|
||||
id_to_imap,
|
||||
uid_to_id,
|
||||
uid_max,
|
||||
modseq: cached_messages.emails.change_id,
|
||||
next_state: None,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn synchronize_messages(&self, mailbox: &SelectedMailbox) -> trc::Result<u64> {
|
||||
// Obtain current modseq
|
||||
let mut current_modseq = mailbox.state.lock().modseq;
|
||||
if let Some(new_state) = self
|
||||
.fetch_messages(&mailbox.id, current_modseq.into())
|
||||
.await?
|
||||
{
|
||||
// Synchronize messages
|
||||
let mut current_state = mailbox.state.lock();
|
||||
current_modseq = new_state.modseq;
|
||||
|
||||
// Add missing uids
|
||||
let mut deletions = current_state
|
||||
.next_state
|
||||
.take()
|
||||
.map(|state| state.deletions)
|
||||
.unwrap_or_default();
|
||||
let mut id_to_imap = AHashMap::with_capacity(current_state.id_to_imap.len());
|
||||
for (id, imap_id) in std::mem::take(&mut current_state.id_to_imap) {
|
||||
if !new_state.uid_to_id.contains_key(&imap_id.uid) {
|
||||
// Add to deletions
|
||||
deletions.push(imap_id);
|
||||
|
||||
// Invalidate entries
|
||||
current_state.uid_to_id.remove(&imap_id.uid);
|
||||
} else {
|
||||
id_to_imap.insert(id, imap_id);
|
||||
}
|
||||
}
|
||||
current_state.id_to_imap = id_to_imap;
|
||||
|
||||
// Update state
|
||||
current_state.modseq = new_state.modseq;
|
||||
current_state.next_state = Some(Box::new(NextMailboxState {
|
||||
next_state: new_state,
|
||||
deletions,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(current_modseq)
|
||||
}
|
||||
|
||||
pub async fn write_mailbox_changes(
|
||||
&self,
|
||||
mailbox: &SelectedMailbox,
|
||||
use_vanished: bool,
|
||||
) -> trc::Result<u64> {
|
||||
// Resync mailbox
|
||||
let modseq = self.synchronize_messages(mailbox).await?;
|
||||
let mut buf = Vec::new();
|
||||
{
|
||||
let mut current_state = mailbox.state.lock();
|
||||
if let Some(next_state) = current_state.next_state.take() {
|
||||
if !next_state.deletions.is_empty() {
|
||||
let mut ids = next_state
|
||||
.deletions
|
||||
.into_iter()
|
||||
.map(|id| if use_vanished { id.uid } else { id.seqnum })
|
||||
.collect::<Vec<u32>>();
|
||||
ids.sort_unstable();
|
||||
expunge::Response { use_vanished, ids }.serialize_to(&mut buf);
|
||||
}
|
||||
if !buf.is_empty()
|
||||
|| next_state
|
||||
.next_state
|
||||
.uid_max
|
||||
.saturating_sub(current_state.uid_max)
|
||||
> 0
|
||||
{
|
||||
Exists {
|
||||
total_messages: next_state.next_state.total_messages,
|
||||
}
|
||||
.serialize(&mut buf);
|
||||
}
|
||||
*current_state = next_state.next_state;
|
||||
}
|
||||
}
|
||||
if !buf.is_empty() {
|
||||
self.write_bytes(buf).await?;
|
||||
}
|
||||
|
||||
Ok(modseq)
|
||||
}
|
||||
|
||||
pub async fn get_uid_next(&self, mailbox: &MailboxId) -> trc::Result<u32> {
|
||||
self.server
|
||||
.core
|
||||
.storage
|
||||
.data
|
||||
.get_counter(ValueKey {
|
||||
account_id: mailbox.account_id,
|
||||
collection: Collection::Mailbox.into(),
|
||||
document_id: mailbox.mailbox_id,
|
||||
class: ValueClass::Property(MailboxField::UidCounter.into()),
|
||||
})
|
||||
.await
|
||||
.map(|v| (v + 1) as u32)
|
||||
}
|
||||
|
||||
pub fn mailbox_state(&self, mailbox: &MailboxId) -> Option<Mailbox> {
|
||||
self.mailboxes
|
||||
.lock()
|
||||
.iter()
|
||||
.find(|m| m.account_id == mailbox.account_id)
|
||||
.and_then(|m| m.mailbox_state.get(&mailbox.mailbox_id))
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedMailbox {
|
||||
pub async fn sequence_to_ids(
|
||||
&self,
|
||||
sequence: &Sequence,
|
||||
is_uid: bool,
|
||||
) -> trc::Result<AHashMap<u32, ImapId>> {
|
||||
if !sequence.is_saved_search() {
|
||||
let mut ids = AHashMap::new();
|
||||
let state = self.state.lock();
|
||||
let (id_to_imap, uid_max, total_messages) =
|
||||
if let Some(next) = state.next_state.as_ref() {
|
||||
(
|
||||
&next.next_state.id_to_imap,
|
||||
next.next_state.uid_max,
|
||||
next.next_state.total_messages,
|
||||
)
|
||||
} else {
|
||||
(&state.id_to_imap, state.uid_max, state.total_messages)
|
||||
};
|
||||
|
||||
if is_uid {
|
||||
if !id_to_imap.is_empty() {
|
||||
for (id, imap_id) in id_to_imap {
|
||||
if sequence.contains(imap_id.uid, uid_max) {
|
||||
ids.insert(*id, *imap_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if !id_to_imap.is_empty() {
|
||||
for (id, imap_id) in id_to_imap {
|
||||
if sequence.contains(imap_id.seqnum, total_messages as u32) {
|
||||
ids.insert(*id, *imap_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ids)
|
||||
} else {
|
||||
let saved_ids = self.get_saved_search().await.ok_or_else(|| {
|
||||
trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("No saved search found.")
|
||||
})?;
|
||||
let mut ids = AHashMap::with_capacity(saved_ids.len());
|
||||
let state = self.state.lock();
|
||||
|
||||
for imap_id in saved_ids.iter() {
|
||||
if let Some(id) = state.uid_to_id.get(&imap_id.uid) {
|
||||
ids.insert(*id, *imap_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ids)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uids_in_range(&self, min: Option<u32>, max: Option<u32>) -> RoaringBitmap {
|
||||
let state = self.state.lock();
|
||||
let id_to_imap = state
|
||||
.next_state
|
||||
.as_ref()
|
||||
.map_or(&state.id_to_imap, |next| &next.next_state.id_to_imap);
|
||||
|
||||
RoaringBitmap::from_iter(id_to_imap.iter().filter_map(|(id, imap_id)| {
|
||||
(min.is_none_or(|min| imap_id.uid >= min) && max.is_none_or(|max| imap_id.uid <= max))
|
||||
.then_some(*id)
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn seqnum_to_uid(&self, seqnum: u32) -> Option<u32> {
|
||||
let state = self.state.lock();
|
||||
state
|
||||
.next_state
|
||||
.as_ref()
|
||||
.map_or(&state.id_to_imap, |next| &next.next_state.id_to_imap)
|
||||
.values()
|
||||
.find(|imap_id| imap_id.seqnum == seqnum)
|
||||
.map(|imap_id| imap_id.uid)
|
||||
}
|
||||
|
||||
pub fn uids_descending(&self) -> Vec<u32> {
|
||||
let mut uids = {
|
||||
let state = self.state.lock();
|
||||
state
|
||||
.next_state
|
||||
.as_ref()
|
||||
.map_or(&state.uid_to_id, |next| &next.next_state.uid_to_id)
|
||||
.keys()
|
||||
.copied()
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
uids.sort_unstable_by(|a, b| b.cmp(a));
|
||||
uids
|
||||
}
|
||||
|
||||
pub async fn sequence_expand_missing(&self, sequence: &Sequence, is_uid: bool) -> Vec<u32> {
|
||||
let mut deleted_ids = Vec::new();
|
||||
if !sequence.is_saved_search() {
|
||||
let state = self.state.lock();
|
||||
if is_uid {
|
||||
for uid in sequence.expand(state.uid_max) {
|
||||
if !state.uid_to_id.contains_key(&uid) {
|
||||
deleted_ids.push(uid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for seqnum in sequence.expand(state.total_messages as u32) {
|
||||
if seqnum > state.total_messages as u32 {
|
||||
deleted_ids.push(seqnum);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(saved_ids) = self.get_saved_search().await {
|
||||
let state = self.state.lock();
|
||||
for id in saved_ids.iter() {
|
||||
if !state.uid_to_id.contains_key(&id.uid) {
|
||||
deleted_ids.push(if is_uid { id.uid } else { id.seqnum });
|
||||
}
|
||||
}
|
||||
}
|
||||
deleted_ids.sort_unstable();
|
||||
deleted_ids
|
||||
}
|
||||
|
||||
pub fn append_messages(&self, ids: Vec<ImapUidToId>, modseq: Option<u64>) {
|
||||
let mut mailbox = self.state.lock();
|
||||
if modseq.unwrap_or(0) > mailbox.modseq {
|
||||
let mut uid_max = 0;
|
||||
for id in ids {
|
||||
mailbox.total_messages += 1;
|
||||
let seqnum = mailbox.total_messages as u32;
|
||||
mailbox.uid_to_id.insert(id.uid, id.uid);
|
||||
mailbox.id_to_imap.insert(
|
||||
id.id,
|
||||
ImapId {
|
||||
uid: id.uid,
|
||||
seqnum,
|
||||
},
|
||||
);
|
||||
uid_max = id.uid;
|
||||
}
|
||||
mailbox.uid_max = uid_max;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use ahash::AHashMap;
|
||||
use common::{
|
||||
Inner, Server,
|
||||
auth::AccessToken,
|
||||
network::{ServerInstance, SessionStream, limiter::InFlight},
|
||||
};
|
||||
use imap_proto::{
|
||||
Command,
|
||||
protocol::{ProtocolVersion, list::Attribute},
|
||||
receiver::Receiver,
|
||||
};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
net::IpAddr,
|
||||
sync::{Arc, atomic::AtomicU32},
|
||||
};
|
||||
use tokio::{
|
||||
io::{ReadHalf, WriteHalf},
|
||||
sync::watch,
|
||||
};
|
||||
use trc::AddContext;
|
||||
|
||||
pub mod client;
|
||||
pub mod mailbox;
|
||||
pub mod message;
|
||||
pub mod session;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ImapSessionManager {
|
||||
pub inner: Arc<Inner>,
|
||||
}
|
||||
|
||||
impl ImapSessionManager {
|
||||
pub fn new(inner: Arc<Inner>) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Session<T: SessionStream> {
|
||||
pub server: Server,
|
||||
pub instance: Arc<ServerInstance>,
|
||||
pub receiver: Receiver<Command>,
|
||||
pub version: ProtocolVersion,
|
||||
pub state: State<T>,
|
||||
pub is_tls: bool,
|
||||
pub is_condstore: bool,
|
||||
pub is_qresync: bool,
|
||||
pub is_utf8: bool,
|
||||
pub is_objectid: bool,
|
||||
pub is_uidonly: bool,
|
||||
pub stream_rx: ReadHalf<T>,
|
||||
pub stream_tx: Arc<tokio::sync::Mutex<WriteHalf<T>>>,
|
||||
pub in_flight: InFlight,
|
||||
pub remote_addr: IpAddr,
|
||||
pub session_id: u64,
|
||||
}
|
||||
|
||||
pub struct SessionData<T: SessionStream> {
|
||||
pub account_id: u32,
|
||||
pub access_token: AccessToken,
|
||||
pub server: Server,
|
||||
pub session_id: u64,
|
||||
pub mailboxes: parking_lot::Mutex<Vec<Account>>,
|
||||
pub stream_tx: Arc<tokio::sync::Mutex<WriteHalf<T>>>,
|
||||
pub state: AtomicU32,
|
||||
pub remote_addr: IpAddr,
|
||||
pub in_flight: Option<InFlight>,
|
||||
}
|
||||
|
||||
pub struct SelectedMailbox {
|
||||
pub id: MailboxId,
|
||||
pub state: parking_lot::Mutex<MailboxState>,
|
||||
pub saved_search: parking_lot::Mutex<SavedSearch>,
|
||||
pub is_select: bool,
|
||||
pub is_condstore: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
|
||||
pub struct MailboxId {
|
||||
pub account_id: u32,
|
||||
pub mailbox_id: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Account {
|
||||
pub account_id: u32,
|
||||
pub prefix: Option<String>,
|
||||
pub mailbox_names: BTreeMap<String, u32>,
|
||||
pub mailbox_state: AHashMap<u32, Mailbox>,
|
||||
pub last_change_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Mailbox {
|
||||
pub has_children: bool,
|
||||
pub is_subscribed: bool,
|
||||
pub special_use: Option<Attribute>,
|
||||
pub total_messages: u64,
|
||||
pub total_unseen: u64,
|
||||
pub total_deleted: u64,
|
||||
pub total_deleted_storage: Option<u64>,
|
||||
pub uid_validity: u64,
|
||||
pub uid_next: u64,
|
||||
pub size: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MailboxState {
|
||||
pub uid_max: u32,
|
||||
pub id_to_imap: AHashMap<u32, ImapId>,
|
||||
pub uid_to_id: AHashMap<u32, u32>,
|
||||
pub total_messages: usize,
|
||||
pub modseq: u64,
|
||||
pub next_state: Option<Box<NextMailboxState>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NextMailboxState {
|
||||
pub next_state: MailboxState,
|
||||
pub deletions: Vec<ImapId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ImapId {
|
||||
pub uid: u32,
|
||||
pub seqnum: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MailboxSync {
|
||||
pub added: Vec<String>,
|
||||
pub changed: Vec<String>,
|
||||
pub deleted: Vec<String>,
|
||||
}
|
||||
|
||||
pub enum SavedSearch {
|
||||
InFlight {
|
||||
rx: watch::Receiver<Arc<Vec<ImapId>>>,
|
||||
},
|
||||
Results {
|
||||
items: Arc<Vec<ImapId>>,
|
||||
},
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct ImapUidToId {
|
||||
pub uid: u32,
|
||||
pub id: u32,
|
||||
}
|
||||
|
||||
pub enum State<T: SessionStream> {
|
||||
NotAuthenticated {
|
||||
auth_failures: u32,
|
||||
},
|
||||
Authenticated {
|
||||
data: Arc<SessionData<T>>,
|
||||
},
|
||||
Selected {
|
||||
data: Arc<SessionData<T>>,
|
||||
mailbox: Arc<SelectedMailbox>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<T: SessionStream> State<T> {
|
||||
pub fn try_replace_stream_tx<U: SessionStream>(
|
||||
self,
|
||||
new_stream: Arc<tokio::sync::Mutex<WriteHalf<U>>>,
|
||||
) -> Option<State<U>> {
|
||||
match self {
|
||||
State::NotAuthenticated { auth_failures } => {
|
||||
State::NotAuthenticated { auth_failures }.into()
|
||||
}
|
||||
State::Authenticated { data } => {
|
||||
Arc::try_unwrap(data).ok().map(|data| State::Authenticated {
|
||||
data: Arc::new(data.replace_stream_tx(new_stream)),
|
||||
})
|
||||
}
|
||||
State::Selected { data, mailbox } => {
|
||||
Arc::try_unwrap(data).ok().map(|data| State::Selected {
|
||||
data: Arc::new(data.replace_stream_tx(new_stream)),
|
||||
mailbox,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
pub async fn refresh_access_token(&self) -> trc::Result<AccessToken> {
|
||||
self.server
|
||||
.access_token(self.account_id)
|
||||
.await
|
||||
.and_then(|inner| {
|
||||
AccessToken::renew(inner, self.access_token.credential_id(), self.remote_addr)
|
||||
})
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
|
||||
pub fn replace_stream_tx<U: SessionStream>(
|
||||
self,
|
||||
new_stream: Arc<tokio::sync::Mutex<WriteHalf<U>>>,
|
||||
) -> SessionData<U> {
|
||||
SessionData {
|
||||
account_id: self.account_id,
|
||||
server: self.server,
|
||||
session_id: self.session_id,
|
||||
mailboxes: self.mailboxes,
|
||||
stream_tx: new_stream,
|
||||
state: self.state,
|
||||
in_flight: self.in_flight,
|
||||
access_token: self.access_token,
|
||||
remote_addr: self.remote_addr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MailboxState {
|
||||
pub fn map_result_id(&self, document_id: u32, is_uid: bool) -> Option<(u32, ImapId)> {
|
||||
if let Some(imap_id) = self.id_to_imap.get(&document_id) {
|
||||
Some((if is_uid { imap_id.uid } else { imap_id.seqnum }, *imap_id))
|
||||
} else if is_uid {
|
||||
self.next_state.as_ref().and_then(|s| {
|
||||
s.next_state
|
||||
.id_to_imap
|
||||
.get(&document_id)
|
||||
.map(|imap_id| (imap_id.uid, *imap_id))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{ImapSessionManager, Session, State};
|
||||
use crate::{
|
||||
GREETING_WITH_TLS, GREETING_WITH_TLS_LOGIN_DISABLED, GREETING_WITHOUT_TLS,
|
||||
GREETING_WITHOUT_TLS_LOGIN_DISABLED,
|
||||
};
|
||||
use common::{
|
||||
BuildServer,
|
||||
network::{SessionData, SessionManager, SessionResult, SessionStream, stream::NullIo},
|
||||
};
|
||||
use imap_proto::{
|
||||
protocol::{ProtocolVersion, SerializeResponse},
|
||||
receiver::Receiver,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio_rustls::server::TlsStream;
|
||||
|
||||
impl SessionManager for ImapSessionManager {
|
||||
#[allow(clippy::manual_async_fn)]
|
||||
fn handle<T: SessionStream>(
|
||||
self,
|
||||
session: SessionData<T>,
|
||||
) -> impl std::future::Future<Output = ()> + Send {
|
||||
async move {
|
||||
if let Ok(mut session) = Session::new(session, self).await
|
||||
&& session.handle_conn().await
|
||||
&& session.instance.acceptor.is_tls()
|
||||
&& let Ok(mut session) = session.into_tls().await
|
||||
{
|
||||
session.handle_conn().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::manual_async_fn)]
|
||||
fn shutdown(&self) -> impl std::future::Future<Output = ()> + Send {
|
||||
async {}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_conn(&mut self) -> bool {
|
||||
let mut buf = vec![0; 8192];
|
||||
let mut shutdown_rx = self.instance.shutdown_rx.clone();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = tokio::time::timeout(
|
||||
if !matches!(self.state, State::NotAuthenticated {..}) {
|
||||
self.server.core.imap.timeout_auth
|
||||
} else {
|
||||
self.server.core.imap.timeout_unauth
|
||||
},
|
||||
self.stream_rx.read(&mut buf)) => {
|
||||
match result {
|
||||
Ok(Ok(bytes_read)) => {
|
||||
if bytes_read > 0 {
|
||||
match self.ingest(&buf[..bytes_read]).await {
|
||||
SessionResult::Continue => (),
|
||||
SessionResult::UpgradeTls => {
|
||||
return true;
|
||||
}
|
||||
SessionResult::Close => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
trc::event!(
|
||||
Network(trc::NetworkEvent::Closed),
|
||||
SpanId = self.session_id,
|
||||
CausedBy = trc::location!()
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
Ok(Err(err)) => {
|
||||
trc::event!(
|
||||
Network(trc::NetworkEvent::ReadError),
|
||||
SpanId = self.session_id,
|
||||
Reason = err.to_string(),
|
||||
CausedBy = trc::location!()
|
||||
);
|
||||
break;
|
||||
},
|
||||
Err(_) => {
|
||||
trc::event!(
|
||||
Network(trc::NetworkEvent::Timeout),
|
||||
SpanId = self.session_id,
|
||||
CausedBy = trc::location!()
|
||||
);
|
||||
self.write_bytes(&b"* BYE Connection timed out.\r\n"[..]).await.ok();
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
_ = shutdown_rx.changed() => {
|
||||
trc::event!(
|
||||
Network(trc::NetworkEvent::Closed),
|
||||
SpanId = self.session_id,
|
||||
Reason = "Server shutting down",
|
||||
CausedBy = trc::location!()
|
||||
);
|
||||
self.write_bytes(&b"* BYE Server shutting down.\r\n"[..]).await.ok();
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn new(
|
||||
mut session: SessionData<T>,
|
||||
manager: ImapSessionManager,
|
||||
) -> Result<Session<T>, ()> {
|
||||
// Write greeting
|
||||
let is_tls = session.stream.is_tls();
|
||||
let server = manager.inner.build_server();
|
||||
let offer_tls = !is_tls && session.instance.acceptor.is_tls();
|
||||
let allow_auth = is_tls || server.core.imap.allow_plain_auth;
|
||||
let greeting = match (offer_tls, allow_auth) {
|
||||
(true, true) => &GREETING_WITH_TLS,
|
||||
(true, false) => &GREETING_WITH_TLS_LOGIN_DISABLED,
|
||||
(false, true) => &GREETING_WITHOUT_TLS,
|
||||
(false, false) => &GREETING_WITHOUT_TLS_LOGIN_DISABLED,
|
||||
};
|
||||
|
||||
if let Err(err) = session.stream.write_all(greeting).await {
|
||||
trc::event!(
|
||||
Network(trc::NetworkEvent::WriteError),
|
||||
Reason = err.to_string(),
|
||||
SpanId = session.session_id,
|
||||
Details = "Failed to write to stream"
|
||||
);
|
||||
return Err(());
|
||||
}
|
||||
let _ = session.stream.flush().await;
|
||||
|
||||
// Split stream into read and write halves
|
||||
let (stream_rx, stream_tx) = tokio::io::split(session.stream);
|
||||
|
||||
Ok(Session {
|
||||
receiver: Receiver::with_max_request_size(server.core.imap.max_request_size),
|
||||
version: ProtocolVersion::Rev1,
|
||||
state: State::NotAuthenticated { auth_failures: 0 },
|
||||
is_tls,
|
||||
is_condstore: false,
|
||||
is_qresync: false,
|
||||
is_utf8: false,
|
||||
is_objectid: false,
|
||||
is_uidonly: false,
|
||||
server,
|
||||
instance: session.instance,
|
||||
session_id: session.session_id,
|
||||
in_flight: session.in_flight,
|
||||
remote_addr: session.remote_ip,
|
||||
stream_rx,
|
||||
stream_tx: Arc::new(tokio::sync::Mutex::new(stream_tx)),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn into_tls(self) -> Result<Session<TlsStream<T>>, ()> {
|
||||
// Drop references to write half from state
|
||||
let state = if let Some(state) =
|
||||
self.state
|
||||
.try_replace_stream_tx(Arc::new(tokio::sync::Mutex::new(
|
||||
tokio::io::split(NullIo::default()).1,
|
||||
))) {
|
||||
state
|
||||
} else {
|
||||
trc::event!(
|
||||
Network(trc::NetworkEvent::SplitError),
|
||||
SpanId = self.session_id,
|
||||
Details = "Failed to obtain write half state"
|
||||
);
|
||||
return Err(());
|
||||
};
|
||||
|
||||
// Take ownership of WriteHalf and unsplit it from ReadHalf
|
||||
let stream = if let Ok(stream_tx) =
|
||||
Arc::try_unwrap(self.stream_tx).map(|mutex| mutex.into_inner())
|
||||
{
|
||||
self.stream_rx.unsplit(stream_tx)
|
||||
} else {
|
||||
trc::event!(
|
||||
Network(trc::NetworkEvent::SplitError),
|
||||
SpanId = self.session_id,
|
||||
Details = "Failed to take ownership of write half"
|
||||
);
|
||||
|
||||
return Err(());
|
||||
};
|
||||
|
||||
// Upgrade to TLS
|
||||
let (stream_rx, stream_tx) =
|
||||
tokio::io::split(self.instance.tls_accept(stream, self.session_id).await?);
|
||||
let stream_tx = Arc::new(tokio::sync::Mutex::new(stream_tx));
|
||||
let receiver = Receiver::with_max_request_size(self.server.core.imap.max_request_size);
|
||||
|
||||
Ok(Session {
|
||||
server: self.server,
|
||||
instance: self.instance,
|
||||
receiver,
|
||||
version: self.version,
|
||||
state: state.try_replace_stream_tx(stream_tx.clone()).unwrap(),
|
||||
is_tls: true,
|
||||
is_condstore: self.is_condstore,
|
||||
is_qresync: self.is_qresync,
|
||||
is_utf8: self.is_utf8,
|
||||
is_objectid: self.is_objectid,
|
||||
is_uidonly: self.is_uidonly,
|
||||
session_id: self.session_id,
|
||||
in_flight: self.in_flight,
|
||||
remote_addr: self.remote_addr,
|
||||
stream_rx,
|
||||
stream_tx,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub fn activate_objectid(&mut self) -> Option<&'static [u8]> {
|
||||
if self.is_objectid {
|
||||
None
|
||||
} else {
|
||||
self.is_objectid = true;
|
||||
Some(b"* ENABLED OBJECTID+\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn write_bytes(&self, bytes: impl AsRef<[u8]>) -> trc::Result<()> {
|
||||
let bytes = bytes.as_ref();
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::RawOutput),
|
||||
SpanId = self.session_id,
|
||||
Size = bytes.len(),
|
||||
Contents = trc::Value::from_maybe_string(bytes),
|
||||
);
|
||||
|
||||
let mut stream = self.stream_tx.lock().await;
|
||||
if let Err(err) = stream.write_all(bytes).await {
|
||||
Err(trc::NetworkEvent::WriteError
|
||||
.into_err()
|
||||
.reason(err)
|
||||
.details("Failed to write to stream"))
|
||||
} else {
|
||||
let _ = stream.flush().await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn write_error(&self, err: trc::Error) -> bool {
|
||||
if err.should_write_err() {
|
||||
let disconnect = err.must_disconnect();
|
||||
let bytes = err.serialize();
|
||||
trc::error!(err.span_id(self.session_id));
|
||||
|
||||
if let Err(err) = self.write_bytes(bytes).await {
|
||||
trc::error!(err.span_id(self.session_id));
|
||||
false
|
||||
} else {
|
||||
!disconnect
|
||||
}
|
||||
} else {
|
||||
trc::error!(err);
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> super::SessionData<T> {
|
||||
pub async fn write_bytes(&self, bytes: impl AsRef<[u8]>) -> trc::Result<()> {
|
||||
let bytes = bytes.as_ref();
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::RawOutput),
|
||||
SpanId = self.session_id,
|
||||
Size = bytes.len(),
|
||||
Contents = trc::Value::from_maybe_string(bytes),
|
||||
);
|
||||
|
||||
let mut stream = self.stream_tx.lock().await;
|
||||
if let Err(err) = stream.write_all(bytes.as_ref()).await {
|
||||
Err(trc::NetworkEvent::WriteError
|
||||
.into_err()
|
||||
.reason(err)
|
||||
.details("Failed to write to stream"))
|
||||
} else {
|
||||
let _ = stream.flush().await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn write_error(&self, err: trc::Error) -> trc::Result<()> {
|
||||
if err.should_write_err() {
|
||||
let bytes = err.serialize();
|
||||
trc::error!(err.span_id(self.session_id));
|
||||
self.write_bytes(bytes).await
|
||||
} else {
|
||||
trc::error!(err.span_id(self.session_id));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
#![warn(clippy::large_futures)]
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use imap_proto::{ResponseCode, StatusResponse, protocol::capability::Capability};
|
||||
|
||||
pub mod core;
|
||||
pub mod op;
|
||||
|
||||
static SERVER_GREETING: &str = "Stalwart IMAP4rev2 at your service.";
|
||||
|
||||
pub(crate) static GREETING_WITH_TLS: LazyLock<Vec<u8>> =
|
||||
LazyLock::new(|| build_greeting(true, true));
|
||||
|
||||
pub(crate) static GREETING_WITH_TLS_LOGIN_DISABLED: LazyLock<Vec<u8>> =
|
||||
LazyLock::new(|| build_greeting(true, false));
|
||||
|
||||
pub(crate) static GREETING_WITHOUT_TLS: LazyLock<Vec<u8>> =
|
||||
LazyLock::new(|| build_greeting(false, true));
|
||||
|
||||
pub(crate) static GREETING_WITHOUT_TLS_LOGIN_DISABLED: LazyLock<Vec<u8>> =
|
||||
LazyLock::new(|| build_greeting(false, false));
|
||||
|
||||
fn build_greeting(offer_tls: bool, allow_auth: bool) -> Vec<u8> {
|
||||
StatusResponse::ok(SERVER_GREETING)
|
||||
.with_code(ResponseCode::Capability {
|
||||
capabilities: Capability::all_capabilities(false, offer_tls, allow_auth, 0, 0),
|
||||
})
|
||||
.into_bytes()
|
||||
}
|
||||
|
||||
pub struct ImapError;
|
||||
@@ -0,0 +1,499 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
core::{MailboxId, Session, SessionData, State},
|
||||
op::ImapContext,
|
||||
spawn_op,
|
||||
};
|
||||
use common::{
|
||||
auth::AccessToken, ipc::CacheInvalidation, network::SessionStream, sharing::EffectiveAcl,
|
||||
storage::index::ObjectIndexBuilder,
|
||||
};
|
||||
use compact_str::ToCompactString;
|
||||
use imap_proto::{
|
||||
Command, ResponseCode, StatusResponse,
|
||||
protocol::acl::{
|
||||
Arguments, GetAclResponse, ListRightsResponse, ModRightsOp, MyRightsResponse, Rights,
|
||||
},
|
||||
receiver::Request,
|
||||
};
|
||||
use registry::schema::enums::Permission;
|
||||
use std::time::Instant;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive, BatchBuilder},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::{Acl, AclGrant},
|
||||
collection::Collection,
|
||||
};
|
||||
use utils::map::bitmap::Bitmap;
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_get_acl(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapAclGet)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let arguments = request.parse_acl(self.is_utf8)?;
|
||||
let is_utf8 = self.version.is_rev2() || self.is_utf8;
|
||||
let data = self.state.session_data();
|
||||
|
||||
spawn_op!(data, {
|
||||
let (mailbox_id, mailbox_, _) = data
|
||||
.get_acl_mailbox(&arguments, true)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
let mut permissions = Vec::new();
|
||||
let mailbox = mailbox_
|
||||
.to_unarchived::<email::mailbox::Mailbox>()
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
// Add the current user if they are the owner or a group member
|
||||
if data.access_token.is_member(mailbox_id.account_id) {
|
||||
let account_name = data
|
||||
.server
|
||||
.account(mailbox_id.account_id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
.name()
|
||||
.to_string();
|
||||
|
||||
permissions.push((
|
||||
account_name,
|
||||
vec![
|
||||
Rights::Read,
|
||||
Rights::Lookup,
|
||||
Rights::Insert,
|
||||
Rights::DeleteMessages,
|
||||
Rights::Expunge,
|
||||
Rights::Seen,
|
||||
Rights::Write,
|
||||
Rights::CreateMailbox,
|
||||
Rights::DeleteMailbox,
|
||||
Rights::Post,
|
||||
Rights::Administer,
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
for item in mailbox.inner.acls.iter() {
|
||||
if item.account_id == mailbox_id.account_id {
|
||||
// Skip the current user, as they are already added above
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut rights = Vec::new();
|
||||
|
||||
for acl in Bitmap::from(&item.grants) {
|
||||
match acl {
|
||||
Acl::Read => {
|
||||
rights.push(Rights::Lookup);
|
||||
}
|
||||
Acl::Modify => {
|
||||
rights.push(Rights::CreateMailbox);
|
||||
}
|
||||
Acl::Delete => {
|
||||
rights.push(Rights::DeleteMailbox);
|
||||
}
|
||||
Acl::ReadItems => {
|
||||
rights.push(Rights::Read);
|
||||
}
|
||||
Acl::AddItems => {
|
||||
rights.push(Rights::Insert);
|
||||
}
|
||||
Acl::ModifyItems => {
|
||||
rights.push(Rights::Write);
|
||||
rights.push(Rights::Seen);
|
||||
}
|
||||
Acl::RemoveItems => {
|
||||
rights.push(Rights::DeleteMessages);
|
||||
rights.push(Rights::Expunge);
|
||||
}
|
||||
Acl::CreateChild => {
|
||||
rights.push(Rights::CreateMailbox);
|
||||
}
|
||||
Acl::Share => {
|
||||
rights.push(Rights::Administer);
|
||||
}
|
||||
Acl::Submit => {
|
||||
rights.push(Rights::Post);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let account_name = data
|
||||
.server
|
||||
.account(item.account_id.into())
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
.name()
|
||||
.to_string();
|
||||
|
||||
permissions.push((account_name, rights));
|
||||
}
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::GetAcl),
|
||||
SpanId = data.session_id,
|
||||
MailboxName = arguments.mailbox_name.clone(),
|
||||
AccountId = mailbox_id.account_id,
|
||||
MailboxId = mailbox_id.mailbox_id,
|
||||
Total = permissions.len(),
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
data.write_bytes(
|
||||
StatusResponse::completed(Command::GetAcl)
|
||||
.with_tag(arguments.tag)
|
||||
.serialize(
|
||||
GetAclResponse {
|
||||
mailbox_name: arguments.mailbox_name.to_string(),
|
||||
permissions,
|
||||
}
|
||||
.into_bytes(is_utf8),
|
||||
),
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn handle_my_rights(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapMyRights)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let arguments = request.parse_acl(self.is_utf8)?;
|
||||
let data = self.state.session_data();
|
||||
let is_utf8 = self.version.is_rev2() || self.is_utf8;
|
||||
|
||||
spawn_op!(data, {
|
||||
let (mailbox_id, mailbox_, access_token) = data
|
||||
.get_acl_mailbox(&arguments, false)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
let mailbox = mailbox_
|
||||
.to_unarchived::<email::mailbox::Mailbox>()
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
let rights = if access_token.is_shared(mailbox_id.account_id) {
|
||||
let acl = mailbox.inner.acls.effective_acl(&access_token);
|
||||
let mut rights = Vec::with_capacity(5);
|
||||
if acl.contains(Acl::ReadItems) {
|
||||
rights.push(Rights::Read);
|
||||
rights.push(Rights::Lookup);
|
||||
}
|
||||
if acl.contains(Acl::AddItems) {
|
||||
rights.push(Rights::Insert);
|
||||
}
|
||||
if acl.contains(Acl::RemoveItems) {
|
||||
rights.push(Rights::DeleteMessages);
|
||||
rights.push(Rights::Expunge);
|
||||
}
|
||||
if acl.contains(Acl::ModifyItems) {
|
||||
rights.push(Rights::Seen);
|
||||
rights.push(Rights::Write);
|
||||
}
|
||||
if acl.contains(Acl::CreateChild) {
|
||||
rights.push(Rights::CreateMailbox);
|
||||
}
|
||||
if acl.contains(Acl::Delete) {
|
||||
rights.push(Rights::DeleteMailbox);
|
||||
}
|
||||
if acl.contains(Acl::Submit) {
|
||||
rights.push(Rights::Post);
|
||||
}
|
||||
rights
|
||||
} else {
|
||||
vec![
|
||||
Rights::Read,
|
||||
Rights::Lookup,
|
||||
Rights::Insert,
|
||||
Rights::DeleteMessages,
|
||||
Rights::Expunge,
|
||||
Rights::Seen,
|
||||
Rights::Write,
|
||||
Rights::CreateMailbox,
|
||||
Rights::DeleteMailbox,
|
||||
Rights::Post,
|
||||
Rights::Administer,
|
||||
]
|
||||
};
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::MyRights),
|
||||
SpanId = data.session_id,
|
||||
MailboxName = arguments.mailbox_name.clone(),
|
||||
AccountId = mailbox_id.account_id,
|
||||
MailboxId = mailbox_id.mailbox_id,
|
||||
Details = rights
|
||||
.iter()
|
||||
.map(|r| trc::Value::String(r.to_compact_string()))
|
||||
.collect::<Vec<_>>(),
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
data.write_bytes(
|
||||
StatusResponse::completed(Command::MyRights)
|
||||
.with_tag(arguments.tag)
|
||||
.serialize(
|
||||
MyRightsResponse {
|
||||
mailbox_name: arguments.mailbox_name.to_string(),
|
||||
rights,
|
||||
}
|
||||
.into_bytes(is_utf8),
|
||||
),
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn handle_set_acl(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapAclSet)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let command = request.command;
|
||||
let arguments = request.parse_acl(self.is_utf8)?;
|
||||
let data = self.state.session_data();
|
||||
|
||||
spawn_op!(data, {
|
||||
// Validate mailbox
|
||||
let (mailbox_id, current_mailbox, _) = data
|
||||
.get_acl_mailbox(&arguments, true)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
let current_mailbox = current_mailbox
|
||||
.into_deserialized::<email::mailbox::Mailbox>()
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
// Obtain principal id
|
||||
let acl_account_id = data
|
||||
.server
|
||||
.account_id_from_email(arguments.identifier.as_ref().unwrap(), false)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
.ok_or_else(|| {
|
||||
trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Account does not exist")
|
||||
.id(arguments.tag.to_string())
|
||||
.caused_by(trc::location!())
|
||||
})?;
|
||||
|
||||
// Prepare changes
|
||||
let mut mailbox = current_mailbox.inner.clone();
|
||||
let (op, rights) = arguments
|
||||
.mod_rights
|
||||
.map(|mr| {
|
||||
(
|
||||
mr.op,
|
||||
Bitmap::from_iter(mr.rights.into_iter().map(Acl::from)),
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| (ModRightsOp::Replace, Bitmap::new()));
|
||||
|
||||
if let Some(item) = mailbox
|
||||
.acls
|
||||
.iter_mut()
|
||||
.find(|item| item.account_id == acl_account_id)
|
||||
{
|
||||
match op {
|
||||
ModRightsOp::Replace => {
|
||||
if !rights.is_empty() {
|
||||
item.grants = rights;
|
||||
} else {
|
||||
mailbox
|
||||
.acls
|
||||
.retain(|item| item.account_id != acl_account_id);
|
||||
}
|
||||
}
|
||||
ModRightsOp::Add => {
|
||||
item.grants.union(&rights);
|
||||
}
|
||||
ModRightsOp::Remove => {
|
||||
for right in rights {
|
||||
item.grants.remove(right);
|
||||
}
|
||||
if item.grants.is_empty() {
|
||||
mailbox
|
||||
.acls
|
||||
.retain(|item| item.account_id != acl_account_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if !rights.is_empty() {
|
||||
match op {
|
||||
ModRightsOp::Add | ModRightsOp::Replace => {
|
||||
mailbox.acls.push(AclGrant {
|
||||
account_id: acl_account_id,
|
||||
grants: rights,
|
||||
});
|
||||
}
|
||||
ModRightsOp::Remove => (),
|
||||
}
|
||||
}
|
||||
|
||||
if mailbox.acls.len() > data.server.core.groupware.max_shares_per_item {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Maximum shares per item exceeded")
|
||||
.caused_by(trc::location!()));
|
||||
}
|
||||
|
||||
let grants = mailbox
|
||||
.acls
|
||||
.iter()
|
||||
.map(|r| trc::Value::from(r.account_id))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Write changes
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.with_account_id(mailbox_id.account_id)
|
||||
.with_collection(Collection::Mailbox)
|
||||
.with_document(mailbox_id.mailbox_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::new()
|
||||
.with_changes(mailbox)
|
||||
.with_current(current_mailbox),
|
||||
)
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
if !batch.is_empty() {
|
||||
data.server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
}
|
||||
|
||||
// Invalidate ACLs
|
||||
data.server
|
||||
.invalidate_caches(CacheInvalidation::AccessToken(acl_account_id).into())
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::SetAcl),
|
||||
SpanId = data.session_id,
|
||||
MailboxName = arguments.mailbox_name.clone(),
|
||||
AccountId = mailbox_id.account_id,
|
||||
MailboxId = mailbox_id.mailbox_id,
|
||||
Details = grants,
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
data.write_bytes(
|
||||
StatusResponse::completed(command)
|
||||
.with_tag(arguments.tag)
|
||||
.into_bytes(),
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn handle_list_rights(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapListRights)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let arguments = request.parse_acl(self.is_utf8)?;
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::ListRights),
|
||||
SpanId = self.session_id,
|
||||
MailboxName = arguments.mailbox_name.clone(),
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
self.write_bytes(
|
||||
StatusResponse::completed(Command::ListRights)
|
||||
.with_tag(arguments.tag)
|
||||
.serialize(
|
||||
ListRightsResponse {
|
||||
mailbox_name: arguments.mailbox_name,
|
||||
identifier: arguments.identifier.unwrap(),
|
||||
permissions: vec![
|
||||
vec![Rights::Read],
|
||||
vec![Rights::Lookup],
|
||||
vec![Rights::Write, Rights::Seen],
|
||||
vec![Rights::Insert],
|
||||
vec![Rights::Expunge, Rights::DeleteMessages],
|
||||
vec![Rights::CreateMailbox],
|
||||
vec![Rights::DeleteMailbox],
|
||||
vec![Rights::Post],
|
||||
vec![Rights::Administer],
|
||||
],
|
||||
}
|
||||
.into_bytes(self.version.is_rev2() || self.is_utf8),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<bool> {
|
||||
match &self.state {
|
||||
State::Authenticated { data } | State::Selected { data, .. } => data
|
||||
.access_token
|
||||
.enforce_permission(permission)
|
||||
.map(|_| true),
|
||||
State::NotAuthenticated { .. } => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
async fn get_acl_mailbox(
|
||||
&self,
|
||||
arguments: &Arguments,
|
||||
validate: bool,
|
||||
) -> trc::Result<(MailboxId, Archive<AlignedBytes>, AccessToken)> {
|
||||
if let Some(mailbox) = self.get_mailbox_by_name(&arguments.mailbox_name) {
|
||||
if let Some(values) = self
|
||||
.server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
mailbox.account_id,
|
||||
Collection::Mailbox,
|
||||
mailbox.mailbox_id,
|
||||
))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
let access_token = self
|
||||
.refresh_access_token()
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if !validate
|
||||
|| access_token.is_member(mailbox.account_id)
|
||||
|| values
|
||||
.unarchive::<email::mailbox::Mailbox>()
|
||||
.caused_by(trc::location!())?
|
||||
.acls
|
||||
.effective_acl(&access_token)
|
||||
.contains(Acl::Share)
|
||||
{
|
||||
Ok((mailbox, values, access_token))
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("You do not have enough permissions to perform this operation.")
|
||||
.code(ResponseCode::NoPerm))
|
||||
}
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.caused_by(trc::location!())
|
||||
.details("Mailbox does not exist."))
|
||||
}
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Mailbox does not exist."))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{ImapContext, ToModSeq};
|
||||
use crate::{
|
||||
core::{ImapUidToId, MailboxId, SelectedMailbox, Session, SessionData},
|
||||
spawn_op,
|
||||
};
|
||||
use common::{auth::BuildAccessToken, ipc::PushNotification, network::SessionStream};
|
||||
use email::message::ingest::{EmailIngest, IngestEmail, IngestSource};
|
||||
use imap_proto::{
|
||||
Command, ResponseCode, StatusResponse,
|
||||
protocol::{append::Arguments, select::HighestModSeq},
|
||||
receiver::Request,
|
||||
};
|
||||
use mail_parser::MessageParser;
|
||||
use registry::schema::enums::Permission;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use types::{
|
||||
acl::Acl,
|
||||
keyword::Keyword,
|
||||
type_state::{DataType, StateChange},
|
||||
};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_append(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapAppend)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let arguments = request.parse_append(self.is_utf8)?;
|
||||
let (data, selected_mailbox) = self.state.session_mailbox_state();
|
||||
|
||||
// RFC 9738 makes APPEND atomic, so an oversized MULTIAPPEND stores nothing
|
||||
let message_limit = self.server.core.imap.max_messages_per_save;
|
||||
if arguments.messages.len() > message_limit as usize {
|
||||
return self
|
||||
.write_bytes(
|
||||
StatusResponse::no("Too many messages to append, try a smaller subset.")
|
||||
.with_tag(arguments.tag)
|
||||
.with_code(ResponseCode::MessageLimit {
|
||||
limit: message_limit,
|
||||
uid: None,
|
||||
})
|
||||
.into_bytes(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Refresh mailboxes
|
||||
data.synchronize_mailboxes(false)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
// Obtain mailbox
|
||||
let mailbox = if let Some(mailbox) = data.get_mailbox_by_name(&arguments.mailbox_name) {
|
||||
mailbox
|
||||
} else {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Mailbox does not exist.")
|
||||
.code(ResponseCode::TryCreate)
|
||||
.id(arguments.tag));
|
||||
};
|
||||
let is_qresync = self.is_qresync;
|
||||
|
||||
spawn_op!(data, {
|
||||
let response = data
|
||||
.append_messages(arguments, selected_mailbox, mailbox, is_qresync, op_start)
|
||||
.await?
|
||||
.into_bytes();
|
||||
|
||||
data.write_bytes(response).await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
async fn append_messages(
|
||||
&self,
|
||||
arguments: Arguments,
|
||||
selected_mailbox: Option<Arc<SelectedMailbox>>,
|
||||
mailbox: MailboxId,
|
||||
is_qresync: bool,
|
||||
op_start: Instant,
|
||||
) -> trc::Result<StatusResponse> {
|
||||
// Verify ACLs
|
||||
let account_id = mailbox.account_id;
|
||||
let mailbox_id = mailbox.mailbox_id;
|
||||
if !self
|
||||
.check_mailbox_acl(account_id, mailbox_id, Acl::AddItems)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(
|
||||
"You do not have the required permissions to append messages to this mailbox.",
|
||||
)
|
||||
.code(ResponseCode::NoPerm)
|
||||
.id(arguments.tag));
|
||||
}
|
||||
|
||||
// Obtain access token
|
||||
let access_token = if mailbox.account_id == self.account_id {
|
||||
self.refresh_access_token()
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
} else {
|
||||
self.server
|
||||
.access_token(mailbox.account_id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
.build()
|
||||
};
|
||||
|
||||
// Append messages
|
||||
let mut response = StatusResponse::completed(Command::Append);
|
||||
let mut created_ids = Vec::with_capacity(arguments.messages.len());
|
||||
let mut last_change_id = None;
|
||||
for message in arguments.messages {
|
||||
match self
|
||||
.server
|
||||
.email_ingest(IngestEmail {
|
||||
raw_message: &message.message,
|
||||
message: MessageParser::new().parse(&message.message),
|
||||
blob_hash: None,
|
||||
access_token: &access_token,
|
||||
mailbox_ids: vec![mailbox_id],
|
||||
keywords: message.flags.into_iter().map(Keyword::from).collect(),
|
||||
received_at: message.received_at.map(|d| d as u64),
|
||||
source: IngestSource::Imap {
|
||||
train_classifier: true,
|
||||
},
|
||||
session_id: self.session_id,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(email) => {
|
||||
created_ids.push(ImapUidToId {
|
||||
uid: email.imap_uids[0],
|
||||
id: email.document_id,
|
||||
});
|
||||
last_change_id = Some(email.change_id);
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(
|
||||
if err.matches(trc::EventType::Limit(trc::LimitEvent::Quota)) {
|
||||
err.details("Disk quota exceeded.")
|
||||
.code(ResponseCode::OverQuota)
|
||||
} else if err.matches(trc::EventType::Limit(trc::LimitEvent::TenantQuota)) {
|
||||
err.details("Organization disk quota exceeded.")
|
||||
.code(ResponseCode::OverQuota)
|
||||
} else {
|
||||
err
|
||||
}
|
||||
.id(arguments.tag),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast changes
|
||||
if let Some(change_id) = last_change_id {
|
||||
self.server
|
||||
.broadcast_push_notification(PushNotification::StateChange(
|
||||
StateChange::new(account_id)
|
||||
.with_change_id(change_id)
|
||||
.with_change(DataType::Email)
|
||||
.with_change(DataType::Mailbox)
|
||||
.with_change(DataType::Thread),
|
||||
))
|
||||
.await;
|
||||
}
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::Append),
|
||||
SpanId = self.session_id,
|
||||
MailboxName = arguments.mailbox_name.clone(),
|
||||
AccountId = account_id,
|
||||
MailboxId = mailbox_id,
|
||||
DocumentId = created_ids
|
||||
.iter()
|
||||
.map(|r| trc::Value::from(r.id))
|
||||
.collect::<Vec<_>>(),
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
if !created_ids.is_empty() {
|
||||
let uids = created_ids.iter().map(|id| id.uid).collect();
|
||||
match selected_mailbox {
|
||||
Some(selected_mailbox) if selected_mailbox.id == mailbox => {
|
||||
// Write updated modseq
|
||||
if is_qresync {
|
||||
self.write_bytes(
|
||||
HighestModSeq::new(last_change_id.unwrap_or_default().to_modseq())
|
||||
.into_bytes(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
selected_mailbox.append_messages(created_ids, last_change_id);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
let uid_validity = self
|
||||
.mailbox_state(&mailbox)
|
||||
.map(|m| m.uid_validity as u32)
|
||||
.unwrap_or_default();
|
||||
|
||||
response = response.with_code(ResponseCode::AppendUid { uid_validity, uids });
|
||||
}
|
||||
|
||||
Ok(response.with_tag(arguments.tag))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::core::{Session, SessionData, State};
|
||||
use common::{
|
||||
auth::AuthRequest,
|
||||
network::{SessionStream, limiter::LimiterResult},
|
||||
};
|
||||
use directory::Credentials;
|
||||
use imap_proto::{
|
||||
Command, ResponseCode, StatusResponse,
|
||||
protocol::{authenticate::Mechanism, capability::Capability},
|
||||
receiver::{self, Request},
|
||||
};
|
||||
use mail_parser::decoders::base64::base64_decode;
|
||||
use registry::schema::enums::Permission;
|
||||
use std::sync::Arc;
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_authenticate(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
let mut args = request.parse_authenticate()?;
|
||||
|
||||
match args.mechanism {
|
||||
Mechanism::Plain | Mechanism::OAuthBearer | Mechanism::XOauth2 => {
|
||||
if !args.params.is_empty() {
|
||||
let challenge = base64_decode(args.params.pop().unwrap().as_bytes())
|
||||
.ok_or_else(|| {
|
||||
trc::AuthEvent::Error
|
||||
.into_err()
|
||||
.details("Failed to decode challenge.")
|
||||
.id(args.tag.clone())
|
||||
.code(ResponseCode::Parse)
|
||||
})?;
|
||||
|
||||
let credentials = if args.mechanism == Mechanism::Plain {
|
||||
Credentials::decode_sasl_challenge_plain(&challenge)
|
||||
} else {
|
||||
Credentials::decode_sasl_challenge_oauth(&challenge)
|
||||
}
|
||||
.ok_or_else(|| {
|
||||
trc::AuthEvent::Error
|
||||
.into_err()
|
||||
.details("Invalid SASL challenge.")
|
||||
.id(args.tag.clone())
|
||||
})?;
|
||||
|
||||
self.authenticate(credentials, args.tag).await
|
||||
} else {
|
||||
self.receiver.request = receiver::Request {
|
||||
tag: args.tag,
|
||||
command: Command::Authenticate,
|
||||
tokens: vec![receiver::Token::Argument(args.mechanism.into_bytes())],
|
||||
};
|
||||
self.receiver.state = receiver::State::Argument { last_ch: b' ' };
|
||||
self.write_bytes(b"+ \r\n".to_vec()).await
|
||||
}
|
||||
}
|
||||
_ => Err(trc::AuthEvent::Error
|
||||
.into_err()
|
||||
.details("Authentication mechanism not supported.")
|
||||
.id(args.tag)
|
||||
.code(ResponseCode::Cannot)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn authenticate(&mut self, credentials: Credentials, tag: String) -> trc::Result<()> {
|
||||
// Authenticate
|
||||
let access_token = self
|
||||
.server
|
||||
.authenticate(&AuthRequest::from_credentials(
|
||||
credentials,
|
||||
self.session_id,
|
||||
self.remote_addr,
|
||||
))
|
||||
.await
|
||||
.map_err(|err| {
|
||||
if err.matches(trc::EventType::Auth(trc::AuthEvent::Failed)) {
|
||||
let auth_failures = self.state.auth_failures();
|
||||
if auth_failures < self.server.core.imap.max_auth_failures {
|
||||
self.state = State::NotAuthenticated {
|
||||
auth_failures: auth_failures + 1,
|
||||
};
|
||||
} else {
|
||||
return trc::AuthEvent::TooManyAttempts.into_err().caused_by(err);
|
||||
}
|
||||
}
|
||||
|
||||
err.id(tag.clone())
|
||||
})
|
||||
.and_then(|token| token.assert_has_permission(Permission::ImapAuthenticate))?;
|
||||
|
||||
// Enforce concurrency limits
|
||||
let in_flight = match access_token.is_imap_request_allowed() {
|
||||
LimiterResult::Allowed(in_flight) => Some(in_flight),
|
||||
LimiterResult::Forbidden => {
|
||||
return Err(trc::LimitEvent::ConcurrentRequest
|
||||
.into_err()
|
||||
.id(tag.clone()));
|
||||
}
|
||||
LimiterResult::Disabled => None,
|
||||
};
|
||||
|
||||
// Create session
|
||||
self.state = State::Authenticated {
|
||||
data: Arc::new(
|
||||
SessionData::new(self, access_token, in_flight)
|
||||
.await
|
||||
.map_err(|err| err.id(tag.clone()))?,
|
||||
),
|
||||
};
|
||||
self.write_bytes(
|
||||
StatusResponse::ok("Authentication successful")
|
||||
.with_code(ResponseCode::Capability {
|
||||
capabilities: Capability::all_capabilities(
|
||||
true,
|
||||
!self.is_tls && self.instance.acceptor.is_tls(),
|
||||
true,
|
||||
self.server.core.imap.max_messages_per_command,
|
||||
self.server.core.imap.max_messages_per_save,
|
||||
),
|
||||
})
|
||||
.with_tag(tag)
|
||||
.into_bytes(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn handle_unauthenticate(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
self.state = State::NotAuthenticated { auth_failures: 0 };
|
||||
self.is_condstore = false;
|
||||
self.is_qresync = false;
|
||||
self.is_utf8 = false;
|
||||
self.is_objectid = false;
|
||||
self.is_uidonly = false;
|
||||
|
||||
self.write_bytes(
|
||||
StatusResponse::completed(Command::Unauthenticate)
|
||||
.with_tag(request.tag)
|
||||
.into_bytes(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::core::Session;
|
||||
use common::network::SessionStream;
|
||||
use imap_proto::{
|
||||
Command, StatusResponse,
|
||||
protocol::{
|
||||
ImapResponse,
|
||||
capability::{Capability, Response},
|
||||
quoted_string,
|
||||
},
|
||||
receiver::Request,
|
||||
};
|
||||
use registry::schema::enums::Permission;
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_capability(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapCapability)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::Capabilities),
|
||||
SpanId = self.session_id,
|
||||
Tls = self.is_tls,
|
||||
Strict = !self.server.core.imap.allow_plain_auth,
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
self.write_bytes(
|
||||
StatusResponse::completed(Command::Capability)
|
||||
.with_tag(request.tag)
|
||||
.serialize(
|
||||
Response {
|
||||
capabilities: Capability::all_capabilities(
|
||||
self.state.is_authenticated(),
|
||||
!self.is_tls && self.instance.acceptor.is_tls(),
|
||||
self.is_tls || self.server.core.imap.allow_plain_auth,
|
||||
self.server.core.imap.max_messages_per_command,
|
||||
self.server.core.imap.max_messages_per_save,
|
||||
),
|
||||
}
|
||||
.serialize(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn handle_id(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapId)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::Id),
|
||||
SpanId = self.session_id,
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
self.write_bytes(
|
||||
StatusResponse::completed(Command::Id)
|
||||
.with_tag(request.tag)
|
||||
.serialize(
|
||||
concat!(
|
||||
"* ID (\"name\" \"Stalwart\" \"version\" \"1.0.0\" \"vendor\" \"Stalwart Labs LLC\" ",
|
||||
"\"support-url\" \"https://stalw.art\")\r\n"
|
||||
)
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn handle_jmap_access(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapCapability)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::Capabilities),
|
||||
SpanId = self.session_id,
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
let mut response = b"* JMAPACCESS ".to_vec();
|
||||
quoted_string(
|
||||
&mut response,
|
||||
&format!(
|
||||
"{}/.well-known/jmap",
|
||||
self.server.core.network.http.url_https
|
||||
),
|
||||
);
|
||||
response.extend_from_slice(b"\r\n");
|
||||
|
||||
self.write_bytes(
|
||||
StatusResponse::completed(Command::GetJmapAccess)
|
||||
.with_tag(request.tag)
|
||||
.serialize(response),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::core::{Session, State};
|
||||
use common::network::SessionStream;
|
||||
use imap_proto::{Command, StatusResponse, receiver::Request};
|
||||
use trc::AddContext;
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_close(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
let op_start = Instant::now();
|
||||
let (data, mailbox) = self.state.select_data();
|
||||
|
||||
if mailbox.is_select {
|
||||
data.expunge(mailbox.clone(), None, u32::MAX, op_start)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::Close),
|
||||
SpanId = self.session_id,
|
||||
AccountId = mailbox.id.account_id,
|
||||
MailboxId = mailbox.id.mailbox_id,
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
self.state = State::Authenticated { data };
|
||||
self.write_bytes(
|
||||
StatusResponse::completed(Command::Close)
|
||||
.with_tag(request.tag)
|
||||
.into_bytes(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::ImapContext;
|
||||
use crate::{
|
||||
core::{MailboxId, SelectedMailbox, Session, SessionData},
|
||||
spawn_op,
|
||||
};
|
||||
use common::{ipc::PushNotification, network::SessionStream, storage::index::ObjectIndexBuilder};
|
||||
use email::{
|
||||
cache::{MessageCacheFetch, email::MessageCacheAccess},
|
||||
mailbox::{JUNK_ID, TRASH_ID, UidMailbox},
|
||||
message::{
|
||||
copy::{CopyMessageError, EmailCopy},
|
||||
ingest::EmailIngest,
|
||||
metadata::MessageData,
|
||||
},
|
||||
};
|
||||
use imap_proto::{
|
||||
Command, ResponseCode, StatusResponse, protocol::copy_move::Arguments, receiver::Request,
|
||||
};
|
||||
use registry::schema::enums::Permission;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use store::{
|
||||
ValueKey,
|
||||
roaring::RoaringBitmap,
|
||||
write::{AlignedBytes, Archive, BatchBuilder},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, VanishedCollection},
|
||||
type_state::{DataType, StateChange},
|
||||
};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_copy_move(
|
||||
&mut self,
|
||||
request: Request<Command>,
|
||||
is_move: bool,
|
||||
is_uid: bool,
|
||||
) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(if is_move {
|
||||
Permission::ImapMove
|
||||
} else {
|
||||
Permission::ImapCopy
|
||||
})?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let arguments = request.parse_copy_move(self.is_utf8)?;
|
||||
let (data, src_mailbox) = self.state.mailbox_state();
|
||||
let use_vanished = self.is_qresync || self.is_uidonly;
|
||||
// RFC 9738 places COPY under SAVELIMIT but leaves MOVE under MESSAGELIMIT
|
||||
let message_limit = if is_move {
|
||||
self.server.core.imap.max_messages_per_command
|
||||
} else {
|
||||
self.server.core.imap.max_messages_per_save
|
||||
};
|
||||
|
||||
spawn_op!(data, {
|
||||
// Refresh mailboxes
|
||||
data.synchronize_mailboxes(false)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
// Make sure the mailbox exists.
|
||||
let dest_mailbox =
|
||||
if let Some(mailbox) = data.get_mailbox_by_name(&arguments.mailbox_name) {
|
||||
mailbox
|
||||
} else {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Destination mailbox does not exist.")
|
||||
.code(ResponseCode::TryCreate)
|
||||
.id(arguments.tag));
|
||||
};
|
||||
|
||||
// Check that the destination mailbox is not the same as the source mailbox.
|
||||
if src_mailbox.id.account_id == dest_mailbox.account_id
|
||||
&& src_mailbox.id.mailbox_id == dest_mailbox.mailbox_id
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Source and destination mailboxes are the same.")
|
||||
.code(ResponseCode::Cannot)
|
||||
.id(arguments.tag));
|
||||
}
|
||||
|
||||
data.copy_move(
|
||||
arguments,
|
||||
src_mailbox,
|
||||
dest_mailbox,
|
||||
is_move,
|
||||
is_uid,
|
||||
use_vanished,
|
||||
message_limit,
|
||||
op_start,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn copy_move(
|
||||
&self,
|
||||
arguments: Arguments,
|
||||
src_mailbox: Arc<SelectedMailbox>,
|
||||
dest_mailbox: MailboxId,
|
||||
is_move: bool,
|
||||
is_uid: bool,
|
||||
use_vanished: bool,
|
||||
message_limit: u32,
|
||||
op_start: Instant,
|
||||
) -> trc::Result<()> {
|
||||
self.synchronize_messages(&src_mailbox)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
// Convert IMAP ids to JMAP ids.
|
||||
let ids = src_mailbox
|
||||
.sequence_to_ids(&arguments.sequence_set, is_uid)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
if ids.is_empty() {
|
||||
trc::event!(
|
||||
Imap(if is_move {
|
||||
trc::ImapEvent::Move
|
||||
} else {
|
||||
trc::ImapEvent::Copy
|
||||
}),
|
||||
SpanId = self.session_id,
|
||||
Source = src_mailbox.id.account_id,
|
||||
Details = trc::Value::None,
|
||||
Uid = trc::Value::None,
|
||||
AccountId = dest_mailbox.account_id,
|
||||
MailboxId = dest_mailbox.mailbox_id,
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
return self
|
||||
.write_bytes(
|
||||
StatusResponse::ok(if is_move {
|
||||
"No messages were moved."
|
||||
} else {
|
||||
"No messages were copied."
|
||||
})
|
||||
.with_tag(arguments.tag)
|
||||
.into_bytes(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Verify that the user can delete messages from the source mailbox.
|
||||
if is_move
|
||||
&& !self
|
||||
.check_mailbox_acl(
|
||||
src_mailbox.id.account_id,
|
||||
src_mailbox.id.mailbox_id,
|
||||
Acl::RemoveItems,
|
||||
)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(concat!(
|
||||
"You do not have the required permissions to ",
|
||||
"remove messages from the source mailbox."
|
||||
))
|
||||
.code(ResponseCode::NoPerm)
|
||||
.id(arguments.tag));
|
||||
}
|
||||
|
||||
// Verify that the user can append messages to the destination mailbox.
|
||||
let dest_mailbox_id = dest_mailbox.mailbox_id;
|
||||
if !self
|
||||
.check_mailbox_acl(dest_mailbox.account_id, dest_mailbox_id, Acl::AddItems)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(concat!(
|
||||
"You do not have the required permissions to ",
|
||||
"add messages to the destination mailbox."
|
||||
))
|
||||
.code(ResponseCode::NoPerm)
|
||||
.id(arguments.tag));
|
||||
}
|
||||
|
||||
// RFC 9738 requires the highest UIDs to be processed first when truncating.
|
||||
// COPY is atomic, so it is refused outright rather than partially applied.
|
||||
let mut ids = ids;
|
||||
let message_limit = message_limit as usize;
|
||||
let mut limited_uid = None;
|
||||
if ids.len() > message_limit {
|
||||
let mut uids = ids.values().map(|imap_id| imap_id.uid).collect::<Vec<_>>();
|
||||
let cutoff = uids.len() - message_limit;
|
||||
let lowest_uid = *uids.select_nth_unstable(cutoff).1;
|
||||
|
||||
if !is_move {
|
||||
return self
|
||||
.write_bytes(
|
||||
StatusResponse::no("Too many messages to copy, try a smaller subset.")
|
||||
.with_tag(arguments.tag)
|
||||
.with_code(ResponseCode::MessageLimit {
|
||||
limit: message_limit as u32,
|
||||
uid: lowest_uid.into(),
|
||||
})
|
||||
.into_bytes(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
ids.retain(|_, imap_id| imap_id.uid >= lowest_uid);
|
||||
limited_uid = Some(lowest_uid);
|
||||
}
|
||||
|
||||
let response = StatusResponse::completed(if is_move {
|
||||
Command::Move(is_uid)
|
||||
} else {
|
||||
Command::Copy(is_uid)
|
||||
});
|
||||
let mut error: Option<(ResponseCode, &'static str)> = None;
|
||||
let mut did_move = false;
|
||||
let mut copied_ids = Vec::with_capacity(ids.len());
|
||||
|
||||
if src_mailbox.id.account_id == dest_mailbox.account_id {
|
||||
// Mailboxes are in the same account
|
||||
let account_id = src_mailbox.id.account_id;
|
||||
let dest_mailbox_id = UidMailbox::new_unassigned(dest_mailbox_id);
|
||||
let mut batch = BatchBuilder::new();
|
||||
|
||||
for (id, imap_id) in ids {
|
||||
// Obtain mailbox tags
|
||||
let data_ = if let Some(result) = self
|
||||
.get_message_data(account_id, id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
{
|
||||
result
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Deserialize
|
||||
let data = data_
|
||||
.to_unarchived::<MessageData>()
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
// Make sure the message still belongs to this mailbox
|
||||
if !data
|
||||
.inner
|
||||
.mailboxes
|
||||
.iter()
|
||||
.any(|mailbox| mailbox.mailbox_id == src_mailbox.id.mailbox_id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the message is already in the destination mailbox, skip it.
|
||||
if let Some(mailbox) = data
|
||||
.inner
|
||||
.mailboxes
|
||||
.iter()
|
||||
.find(|mailbox| mailbox.mailbox_id == dest_mailbox_id.mailbox_id)
|
||||
{
|
||||
copied_ids.push((imap_id.uid, mailbox.uid.to_native()));
|
||||
|
||||
if is_move {
|
||||
let mut new_data = data.inner.to_builder();
|
||||
new_data.remove_mailbox(src_mailbox.id.mailbox_id);
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::Email)
|
||||
.with_document(id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::new()
|
||||
.with_current(data)
|
||||
.with_changes(new_data.seal()),
|
||||
)
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
.log_vanished_item(
|
||||
VanishedCollection::Email,
|
||||
(src_mailbox.id.mailbox_id, imap_id.uid),
|
||||
)
|
||||
.commit_point();
|
||||
did_move = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prepare changes
|
||||
let mut new_data = data.inner.to_builder();
|
||||
|
||||
// Add destination folder
|
||||
new_data.add_mailbox(dest_mailbox_id);
|
||||
if is_move {
|
||||
new_data.remove_mailbox(src_mailbox.id.mailbox_id);
|
||||
}
|
||||
|
||||
// Assign IMAP UIDs
|
||||
let ids = self
|
||||
.server
|
||||
.assign_email_ids(
|
||||
account_id,
|
||||
new_data
|
||||
.mailboxes
|
||||
.iter()
|
||||
.filter(|m| m.uid == 0)
|
||||
.map(|m| m.mailbox_id),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
for (uid_mailbox, uid) in new_data
|
||||
.mailboxes
|
||||
.iter_mut()
|
||||
.filter(|m| m.uid == 0)
|
||||
.zip(ids)
|
||||
{
|
||||
copied_ids.push((imap_id.uid, uid));
|
||||
uid_mailbox.uid = uid;
|
||||
}
|
||||
|
||||
// Prepare write batch
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::Email)
|
||||
.with_document(id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::new()
|
||||
.with_current(data)
|
||||
.with_changes(new_data.seal()),
|
||||
)
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
if is_move {
|
||||
batch.log_vanished_item(
|
||||
VanishedCollection::Email,
|
||||
(src_mailbox.id.mailbox_id, imap_id.uid),
|
||||
);
|
||||
}
|
||||
|
||||
// Add message to training queue
|
||||
if dest_mailbox_id.mailbox_id == JUNK_ID {
|
||||
self.server
|
||||
.add_account_spam_sample(&mut batch, account_id, id, true, self.session_id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
} else if src_mailbox.id.mailbox_id == JUNK_ID
|
||||
&& dest_mailbox_id.mailbox_id != TRASH_ID
|
||||
{
|
||||
self.server
|
||||
.add_account_spam_sample(&mut batch, account_id, id, false, self.session_id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
}
|
||||
|
||||
batch.commit_point();
|
||||
|
||||
// Update changelog
|
||||
if is_move {
|
||||
did_move = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Write changes
|
||||
self.server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
} else {
|
||||
// Obtain quota for target account
|
||||
let src_account_id = src_mailbox.id.account_id;
|
||||
let mut dest_change_id = None;
|
||||
let dest_account_id = dest_mailbox.account_id;
|
||||
let mut destroy_ids = RoaringBitmap::new();
|
||||
let cache = self
|
||||
.server
|
||||
.get_cached_messages(src_account_id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
let mut dest_cache = None;
|
||||
for (id, imap_id) in ids {
|
||||
match self
|
||||
.server
|
||||
.copy_message(
|
||||
src_account_id,
|
||||
id,
|
||||
dest_account_id,
|
||||
vec![dest_mailbox_id],
|
||||
cache
|
||||
.email_by_id(&id)
|
||||
.map(|e| cache.expand_keywords(e).collect())
|
||||
.unwrap_or_default(),
|
||||
None,
|
||||
self.session_id,
|
||||
)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
{
|
||||
Ok(email) => {
|
||||
dest_change_id = email.change_id.into();
|
||||
if let Some(assigned_uid) = email.imap_uids.first() {
|
||||
debug_assert!(*assigned_uid > 0);
|
||||
copied_ids.push((imap_id.uid, *assigned_uid));
|
||||
}
|
||||
}
|
||||
Err(CopyMessageError::AlreadyExists(existing_id)) => {
|
||||
if dest_cache.is_none() {
|
||||
dest_cache = self
|
||||
.server
|
||||
.get_cached_messages(dest_account_id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
.into();
|
||||
}
|
||||
|
||||
if let Some(uid) = dest_cache
|
||||
.as_ref()
|
||||
.and_then(|cache| cache.email_by_id(&existing_id))
|
||||
.and_then(|message| {
|
||||
message
|
||||
.mailboxes
|
||||
.iter()
|
||||
.find(|mailbox| mailbox.mailbox_id == dest_mailbox_id)
|
||||
})
|
||||
.map(|mailbox| mailbox.uid)
|
||||
{
|
||||
copied_ids.push((imap_id.uid, uid));
|
||||
} else {
|
||||
let data_ = if let Some(data_) = self
|
||||
.get_message_data(dest_account_id, existing_id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
{
|
||||
data_
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let data = data_
|
||||
.to_unarchived::<MessageData>()
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
if let Some(uid) = data.inner.message_uid(dest_mailbox_id) {
|
||||
copied_ids.push((imap_id.uid, uid));
|
||||
} else {
|
||||
let mut new_data = data.inner.to_builder();
|
||||
new_data.add_mailbox(UidMailbox::new_unassigned(dest_mailbox_id));
|
||||
|
||||
let uids = self
|
||||
.server
|
||||
.assign_email_ids(
|
||||
dest_account_id,
|
||||
new_data
|
||||
.mailboxes
|
||||
.iter()
|
||||
.filter(|m| m.uid == 0)
|
||||
.map(|m| m.mailbox_id),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
let mut assigned_uid = 0;
|
||||
for (uid_mailbox, uid) in new_data
|
||||
.mailboxes
|
||||
.iter_mut()
|
||||
.filter(|m| m.uid == 0)
|
||||
.zip(uids)
|
||||
{
|
||||
uid_mailbox.uid = uid;
|
||||
assigned_uid = uid;
|
||||
}
|
||||
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.with_account_id(dest_account_id)
|
||||
.with_collection(Collection::Email)
|
||||
.with_document(existing_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::new()
|
||||
.with_current(data)
|
||||
.with_changes(new_data.seal()),
|
||||
)
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
dest_change_id = self
|
||||
.server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.and_then(|ids| ids.last_change_id(dest_account_id))
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
.into();
|
||||
|
||||
copied_ids.push((imap_id.uid, assigned_uid));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(CopyMessageError::OverQuota) => {
|
||||
error = Some((ResponseCode::OverQuota, "Mailbox quota exceeded"));
|
||||
continue;
|
||||
}
|
||||
Err(CopyMessageError::NotFound) => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if is_move {
|
||||
destroy_ids.insert(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Untag or delete emails
|
||||
if !destroy_ids.is_empty() {
|
||||
let mut batch = BatchBuilder::new();
|
||||
self.email_untag_or_delete(
|
||||
src_account_id,
|
||||
src_mailbox.id.mailbox_id,
|
||||
&destroy_ids,
|
||||
&mut batch,
|
||||
)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
self.server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
did_move = true;
|
||||
}
|
||||
|
||||
// Broadcast changes on destination account
|
||||
if let Some(change_id) = dest_change_id {
|
||||
self.server
|
||||
.broadcast_push_notification(PushNotification::StateChange(
|
||||
StateChange::new(dest_account_id)
|
||||
.with_change_id(change_id)
|
||||
.with_change(DataType::Email)
|
||||
.with_change(DataType::Thread)
|
||||
.with_change(DataType::Mailbox),
|
||||
))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
// Map copied JMAP Ids to IMAP UIDs in the destination folder.
|
||||
if copied_ids.is_empty() {
|
||||
return if let Some((code, message)) = error {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(message)
|
||||
.ctx(trc::Key::Code, code)
|
||||
.id(arguments.tag))
|
||||
} else {
|
||||
trc::event!(
|
||||
Imap(if is_move {
|
||||
trc::ImapEvent::Move
|
||||
} else {
|
||||
trc::ImapEvent::Copy
|
||||
}),
|
||||
SpanId = self.session_id,
|
||||
Source = src_mailbox.id.account_id,
|
||||
Details = trc::Value::None,
|
||||
Uid = trc::Value::None,
|
||||
AccountId = dest_mailbox.account_id,
|
||||
MailboxId = dest_mailbox.mailbox_id,
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
self.write_bytes(
|
||||
StatusResponse::ok(if is_move {
|
||||
"No messages were moved."
|
||||
} else {
|
||||
"No messages were copied."
|
||||
})
|
||||
.with_tag(arguments.tag)
|
||||
.into_bytes(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
}
|
||||
|
||||
// Prepare response
|
||||
let uid_validity = self
|
||||
.mailbox_state(&dest_mailbox)
|
||||
.map(|m| m.uid_validity as u32)
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut src_uids = Vec::with_capacity(copied_ids.len());
|
||||
let mut dest_uids = Vec::with_capacity(copied_ids.len());
|
||||
for (src_uid, dest_uid) in copied_ids {
|
||||
src_uids.push(src_uid);
|
||||
dest_uids.push(dest_uid);
|
||||
}
|
||||
src_uids.sort_unstable();
|
||||
dest_uids.sort_unstable();
|
||||
|
||||
trc::event!(
|
||||
Imap(if is_move {
|
||||
trc::ImapEvent::Move
|
||||
} else {
|
||||
trc::ImapEvent::Copy
|
||||
}),
|
||||
SpanId = self.session_id,
|
||||
Source = src_mailbox.id.account_id,
|
||||
Details = src_uids
|
||||
.iter()
|
||||
.map(|r| trc::Value::from(*r))
|
||||
.collect::<Vec<_>>(),
|
||||
AccountId = dest_mailbox.account_id,
|
||||
MailboxId = dest_mailbox.mailbox_id,
|
||||
Uid = dest_uids
|
||||
.iter()
|
||||
.map(|r| trc::Value::from(*r))
|
||||
.collect::<Vec<_>>(),
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
let response = if is_move {
|
||||
self.write_bytes(
|
||||
StatusResponse::ok("Copied UIDs")
|
||||
.with_code(ResponseCode::CopyUid {
|
||||
uid_validity,
|
||||
src_uids,
|
||||
dest_uids,
|
||||
})
|
||||
.into_bytes(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if did_move {
|
||||
// Resynchronize source mailbox on a successful move
|
||||
self.write_mailbox_changes(&src_mailbox, use_vanished)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
}
|
||||
|
||||
let response = response.with_tag(arguments.tag);
|
||||
match limited_uid {
|
||||
Some(uid) => response.with_code(ResponseCode::MessageLimit {
|
||||
limit: message_limit as u32,
|
||||
uid: uid.into(),
|
||||
}),
|
||||
None => response,
|
||||
}
|
||||
.into_bytes()
|
||||
} else {
|
||||
response
|
||||
.with_tag(arguments.tag)
|
||||
.with_code(ResponseCode::CopyUid {
|
||||
uid_validity,
|
||||
src_uids,
|
||||
dest_uids,
|
||||
})
|
||||
.into_bytes()
|
||||
};
|
||||
|
||||
self.write_bytes(response).await
|
||||
}
|
||||
|
||||
pub async fn get_message_data(
|
||||
&self,
|
||||
account_id: u32,
|
||||
id: u32,
|
||||
) -> trc::Result<Option<Archive<AlignedBytes>>> {
|
||||
if let Some(data) = self
|
||||
.server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::Email,
|
||||
id,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
Ok(Some(data))
|
||||
} else {
|
||||
trc::event!(
|
||||
Store(trc::StoreEvent::NotFound),
|
||||
AccountId = account_id,
|
||||
Collection = Collection::Email,
|
||||
MessageId = id,
|
||||
SpanId = self.session_id,
|
||||
Details = "Message not found"
|
||||
);
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
core::{Session, SessionData},
|
||||
op::ImapContext,
|
||||
spawn_op,
|
||||
};
|
||||
use common::{network::SessionStream, storage::index::ObjectIndexBuilder};
|
||||
use email::cache::{MessageCacheFetch, mailbox::MailboxCacheAccess};
|
||||
use imap_proto::{
|
||||
Command, ResponseCode, StatusResponse,
|
||||
protocol::{ObjectId, create::Arguments, list::Attribute},
|
||||
receiver::Request,
|
||||
};
|
||||
use registry::schema::enums::{Permission, StorageQuota};
|
||||
use std::time::Instant;
|
||||
use store::write::BatchBuilder;
|
||||
use trc::AddContext;
|
||||
use types::{acl::Acl, collection::Collection, id::Id, special_use::SpecialUse};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_create(&mut self, requests: Vec<Request<Command>>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapCreate)?;
|
||||
|
||||
let data = self.state.session_data();
|
||||
let is_utf8 = self.is_utf8;
|
||||
let is_objectid = self.is_objectid;
|
||||
|
||||
spawn_op!(data, {
|
||||
for request in requests {
|
||||
match request.parse_create(is_utf8) {
|
||||
Ok(argument) => match data.create_folder(argument, is_objectid).await {
|
||||
Ok(response) => {
|
||||
data.write_bytes(response.into_bytes()).await?;
|
||||
}
|
||||
Err(error) => {
|
||||
data.write_error(error).await?;
|
||||
}
|
||||
},
|
||||
Err(err) => data.write_error(err).await?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
pub async fn create_folder(
|
||||
&self,
|
||||
arguments: Arguments,
|
||||
is_objectid: bool,
|
||||
) -> trc::Result<StatusResponse> {
|
||||
let op_start = Instant::now();
|
||||
|
||||
// Refresh mailboxes
|
||||
self.synchronize_mailboxes(false)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
// Validate mailbox name
|
||||
let params = self
|
||||
.validate_mailbox_create(&arguments.mailbox_name, arguments.mailbox_role)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
debug_assert!(!params.path.is_empty());
|
||||
|
||||
// Validate quota
|
||||
let account = self
|
||||
.server
|
||||
.account(params.account_id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
let mailbox_count = self
|
||||
.server
|
||||
.get_cached_messages(params.account_id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
.mailboxes
|
||||
.items
|
||||
.len();
|
||||
if mailbox_count + params.path.len()
|
||||
> self
|
||||
.server
|
||||
.object_quota(account.object_quotas(), StorageQuota::MaxMailboxes)
|
||||
as usize
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(concat!(
|
||||
"There are too many mailboxes, ",
|
||||
"please delete some before adding a new one."
|
||||
))
|
||||
.code(ResponseCode::OverQuota)
|
||||
.id(arguments.tag.clone()));
|
||||
}
|
||||
|
||||
// Build batch
|
||||
let mut parent_id = params.parent_mailbox_id.map(|id| id + 1).unwrap_or(0);
|
||||
let mut create_ids = Vec::with_capacity(params.path.len());
|
||||
let mut next_document_id = self
|
||||
.server
|
||||
.store()
|
||||
.assign_document_ids(
|
||||
params.account_id,
|
||||
Collection::Mailbox,
|
||||
params.path.len() as u64,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let mut batch = BatchBuilder::new();
|
||||
for (pos, &path_item) in params.path.iter().enumerate() {
|
||||
let mut mailbox = email::mailbox::Mailbox::new(path_item).with_parent_id(parent_id);
|
||||
|
||||
if pos == params.path.len() - 1
|
||||
&& let Some(mailbox_role) = arguments.mailbox_role.map(attr_to_role)
|
||||
{
|
||||
mailbox.role = mailbox_role;
|
||||
}
|
||||
let mailbox_id = next_document_id;
|
||||
next_document_id -= 1;
|
||||
batch
|
||||
.with_account_id(params.account_id)
|
||||
.with_collection(Collection::Mailbox)
|
||||
.with_document(mailbox_id)
|
||||
.custom(ObjectIndexBuilder::<(), _>::new().with_changes(mailbox))
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
.commit_point();
|
||||
parent_id = mailbox_id + 1;
|
||||
create_ids.push(mailbox_id);
|
||||
}
|
||||
|
||||
self.server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::CreateMailbox),
|
||||
SpanId = self.session_id,
|
||||
MailboxName = arguments.mailbox_name.clone(),
|
||||
AccountId = params.account_id,
|
||||
MailboxId = create_ids
|
||||
.iter()
|
||||
.map(|&id| trc::Value::from(id))
|
||||
.collect::<Vec<_>>(),
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
// Build response
|
||||
let response = StatusResponse::ok("Mailbox created.").with_tag(arguments.tag);
|
||||
Ok(if is_objectid {
|
||||
response.with_code(ResponseCode::ObjectId(ObjectId {
|
||||
mailbox_id: Some(Id::from(parent_id - 1)),
|
||||
account_id: Some(Id::from(params.account_id)),
|
||||
..Default::default()
|
||||
}))
|
||||
} else {
|
||||
response
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn validate_mailbox_create<'x>(
|
||||
&self,
|
||||
mailbox_name: &'x str,
|
||||
mailbox_role: Option<Attribute>,
|
||||
) -> trc::Result<CreateParams<'x>> {
|
||||
// Remove leading and trailing separators
|
||||
let mut name = mailbox_name.trim();
|
||||
if let Some(suffix) = name.strip_prefix('/') {
|
||||
name = suffix.trim();
|
||||
};
|
||||
if let Some(prefix) = name.strip_suffix('/') {
|
||||
name = prefix.trim();
|
||||
}
|
||||
if name.is_empty() {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(format!("Invalid folder name '{mailbox_name}'.",)));
|
||||
}
|
||||
|
||||
// Build path
|
||||
let mut path = Vec::new();
|
||||
if name.contains('/') {
|
||||
// Locate parent mailbox
|
||||
for path_item in name.split('/') {
|
||||
let path_item = path_item.trim();
|
||||
if path_item.is_empty() {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Invalid empty path item."));
|
||||
} else if path_item.len() > self.server.core.email.mailbox_name_max_len {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Mailbox name is too long."));
|
||||
}
|
||||
path.push(path_item);
|
||||
}
|
||||
|
||||
if path.len() > self.server.core.email.mailbox_max_depth {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Mailbox path is too deep."));
|
||||
}
|
||||
} else {
|
||||
path.push(name);
|
||||
}
|
||||
|
||||
// Validate special folders
|
||||
let mut parent_mailbox_id = None;
|
||||
let mut parent_mailbox_name = None;
|
||||
let (account_id, path) = {
|
||||
let mailboxes = self.mailboxes.lock();
|
||||
let (account, full_path, prefix) =
|
||||
if path.first() == Some(&self.server.core.email.shared_folder.as_str()) {
|
||||
// Shared Folders/<username>/<folder>
|
||||
if path.len() < 3 {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Mailboxes under root shared folders are not allowed.")
|
||||
.code(ResponseCode::Cannot));
|
||||
}
|
||||
|
||||
// Build path
|
||||
let root = &mut path[2];
|
||||
if root.eq_ignore_ascii_case("INBOX") {
|
||||
*root = "INBOX";
|
||||
}
|
||||
let full_path = path.join("/");
|
||||
let prefix = Some(format!("{}/{}", path[0], path[1]));
|
||||
|
||||
// Locate account
|
||||
if let Some(account) = mailboxes
|
||||
.iter()
|
||||
.skip(1)
|
||||
.find(|account| account.prefix == prefix)
|
||||
{
|
||||
(account, full_path, prefix)
|
||||
} else {
|
||||
#[allow(clippy::unnecessary_literal_unwrap)]
|
||||
return Err(trc::ImapEvent::Error.into_err().details(format!(
|
||||
"Shared account '{}' not found.",
|
||||
prefix.unwrap_or_default()
|
||||
)));
|
||||
}
|
||||
} else if let Some(account) = mailboxes.first() {
|
||||
let root = &mut path[0];
|
||||
if root.eq_ignore_ascii_case("INBOX") {
|
||||
*root = "INBOX";
|
||||
}
|
||||
|
||||
(account, path.join("/"), None)
|
||||
} else {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Internal server error.")
|
||||
.caused_by(trc::location!())
|
||||
.code(ResponseCode::ContactAdmin));
|
||||
};
|
||||
|
||||
// Locate parent mailbox
|
||||
if account.mailbox_names.contains_key(&full_path) {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(format!("Mailbox '{}' already exists.", full_path))
|
||||
.code(ResponseCode::AlreadyExists));
|
||||
}
|
||||
|
||||
(
|
||||
account.account_id,
|
||||
if path.len() > 1 {
|
||||
let mut create_path = Vec::with_capacity(path.len());
|
||||
while !path.is_empty() {
|
||||
let mailbox_name: String = path.join("/");
|
||||
if let Some(&mailbox_id) = account.mailbox_names.get(&mailbox_name) {
|
||||
parent_mailbox_id = mailbox_id.into();
|
||||
parent_mailbox_name = mailbox_name.into();
|
||||
break;
|
||||
} else if prefix
|
||||
.as_ref()
|
||||
.is_some_and(|prefix| prefix == &mailbox_name)
|
||||
{
|
||||
break;
|
||||
} else {
|
||||
create_path.push(path.pop().unwrap());
|
||||
}
|
||||
}
|
||||
create_path.reverse();
|
||||
create_path
|
||||
} else {
|
||||
path
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
// Validate ACLs
|
||||
if let Some(parent_mailbox_id) = parent_mailbox_id {
|
||||
if !self
|
||||
.check_mailbox_acl(account_id, parent_mailbox_id, Acl::CreateChild)
|
||||
.await?
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("You are not allowed to create sub mailboxes under this mailbox.")
|
||||
.code(ResponseCode::NoPerm));
|
||||
}
|
||||
} else if self.account_id != account_id
|
||||
&& !self
|
||||
.refresh_access_token()
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.is_member(account_id)
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("You are not allowed to create root folders under shared folders.")
|
||||
.code(ResponseCode::Cannot));
|
||||
}
|
||||
|
||||
Ok(CreateParams {
|
||||
account_id,
|
||||
path,
|
||||
parent_mailbox_id,
|
||||
parent_mailbox_name,
|
||||
special_use: if let Some(mailbox_role) = mailbox_role {
|
||||
// Make sure role is unique
|
||||
let special_use = attr_to_role(mailbox_role);
|
||||
if self
|
||||
.server
|
||||
.get_cached_messages(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.mailbox_by_role(&special_use)
|
||||
.is_some()
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(format!(
|
||||
"A mailbox with role '{}' already exists.",
|
||||
special_use.as_str().unwrap_or_default()
|
||||
))
|
||||
.code(ResponseCode::UseAttr));
|
||||
}
|
||||
Some(mailbox_role)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
is_rename: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CreateParams<'x> {
|
||||
pub account_id: u32,
|
||||
pub path: Vec<&'x str>,
|
||||
pub parent_mailbox_id: Option<u32>,
|
||||
pub parent_mailbox_name: Option<String>,
|
||||
pub special_use: Option<Attribute>,
|
||||
pub is_rename: bool,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn attr_to_role(attr: Attribute) -> SpecialUse {
|
||||
match attr {
|
||||
Attribute::Archive => SpecialUse::Archive,
|
||||
Attribute::Drafts => SpecialUse::Drafts,
|
||||
Attribute::Junk => SpecialUse::Junk,
|
||||
Attribute::Sent => SpecialUse::Sent,
|
||||
Attribute::Trash => SpecialUse::Trash,
|
||||
Attribute::Important => SpecialUse::Important,
|
||||
Attribute::Memos => SpecialUse::Memos,
|
||||
Attribute::Scheduled => SpecialUse::Scheduled,
|
||||
Attribute::Snoozed => SpecialUse::Snoozed,
|
||||
_ => SpecialUse::None,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::ImapContext;
|
||||
use crate::{
|
||||
core::{Session, SessionData},
|
||||
spawn_op,
|
||||
};
|
||||
use common::network::SessionStream;
|
||||
use email::mailbox::destroy::{MailboxDestroy, MailboxDestroyError};
|
||||
use imap_proto::{
|
||||
Command, ResponseCode, StatusResponse, protocol::delete::Arguments, receiver::Request,
|
||||
};
|
||||
use registry::schema::enums::Permission;
|
||||
use std::time::Instant;
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_delete(&mut self, requests: Vec<Request<Command>>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapDelete)?;
|
||||
|
||||
let data = self.state.session_data();
|
||||
let is_utf8 = self.is_utf8;
|
||||
|
||||
spawn_op!(data, {
|
||||
for request in requests {
|
||||
match request.parse_delete(is_utf8) {
|
||||
Ok(argument) => match data.delete_folder(argument).await {
|
||||
Ok(response) => {
|
||||
data.write_bytes(response.into_bytes()).await?;
|
||||
}
|
||||
Err(error) => {
|
||||
data.write_error(error).await?;
|
||||
}
|
||||
},
|
||||
Err(response) => data.write_error(response).await?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
pub async fn delete_folder(&self, arguments: Arguments) -> trc::Result<StatusResponse> {
|
||||
let op_start = Instant::now();
|
||||
|
||||
// Refresh mailboxes
|
||||
self.synchronize_mailboxes(false)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
// Validate mailbox
|
||||
let (account_id, mailbox_id) =
|
||||
if let Some(mailbox) = self.get_mailbox_by_name(&arguments.mailbox_name) {
|
||||
(mailbox.account_id, mailbox.mailbox_id)
|
||||
} else {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Mailbox does not exist.")
|
||||
.code(ResponseCode::TryCreate)
|
||||
.id(arguments.tag));
|
||||
};
|
||||
|
||||
// Delete message
|
||||
let access_token = self
|
||||
.refresh_access_token()
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
if let Err(err) = self
|
||||
.server
|
||||
.mailbox_destroy(account_id, mailbox_id, &access_token, true)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
{
|
||||
let (code, message) = match err {
|
||||
MailboxDestroyError::CannotDestroy => {
|
||||
(ResponseCode::NoPerm, "You cannot delete system mailboxes")
|
||||
}
|
||||
MailboxDestroyError::Forbidden => (
|
||||
ResponseCode::NoPerm,
|
||||
"You do not have enough permissions to delete this mailbox",
|
||||
),
|
||||
MailboxDestroyError::HasChildren => {
|
||||
(ResponseCode::HasChildren, "Mailbox has children")
|
||||
}
|
||||
MailboxDestroyError::HasEmails => (ResponseCode::HasChildren, "Mailbox has emails"),
|
||||
MailboxDestroyError::NotFound => (ResponseCode::NonExistent, "Mailbox not found"),
|
||||
MailboxDestroyError::AssertionFailed => (
|
||||
ResponseCode::Cannot,
|
||||
"Another process is accessing this mailbox",
|
||||
),
|
||||
};
|
||||
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(message)
|
||||
.code(code)
|
||||
.id(arguments.tag));
|
||||
}
|
||||
|
||||
// Update mailbox cache
|
||||
for account in self.mailboxes.lock().iter_mut() {
|
||||
if account.account_id == account_id {
|
||||
account.mailbox_names.remove(&arguments.mailbox_name);
|
||||
account.mailbox_state.remove(&mailbox_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::DeleteMailbox),
|
||||
SpanId = self.session_id,
|
||||
MailboxName = arguments.mailbox_name,
|
||||
AccountId = account_id,
|
||||
MailboxId = mailbox_id,
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
Ok(StatusResponse::ok("Mailbox deleted.").with_tag(arguments.tag))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::core::Session;
|
||||
use common::network::SessionStream;
|
||||
use imap_proto::{
|
||||
Command, StatusResponse,
|
||||
protocol::{ImapResponse, ProtocolVersion, capability::Capability, enable},
|
||||
receiver::Request,
|
||||
};
|
||||
use registry::schema::enums::Permission;
|
||||
use std::time::Instant;
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_enable(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapEnable)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
|
||||
let arguments = request.parse_enable()?;
|
||||
let mut response = enable::Response {
|
||||
enabled: Vec::with_capacity(arguments.capabilities.len()),
|
||||
};
|
||||
|
||||
for capability in arguments.capabilities {
|
||||
match capability {
|
||||
Capability::IMAP4rev2 => {
|
||||
self.version = ProtocolVersion::Rev2;
|
||||
self.is_utf8 = true;
|
||||
}
|
||||
Capability::IMAP4rev1 => {
|
||||
self.version = ProtocolVersion::Rev1;
|
||||
}
|
||||
Capability::CondStore => {
|
||||
self.is_condstore = true;
|
||||
}
|
||||
Capability::QResync => {
|
||||
self.is_qresync = true;
|
||||
self.is_condstore = true;
|
||||
}
|
||||
Capability::Utf8Accept => {
|
||||
self.is_utf8 = true;
|
||||
}
|
||||
Capability::ObjectIdPlus => {
|
||||
self.is_objectid = true;
|
||||
}
|
||||
Capability::UidOnly => {
|
||||
self.is_uidonly = true;
|
||||
}
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
response.enabled.push(capability);
|
||||
}
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::Enable),
|
||||
SpanId = self.session_id,
|
||||
Details = response
|
||||
.enabled
|
||||
.iter()
|
||||
.map(|c| trc::Value::from(format!("{c:?}")))
|
||||
.collect::<Vec<_>>(),
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
self.write_bytes(
|
||||
StatusResponse::ok("ENABLE successful.")
|
||||
.with_tag(arguments.tag)
|
||||
.serialize(response.serialize()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{ImapContext, ToModSeq};
|
||||
use crate::core::{ImapId, SavedSearch, SelectedMailbox, Session, SessionData};
|
||||
use ahash::AHashMap;
|
||||
use common::{network::SessionStream, storage::index::ObjectIndexBuilder};
|
||||
use email::{
|
||||
cache::{MessageCacheFetch, email::MessageCacheAccess},
|
||||
message::{delete::EmailDeletion, metadata::MessageData},
|
||||
};
|
||||
use imap_proto::{
|
||||
Command, ResponseCode, ResponseType, StatusResponse,
|
||||
parser::parse_sequence_set,
|
||||
receiver::{Request, Token},
|
||||
};
|
||||
use registry::schema::{
|
||||
enums::{IndexDocumentType, Permission},
|
||||
structs::{Task, TaskIndexDocument, TaskStatus},
|
||||
};
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use store::{roaring::RoaringBitmap, write::BatchBuilder};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, VanishedCollection},
|
||||
keyword::Keyword,
|
||||
};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_expunge(
|
||||
&mut self,
|
||||
request: Request<Command>,
|
||||
is_uid: bool,
|
||||
) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapExpunge)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let (data, mailbox) = self.state.select_data();
|
||||
|
||||
// Validate ACL
|
||||
if !data
|
||||
.check_mailbox_acl(
|
||||
mailbox.id.account_id,
|
||||
mailbox.id.mailbox_id,
|
||||
Acl::RemoveItems,
|
||||
)
|
||||
.await
|
||||
.imap_ctx(&request.tag, trc::location!())?
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(concat!(
|
||||
"You do not have the required permissions ",
|
||||
"to remove messages from this mailbox."
|
||||
))
|
||||
.code(ResponseCode::NoPerm)
|
||||
.id(request.tag));
|
||||
}
|
||||
|
||||
// Parse sequence to operate on
|
||||
let sequence = match request.tokens.into_iter().next() {
|
||||
Some(Token::Argument(value)) if is_uid => {
|
||||
let sequence = parse_sequence_set(&value).map_err(|err| {
|
||||
trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(err)
|
||||
.ctx(trc::Key::Type, ResponseType::Bad)
|
||||
.id(request.tag.clone())
|
||||
})?;
|
||||
Some(
|
||||
mailbox
|
||||
.sequence_to_ids(&sequence, true)
|
||||
.await
|
||||
.map_err(|err| err.id(request.tag.clone()))?,
|
||||
)
|
||||
}
|
||||
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// RFC 9738 limits UID EXPUNGE but never a plain EXPUNGE
|
||||
let message_limit = if is_uid {
|
||||
self.server.core.imap.max_messages_per_command
|
||||
} else {
|
||||
u32::MAX
|
||||
};
|
||||
|
||||
// Expunge
|
||||
let limited_uid = data
|
||||
.expunge(mailbox.clone(), sequence, message_limit, op_start)
|
||||
.await
|
||||
.imap_ctx(&request.tag, trc::location!())?;
|
||||
|
||||
// Clear saved searches
|
||||
*mailbox.saved_search.lock() = SavedSearch::None;
|
||||
|
||||
// Synchronize messages
|
||||
let modseq = data
|
||||
.write_mailbox_changes(&mailbox, self.is_qresync || self.is_uidonly)
|
||||
.await
|
||||
.imap_ctx(&request.tag, trc::location!())?;
|
||||
let mut response =
|
||||
StatusResponse::completed(Command::Expunge(is_uid)).with_tag(request.tag);
|
||||
|
||||
let mut untagged = Vec::new();
|
||||
if let Some(uid) = limited_uid {
|
||||
let code = ResponseCode::MessageLimit {
|
||||
limit: message_limit,
|
||||
uid: uid.into(),
|
||||
};
|
||||
if self.is_condstore {
|
||||
untagged = StatusResponse::ok("Some messages were not expunged.")
|
||||
.with_code(code)
|
||||
.into_bytes();
|
||||
} else {
|
||||
response = response.with_code(code);
|
||||
}
|
||||
}
|
||||
if self.is_condstore {
|
||||
response = response.with_code(ResponseCode::HighestModseq {
|
||||
modseq: modseq.to_modseq(),
|
||||
});
|
||||
}
|
||||
|
||||
self.write_bytes(response.serialize(untagged)).await
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
pub async fn expunge(
|
||||
&self,
|
||||
mailbox: Arc<SelectedMailbox>,
|
||||
sequence: Option<AHashMap<u32, ImapId>>,
|
||||
message_limit: u32,
|
||||
op_start: Instant,
|
||||
) -> trc::Result<Option<u32>> {
|
||||
// Obtain message ids
|
||||
let account_id = mailbox.id.account_id;
|
||||
let mut deleted_ids = RoaringBitmap::from_iter(
|
||||
self.server
|
||||
.get_cached_messages(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.in_mailbox_with_keyword(mailbox.id.mailbox_id, &Keyword::Deleted)
|
||||
.map(|m| m.document_id),
|
||||
);
|
||||
|
||||
// Filter by sequence
|
||||
if let Some(sequence) = &sequence {
|
||||
deleted_ids &= RoaringBitmap::from_iter(sequence.keys());
|
||||
}
|
||||
|
||||
// RFC 9738 requires the highest UIDs to be processed first when truncating.
|
||||
// Only messages the session has a UID for can be ordered, so the count that
|
||||
// decides whether to truncate has to come from that same set.
|
||||
let mut limited_uid = None;
|
||||
if deleted_ids.len() > message_limit as u64 {
|
||||
let mut uids = {
|
||||
let state = mailbox.state.lock();
|
||||
deleted_ids
|
||||
.iter()
|
||||
.filter_map(|id| state.id_to_imap.get(&id).map(|imap_id| (imap_id.uid, id)))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
if uids.len() > message_limit as usize {
|
||||
let cutoff = uids.len() - message_limit as usize;
|
||||
let (below, lowest, _) = uids.select_nth_unstable(cutoff);
|
||||
limited_uid = Some(lowest.0);
|
||||
for (_, id) in below {
|
||||
deleted_ids.remove(*id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete ids
|
||||
let mut batch = BatchBuilder::new();
|
||||
let (fully_deleted, thread_ids) = self
|
||||
.email_untag_or_delete(account_id, mailbox.id.mailbox_id, &deleted_ids, &mut batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
self.server
|
||||
.log_emptied_threads(account_id, &mut batch, thread_ids, &fully_deleted)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::Expunge),
|
||||
SpanId = self.session_id,
|
||||
AccountId = account_id,
|
||||
MailboxId = mailbox.id.mailbox_id,
|
||||
DocumentId = deleted_ids.iter().map(trc::Value::from).collect::<Vec<_>>(),
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
// Write changes on source account
|
||||
if !batch.is_empty() {
|
||||
self.server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
self.server.notify_task_queue();
|
||||
}
|
||||
|
||||
Ok(limited_uid)
|
||||
}
|
||||
|
||||
pub async fn email_untag_or_delete(
|
||||
&self,
|
||||
account_id: u32,
|
||||
mailbox_id: u32,
|
||||
deleted_ids: &RoaringBitmap,
|
||||
batch: &mut BatchBuilder,
|
||||
) -> trc::Result<(RoaringBitmap, RoaringBitmap)> {
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::Email);
|
||||
|
||||
let mut fully_deleted = RoaringBitmap::new();
|
||||
let mut thread_ids = RoaringBitmap::new();
|
||||
self.server
|
||||
.archives(
|
||||
account_id,
|
||||
Collection::Email,
|
||||
deleted_ids,
|
||||
|document_id, data_| {
|
||||
let metadata = data_
|
||||
.to_unarchived::<MessageData>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if let Some(message_uid) = metadata.inner.message_uid(mailbox_id) {
|
||||
// Add vanished items
|
||||
batch.with_document(document_id);
|
||||
batch.log_vanished_item(
|
||||
VanishedCollection::Email,
|
||||
(mailbox_id, message_uid),
|
||||
);
|
||||
|
||||
if metadata.inner.mailboxes.len() == 1 {
|
||||
// Delete message
|
||||
fully_deleted.insert(document_id);
|
||||
thread_ids.insert(metadata.inner.thread_id.to_native());
|
||||
batch
|
||||
.custom(
|
||||
ObjectIndexBuilder::<_, ()>::new()
|
||||
.with_changed_by(self.access_token.account_tenant_ids())
|
||||
.with_current(metadata),
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.schedule_task(Task::UnindexDocument(TaskIndexDocument {
|
||||
account_id: account_id.into(),
|
||||
document_id: document_id.into(),
|
||||
document_type: IndexDocumentType::Email,
|
||||
status: TaskStatus::now(),
|
||||
}))
|
||||
.commit_point();
|
||||
} else {
|
||||
// Untag message from this mailbox and remove Deleted flag
|
||||
let mut new_metadata = metadata.inner.to_builder();
|
||||
new_metadata.remove_mailbox(mailbox_id);
|
||||
new_metadata.remove_keyword(&Keyword::Deleted);
|
||||
|
||||
// Write changes
|
||||
batch
|
||||
.custom(
|
||||
ObjectIndexBuilder::new()
|
||||
.with_current(metadata)
|
||||
.with_changes(new_metadata.seal()),
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.commit_point();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
Ok((fully_deleted, thread_ids))
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
core::{SelectedMailbox, Session, SessionData, State},
|
||||
op::ImapContext,
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
use common::{ipc::PushNotification, network::SessionStream};
|
||||
use imap_proto::{
|
||||
Command, StatusResponse,
|
||||
protocol::{
|
||||
Sequence, fetch,
|
||||
list::{Attribute, ListItem},
|
||||
status::Status,
|
||||
},
|
||||
receiver::Request,
|
||||
};
|
||||
use registry::schema::enums::Permission;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use store::query::log::Query;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use trc::AddContext;
|
||||
use types::{collection::SyncCollection, type_state::DataType};
|
||||
use utils::map::bitmap::Bitmap;
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_idle(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapIdle)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let (data, mailbox, types) = match &self.state {
|
||||
State::Authenticated { data, .. } => {
|
||||
(data.clone(), None, Bitmap::from_iter([DataType::Mailbox]))
|
||||
}
|
||||
State::Selected { data, mailbox, .. } => (
|
||||
data.clone(),
|
||||
mailbox.clone().into(),
|
||||
Bitmap::from_iter([DataType::Email, DataType::Mailbox, DataType::EmailDelivery]),
|
||||
),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let is_rev2 = self.version.is_rev2();
|
||||
let is_utf8 = self.is_utf8;
|
||||
let is_uidonly = self.is_uidonly;
|
||||
let use_vanished = self.is_qresync || is_uidonly;
|
||||
|
||||
// Register with push manager
|
||||
let mut push_rx = self
|
||||
.server
|
||||
.subscribe_push_manager(&data.access_token, types)
|
||||
.await
|
||||
.imap_ctx(&request.tag, trc::location!())?;
|
||||
|
||||
// Send continuation response
|
||||
self.write_bytes(b"+ Idling, send 'DONE' to stop.\r\n".to_vec())
|
||||
.await?;
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::IdleStart),
|
||||
SpanId = self.session_id,
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
let op_start = Instant::now();
|
||||
let mut buf = vec![0; 4];
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = tokio::time::timeout(self.server.core.imap.timeout_idle, self.stream_rx.read_exact(&mut buf)) => {
|
||||
match result {
|
||||
Ok(Ok(bytes_read)) => {
|
||||
if bytes_read > 0 {
|
||||
if (buf[..bytes_read]).windows(4).any(|w| w == b"DONE") {
|
||||
trc::event!(Imap(trc::ImapEvent::IdleStop), SpanId = self.session_id, Elapsed = op_start.elapsed());
|
||||
return self.write_bytes(StatusResponse::completed(Command::Idle)
|
||||
.with_tag(request.tag)
|
||||
.into_bytes()).await;
|
||||
}
|
||||
} else {
|
||||
return Err(trc::NetworkEvent::Closed.into_err().details("IMAP connection closed by client.").id(request.tag));
|
||||
}
|
||||
},
|
||||
Ok(Err(err)) => {
|
||||
return Err(trc::NetworkEvent::ReadError.into_err().reason(err).details("IMAP connection error.").id(request.tag));
|
||||
},
|
||||
Err(_) => {
|
||||
self.write_bytes(&b"* BYE IDLE timed out.\r\n"[..]).await.ok();
|
||||
return Err(trc::NetworkEvent::Timeout.into_err().details("IMAP IDLE timed out.").id(request.tag));
|
||||
}
|
||||
}
|
||||
}
|
||||
push_notification = push_rx.recv() => {
|
||||
if let Some(push_notification) = push_notification {
|
||||
let mut has_mailbox_changes = false;
|
||||
let mut has_email_changes = false;
|
||||
|
||||
match push_notification {
|
||||
PushNotification::StateChange(state_change) => {
|
||||
for type_state in state_change.types {
|
||||
match type_state {
|
||||
DataType::Email | DataType::EmailDelivery => {
|
||||
has_email_changes = true;
|
||||
}
|
||||
DataType::Mailbox => {
|
||||
has_mailbox_changes = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
},
|
||||
PushNotification::EmailPush(_) => {
|
||||
has_email_changes = true;
|
||||
has_mailbox_changes = true;
|
||||
},
|
||||
PushNotification::CalendarAlert(_) => (),
|
||||
}
|
||||
|
||||
if has_mailbox_changes || has_email_changes {
|
||||
data.write_changes(&mailbox, has_mailbox_changes, has_email_changes, use_vanished, is_uidonly, is_rev2, is_utf8).await?;
|
||||
}
|
||||
} else {
|
||||
self.write_bytes(&b"* BYE Server shutting down.\r\n"[..]).await.ok();
|
||||
return Err(trc::NetworkEvent::Closed.into_err().details("IDLE channel closed.").id(request.tag));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn write_changes(
|
||||
&self,
|
||||
mailbox: &Option<Arc<SelectedMailbox>>,
|
||||
check_mailboxes: bool,
|
||||
check_emails: bool,
|
||||
use_vanished: bool,
|
||||
is_uidonly: bool,
|
||||
is_rev2: bool,
|
||||
is_utf8: bool,
|
||||
) -> trc::Result<()> {
|
||||
// Fetch all changed mailboxes
|
||||
if check_mailboxes {
|
||||
let changes = self
|
||||
.synchronize_mailboxes(true)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.unwrap();
|
||||
|
||||
let mut buf = Vec::with_capacity(64);
|
||||
|
||||
// List deleted mailboxes
|
||||
for mailbox_name in changes.deleted {
|
||||
ListItem {
|
||||
mailbox_name,
|
||||
attributes: vec![Attribute::NonExistent],
|
||||
tags: vec![],
|
||||
}
|
||||
.serialize(&mut buf, is_rev2, is_utf8, false);
|
||||
}
|
||||
|
||||
// List added mailboxes
|
||||
for mailbox_name in changes.added {
|
||||
ListItem {
|
||||
mailbox_name,
|
||||
attributes: vec![],
|
||||
tags: vec![],
|
||||
}
|
||||
.serialize(&mut buf, is_rev2, is_utf8, false);
|
||||
}
|
||||
// Obtain status of changed mailboxes
|
||||
for mailbox_name in changes.changed {
|
||||
if let Ok(status) = self
|
||||
.status(
|
||||
mailbox_name,
|
||||
&[
|
||||
Status::Messages,
|
||||
Status::Unseen,
|
||||
Status::UidNext,
|
||||
Status::UidValidity,
|
||||
],
|
||||
)
|
||||
.await
|
||||
{
|
||||
status.serialize(&mut buf, is_utf8);
|
||||
}
|
||||
}
|
||||
|
||||
if !buf.is_empty() {
|
||||
self.write_bytes(buf).await?;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch selected mailbox changes
|
||||
if check_emails {
|
||||
// Synchronize emails
|
||||
if let Some(mailbox) = mailbox {
|
||||
// Obtain changes since last sync
|
||||
let modseq = mailbox.state.lock().modseq;
|
||||
let new_state = self
|
||||
.write_mailbox_changes(mailbox, use_vanished)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if new_state == modseq {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Obtain changed messages
|
||||
let changelog = self
|
||||
.server
|
||||
.store()
|
||||
.changes(
|
||||
mailbox.id.account_id,
|
||||
SyncCollection::Email.into(),
|
||||
Query::Since(modseq),
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let changed_ids = {
|
||||
let state = mailbox.state.lock();
|
||||
changelog
|
||||
.changes
|
||||
.into_iter()
|
||||
.filter_map(|change| {
|
||||
change.try_unwrap_item_id().and_then(|item_id| {
|
||||
state
|
||||
.id_to_imap
|
||||
.get(&((item_id & u32::MAX as u64) as u32))
|
||||
.map(|id| id.uid)
|
||||
})
|
||||
})
|
||||
.collect::<AHashSet<_>>()
|
||||
};
|
||||
|
||||
if !changed_ids.is_empty() {
|
||||
let op_start = Instant::now();
|
||||
return self
|
||||
.fetch(
|
||||
fetch::Arguments {
|
||||
tag: "".into(),
|
||||
sequence_set: Sequence::List {
|
||||
items: changed_ids
|
||||
.into_iter()
|
||||
.map(|uid| Sequence::Number { value: uid })
|
||||
.collect(),
|
||||
},
|
||||
attributes: vec![fetch::Attribute::Flags, fetch::Attribute::Uid],
|
||||
changed_since: None,
|
||||
include_vanished: false,
|
||||
},
|
||||
mailbox.clone(),
|
||||
true,
|
||||
use_vanished,
|
||||
is_uidonly,
|
||||
false,
|
||||
is_utf8,
|
||||
u32::MAX,
|
||||
op_start,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())
|
||||
.map(|_| ());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::{
|
||||
core::{Session, SessionData},
|
||||
spawn_op,
|
||||
};
|
||||
use common::network::SessionStream;
|
||||
|
||||
use imap_proto::{
|
||||
Command, StatusResponse,
|
||||
protocol::{
|
||||
ImapResponse, ProtocolVersion,
|
||||
list::{
|
||||
self, Arguments, Attribute, ChildInfo, ListItem, ReturnOption, SelectionOption, Tag,
|
||||
},
|
||||
},
|
||||
receiver::Request,
|
||||
};
|
||||
use registry::schema::enums::Permission;
|
||||
use trc::StoreEvent;
|
||||
|
||||
use super::ImapContext;
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_list(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
let op_start = Instant::now();
|
||||
let command = request.command;
|
||||
let is_lsub = command == Command::Lsub;
|
||||
let arguments = if !is_lsub {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapList)?;
|
||||
|
||||
request.parse_list(self.is_utf8)
|
||||
} else {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapLsub)?;
|
||||
|
||||
request.parse_lsub(self.is_utf8)
|
||||
}?;
|
||||
|
||||
if !arguments.is_separator_query() {
|
||||
let data = self.state.session_data();
|
||||
let version = self.version;
|
||||
let is_utf8 = self.is_utf8;
|
||||
|
||||
spawn_op!(
|
||||
data,
|
||||
data.list(arguments, is_lsub, version, is_utf8, op_start)
|
||||
.await
|
||||
)
|
||||
} else {
|
||||
self.write_bytes(
|
||||
StatusResponse::completed(command)
|
||||
.with_tag(arguments.unwrap_tag())
|
||||
.serialize(
|
||||
list::Response {
|
||||
is_rev2: self.version.is_rev2(),
|
||||
is_utf8: self.is_utf8,
|
||||
is_lsub,
|
||||
list_items: vec![ListItem {
|
||||
mailbox_name: "".into(),
|
||||
attributes: vec![Attribute::NoSelect],
|
||||
tags: vec![],
|
||||
}],
|
||||
status_items: Vec::new(),
|
||||
}
|
||||
.serialize(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
pub async fn list(
|
||||
&self,
|
||||
arguments: Arguments,
|
||||
is_lsub: bool,
|
||||
version: ProtocolVersion,
|
||||
is_utf8: bool,
|
||||
op_start: Instant,
|
||||
) -> trc::Result<()> {
|
||||
let (tag, reference_name, mut patterns, selection_options, return_options) = match arguments
|
||||
{
|
||||
Arguments::Basic {
|
||||
tag,
|
||||
reference_name,
|
||||
mailbox_name,
|
||||
} => (
|
||||
tag,
|
||||
reference_name,
|
||||
vec![mailbox_name],
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
),
|
||||
Arguments::Extended {
|
||||
tag,
|
||||
reference_name,
|
||||
mailbox_name,
|
||||
selection_options,
|
||||
return_options,
|
||||
} => (
|
||||
tag,
|
||||
reference_name,
|
||||
mailbox_name,
|
||||
selection_options,
|
||||
return_options,
|
||||
),
|
||||
};
|
||||
|
||||
// Refresh mailboxes
|
||||
self.synchronize_mailboxes(false)
|
||||
.await
|
||||
.imap_ctx(&tag, trc::location!())?;
|
||||
|
||||
// Process arguments
|
||||
let mut filter_subscribed = false;
|
||||
let mut filter_special_use = false;
|
||||
let mut recursive_match = false;
|
||||
let mut include_special_use = true;
|
||||
let mut include_subscribed = false;
|
||||
let mut include_children = false;
|
||||
let mut include_status = None;
|
||||
for selection_option in &selection_options {
|
||||
match selection_option {
|
||||
SelectionOption::Subscribed => {
|
||||
filter_subscribed = true;
|
||||
include_subscribed = true;
|
||||
}
|
||||
SelectionOption::Remote => (),
|
||||
SelectionOption::SpecialUse => {
|
||||
filter_special_use = true;
|
||||
include_special_use = true;
|
||||
}
|
||||
SelectionOption::RecursiveMatch => {
|
||||
recursive_match = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for return_option in &return_options {
|
||||
match return_option {
|
||||
ReturnOption::Subscribed => {
|
||||
include_subscribed = true;
|
||||
}
|
||||
ReturnOption::Children => {
|
||||
include_children = true;
|
||||
}
|
||||
ReturnOption::Status(status) => {
|
||||
include_status = status.into();
|
||||
}
|
||||
ReturnOption::SpecialUse => {
|
||||
include_special_use = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if recursive_match && !filter_subscribed {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("RECURSIVEMATCH requires the SUBSCRIBED selection option.")
|
||||
.id(tag));
|
||||
}
|
||||
|
||||
// Append reference name
|
||||
if !patterns.is_empty() && !reference_name.is_empty() {
|
||||
patterns.iter_mut().for_each(|item| {
|
||||
*item = format!("{}{}", reference_name, item);
|
||||
})
|
||||
}
|
||||
|
||||
let mut list_items = Vec::with_capacity(10);
|
||||
|
||||
// Add mailboxes
|
||||
let mut added_shared_folder = false;
|
||||
for account in self.mailboxes.lock().iter() {
|
||||
if let Some(prefix) = &account.prefix {
|
||||
if !added_shared_folder {
|
||||
if !filter_subscribed
|
||||
&& matches_pattern(&patterns, &self.server.core.email.shared_folder)
|
||||
{
|
||||
list_items.push(ListItem {
|
||||
mailbox_name: self.server.core.email.shared_folder.as_str().into(),
|
||||
attributes: if include_children {
|
||||
vec![Attribute::HasChildren, Attribute::NoSelect]
|
||||
} else {
|
||||
vec![Attribute::NoSelect]
|
||||
},
|
||||
tags: vec![],
|
||||
});
|
||||
}
|
||||
added_shared_folder = true;
|
||||
}
|
||||
if !filter_subscribed && matches_pattern(&patterns, prefix) {
|
||||
list_items.push(ListItem {
|
||||
mailbox_name: prefix.clone(),
|
||||
attributes: if include_children {
|
||||
vec![Attribute::HasChildren, Attribute::NoSelect]
|
||||
} else {
|
||||
vec![Attribute::NoSelect]
|
||||
},
|
||||
tags: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (mailbox_name, mailbox_id) in &account.mailbox_names {
|
||||
if matches_pattern(&patterns, mailbox_name) {
|
||||
let mailbox = if let Some(mailbox) = account.mailbox_state.get(mailbox_id) {
|
||||
mailbox
|
||||
} else {
|
||||
trc::event!(
|
||||
Store(StoreEvent::UnexpectedError),
|
||||
Details = "IMAP mailbox no longer present in account state",
|
||||
Id = *mailbox_id,
|
||||
Details = account
|
||||
.mailbox_state
|
||||
.keys()
|
||||
.copied()
|
||||
.map(trc::Value::from)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let mut has_recursive_match = false;
|
||||
if recursive_match {
|
||||
let prefix = format!("{}/", mailbox_name);
|
||||
for (mailbox_name, mailbox_id) in &account.mailbox_names {
|
||||
if mailbox_name.starts_with(&prefix)
|
||||
&& account.mailbox_state.get(mailbox_id).unwrap().is_subscribed
|
||||
{
|
||||
has_recursive_match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !filter_subscribed || mailbox.is_subscribed || has_recursive_match {
|
||||
let mut attributes = Vec::with_capacity(2);
|
||||
if include_children {
|
||||
attributes.push(if mailbox.has_children {
|
||||
Attribute::HasChildren
|
||||
} else {
|
||||
Attribute::HasNoChildren
|
||||
});
|
||||
}
|
||||
if include_subscribed && mailbox.is_subscribed {
|
||||
attributes.push(Attribute::Subscribed);
|
||||
}
|
||||
if include_special_use {
|
||||
if let Some(special_use) = &mailbox.special_use {
|
||||
attributes.push(*special_use);
|
||||
} else if filter_special_use {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
list_items.push(ListItem {
|
||||
mailbox_name: mailbox_name.clone(),
|
||||
attributes,
|
||||
tags: if !has_recursive_match {
|
||||
vec![]
|
||||
} else {
|
||||
vec![Tag::ChildInfo(vec![ChildInfo::Subscribed])]
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add status response
|
||||
let mut status_items = Vec::new();
|
||||
if let Some(include_status) = include_status {
|
||||
for list_item in &list_items {
|
||||
match self
|
||||
.status(list_item.mailbox_name.clone(), include_status)
|
||||
.await
|
||||
.imap_ctx(&tag, trc::location!())
|
||||
{
|
||||
Ok(status_item) => {
|
||||
status_items.push(status_item);
|
||||
}
|
||||
Err(err) => {
|
||||
self.write_error(err).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trc::event!(
|
||||
Imap(if !is_lsub {
|
||||
trc::ImapEvent::List
|
||||
} else {
|
||||
trc::ImapEvent::Lsub
|
||||
}),
|
||||
SpanId = self.session_id,
|
||||
Details = list_items
|
||||
.iter()
|
||||
.map(|item| trc::Value::from(item.mailbox_name.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
// Write response
|
||||
self.write_bytes(
|
||||
StatusResponse::completed(if !is_lsub {
|
||||
Command::List
|
||||
} else {
|
||||
Command::Lsub
|
||||
})
|
||||
.with_tag(tag)
|
||||
.serialize(
|
||||
list::Response {
|
||||
is_rev2: version.is_rev2(),
|
||||
is_utf8,
|
||||
is_lsub,
|
||||
list_items,
|
||||
status_items,
|
||||
}
|
||||
.serialize(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::while_let_on_iterator)]
|
||||
pub fn matches_pattern(patterns: &[String], mailbox_name: &str) -> bool {
|
||||
if patterns.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
'outer: for pattern in patterns {
|
||||
let mut pattern_bytes = pattern.as_bytes().iter().enumerate().peekable();
|
||||
let mut mailbox_name = mailbox_name.as_bytes().iter().peekable();
|
||||
|
||||
'inner: while let Some((pos, &ch)) = pattern_bytes.next() {
|
||||
if ch == b'%' || ch == b'*' {
|
||||
let mut end_pos = pos;
|
||||
while let Some(&(_, &next_ch)) = pattern_bytes.peek() {
|
||||
if next_ch == b'%' || next_ch == b'*' {
|
||||
break;
|
||||
} else {
|
||||
end_pos = pattern_bytes.next().unwrap().0;
|
||||
}
|
||||
}
|
||||
if end_pos > pos {
|
||||
let match_bytes = &pattern.as_bytes()[pos + 1..end_pos + 1];
|
||||
let mut match_count = 0;
|
||||
let pattern_eof = end_pos == pattern.len() - 1;
|
||||
|
||||
loop {
|
||||
match mailbox_name.next() {
|
||||
Some(&ch) => {
|
||||
if match_bytes[match_count] == ch {
|
||||
match_count += 1;
|
||||
if match_count == match_bytes.len() {
|
||||
if !pattern_eof {
|
||||
continue 'inner;
|
||||
} else if mailbox_name.peek().is_none() {
|
||||
return true;
|
||||
} else {
|
||||
// Match needs to be at the end of the string,
|
||||
// reset counter.
|
||||
match_count = 0;
|
||||
}
|
||||
}
|
||||
} else if match_count > 0 {
|
||||
match_count = 0;
|
||||
}
|
||||
}
|
||||
None => continue 'outer,
|
||||
}
|
||||
}
|
||||
} else if ch == b'*' || !mailbox_name.any(|&ch| ch == b'/') {
|
||||
return true;
|
||||
} else {
|
||||
continue 'outer;
|
||||
}
|
||||
} else {
|
||||
match mailbox_name.next() {
|
||||
Some(&mch) if mch == ch => (),
|
||||
_ => continue 'outer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mailbox_name.next().is_none() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::core::Session;
|
||||
use common::network::SessionStream;
|
||||
use directory::Credentials;
|
||||
use imap_proto::{Command, receiver::Request};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_login(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
let arguments = request.parse_login()?;
|
||||
|
||||
self.authenticate(
|
||||
Credentials::Basic {
|
||||
username: arguments.username.to_string(),
|
||||
secret: arguments.password.to_string(),
|
||||
mfa_token: None,
|
||||
},
|
||||
arguments.tag,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::core::Session;
|
||||
use common::network::SessionStream;
|
||||
use imap_proto::{Command, StatusResponse, receiver::Request};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_logout(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
let op_start = Instant::now();
|
||||
|
||||
let mut response =
|
||||
StatusResponse::bye("Stalwart IMAP4rev2 bids you farewell.".to_string()).into_bytes();
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::Logout),
|
||||
SpanId = self.session_id,
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
response.extend(
|
||||
StatusResponse::completed(Command::Logout)
|
||||
.with_tag(request.tag)
|
||||
.into_bytes(),
|
||||
);
|
||||
self.write_bytes(response).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use ::store::query::log::Query;
|
||||
use imap_proto::ResponseCode;
|
||||
|
||||
pub mod acl;
|
||||
pub mod append;
|
||||
pub mod authenticate;
|
||||
pub mod capability;
|
||||
pub mod close;
|
||||
pub mod copy_move;
|
||||
pub mod create;
|
||||
pub mod delete;
|
||||
pub mod enable;
|
||||
pub mod expunge;
|
||||
pub mod fetch;
|
||||
pub mod idle;
|
||||
pub mod list;
|
||||
pub mod login;
|
||||
pub mod logout;
|
||||
pub mod namespace;
|
||||
pub mod noop;
|
||||
pub mod quota;
|
||||
pub mod rename;
|
||||
pub mod search;
|
||||
pub mod select;
|
||||
pub mod status;
|
||||
pub mod store;
|
||||
pub mod subscribe;
|
||||
pub mod thread;
|
||||
pub mod uidbatches;
|
||||
|
||||
trait FromModSeq {
|
||||
fn from_modseq(modseq: u64) -> Self;
|
||||
}
|
||||
|
||||
trait ToModSeq {
|
||||
fn to_modseq(&self) -> u64;
|
||||
}
|
||||
|
||||
impl FromModSeq for Query {
|
||||
fn from_modseq(modseq: u64) -> Self {
|
||||
if modseq > 0 {
|
||||
Query::Since(modseq - 1)
|
||||
} else {
|
||||
Query::All
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToModSeq for u64 {
|
||||
fn to_modseq(&self) -> u64 {
|
||||
if *self > 0 { *self + 1 } else { 0 }
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! spawn_op {
|
||||
($data:expr, $($code:tt)*) => {
|
||||
{
|
||||
|
||||
tokio::spawn(async move {
|
||||
let data = &($data);
|
||||
|
||||
if let Err(err) = (async {
|
||||
$($code)*
|
||||
})
|
||||
.await
|
||||
{
|
||||
let _ = data.write_error(err).await;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())}
|
||||
};
|
||||
}
|
||||
pub trait ImapContext<T> {
|
||||
fn imap_ctx(self, tag: &str, location: &'static str) -> trc::Result<T>;
|
||||
}
|
||||
|
||||
impl<T> ImapContext<T> for trc::Result<T> {
|
||||
fn imap_ctx(self, tag: &str, location: &'static str) -> trc::Result<T> {
|
||||
match self {
|
||||
Ok(value) => Ok(value),
|
||||
Err(err) => Err(
|
||||
if !err.matches(trc::EventType::Imap(trc::ImapEvent::Error)) {
|
||||
err.ctx(trc::Key::Id, tag.to_string())
|
||||
.ctx(trc::Key::Details, "Internal Server Error")
|
||||
.ctx(trc::Key::Code, ResponseCode::ContactAdmin)
|
||||
.ctx(trc::Key::CausedBy, location)
|
||||
} else {
|
||||
err.ctx(trc::Key::Id, tag.to_string())
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::core::Session;
|
||||
use common::network::SessionStream;
|
||||
use imap_proto::{
|
||||
Command, StatusResponse,
|
||||
protocol::{ImapResponse, namespace::Response},
|
||||
receiver::Request,
|
||||
};
|
||||
use registry::schema::enums::Permission;
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_namespace(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapNamespace)?;
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::Namespace),
|
||||
SpanId = self.session_id,
|
||||
Elapsed = trc::Value::Duration(0)
|
||||
);
|
||||
|
||||
self.write_bytes(
|
||||
StatusResponse::completed(Command::Namespace)
|
||||
.with_tag(request.tag)
|
||||
.serialize(
|
||||
Response {
|
||||
shared_prefix: if self.state.session_data().mailboxes.lock().len() > 1 {
|
||||
Some(self.server.core.email.shared_folder.as_str().into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}
|
||||
.serialize(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::core::{Session, State};
|
||||
use common::network::SessionStream;
|
||||
use imap_proto::{Command, StatusResponse, receiver::Request};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_noop(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
let op_start = Instant::now();
|
||||
|
||||
if let State::Selected { data, mailbox, .. } = &self.state {
|
||||
data.write_changes(
|
||||
&Some(mailbox.clone()),
|
||||
false,
|
||||
true,
|
||||
self.is_qresync || self.is_uidonly,
|
||||
self.is_uidonly,
|
||||
self.version.is_rev2(),
|
||||
self.is_utf8,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::Noop),
|
||||
SpanId = self.session_id,
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
self.write_bytes(
|
||||
StatusResponse::completed(request.command)
|
||||
.with_tag(request.tag)
|
||||
.into_bytes(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
core::{Session, SessionData},
|
||||
op::ImapContext,
|
||||
spawn_op,
|
||||
};
|
||||
use common::network::SessionStream;
|
||||
use imap_proto::{
|
||||
Command, ResponseCode, StatusResponse,
|
||||
protocol::{
|
||||
ImapResponse,
|
||||
capability::QuotaResourceName,
|
||||
quota::{Arguments, QuotaItem, QuotaResource, Response},
|
||||
},
|
||||
receiver::Request,
|
||||
};
|
||||
use registry::schema::enums::Permission;
|
||||
use std::time::Instant;
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_get_quota(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapStatus)?;
|
||||
|
||||
let data = self.state.session_data();
|
||||
|
||||
spawn_op!(data, {
|
||||
match request.parse_get_quota() {
|
||||
Ok(argument) => match data.get_quota(argument).await {
|
||||
Ok(response) => {
|
||||
data.write_bytes(response).await?;
|
||||
}
|
||||
Err(error) => {
|
||||
data.write_error(error).await?;
|
||||
}
|
||||
},
|
||||
Err(err) => data.write_error(err).await?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn handle_get_quota_root(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapStatus)?;
|
||||
|
||||
let data = self.state.session_data();
|
||||
let is_utf8 = self.is_utf8;
|
||||
|
||||
spawn_op!(data, {
|
||||
match request.parse_get_quota_root(is_utf8) {
|
||||
Ok(argument) => match data.get_quota_root(argument).await {
|
||||
Ok(response) => {
|
||||
data.write_bytes(response).await?;
|
||||
}
|
||||
Err(error) => {
|
||||
data.write_error(error).await?;
|
||||
}
|
||||
},
|
||||
Err(err) => data.write_error(err).await?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
pub async fn get_quota(&self, arguments: Arguments) -> trc::Result<Vec<u8>> {
|
||||
let op_start = Instant::now();
|
||||
|
||||
// Refresh mailboxes
|
||||
self.synchronize_mailboxes(false)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
// Validate quota root
|
||||
let account_id: u32 = arguments
|
||||
.name
|
||||
.strip_prefix("#")
|
||||
.and_then(|id| id.parse().ok())
|
||||
.filter(|id| self.access_token.is_member(*id))
|
||||
.ok_or_else(|| {
|
||||
trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Invalid quota root parameter.")
|
||||
.id(arguments.tag.to_string())
|
||||
})?;
|
||||
|
||||
// Obtain access token for mailbox
|
||||
let account = self
|
||||
.server
|
||||
.account(account_id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
let used_quota = self
|
||||
.server
|
||||
.get_used_quota_account(account_id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::GetQuota),
|
||||
SpanId = self.session_id,
|
||||
Id = arguments.name.clone(),
|
||||
Details = vec![
|
||||
trc::Value::from(used_quota),
|
||||
trc::Value::from(account.disk_quota())
|
||||
],
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
// Build response
|
||||
let response = Response {
|
||||
quota_root_items: vec![],
|
||||
quota_items: vec![QuotaItem {
|
||||
name: arguments.name,
|
||||
resources: if account.disk_quota() > 0 {
|
||||
vec![QuotaResource {
|
||||
resource: QuotaResourceName::Storage,
|
||||
total: account.disk_quota(),
|
||||
used: used_quota as u64,
|
||||
}]
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
Ok(StatusResponse::ok("GETQUOTA successful.")
|
||||
.with_tag(arguments.tag)
|
||||
.serialize(response.serialize()))
|
||||
}
|
||||
|
||||
pub async fn get_quota_root(&self, arguments: Arguments) -> trc::Result<Vec<u8>> {
|
||||
let op_start = Instant::now();
|
||||
|
||||
// Refresh mailboxes
|
||||
self.synchronize_mailboxes(false)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
// Validate mailbox
|
||||
let account_id = if let Some(mailbox) = self.get_mailbox_by_name(&arguments.name) {
|
||||
mailbox.account_id
|
||||
} else {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Mailbox does not exist.")
|
||||
.code(ResponseCode::TryCreate)
|
||||
.id(arguments.tag));
|
||||
};
|
||||
|
||||
// Obtain access token for mailbox
|
||||
let account = self
|
||||
.server
|
||||
.account(account_id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
let used_quota = self
|
||||
.server
|
||||
.get_used_quota_account(account_id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::GetQuota),
|
||||
SpanId = self.session_id,
|
||||
MailboxName = arguments.name.clone(),
|
||||
Details = vec![
|
||||
trc::Value::from(used_quota),
|
||||
trc::Value::from(account.disk_quota())
|
||||
],
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
// Build response
|
||||
let response = Response {
|
||||
quota_root_items: vec![arguments.name, format!("#{account_id}")],
|
||||
quota_items: vec![QuotaItem {
|
||||
name: format!("#{account_id}"),
|
||||
resources: if account.disk_quota() > 0 {
|
||||
vec![QuotaResource {
|
||||
resource: QuotaResourceName::Storage,
|
||||
total: account.disk_quota(),
|
||||
used: used_quota as u64,
|
||||
}]
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
Ok(StatusResponse::ok("GETQUOTAROOT successful.")
|
||||
.with_tag(arguments.tag)
|
||||
.serialize(response.serialize()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
core::{Session, SessionData},
|
||||
spawn_op,
|
||||
};
|
||||
use common::{network::SessionStream, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder};
|
||||
use email::cache::MessageCacheFetch;
|
||||
use imap_proto::{
|
||||
Command, ResponseCode, StatusResponse,
|
||||
protocol::{ObjectId, rename::Arguments},
|
||||
receiver::Request,
|
||||
};
|
||||
use registry::schema::enums::{Permission, StorageQuota};
|
||||
use std::time::Instant;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive, BatchBuilder},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{acl::Acl, collection::Collection, id::Id};
|
||||
|
||||
use super::ImapContext;
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_rename(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapRename)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let arguments = request.parse_rename(self.is_utf8)?;
|
||||
let data = self.state.session_data();
|
||||
let is_objectid = self.is_objectid;
|
||||
|
||||
spawn_op!(data, {
|
||||
let response = data.rename_folder(arguments, is_objectid, op_start).await?;
|
||||
data.write_bytes(response.into_bytes()).await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
pub async fn rename_folder(
|
||||
&self,
|
||||
arguments: Arguments,
|
||||
is_objectid: bool,
|
||||
op_start: Instant,
|
||||
) -> trc::Result<StatusResponse> {
|
||||
// Refresh mailboxes
|
||||
self.synchronize_mailboxes(false)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
// Validate mailbox name
|
||||
let mut params = self
|
||||
.validate_mailbox_create(&arguments.new_mailbox_name, None)
|
||||
.await
|
||||
.add_context(|err| err.id(arguments.tag.clone()))?;
|
||||
params.is_rename = true;
|
||||
|
||||
// Validate source mailbox
|
||||
let mailbox_id = {
|
||||
let mut mailbox_id = None;
|
||||
for account in self.mailboxes.lock().iter() {
|
||||
if let Some(mailbox_id_) = account.mailbox_names.get(&arguments.mailbox_name) {
|
||||
if account.account_id == params.account_id {
|
||||
mailbox_id = (*mailbox_id_).into();
|
||||
break;
|
||||
} else {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Cannot move mailboxes between accounts.")
|
||||
.code(ResponseCode::Cannot)
|
||||
.id(arguments.tag));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(mailbox_id) = mailbox_id {
|
||||
mailbox_id
|
||||
} else {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(format!("Mailbox '{}' not found.", arguments.mailbox_name))
|
||||
.code(ResponseCode::NonExistent)
|
||||
.id(arguments.tag));
|
||||
}
|
||||
};
|
||||
|
||||
// Obtain mailbox
|
||||
let mailbox_ = self
|
||||
.server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
params.account_id,
|
||||
Collection::Mailbox,
|
||||
mailbox_id,
|
||||
))
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
.ok_or_else(|| {
|
||||
trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(format!("Mailbox '{}' not found.", arguments.mailbox_name))
|
||||
.caused_by(trc::location!())
|
||||
.code(ResponseCode::NonExistent)
|
||||
.id(arguments.tag.clone())
|
||||
})?;
|
||||
let mailbox = mailbox_
|
||||
.to_unarchived::<email::mailbox::Mailbox>()
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
// Validate ACL
|
||||
let access_token = self
|
||||
.refresh_access_token()
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
if access_token.is_shared(params.account_id)
|
||||
&& !mailbox
|
||||
.inner
|
||||
.acls
|
||||
.effective_acl(&access_token)
|
||||
.contains(Acl::Modify)
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("You are not allowed to rename this mailbox.")
|
||||
.code(ResponseCode::NoPerm)
|
||||
.id(arguments.tag));
|
||||
}
|
||||
|
||||
// Get new mailbox name from path
|
||||
let new_mailbox_name = params.path.pop().unwrap();
|
||||
|
||||
// Validate quota
|
||||
if !params.path.is_empty() {
|
||||
let account = self
|
||||
.server
|
||||
.account(params.account_id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
let mailbox_count = self
|
||||
.server
|
||||
.get_cached_messages(params.account_id)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
.mailboxes
|
||||
.items
|
||||
.len();
|
||||
if mailbox_count + params.path.len()
|
||||
> self
|
||||
.server
|
||||
.object_quota(account.object_quotas(), StorageQuota::MaxMailboxes)
|
||||
as usize
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(concat!(
|
||||
"There are too many mailboxes, ",
|
||||
"please delete some before adding a new one."
|
||||
))
|
||||
.code(ResponseCode::OverQuota)
|
||||
.id(arguments.tag.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// Build batch
|
||||
let mut parent_id = params.parent_mailbox_id.map(|id| id + 1).unwrap_or(0);
|
||||
let mut create_ids = Vec::with_capacity(params.path.len());
|
||||
let mut next_document_id = self
|
||||
.server
|
||||
.store()
|
||||
.assign_document_ids(
|
||||
params.account_id,
|
||||
Collection::Mailbox,
|
||||
params.path.len() as u64,
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let mut batch = BatchBuilder::new();
|
||||
|
||||
for &path_item in params.path.iter() {
|
||||
let mailbox_id = next_document_id;
|
||||
next_document_id -= 1;
|
||||
|
||||
batch
|
||||
.with_account_id(params.account_id)
|
||||
.with_collection(Collection::Mailbox)
|
||||
.with_document(mailbox_id)
|
||||
.custom(ObjectIndexBuilder::<(), _>::new().with_changes(
|
||||
email::mailbox::Mailbox::new(path_item).with_parent_id(parent_id),
|
||||
))
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
.commit_point();
|
||||
|
||||
parent_id = mailbox_id + 1;
|
||||
create_ids.push(mailbox_id);
|
||||
}
|
||||
|
||||
let mut new_mailbox = mailbox
|
||||
.deserialize::<email::mailbox::Mailbox>()
|
||||
.caused_by(trc::location!())?;
|
||||
new_mailbox.name = new_mailbox_name.into();
|
||||
new_mailbox.parent_id = parent_id;
|
||||
new_mailbox.uid_validity = rand::random::<u32>();
|
||||
batch
|
||||
.with_account_id(params.account_id)
|
||||
.with_collection(Collection::Mailbox)
|
||||
.with_document(mailbox_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::new()
|
||||
.with_current(mailbox)
|
||||
.with_changes(new_mailbox),
|
||||
)
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
self.server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
let account_id = params.account_id;
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::RenameMailbox),
|
||||
SpanId = self.session_id,
|
||||
AccountId = account_id,
|
||||
MailboxName = arguments.new_mailbox_name,
|
||||
MailboxId = mailbox_id,
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
let response = StatusResponse::completed(Command::Rename).with_tag(arguments.tag);
|
||||
Ok(if is_objectid {
|
||||
response.with_code(ResponseCode::ObjectId(ObjectId {
|
||||
mailbox_id: Some(Id::from(mailbox_id)),
|
||||
account_id: Some(Id::from(account_id)),
|
||||
..Default::default()
|
||||
}))
|
||||
} else {
|
||||
response
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,789 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{FromModSeq, ToModSeq};
|
||||
use crate::{
|
||||
core::{ImapId, SavedSearch, SelectedMailbox, Session, SessionData},
|
||||
spawn_op,
|
||||
};
|
||||
use common::network::SessionStream;
|
||||
use email::cache::{MessageCacheFetch, email::MessageCacheAccess};
|
||||
use imap_proto::{
|
||||
Command, ResponseCode, ResponseType, StatusResponse,
|
||||
protocol::{
|
||||
Sequence,
|
||||
search::{self, Arguments, Comparator, Filter, Response, ResultOption},
|
||||
},
|
||||
receiver::Request,
|
||||
};
|
||||
use mail_parser::HeaderName;
|
||||
use nlp::language::Language;
|
||||
use registry::schema::enums::Permission;
|
||||
use std::{str::FromStr, sync::Arc, time::Instant};
|
||||
use store::{
|
||||
query::log::Query,
|
||||
roaring::RoaringBitmap,
|
||||
search::{
|
||||
EmailSearchField, SearchComparator, SearchFilter, SearchOperator, SearchQuery, SearchValue,
|
||||
},
|
||||
write::{SearchIndex, now},
|
||||
};
|
||||
use tokio::sync::watch;
|
||||
use trc::AddContext;
|
||||
use types::{collection::SyncCollection, id::Id, keyword::Keyword};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_search(
|
||||
&mut self,
|
||||
request: Request<Command>,
|
||||
is_sort: bool,
|
||||
is_uid: bool,
|
||||
) -> trc::Result<()> {
|
||||
let op_start = Instant::now();
|
||||
let mut arguments = if !is_sort {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapSearch)?;
|
||||
|
||||
request.parse_search(self.version)
|
||||
} else {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapSort)?;
|
||||
|
||||
request.parse_sort()
|
||||
}?;
|
||||
|
||||
// RFC 9586 forbids the sequence set criterion once UIDONLY is enabled
|
||||
if self.is_uidonly
|
||||
&& arguments.filter.iter().any(|filter| {
|
||||
matches!(filter, Filter::Sequence(sequence, false) if !sequence.is_saved_search())
|
||||
})
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("The sequence set search criterion is not allowed once UIDONLY is enabled.")
|
||||
.code(ResponseCode::UidRequired)
|
||||
.ctx(trc::Key::Type, ResponseType::Bad)
|
||||
.id(arguments.tag));
|
||||
}
|
||||
|
||||
let (data, mailbox) = self.state.mailbox_state();
|
||||
let message_limit = self.server.core.imap.max_messages_per_command;
|
||||
|
||||
// Create channel for results
|
||||
let (results_tx, prev_saved_search) =
|
||||
if arguments.result_options.contains(&ResultOption::Save) {
|
||||
let prev_saved_search = Some(mailbox.get_saved_search().await);
|
||||
let (tx, rx) = watch::channel(Arc::new(Vec::new()));
|
||||
*mailbox.saved_search.lock() = SavedSearch::InFlight { rx };
|
||||
(tx.into(), prev_saved_search)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
spawn_op!(data, {
|
||||
let tag = std::mem::take(&mut arguments.tag);
|
||||
let bytes = match data
|
||||
.search(
|
||||
arguments,
|
||||
mailbox.clone(),
|
||||
results_tx,
|
||||
prev_saved_search.clone(),
|
||||
is_uid,
|
||||
message_limit,
|
||||
op_start,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((response, limited_uid)) => {
|
||||
let response = response.serialize(&tag);
|
||||
let status = StatusResponse::completed(if !is_sort {
|
||||
Command::Search(is_uid)
|
||||
} else {
|
||||
Command::Sort(is_uid)
|
||||
})
|
||||
.with_tag(tag);
|
||||
|
||||
match limited_uid {
|
||||
Some(uid) => status.with_code(ResponseCode::MessageLimit {
|
||||
limit: message_limit,
|
||||
uid: uid.into(),
|
||||
}),
|
||||
None => status,
|
||||
}
|
||||
.serialize(response)
|
||||
}
|
||||
Err(err) => {
|
||||
if let Some(prev_saved_search) = prev_saved_search {
|
||||
*mailbox.saved_search.lock() = prev_saved_search
|
||||
.map_or(SavedSearch::None, |s| SavedSearch::Results { items: s });
|
||||
}
|
||||
return Err(err.id(tag));
|
||||
}
|
||||
};
|
||||
data.write_bytes(bytes).await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn search(
|
||||
&self,
|
||||
arguments: Arguments,
|
||||
mailbox: Arc<SelectedMailbox>,
|
||||
results_tx: Option<watch::Sender<Arc<Vec<ImapId>>>>,
|
||||
prev_saved_search: Option<Option<Arc<Vec<ImapId>>>>,
|
||||
is_uid: bool,
|
||||
message_limit: u32,
|
||||
op_start: Instant,
|
||||
) -> trc::Result<(search::Response, Option<u32>)> {
|
||||
// Run query
|
||||
let is_sort = arguments.sort.is_some();
|
||||
let (result_set, include_highest_modseq) = self
|
||||
.query(
|
||||
arguments.filter,
|
||||
arguments.sort.unwrap_or_default(),
|
||||
&mailbox,
|
||||
&prev_saved_search,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Obtain modseq
|
||||
let highest_modseq = if include_highest_modseq {
|
||||
self.synchronize_messages(&mailbox)
|
||||
.await?
|
||||
.to_modseq()
|
||||
.into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Sort and map ids
|
||||
let mut min: Option<(u32, ImapId)> = None;
|
||||
let mut max: Option<(u32, ImapId)> = None;
|
||||
let mut total = 0;
|
||||
let results_len = result_set.len();
|
||||
let mut saved_results = if results_tx.is_some() {
|
||||
Some(Vec::with_capacity(results_len))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut imap_ids = Vec::with_capacity(results_len);
|
||||
mailbox.map_search_results(
|
||||
result_set.into_iter(),
|
||||
is_uid,
|
||||
arguments.result_options.contains(&ResultOption::Min),
|
||||
arguments.result_options.contains(&ResultOption::Max),
|
||||
&mut min,
|
||||
&mut max,
|
||||
&mut total,
|
||||
&mut imap_ids,
|
||||
&mut saved_results,
|
||||
);
|
||||
// RFC 9738 exempts SORT, whose ordering is meaningless once truncated
|
||||
let mut limited_uid = None;
|
||||
if !is_sort {
|
||||
imap_ids.sort_unstable();
|
||||
|
||||
let message_limit = message_limit as usize;
|
||||
if imap_ids.len() > message_limit {
|
||||
let threshold = imap_ids[imap_ids.len() - message_limit];
|
||||
imap_ids.drain(..imap_ids.len() - message_limit);
|
||||
limited_uid = if is_uid {
|
||||
Some(threshold)
|
||||
} else {
|
||||
mailbox.seqnum_to_uid(threshold)
|
||||
};
|
||||
|
||||
// RFC 9738 requires the saved search to be truncated to match
|
||||
if let Some(saved_results) = saved_results.as_mut() {
|
||||
saved_results.retain(|imap_id| {
|
||||
if is_uid {
|
||||
imap_id.uid >= threshold
|
||||
} else {
|
||||
imap_id.seqnum >= threshold
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save results
|
||||
if let (Some(results_tx), Some(saved_results)) = (results_tx, saved_results) {
|
||||
let saved_results = Arc::new(saved_results);
|
||||
*mailbox.saved_search.lock() = SavedSearch::Results {
|
||||
items: saved_results.clone(),
|
||||
};
|
||||
results_tx.send(saved_results).ok();
|
||||
}
|
||||
|
||||
trc::event!(
|
||||
Imap(if !is_sort {
|
||||
trc::ImapEvent::Search
|
||||
} else {
|
||||
trc::ImapEvent::Sort
|
||||
}),
|
||||
SpanId = self.session_id,
|
||||
AccountId = mailbox.id.account_id,
|
||||
MailboxId = mailbox.id.mailbox_id,
|
||||
Total = total,
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
// Build response
|
||||
Ok((
|
||||
Response {
|
||||
is_uid,
|
||||
min: min.map(|(id, _)| id),
|
||||
max: max.map(|(id, _)| id),
|
||||
count: if arguments.result_options.contains(&ResultOption::Count) {
|
||||
Some(total)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
ids: if arguments.result_options.is_empty()
|
||||
|| arguments.result_options.contains(&ResultOption::All)
|
||||
{
|
||||
imap_ids
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
is_sort,
|
||||
is_esearch: arguments.is_esearch,
|
||||
highest_modseq,
|
||||
},
|
||||
limited_uid,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn query(
|
||||
&self,
|
||||
imap_filter: Vec<Filter>,
|
||||
imap_comparator: Vec<Comparator>,
|
||||
mailbox: &SelectedMailbox,
|
||||
prev_saved_search: &Option<Option<Arc<Vec<ImapId>>>>,
|
||||
) -> trc::Result<(Vec<u32>, bool)> {
|
||||
// Obtain message ids
|
||||
let mut filters = Vec::with_capacity(imap_filter.len() + 1);
|
||||
let cache = self
|
||||
.server
|
||||
.get_cached_messages(mailbox.id.account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let message_ids = RoaringBitmap::from_iter(
|
||||
cache
|
||||
.in_mailbox(mailbox.id.mailbox_id)
|
||||
.map(|m| m.document_id),
|
||||
);
|
||||
|
||||
// Convert query
|
||||
let mut include_highest_modseq = false;
|
||||
for filter in imap_filter {
|
||||
match filter {
|
||||
Filter::Sequence(sequence, uid_filter) => {
|
||||
let mut set = RoaringBitmap::new();
|
||||
if let (Sequence::SavedSearch, Some(prev_saved_search)) =
|
||||
(&sequence, &prev_saved_search)
|
||||
{
|
||||
if let Some(prev_saved_search) = prev_saved_search {
|
||||
let state = mailbox.state.lock();
|
||||
for imap_id in prev_saved_search.iter() {
|
||||
if let Some(id) = state.uid_to_id.get(&imap_id.uid) {
|
||||
set.insert(*id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("No saved search found."));
|
||||
}
|
||||
} else {
|
||||
for id in mailbox.sequence_to_ids(&sequence, uid_filter).await?.keys() {
|
||||
set.insert(*id);
|
||||
}
|
||||
}
|
||||
filters.push(SearchFilter::is_in_set(set));
|
||||
}
|
||||
Filter::UidAfter(uid) => {
|
||||
filters.push(SearchFilter::is_in_set(match uid.checked_add(1) {
|
||||
Some(min) => mailbox.uids_in_range(Some(min), None),
|
||||
None => RoaringBitmap::new(),
|
||||
}));
|
||||
}
|
||||
Filter::UidBefore(uid) => {
|
||||
filters.push(SearchFilter::is_in_set(if uid > 1 {
|
||||
mailbox.uids_in_range(None, Some(uid - 1))
|
||||
} else {
|
||||
RoaringBitmap::new()
|
||||
}));
|
||||
}
|
||||
Filter::All => {
|
||||
filters.push(SearchFilter::is_in_set(message_ids.clone()));
|
||||
}
|
||||
Filter::Answered => {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache
|
||||
.with_keyword(&Keyword::Answered)
|
||||
.map(|m| m.document_id),
|
||||
)));
|
||||
}
|
||||
Filter::Before(date) => {
|
||||
filters.push(SearchFilter::lt(EmailSearchField::ReceivedAt, date));
|
||||
}
|
||||
Filter::Deleted => {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.with_keyword(&Keyword::Deleted).map(|m| m.document_id),
|
||||
)));
|
||||
}
|
||||
Filter::Draft => {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.with_keyword(&Keyword::Draft).map(|m| m.document_id),
|
||||
)));
|
||||
}
|
||||
Filter::Flagged => {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.with_keyword(&Keyword::Flagged).map(|m| m.document_id),
|
||||
)));
|
||||
}
|
||||
Filter::Keyword(keyword) => {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache
|
||||
.with_keyword(&Keyword::from(keyword))
|
||||
.map(|m| m.document_id),
|
||||
)));
|
||||
}
|
||||
Filter::Larger(size) => {
|
||||
filters.push(SearchFilter::gt(EmailSearchField::Size, size));
|
||||
}
|
||||
Filter::On(date) => {
|
||||
filters.push(SearchFilter::And);
|
||||
filters.push(SearchFilter::ge(EmailSearchField::ReceivedAt, date));
|
||||
filters.push(SearchFilter::lt(EmailSearchField::ReceivedAt, date + 86400));
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
Filter::Seen => {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.with_keyword(&Keyword::Seen).map(|m| m.document_id),
|
||||
)));
|
||||
}
|
||||
Filter::SentBefore(date) => {
|
||||
filters.push(SearchFilter::lt(EmailSearchField::SentAt, date));
|
||||
}
|
||||
Filter::SentOn(date) => {
|
||||
filters.push(SearchFilter::And);
|
||||
filters.push(SearchFilter::ge(EmailSearchField::SentAt, date));
|
||||
filters.push(SearchFilter::lt(EmailSearchField::SentAt, date + 86400));
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
Filter::SentSince(date) => {
|
||||
filters.push(SearchFilter::ge(EmailSearchField::SentAt, date));
|
||||
}
|
||||
Filter::Since(date) => {
|
||||
filters.push(SearchFilter::ge(EmailSearchField::ReceivedAt, date));
|
||||
}
|
||||
Filter::Smaller(size) => {
|
||||
filters.push(SearchFilter::lt(EmailSearchField::Size, size));
|
||||
}
|
||||
Filter::Unanswered => {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache
|
||||
.without_keyword(&Keyword::Answered)
|
||||
.map(|m| m.document_id),
|
||||
)));
|
||||
}
|
||||
Filter::Undeleted => {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache
|
||||
.without_keyword(&Keyword::Deleted)
|
||||
.map(|m| m.document_id),
|
||||
)));
|
||||
}
|
||||
Filter::Undraft => {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache
|
||||
.without_keyword(&Keyword::Draft)
|
||||
.map(|m| m.document_id),
|
||||
)));
|
||||
}
|
||||
Filter::Unflagged => {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache
|
||||
.without_keyword(&Keyword::Flagged)
|
||||
.map(|m| m.document_id),
|
||||
)));
|
||||
}
|
||||
Filter::Unkeyword(keyword) => {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache
|
||||
.without_keyword(&Keyword::from(keyword))
|
||||
.map(|m| m.document_id),
|
||||
)));
|
||||
}
|
||||
Filter::Unseen => {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.without_keyword(&Keyword::Seen).map(|m| m.document_id),
|
||||
)));
|
||||
}
|
||||
Filter::Recent => {
|
||||
//filters.push(SearchFilter::is_in_set(self.get_recent(&mailbox.id)));
|
||||
}
|
||||
Filter::New => {
|
||||
/*filters.push(SearchFilter::And);
|
||||
filters.push(SearchFilter::is_in_set(self.get_recent(&mailbox.id)));
|
||||
filters.push(SearchFilter::Not);
|
||||
filters.push(SearchFilter::is_in_bitmap(
|
||||
EmailSearchField::Keywords,
|
||||
Keyword::Seen,
|
||||
));
|
||||
filters.push(SearchFilter::End);
|
||||
filters.push(SearchFilter::End);*/
|
||||
}
|
||||
Filter::Old => {
|
||||
/*filters.push(SearchFilter::Not);
|
||||
filters.push(SearchFilter::is_in_set(self.get_recent(&mailbox.id)));
|
||||
filters.push(SearchFilter::End);*/
|
||||
}
|
||||
Filter::Older(secs) => {
|
||||
filters.push(SearchFilter::le(
|
||||
EmailSearchField::ReceivedAt,
|
||||
now().saturating_sub(secs as u64),
|
||||
));
|
||||
}
|
||||
Filter::Younger(secs) => {
|
||||
filters.push(SearchFilter::ge(
|
||||
EmailSearchField::ReceivedAt,
|
||||
now().saturating_sub(secs as u64),
|
||||
));
|
||||
}
|
||||
Filter::ModSeq((modseq, _)) => {
|
||||
let mut set = RoaringBitmap::new();
|
||||
for id in self
|
||||
.server
|
||||
.store()
|
||||
.changes(
|
||||
mailbox.id.account_id,
|
||||
SyncCollection::Email.into(),
|
||||
Query::from_modseq(modseq),
|
||||
)
|
||||
.await?
|
||||
.changes
|
||||
.into_iter()
|
||||
.filter_map(|change| change.try_unwrap_item_id())
|
||||
{
|
||||
let id = (id & u32::MAX as u64) as u32;
|
||||
if message_ids.contains(id) {
|
||||
set.insert(id);
|
||||
}
|
||||
}
|
||||
filters.push(SearchFilter::is_in_set(set));
|
||||
include_highest_modseq = true;
|
||||
}
|
||||
Filter::EmailId(id) => {
|
||||
if let Ok(id) = Id::from_str(&id) {
|
||||
filters.push(SearchFilter::is_in_set(
|
||||
RoaringBitmap::from_sorted_iter([id.document_id()]).unwrap(),
|
||||
));
|
||||
} else {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(format!("Failed to parse email id '{id}'.",)));
|
||||
}
|
||||
}
|
||||
Filter::ThreadId(id) => {
|
||||
if let Ok(id) = Id::from_str(&id) {
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
cache.in_thread(id.document_id()).map(|m| m.document_id),
|
||||
)));
|
||||
} else {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(format!("Failed to parse thread id '{id}'.",)));
|
||||
}
|
||||
}
|
||||
Filter::Bcc(text) => {
|
||||
filters.push(SearchFilter::has_text(
|
||||
EmailSearchField::Bcc,
|
||||
text,
|
||||
Language::None,
|
||||
));
|
||||
}
|
||||
Filter::Body(text) => {
|
||||
filters.push(SearchFilter::has_text_detect(
|
||||
EmailSearchField::Body,
|
||||
text,
|
||||
self.server.core.email.default_language,
|
||||
));
|
||||
}
|
||||
Filter::Cc(text) => {
|
||||
filters.push(SearchFilter::has_text(
|
||||
EmailSearchField::Cc,
|
||||
text,
|
||||
Language::None,
|
||||
));
|
||||
}
|
||||
Filter::From(text) => {
|
||||
filters.push(SearchFilter::has_text(
|
||||
EmailSearchField::From,
|
||||
text,
|
||||
Language::None,
|
||||
));
|
||||
}
|
||||
Filter::Header(header, value) => {
|
||||
if let Some(header) = HeaderName::parse(header) {
|
||||
match header {
|
||||
HeaderName::Subject => {
|
||||
filters.push(SearchFilter::has_text_detect(
|
||||
EmailSearchField::Subject,
|
||||
value,
|
||||
self.server.core.email.default_language,
|
||||
));
|
||||
}
|
||||
header @ (HeaderName::From
|
||||
| HeaderName::To
|
||||
| HeaderName::Cc
|
||||
| HeaderName::Bcc) => {
|
||||
filters.push(SearchFilter::has_text(
|
||||
match header {
|
||||
HeaderName::From => EmailSearchField::From,
|
||||
HeaderName::To => EmailSearchField::To,
|
||||
HeaderName::Cc => EmailSearchField::Cc,
|
||||
HeaderName::Bcc => EmailSearchField::Bcc,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
value,
|
||||
Language::None,
|
||||
));
|
||||
}
|
||||
header => {
|
||||
let op = if matches!(
|
||||
header,
|
||||
HeaderName::MessageId
|
||||
| HeaderName::InReplyTo
|
||||
| HeaderName::References
|
||||
| HeaderName::ResentMessageId
|
||||
) || value.is_empty()
|
||||
{
|
||||
SearchOperator::Equal
|
||||
} else {
|
||||
SearchOperator::Contains
|
||||
};
|
||||
|
||||
filters.push(SearchFilter::cond(
|
||||
EmailSearchField::Headers,
|
||||
op,
|
||||
SearchValue::KeyValues(
|
||||
VecMap::with_capacity(1)
|
||||
.with_append(header.as_str().to_lowercase(), value),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Filter::Subject(text) => {
|
||||
filters.push(SearchFilter::has_text_detect(
|
||||
EmailSearchField::Subject,
|
||||
text,
|
||||
self.server.core.email.default_language,
|
||||
));
|
||||
}
|
||||
Filter::Text(text) => {
|
||||
let (text, language) =
|
||||
Language::detect(text, self.server.core.email.default_language);
|
||||
|
||||
filters.push(SearchFilter::Or);
|
||||
filters.push(SearchFilter::has_text(
|
||||
EmailSearchField::From,
|
||||
&text,
|
||||
Language::None,
|
||||
));
|
||||
filters.push(SearchFilter::has_text(
|
||||
EmailSearchField::To,
|
||||
&text,
|
||||
Language::None,
|
||||
));
|
||||
filters.push(SearchFilter::has_text(
|
||||
EmailSearchField::Cc,
|
||||
&text,
|
||||
Language::None,
|
||||
));
|
||||
filters.push(SearchFilter::has_text(
|
||||
EmailSearchField::Bcc,
|
||||
&text,
|
||||
Language::None,
|
||||
));
|
||||
filters.push(SearchFilter::has_text(
|
||||
EmailSearchField::Subject,
|
||||
&text,
|
||||
language,
|
||||
));
|
||||
filters.push(SearchFilter::has_text(
|
||||
EmailSearchField::Body,
|
||||
&text,
|
||||
language,
|
||||
));
|
||||
filters.push(SearchFilter::has_text(
|
||||
EmailSearchField::Attachment,
|
||||
text,
|
||||
language,
|
||||
));
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
Filter::To(text) => {
|
||||
filters.push(SearchFilter::has_text(
|
||||
EmailSearchField::To,
|
||||
text,
|
||||
Language::None,
|
||||
));
|
||||
}
|
||||
Filter::And => {
|
||||
filters.push(SearchFilter::And);
|
||||
}
|
||||
Filter::Or => {
|
||||
filters.push(SearchFilter::Or);
|
||||
}
|
||||
Filter::Not => {
|
||||
filters.push(SearchFilter::Not);
|
||||
}
|
||||
Filter::End => {
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert comparators
|
||||
let mut comparators = Vec::with_capacity(imap_comparator.len());
|
||||
for comparator in imap_comparator {
|
||||
comparators.push(match comparator.sort {
|
||||
search::Sort::Arrival => {
|
||||
SearchComparator::field(EmailSearchField::ReceivedAt, comparator.ascending)
|
||||
}
|
||||
search::Sort::Cc => {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Sorting by CC is not supported."));
|
||||
}
|
||||
search::Sort::Date => {
|
||||
SearchComparator::field(EmailSearchField::SentAt, comparator.ascending)
|
||||
}
|
||||
search::Sort::From | search::Sort::DisplayFrom => {
|
||||
SearchComparator::field(EmailSearchField::From, comparator.ascending)
|
||||
}
|
||||
search::Sort::Size => {
|
||||
SearchComparator::field(EmailSearchField::Size, comparator.ascending)
|
||||
}
|
||||
search::Sort::Subject => {
|
||||
SearchComparator::field(EmailSearchField::Subject, comparator.ascending)
|
||||
}
|
||||
search::Sort::To | search::Sort::DisplayTo => {
|
||||
SearchComparator::field(EmailSearchField::To, comparator.ascending)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Run query
|
||||
self.server
|
||||
.search_store()
|
||||
.query_account(
|
||||
SearchQuery::new(SearchIndex::Email)
|
||||
.with_filters(filters)
|
||||
.with_comparators(comparators)
|
||||
.with_account_id(mailbox.id.account_id)
|
||||
.with_mask(message_ids),
|
||||
)
|
||||
.await
|
||||
.map(|res| (res, include_highest_modseq))
|
||||
.caused_by(trc::location!())
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedMailbox {
|
||||
pub async fn get_saved_search(&self) -> Option<Arc<Vec<ImapId>>> {
|
||||
let mut rx = match &*self.saved_search.lock() {
|
||||
SavedSearch::InFlight { rx } => rx.clone(),
|
||||
SavedSearch::Results { items } => {
|
||||
return Some(items.clone());
|
||||
}
|
||||
SavedSearch::None => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
rx.changed().await.ok();
|
||||
let v = rx.borrow();
|
||||
Some(v.clone())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn map_search_results(
|
||||
&self,
|
||||
ids: impl Iterator<Item = u32>,
|
||||
is_uid: bool,
|
||||
find_min: bool,
|
||||
find_max: bool,
|
||||
min: &mut Option<(u32, ImapId)>,
|
||||
max: &mut Option<(u32, ImapId)>,
|
||||
total: &mut u32,
|
||||
imap_ids: &mut Vec<u32>,
|
||||
saved_results: &mut Option<Vec<ImapId>>,
|
||||
) {
|
||||
let state = self.state.lock();
|
||||
let find_min_or_max = find_min || find_max;
|
||||
for document_id in ids {
|
||||
if let Some((id, imap_id)) = state.map_result_id(document_id, is_uid) {
|
||||
if find_min_or_max {
|
||||
if find_min {
|
||||
if let Some((prev_min, _)) = min {
|
||||
if id < *prev_min {
|
||||
*min = Some((id, imap_id));
|
||||
}
|
||||
} else {
|
||||
*min = Some((id, imap_id));
|
||||
}
|
||||
}
|
||||
if find_max {
|
||||
if let Some((prev_max, _)) = max {
|
||||
if id > *prev_max {
|
||||
*max = Some((id, imap_id));
|
||||
}
|
||||
} else {
|
||||
*max = Some((id, imap_id));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
imap_ids.push(id);
|
||||
if let Some(r) = saved_results.as_mut() {
|
||||
r.push(imap_id)
|
||||
}
|
||||
}
|
||||
*total += 1;
|
||||
}
|
||||
}
|
||||
if find_min || find_max {
|
||||
for (id, imap_id) in [min, max].into_iter().flatten() {
|
||||
imap_ids.push(*id);
|
||||
if let Some(r) = saved_results.as_mut() {
|
||||
r.push(*imap_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SavedSearch {
|
||||
pub async fn unwrap(&self) -> Option<Arc<Vec<ImapId>>> {
|
||||
match self {
|
||||
SavedSearch::InFlight { rx } => {
|
||||
let mut rx = rx.clone();
|
||||
rx.changed().await.ok();
|
||||
let v = rx.borrow();
|
||||
Some(v.clone())
|
||||
}
|
||||
SavedSearch::Results { items } => Some(items.clone()),
|
||||
SavedSearch::None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{ImapContext, ToModSeq};
|
||||
use crate::core::{SavedSearch, SelectedMailbox, Session, State};
|
||||
use common::network::SessionStream;
|
||||
use imap_proto::{
|
||||
Command, ResponseCode, ResponseType, StatusResponse,
|
||||
protocol::{
|
||||
ImapResponse, ObjectId, Sequence, fetch,
|
||||
list::ListItem,
|
||||
select::{HighestModSeq, Response},
|
||||
},
|
||||
receiver::Request,
|
||||
};
|
||||
use registry::schema::enums::Permission;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use types::{acl::Acl, id::Id};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_select(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(if request.command == Command::Select {
|
||||
Permission::ImapSelect
|
||||
} else {
|
||||
Permission::ImapExamine
|
||||
})?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let is_select = request.command == Command::Select;
|
||||
let command = request.command;
|
||||
let arguments = request.parse_select(self.is_utf8)?;
|
||||
let data = self.state.session_data();
|
||||
|
||||
// Activate OBJECTID+ when the OBJECTID parameter is supplied
|
||||
if arguments.objectid.is_some()
|
||||
&& let Some(enabled) = self.activate_objectid()
|
||||
{
|
||||
self.write_bytes(enabled).await?;
|
||||
}
|
||||
|
||||
// Once activated, every SELECT/EXAMINE returns the compound OBJECTID response code
|
||||
let want_objectid = self.is_objectid;
|
||||
|
||||
// Refresh mailboxes
|
||||
data.synchronize_mailboxes(false)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
// Resolve the mailbox by its object identifiers (with fallback to the name)
|
||||
let mailbox = arguments
|
||||
.objectid
|
||||
.as_ref()
|
||||
.and_then(
|
||||
|objectid| match (objectid.account_id, objectid.mailbox_id) {
|
||||
(Some(account_id), Some(mailbox_id)) => {
|
||||
data.get_mailbox_by_id(account_id.document_id(), mailbox_id.document_id())
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
.or_else(|| data.get_mailbox_by_name(&arguments.mailbox_name));
|
||||
|
||||
if let Some(mailbox) = mailbox {
|
||||
if !data
|
||||
.check_mailbox_acl(mailbox.account_id, mailbox.mailbox_id, Acl::ReadItems)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("You do not have the required permissions to read this mailbox.")
|
||||
.code(ResponseCode::NoPerm)
|
||||
.id(arguments.tag));
|
||||
}
|
||||
|
||||
// Try obtaining the mailbox from the cache
|
||||
let state = data
|
||||
.fetch_messages(&mailbox, None)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
.unwrap();
|
||||
|
||||
// Synchronize messages
|
||||
let closed_previous = self.state.close_mailbox();
|
||||
let is_condstore = self.is_condstore || arguments.condstore;
|
||||
|
||||
// Build new state
|
||||
let is_rev2 = self.version.is_rev2();
|
||||
let is_utf8 = self.is_utf8;
|
||||
let mailbox_state = data.mailbox_state(&mailbox).unwrap();
|
||||
let total_messages = state.total_messages;
|
||||
let highest_modseq = if is_condstore {
|
||||
HighestModSeq::new(state.modseq.to_modseq()).into()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mailbox = Arc::new(SelectedMailbox {
|
||||
id: mailbox,
|
||||
state: parking_lot::Mutex::new(state),
|
||||
saved_search: parking_lot::Mutex::new(SavedSearch::None),
|
||||
is_select,
|
||||
is_condstore,
|
||||
});
|
||||
|
||||
// Validate QRESYNC arguments
|
||||
if let Some(qresync) = arguments.qresync {
|
||||
if !self.is_qresync {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("QRESYNC is not enabled.")
|
||||
.id(arguments.tag));
|
||||
}
|
||||
if self.is_uidonly && qresync.seq_match.is_some() {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(concat!(
|
||||
"The QRESYNC sequence matching parameter ",
|
||||
"is not allowed once UIDONLY is enabled."
|
||||
))
|
||||
.code(ResponseCode::UidRequired)
|
||||
.ctx(trc::Key::Type, ResponseType::Bad)
|
||||
.id(arguments.tag));
|
||||
}
|
||||
if qresync.uid_validity == mailbox_state.uid_validity as u32 {
|
||||
// Send flags for changed messages
|
||||
data.fetch(
|
||||
fetch::Arguments {
|
||||
tag: "".into(),
|
||||
sequence_set: qresync
|
||||
.known_uids
|
||||
.or_else(|| qresync.seq_match.map(|(_, s)| s))
|
||||
.unwrap_or(Sequence::Range {
|
||||
start: 1.into(),
|
||||
end: None,
|
||||
}),
|
||||
attributes: vec![fetch::Attribute::Flags],
|
||||
changed_since: qresync.modseq.into(),
|
||||
include_vanished: true,
|
||||
},
|
||||
mailbox.clone(),
|
||||
true,
|
||||
true,
|
||||
self.is_uidonly,
|
||||
false,
|
||||
self.is_utf8,
|
||||
u32::MAX,
|
||||
Instant::now(),
|
||||
)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
}
|
||||
}
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::Select),
|
||||
SpanId = self.session_id,
|
||||
MailboxName = arguments.mailbox_name.clone(),
|
||||
AccountId = mailbox.id.account_id,
|
||||
MailboxId = mailbox.id.mailbox_id,
|
||||
Total = total_messages,
|
||||
UidNext = mailbox_state.uid_next,
|
||||
UidValidity = mailbox_state.uid_validity,
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
// Build response
|
||||
let response = Response {
|
||||
mailbox: ListItem::new(arguments.mailbox_name),
|
||||
total_messages,
|
||||
recent_messages: 0,
|
||||
unseen_seq: 0,
|
||||
uid_validity: mailbox_state.uid_validity as u32,
|
||||
uid_next: mailbox_state.uid_next as u32,
|
||||
closed_previous,
|
||||
is_rev2,
|
||||
is_utf8,
|
||||
highest_modseq,
|
||||
objectid: want_objectid.then(|| ObjectId {
|
||||
mailbox_id: Some(Id::from(mailbox.id.mailbox_id)),
|
||||
account_id: Some(Id::from(mailbox.id.account_id)),
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
|
||||
// Update state
|
||||
self.state = State::Selected { data, mailbox };
|
||||
|
||||
self.write_bytes(
|
||||
StatusResponse::completed(command)
|
||||
.with_tag(arguments.tag)
|
||||
.with_code(if is_select {
|
||||
ResponseCode::ReadWrite
|
||||
} else {
|
||||
ResponseCode::ReadOnly
|
||||
})
|
||||
.serialize(response.serialize()),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Mailbox does not exist.")
|
||||
.code(ResponseCode::NonExistent)
|
||||
.id(arguments.tag))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_unselect(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
self.state.close_mailbox();
|
||||
self.state = State::Authenticated {
|
||||
data: self.state.session_data(),
|
||||
};
|
||||
self.write_bytes(
|
||||
StatusResponse::completed(Command::Unselect)
|
||||
.with_tag(request.tag)
|
||||
.into_bytes(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::ToModSeq;
|
||||
use crate::{
|
||||
core::{Mailbox, Session, SessionData},
|
||||
op::ImapContext,
|
||||
spawn_op,
|
||||
};
|
||||
use common::network::SessionStream;
|
||||
use email::cache::{MessageCacheFetch, email::MessageCacheAccess};
|
||||
use imap_proto::{
|
||||
Command, ResponseCode, StatusResponse,
|
||||
parser::PushUnique,
|
||||
protocol::{
|
||||
ObjectId,
|
||||
status::{Status, StatusItem, StatusItemType},
|
||||
},
|
||||
receiver::Request,
|
||||
};
|
||||
use registry::schema::enums::Permission;
|
||||
use std::time::Instant;
|
||||
use trc::AddContext;
|
||||
use types::{acl::Acl, id::Id, keyword::Keyword};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_status(&mut self, requests: Vec<Request<Command>>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapStatus)?;
|
||||
|
||||
let is_utf8 = self.is_utf8;
|
||||
|
||||
// Parse requests and activate OBJECTID+ if the OBJECTID attribute is requested
|
||||
let mut parsed = Vec::with_capacity(requests.len());
|
||||
let mut activate = false;
|
||||
for request in requests {
|
||||
match request.parse_status(is_utf8) {
|
||||
Ok(arguments) => {
|
||||
if arguments.items.contains(&Status::ObjectId) {
|
||||
activate = true;
|
||||
}
|
||||
parsed.push(Ok(arguments));
|
||||
}
|
||||
Err(err) => parsed.push(Err(err)),
|
||||
}
|
||||
}
|
||||
if activate && let Some(enabled) = self.activate_objectid() {
|
||||
self.write_bytes(enabled).await?;
|
||||
}
|
||||
|
||||
let data = self.state.session_data();
|
||||
|
||||
spawn_op!(data, {
|
||||
let mut did_sync = false;
|
||||
|
||||
for request in parsed {
|
||||
match request {
|
||||
Ok(arguments) => {
|
||||
let op_start = Instant::now();
|
||||
let synchronized = if did_sync {
|
||||
Ok(())
|
||||
} else {
|
||||
// Refresh mailboxes
|
||||
data.synchronize_mailboxes(false)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())
|
||||
.map(|_| ())
|
||||
};
|
||||
|
||||
// Fetch status
|
||||
let status = match synchronized {
|
||||
Ok(()) => {
|
||||
did_sync = true;
|
||||
data.status(arguments.mailbox_name, &arguments.items)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
|
||||
match status {
|
||||
Ok(status) => {
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::Status),
|
||||
SpanId = data.session_id,
|
||||
MailboxName = status.mailbox_name.clone(),
|
||||
Details = arguments
|
||||
.items
|
||||
.iter()
|
||||
.map(|c| trc::Value::from(format!("{c:?}")))
|
||||
.collect::<Vec<_>>(),
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
let mut buf = Vec::with_capacity(32);
|
||||
status.serialize(&mut buf, is_utf8);
|
||||
data.write_bytes(
|
||||
StatusResponse::completed(Command::Status)
|
||||
.with_tag(arguments.tag)
|
||||
.serialize(buf),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Err(err) => data.write_error(err).await?,
|
||||
}
|
||||
}
|
||||
Err(err) => data.write_error(err).await?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
pub async fn status(&self, mailbox_name: String, items: &[Status]) -> trc::Result<StatusItem> {
|
||||
// Get mailbox id
|
||||
let mailbox = if let Some(mailbox) = self.get_mailbox_by_name(&mailbox_name) {
|
||||
mailbox
|
||||
} else {
|
||||
// Some IMAP clients will try to get the status of a mailbox with the NoSelect flag
|
||||
return if mailbox_name == self.server.core.email.shared_folder
|
||||
|| mailbox_name
|
||||
.split_once('/')
|
||||
.is_some_and(|(base_name, path)| {
|
||||
base_name == self.server.core.email.shared_folder && !path.contains('/')
|
||||
})
|
||||
{
|
||||
Ok(StatusItem {
|
||||
mailbox_name,
|
||||
items: items
|
||||
.iter()
|
||||
.map(|item| {
|
||||
(
|
||||
*item,
|
||||
match item {
|
||||
Status::Messages
|
||||
| Status::Size
|
||||
| Status::Unseen
|
||||
| Status::Recent
|
||||
| Status::Deleted
|
||||
| Status::HighestModSeq
|
||||
| Status::DeletedStorage => StatusItemType::Number(0),
|
||||
Status::UidNext | Status::UidValidity => {
|
||||
StatusItemType::Number(1)
|
||||
}
|
||||
Status::ObjectId => {
|
||||
StatusItemType::ObjectId(ObjectId::default())
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
} else {
|
||||
Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Mailbox does not exist.")
|
||||
.code(ResponseCode::NonExistent))
|
||||
};
|
||||
};
|
||||
|
||||
if !self
|
||||
.check_mailbox_acl(mailbox.account_id, mailbox.mailbox_id, Acl::ReadItems)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("You do not have the required permissions to read this mailbox.")
|
||||
.code(ResponseCode::NoPerm));
|
||||
}
|
||||
|
||||
// Make sure all requested fields are up to date
|
||||
let mut items_update = Vec::with_capacity(items.len());
|
||||
let mut items_response = Vec::with_capacity(items.len());
|
||||
|
||||
for account in self.mailboxes.lock().iter_mut() {
|
||||
if account.account_id == mailbox.account_id {
|
||||
let mailbox_state =
|
||||
if let Some(mailbox_state) = account.mailbox_state.get(&mailbox.mailbox_id) {
|
||||
mailbox_state
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
for item in items {
|
||||
match item {
|
||||
Status::Messages => {
|
||||
items_response.push((
|
||||
*item,
|
||||
StatusItemType::Number(mailbox_state.total_messages),
|
||||
));
|
||||
}
|
||||
Status::UidNext => {
|
||||
items_response
|
||||
.push((*item, StatusItemType::Number(mailbox_state.uid_next)));
|
||||
}
|
||||
Status::UidValidity => {
|
||||
items_response
|
||||
.push((*item, StatusItemType::Number(mailbox_state.uid_validity)));
|
||||
}
|
||||
Status::Unseen => {
|
||||
items_response
|
||||
.push((*item, StatusItemType::Number(mailbox_state.total_unseen)));
|
||||
}
|
||||
Status::Deleted => {
|
||||
items_response
|
||||
.push((*item, StatusItemType::Number(mailbox_state.total_deleted)));
|
||||
}
|
||||
Status::DeletedStorage => {
|
||||
if let Some(value) = mailbox_state.total_deleted_storage {
|
||||
items_response.push((*item, StatusItemType::Number(value)));
|
||||
} else {
|
||||
items_update.push_unique(*item);
|
||||
}
|
||||
}
|
||||
Status::Size => {
|
||||
if let Some(value) = mailbox_state.size {
|
||||
items_response.push((*item, StatusItemType::Number(value)));
|
||||
} else {
|
||||
items_update.push_unique(*item);
|
||||
}
|
||||
}
|
||||
Status::HighestModSeq => {
|
||||
items_response.push((
|
||||
*item,
|
||||
StatusItemType::Number(account.last_change_id.to_modseq()),
|
||||
));
|
||||
}
|
||||
Status::ObjectId => {
|
||||
items_response.push((
|
||||
*item,
|
||||
StatusItemType::ObjectId(ObjectId {
|
||||
mailbox_id: Some(Id::from(mailbox.mailbox_id)),
|
||||
account_id: Some(Id::from(mailbox.account_id)),
|
||||
..Default::default()
|
||||
}),
|
||||
));
|
||||
}
|
||||
Status::Recent => {
|
||||
items_response.push((*item, StatusItemType::Number(0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !items_update.is_empty() {
|
||||
// Retrieve latest values
|
||||
let mut values_update = Vec::with_capacity(items_update.len());
|
||||
|
||||
let cache = self
|
||||
.server
|
||||
.get_cached_messages(mailbox.account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
for item in items_update {
|
||||
let result = match item {
|
||||
Status::DeletedStorage => cache
|
||||
.in_mailbox_with_keyword(mailbox.mailbox_id, &Keyword::Deleted)
|
||||
.map(|x| x.size)
|
||||
.sum::<u32>() as u64,
|
||||
Status::Size => cache
|
||||
.in_mailbox(mailbox.mailbox_id)
|
||||
.map(|x| x.size)
|
||||
.sum::<u32>() as u64,
|
||||
_ => {
|
||||
unreachable!()
|
||||
}
|
||||
};
|
||||
|
||||
items_response.push((item, StatusItemType::Number(result)));
|
||||
values_update.push((item, result));
|
||||
}
|
||||
|
||||
// Update cache
|
||||
for account in self.mailboxes.lock().iter_mut() {
|
||||
if account.account_id == mailbox.account_id {
|
||||
let mailbox_state = account
|
||||
.mailbox_state
|
||||
.entry(mailbox.mailbox_id)
|
||||
.or_insert_with(Mailbox::default);
|
||||
|
||||
for (item, value) in values_update {
|
||||
match item {
|
||||
Status::DeletedStorage => {
|
||||
mailbox_state.total_deleted_storage = value.into()
|
||||
}
|
||||
Status::Size => mailbox_state.size = value.into(),
|
||||
Status::Recent => {
|
||||
items_response
|
||||
.iter_mut()
|
||||
.find(|(i, _)| *i == Status::Recent)
|
||||
.unwrap()
|
||||
.1 = StatusItemType::Number(0);
|
||||
}
|
||||
_ => {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate response
|
||||
Ok(StatusItem {
|
||||
mailbox_name,
|
||||
items: items_response,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{FromModSeq, ImapContext};
|
||||
use crate::{
|
||||
core::{SelectedMailbox, Session, SessionData},
|
||||
spawn_op,
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
use common::{network::SessionStream, storage::index::ObjectIndexBuilder};
|
||||
use email::{
|
||||
mailbox::TRASH_ID,
|
||||
message::{ingest::EmailIngest, metadata::MessageData},
|
||||
};
|
||||
use imap_proto::{
|
||||
Command, ResponseCode, ResponseType, StatusResponse,
|
||||
protocol::{
|
||||
Flag, ImapResponse,
|
||||
fetch::{DataItem, FetchItem},
|
||||
store::{Arguments, Operation, Response},
|
||||
},
|
||||
receiver::Request,
|
||||
};
|
||||
use registry::schema::enums::Permission;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use store::{
|
||||
ValueKey,
|
||||
query::log::{Change, Query},
|
||||
write::{AlignedBytes, Archive, BatchBuilder},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
collection::{Collection, SyncCollection},
|
||||
keyword::Keyword,
|
||||
};
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_store(
|
||||
&mut self,
|
||||
request: Request<Command>,
|
||||
is_uid: bool,
|
||||
spawn: bool,
|
||||
) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapStore)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let arguments = request.parse_store()?;
|
||||
let (data, mailbox) = self.state.select_data();
|
||||
let is_condstore = self.is_condstore || mailbox.is_condstore;
|
||||
let is_utf8 = self.is_utf8;
|
||||
let is_uidonly = self.is_uidonly;
|
||||
let message_limit = self.server.core.imap.max_messages_per_command;
|
||||
|
||||
if spawn {
|
||||
spawn_op!(data, {
|
||||
let response = data
|
||||
.store(
|
||||
arguments,
|
||||
mailbox,
|
||||
is_uid,
|
||||
is_condstore,
|
||||
is_utf8,
|
||||
is_uidonly,
|
||||
message_limit,
|
||||
op_start,
|
||||
)
|
||||
.await?;
|
||||
|
||||
data.write_bytes(response).await
|
||||
})
|
||||
} else {
|
||||
let response = data
|
||||
.store(
|
||||
arguments,
|
||||
mailbox,
|
||||
is_uid,
|
||||
is_condstore,
|
||||
is_utf8,
|
||||
is_uidonly,
|
||||
message_limit,
|
||||
op_start,
|
||||
)
|
||||
.await?;
|
||||
|
||||
data.write_bytes(response).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn store(
|
||||
&self,
|
||||
arguments: Arguments,
|
||||
mailbox: Arc<SelectedMailbox>,
|
||||
is_uid: bool,
|
||||
is_condstore: bool,
|
||||
is_utf8: bool,
|
||||
is_uidonly: bool,
|
||||
message_limit: u32,
|
||||
op_start: Instant,
|
||||
) -> trc::Result<Vec<u8>> {
|
||||
// Resync messages if needed
|
||||
let account_id = mailbox.id.account_id;
|
||||
self.synchronize_messages(&mailbox)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
// Convert IMAP ids to JMAP ids.
|
||||
let mut ids = mailbox
|
||||
.sequence_to_ids(&arguments.sequence_set, is_uid)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
if ids.is_empty() {
|
||||
return Ok(StatusResponse::completed(Command::Store(is_uid))
|
||||
.with_tag(arguments.tag)
|
||||
.into_bytes());
|
||||
}
|
||||
|
||||
// Verify that the user can modify messages in this mailbox.
|
||||
if !self
|
||||
.check_mailbox_acl(
|
||||
mailbox.id.account_id,
|
||||
mailbox.id.mailbox_id,
|
||||
Acl::ModifyItems,
|
||||
)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(
|
||||
"You do not have the required permissions to modify messages in this mailbox.",
|
||||
)
|
||||
.id(arguments.tag)
|
||||
.code(ResponseCode::NoPerm)
|
||||
.caused_by(trc::location!()));
|
||||
}
|
||||
|
||||
// Filter out unchanged since ids
|
||||
let mut response_code = None;
|
||||
let mut unchanged_failed = false;
|
||||
if let Some(unchanged_since) = arguments.unchanged_since {
|
||||
// Obtain changes since the modseq.
|
||||
let changelog = self
|
||||
.server
|
||||
.store()
|
||||
.changes(
|
||||
account_id,
|
||||
SyncCollection::Email.into(),
|
||||
Query::from_modseq(unchanged_since),
|
||||
)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
let mut modified = mailbox
|
||||
.sequence_expand_missing(&arguments.sequence_set, is_uid)
|
||||
.await;
|
||||
|
||||
// Add all IDs that changed in this mailbox
|
||||
for (id, is_delete) in changelog.changes.into_iter().filter_map(|change| {
|
||||
change.item_id().map(|id| {
|
||||
(
|
||||
(id & u32::MAX as u64) as u32,
|
||||
matches!(change, Change::DeleteItem(_)),
|
||||
)
|
||||
})
|
||||
}) {
|
||||
if let Some(imap_id) = ids.remove(&id) {
|
||||
if is_uid {
|
||||
modified.push(imap_id.uid);
|
||||
} else {
|
||||
modified.push(imap_id.seqnum);
|
||||
if is_delete {
|
||||
unchanged_failed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !modified.is_empty() {
|
||||
modified.sort_unstable();
|
||||
response_code = ResponseCode::Modified { ids: modified }.into();
|
||||
}
|
||||
}
|
||||
|
||||
// Build response
|
||||
let mut response = if !unchanged_failed {
|
||||
StatusResponse::completed(Command::Store(is_uid))
|
||||
} else {
|
||||
StatusResponse::no("Some of the messages no longer exist.")
|
||||
}
|
||||
.with_tag(arguments.tag);
|
||||
if let Some(response_code) = response_code {
|
||||
response = response.with_code(response_code)
|
||||
}
|
||||
if ids.is_empty() {
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::Store),
|
||||
SpanId = self.session_id,
|
||||
AccountId = mailbox.id.account_id,
|
||||
MailboxId = mailbox.id.mailbox_id,
|
||||
Type = format!("{:?}", arguments.operation),
|
||||
Details = arguments
|
||||
.keywords
|
||||
.iter()
|
||||
.map(|c| trc::Value::from(format!("{c:?}")))
|
||||
.collect::<Vec<_>>(),
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
return Ok(response.into_bytes());
|
||||
}
|
||||
// RFC 9738 requires the highest UIDs to be processed first when truncating.
|
||||
let message_limit = message_limit as usize;
|
||||
let mut untagged = Vec::new();
|
||||
if ids.len() > message_limit {
|
||||
let mut uids = ids.values().map(|imap_id| imap_id.uid).collect::<Vec<_>>();
|
||||
let cutoff = uids.len() - message_limit;
|
||||
let lowest_uid = *uids.select_nth_unstable(cutoff).1;
|
||||
ids.retain(|_, imap_id| imap_id.uid >= lowest_uid);
|
||||
|
||||
let code = ResponseCode::MessageLimit {
|
||||
limit: message_limit as u32,
|
||||
uid: lowest_uid.into(),
|
||||
};
|
||||
if response.code.is_none() {
|
||||
response = response.with_code(code);
|
||||
} else {
|
||||
untagged = StatusResponse::ok("Some messages were not modified.")
|
||||
.with_code(code)
|
||||
.into_bytes();
|
||||
}
|
||||
}
|
||||
|
||||
let mut items = Response {
|
||||
is_utf8,
|
||||
items: Vec::with_capacity(ids.len()),
|
||||
};
|
||||
|
||||
// Process each change
|
||||
let set_keywords = arguments
|
||||
.keywords
|
||||
.iter()
|
||||
.map(|k| Keyword::from(k.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let mut changed_mailboxes = AHashSet::new();
|
||||
let mut batch = BatchBuilder::new();
|
||||
|
||||
for (id, imap_id) in &ids {
|
||||
// Obtain message data
|
||||
let data_ = if let Some(data) = self
|
||||
.server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::Email,
|
||||
*id,
|
||||
))
|
||||
.await
|
||||
.imap_ctx(response.tag.as_ref().unwrap(), trc::location!())?
|
||||
{
|
||||
data
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Deserialize
|
||||
let data = data_
|
||||
.to_unarchived::<MessageData>()
|
||||
.imap_ctx(response.tag.as_ref().unwrap(), trc::location!())?;
|
||||
let mut new_data = data.inner.to_builder();
|
||||
|
||||
// Apply changes
|
||||
let mut seen_changed = false;
|
||||
match arguments.operation {
|
||||
Operation::Set => {
|
||||
seen_changed = set_keywords.contains(&Keyword::Seen)
|
||||
!= new_data.has_keyword(&Keyword::Seen);
|
||||
new_data.set_keywords(set_keywords.clone());
|
||||
}
|
||||
Operation::Add => {
|
||||
for keyword in &set_keywords {
|
||||
if new_data.add_keyword(keyword.clone()) && keyword == &Keyword::Seen {
|
||||
seen_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Operation::Clear => {
|
||||
for keyword in &set_keywords {
|
||||
if new_data.remove_keyword(keyword) && keyword == &Keyword::Seen {
|
||||
seen_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !new_data.has_keyword_changes(data.inner) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Train spam filter
|
||||
let mut train_spam = None;
|
||||
for keyword in new_data.added_keywords(data.inner) {
|
||||
if keyword == &Keyword::Junk {
|
||||
train_spam = Some(true);
|
||||
break;
|
||||
} else if keyword == &Keyword::NotJunk && !data.inner.has_mailbox_id(TRASH_ID) {
|
||||
// Only train as ham if not in Trash (Apple likes to add NotJunk to trashed items, which would be spammy)
|
||||
train_spam = Some(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if train_spam.is_none() {
|
||||
for keyword in new_data.removed_keywords(data.inner) {
|
||||
if keyword == &Keyword::Junk {
|
||||
if !data.inner.has_mailbox_id(TRASH_ID) {
|
||||
train_spam = Some(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert keywords to flags
|
||||
let flags = if !arguments.is_silent {
|
||||
new_data
|
||||
.keywords
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(Flag::from)
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
// Set all current mailboxes as changed if the Seen tag changed
|
||||
if seen_changed {
|
||||
for mailbox_id in new_data.mailboxes.iter() {
|
||||
changed_mailboxes.insert(mailbox_id.mailbox_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Write changes
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::Email)
|
||||
.with_document(*id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::new()
|
||||
.with_current(data)
|
||||
.with_changes(new_data.seal()),
|
||||
)
|
||||
.imap_ctx(response.tag.as_ref().unwrap(), trc::location!())?;
|
||||
|
||||
// Add spam train task
|
||||
if let Some(learn_spam) = train_spam {
|
||||
self.server
|
||||
.add_account_spam_sample(
|
||||
&mut batch,
|
||||
account_id,
|
||||
*id,
|
||||
learn_spam,
|
||||
self.session_id,
|
||||
)
|
||||
.await
|
||||
.imap_ctx(response.tag.as_ref().unwrap(), trc::location!())?;
|
||||
}
|
||||
|
||||
// Set commit point
|
||||
batch.commit_point();
|
||||
|
||||
// Add item to response
|
||||
if !arguments.is_silent {
|
||||
let mut data_items = vec![DataItem::Flags { flags }];
|
||||
if is_uid {
|
||||
data_items.push(DataItem::Uid { uid: imap_id.uid });
|
||||
}
|
||||
items.items.push(FetchItem {
|
||||
id: if is_uidonly {
|
||||
imap_id.uid
|
||||
} else {
|
||||
imap_id.seqnum
|
||||
},
|
||||
is_uidonly,
|
||||
items: data_items,
|
||||
});
|
||||
} else if is_condstore {
|
||||
items.items.push(FetchItem {
|
||||
id: if is_uidonly {
|
||||
imap_id.uid
|
||||
} else {
|
||||
imap_id.seqnum
|
||||
},
|
||||
is_uidonly,
|
||||
items: if is_uid {
|
||||
vec![DataItem::Uid { uid: imap_id.uid }]
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Log mailbox changes
|
||||
if !changed_mailboxes.is_empty() {
|
||||
for parent_id in changed_mailboxes {
|
||||
batch.log_container_property_change(SyncCollection::Email, parent_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Write changes
|
||||
if !batch.is_empty() {
|
||||
match self
|
||||
.server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.and_then(|ids| ids.last_change_id(mailbox.id.account_id))
|
||||
.caused_by(trc::location!())
|
||||
{
|
||||
Ok(change_id) => {
|
||||
if is_condstore {
|
||||
let modseq = change_id + 1;
|
||||
for item in items.items.iter_mut() {
|
||||
item.items.push(DataItem::ModSeq { modseq });
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) if err.is_assertion_failure() => {
|
||||
items.items.clear();
|
||||
response.rtype = ResponseType::No;
|
||||
response.message = "Some messages were modified by another process.".into();
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err.id(response.tag.unwrap()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::Store),
|
||||
SpanId = self.session_id,
|
||||
AccountId = mailbox.id.account_id,
|
||||
MailboxId = mailbox.id.mailbox_id,
|
||||
DocumentId = ids
|
||||
.iter()
|
||||
.map(|id| trc::Value::from(*id.0))
|
||||
.collect::<Vec<_>>(),
|
||||
Type = format!("{:?}", arguments.operation),
|
||||
Details = arguments
|
||||
.keywords
|
||||
.iter()
|
||||
.map(|c| trc::Value::from(format!("{c:?}")))
|
||||
.collect::<Vec<_>>(),
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
// Send response
|
||||
let items = items.serialize();
|
||||
Ok(response.serialize(if untagged.is_empty() {
|
||||
items
|
||||
} else {
|
||||
untagged.extend_from_slice(&items);
|
||||
untagged
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::ImapContext;
|
||||
use crate::{
|
||||
core::{Session, SessionData},
|
||||
spawn_op,
|
||||
};
|
||||
use common::{network::SessionStream, storage::index::ObjectIndexBuilder};
|
||||
use imap_proto::{Command, ResponseCode, StatusResponse, receiver::Request};
|
||||
use registry::schema::enums::Permission;
|
||||
use std::time::Instant;
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive, BatchBuilder},
|
||||
};
|
||||
use types::collection::Collection;
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_subscribe(
|
||||
&mut self,
|
||||
request: Request<Command>,
|
||||
is_subscribe: bool,
|
||||
) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapSubscribe)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let arguments = request.parse_subscribe(self.is_utf8)?;
|
||||
let data = self.state.session_data();
|
||||
|
||||
spawn_op!(data, {
|
||||
let response = data
|
||||
.subscribe_folder(
|
||||
arguments.tag,
|
||||
arguments.mailbox_name,
|
||||
is_subscribe,
|
||||
op_start,
|
||||
)
|
||||
.await?;
|
||||
|
||||
data.write_bytes(response.into_bytes()).await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
pub async fn subscribe_folder(
|
||||
&self,
|
||||
tag: String,
|
||||
mailbox_name: String,
|
||||
subscribe: bool,
|
||||
op_start: Instant,
|
||||
) -> trc::Result<StatusResponse> {
|
||||
// Refresh mailboxes
|
||||
self.synchronize_mailboxes(false)
|
||||
.await
|
||||
.imap_ctx(&tag, trc::location!())?;
|
||||
|
||||
// Validate mailbox
|
||||
let (account_id, mailbox_id) = match self.get_mailbox_by_name(&mailbox_name) {
|
||||
Some(mailbox) => (mailbox.account_id, mailbox.mailbox_id),
|
||||
None => {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Mailbox does not exist.")
|
||||
.code(ResponseCode::NonExistent)
|
||||
.id(tag)
|
||||
.caused_by(trc::location!()));
|
||||
}
|
||||
};
|
||||
|
||||
// Verify if mailbox is already subscribed/unsubscribed
|
||||
for account in self.mailboxes.lock().iter_mut() {
|
||||
if account.account_id == account_id {
|
||||
if let Some(mailbox) = account.mailbox_state.get(&mailbox_id)
|
||||
&& mailbox.is_subscribed == subscribe
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(if subscribe {
|
||||
"Mailbox is already subscribed."
|
||||
} else {
|
||||
"Mailbox is already unsubscribed."
|
||||
})
|
||||
.id(tag));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain mailbox
|
||||
let mailbox_ = self
|
||||
.server
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::Mailbox,
|
||||
mailbox_id,
|
||||
))
|
||||
.await
|
||||
.imap_ctx(&tag, trc::location!())?
|
||||
.ok_or_else(|| {
|
||||
trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Mailbox does not exist.")
|
||||
.code(ResponseCode::NonExistent)
|
||||
.id(tag.clone())
|
||||
.caused_by(trc::location!())
|
||||
})?;
|
||||
let mailbox = mailbox_
|
||||
.to_unarchived::<email::mailbox::Mailbox>()
|
||||
.imap_ctx(&tag, trc::location!())?;
|
||||
|
||||
if (subscribe && !mailbox.inner.is_subscribed(self.account_id))
|
||||
|| (!subscribe && mailbox.inner.is_subscribed(self.account_id))
|
||||
{
|
||||
// Build batch
|
||||
let mut new_mailbox = mailbox.deserialize().imap_ctx(&tag, trc::location!())?;
|
||||
if subscribe {
|
||||
new_mailbox.subscribers.push(self.account_id);
|
||||
} else {
|
||||
new_mailbox.remove_subscriber(self.account_id);
|
||||
}
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::Mailbox)
|
||||
.with_document(mailbox_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::new()
|
||||
.with_current(mailbox)
|
||||
.with_changes(new_mailbox),
|
||||
)
|
||||
.imap_ctx(&tag, trc::location!())?;
|
||||
self.server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.imap_ctx(&tag, trc::location!())?;
|
||||
|
||||
// Update mailbox cache
|
||||
for account in self.mailboxes.lock().iter_mut() {
|
||||
if account.account_id == account_id {
|
||||
if let Some(mailbox) = account.mailbox_state.get_mut(&mailbox_id) {
|
||||
mailbox.is_subscribed = subscribe;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trc::event!(
|
||||
Imap(if subscribe {
|
||||
trc::ImapEvent::Subscribe
|
||||
} else {
|
||||
trc::ImapEvent::Unsubscribe
|
||||
}),
|
||||
SpanId = self.session_id,
|
||||
AccountId = account_id,
|
||||
MailboxId = mailbox_id,
|
||||
MailboxName = mailbox_name,
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
Ok(StatusResponse::ok(if subscribe {
|
||||
"Mailbox subscribed."
|
||||
} else {
|
||||
"Mailbox unsubscribed."
|
||||
})
|
||||
.with_tag(tag))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
core::{SelectedMailbox, Session, SessionData},
|
||||
spawn_op,
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
use common::network::SessionStream;
|
||||
use email::cache::{MessageCacheFetch, email::MessageCacheAccess};
|
||||
use imap_proto::{
|
||||
Command, StatusResponse,
|
||||
protocol::{
|
||||
ImapResponse,
|
||||
thread::{Arguments, Response},
|
||||
},
|
||||
receiver::Request,
|
||||
};
|
||||
use registry::schema::enums::Permission;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use trc::AddContext;
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_thread(
|
||||
&mut self,
|
||||
request: Request<Command>,
|
||||
is_uid: bool,
|
||||
) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapThread)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let command = request.command;
|
||||
let mut arguments = request.parse_thread()?;
|
||||
let (data, mailbox) = self.state.mailbox_state();
|
||||
|
||||
spawn_op!(data, {
|
||||
let tag = std::mem::take(&mut arguments.tag);
|
||||
|
||||
match data.thread(arguments, mailbox, is_uid, op_start).await {
|
||||
Ok(response) => {
|
||||
data.write_bytes(
|
||||
StatusResponse::completed(command)
|
||||
.with_tag(tag)
|
||||
.serialize(response.serialize()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
Err(err) => Err(err.id(tag)),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SessionStream> SessionData<T> {
|
||||
pub async fn thread(
|
||||
&self,
|
||||
arguments: Arguments,
|
||||
mailbox: Arc<SelectedMailbox>,
|
||||
is_uid: bool,
|
||||
op_start: Instant,
|
||||
) -> trc::Result<Response> {
|
||||
// Run query
|
||||
let (result_set, _) = self
|
||||
.query(arguments.filter, vec![], &mailbox, &None)
|
||||
.await?;
|
||||
|
||||
// Synchronize mailbox
|
||||
if !result_set.is_empty() {
|
||||
self.synchronize_messages(&mailbox)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
} else {
|
||||
return Ok(Response {
|
||||
is_uid,
|
||||
threads: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
// Lock the cache
|
||||
let cache = self
|
||||
.server
|
||||
.get_cached_messages(mailbox.id.account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Group messages by thread
|
||||
let mut threads: AHashMap<u32, Vec<u32>> = AHashMap::new();
|
||||
let state = mailbox.state.lock();
|
||||
for document_id in result_set {
|
||||
if let Some(item) = cache.email_by_id(&document_id)
|
||||
&& let Some((imap_id, _)) = state.map_result_id(document_id, is_uid)
|
||||
{
|
||||
threads.entry(item.thread_id).or_default().push(imap_id);
|
||||
}
|
||||
}
|
||||
|
||||
let mut threads = threads
|
||||
.into_iter()
|
||||
.map(|(_, mut messages)| {
|
||||
messages.sort_unstable();
|
||||
messages
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
threads.sort_unstable();
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::Thread),
|
||||
SpanId = self.session_id,
|
||||
AccountId = mailbox.id.account_id,
|
||||
MailboxId = mailbox.id.mailbox_id,
|
||||
Total = threads.len(),
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
// Build response
|
||||
Ok(Response { is_uid, threads })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::ImapContext;
|
||||
use crate::core::Session;
|
||||
use common::network::SessionStream;
|
||||
use imap_proto::{
|
||||
Command, ResponseCode, ResponseType, StatusResponse, protocol::uidbatches, receiver::Request,
|
||||
};
|
||||
use registry::schema::enums::Permission;
|
||||
use std::time::Instant;
|
||||
|
||||
impl<T: SessionStream> Session<T> {
|
||||
pub async fn handle_uidbatches(&mut self, request: Request<Command>) -> trc::Result<()> {
|
||||
// Validate access
|
||||
self.assert_has_permission(Permission::ImapSearch)?;
|
||||
|
||||
let op_start = Instant::now();
|
||||
let arguments = request.parse_uidbatches()?;
|
||||
let (data, mailbox) = self.state.select_data();
|
||||
|
||||
let min_batch_size = self.server.core.imap.min_uid_batch_size;
|
||||
if arguments.batch_size < min_batch_size {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(format!("Minimum batch size is {min_batch_size}."))
|
||||
.code(ResponseCode::TooFew)
|
||||
.id(arguments.tag));
|
||||
}
|
||||
|
||||
if let Some((from, to)) = arguments.batch_range
|
||||
&& from > to
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details("Batch ranges must be ordered from lowest to highest.")
|
||||
.code(ResponseCode::ClientBug)
|
||||
.ctx(trc::Key::Type, ResponseType::Bad)
|
||||
.id(arguments.tag));
|
||||
}
|
||||
|
||||
// Reject oversized requests before doing any work on their behalf
|
||||
let max_uid_batches = self.server.core.imap.max_uid_batches;
|
||||
if arguments
|
||||
.batch_range
|
||||
.is_some_and(|(from, to)| to - from + 1 > max_uid_batches)
|
||||
{
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(format!(
|
||||
"A single UIDBATCHES response is limited to {max_uid_batches} ranges."
|
||||
))
|
||||
.code(ResponseCode::TooMany)
|
||||
.id(arguments.tag));
|
||||
}
|
||||
|
||||
// Resynchronize so that batches reflect the current mailbox contents
|
||||
data.synchronize_messages(&mailbox)
|
||||
.await
|
||||
.imap_ctx(&arguments.tag, trc::location!())?;
|
||||
|
||||
let uids = mailbox.uids_descending();
|
||||
let batch_size = arguments.batch_size as usize;
|
||||
let total_batches = uids.len().div_ceil(batch_size);
|
||||
|
||||
if arguments.batch_range.is_none() && total_batches > max_uid_batches as usize {
|
||||
return Err(trc::ImapEvent::Error
|
||||
.into_err()
|
||||
.details(format!(
|
||||
"A single UIDBATCHES response is limited to {max_uid_batches} ranges."
|
||||
))
|
||||
.code(ResponseCode::TooMany)
|
||||
.id(arguments.tag));
|
||||
}
|
||||
|
||||
// Batch ranges tile the whole UID space, so each range starts right below
|
||||
// the previous one and the oldest batch always reaches down to UID 1.
|
||||
let (first, last) = match arguments.batch_range {
|
||||
Some((from, to)) => (
|
||||
(from as usize - 1).min(total_batches),
|
||||
(to as usize).min(total_batches),
|
||||
),
|
||||
None => (0, total_batches),
|
||||
};
|
||||
let mut ranges = Vec::with_capacity(last.saturating_sub(first));
|
||||
let mut high = uids.first().copied().unwrap_or(0);
|
||||
for batch in 0..last {
|
||||
let end = ((batch + 1) * batch_size).min(uids.len());
|
||||
let low = if end == uids.len() { 1 } else { uids[end - 1] };
|
||||
if batch >= first {
|
||||
ranges.push((high, low));
|
||||
}
|
||||
high = low.saturating_sub(1);
|
||||
}
|
||||
|
||||
trc::event!(
|
||||
Imap(trc::ImapEvent::UidBatches),
|
||||
SpanId = self.session_id,
|
||||
AccountId = mailbox.id.account_id,
|
||||
MailboxId = mailbox.id.mailbox_id,
|
||||
Limit = arguments.batch_size,
|
||||
Total = ranges.len(),
|
||||
Elapsed = op_start.elapsed()
|
||||
);
|
||||
|
||||
let response = uidbatches::Response { ranges }.serialize(&arguments.tag);
|
||||
self.write_bytes(
|
||||
StatusResponse::completed(Command::UidBatches)
|
||||
.with_tag(arguments.tag)
|
||||
.serialize(response),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user