/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use common::{ Server, config::smtp::queue::{QueueExpiry, QueueName}, }; use smtp::queue::{ Error, ErrorDetails, HostResponse, Message, QuotaKey, Recipient, Schedule, Status, UnexpectedResponse, }; use smtp_proto::Response; use std::net::IpAddr; use store::{ Deserialize, IterateParams, Serialize, ValueKey, write::{ AlignedBytes, Archive, Archiver, BatchBuilder, QueueClass, ValueClass, key::DeserializeBigEndian, }, }; use trc::AddContext; use types::blob_hash::BlobHash; #[derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, Debug, Clone, PartialEq, Eq)] pub struct LegacyMessage { pub created: u64, pub blob_hash: BlobHash, pub return_path: String, pub recipients: Vec, pub received_from_ip: IpAddr, pub received_via_port: u16, pub flags: u64, pub env_id: Option, pub priority: i16, pub size: u64, pub quota_keys: Vec, } #[derive( rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, Debug, Clone, PartialEq, Eq, serde::Deserialize, )] pub struct LegacyRecipient { pub address: String, pub retry: Schedule, pub notify: Schedule, pub expires: QueueExpiry, pub queue: QueueName, pub status: Status, LegacyErrorDetails>, pub flags: u64, pub orcpt: Option, } #[derive( Debug, Clone, PartialEq, Eq, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, serde::Deserialize, )] pub struct LegacyHostResponse { pub hostname: T, pub response: Response, } #[derive( Debug, Clone, PartialEq, Eq, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, serde::Deserialize, )] pub struct LegacyUnexpectedResponse { pub command: String, pub response: Response, } #[derive( Debug, Clone, PartialEq, Eq, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, Default, serde::Deserialize, )] pub struct LegacyErrorDetails { pub entity: String, pub details: LegacyError, } #[derive( Debug, Clone, PartialEq, Eq, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, serde::Deserialize, Default, )] pub enum LegacyError { DnsError(String), UnexpectedResponse(LegacyUnexpectedResponse), ConnectionError(String), TlsError(String), DaneError(String), MtaStsError(String), RateLimited, #[default] ConcurrencyLimited, Io(String), } #[derive( rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, Debug, Clone, PartialEq, Eq, serde::Deserialize, )] pub enum LegacyQuotaKey { Size { key: Vec, id: u64 }, Count { key: Vec, id: u64 }, } pub(crate) async fn migrate_queue_v014(server: &Server) -> trc::Result<()> { let mut messages = Vec::new(); server .store() .iterate( IterateParams::new( ValueKey::from(ValueClass::Queue(QueueClass::Message(0))), ValueKey::from(ValueClass::Queue(QueueClass::Message(u64::MAX))), ), |key, value| { let archive = as Deserialize>::deserialize(value) .caused_by(trc::location!())?; match archive.deserialize_untrusted::() { Ok(message) => { messages.push((key.deserialize_be_u64(0)?, Message::from(message))); } Err(err) => { if archive.deserialize_untrusted::().is_err() { return Err(err.caused_by(trc::location!())); } } } Ok(true) }, ) .await .caused_by(trc::location!())?; let mut batch = BatchBuilder::new(); let count = messages.len(); for (queue_id, message) in messages { batch.set( ValueClass::Queue(QueueClass::Message(queue_id)), Archiver::new(message) .serialize() .caused_by(trc::location!())?, ); if batch.is_large_batch() { server .store() .write(batch.build_all()) .await .caused_by(trc::location!())?; batch = BatchBuilder::new(); } } if !batch.is_empty() { server .store() .write(batch.build_all()) .await .caused_by(trc::location!())?; } trc::event!( Server(trc::ServerEvent::Startup), Details = format!("Migrated {count} queued messages",) ); Ok(()) } impl From for Message { fn from(legacy: LegacyMessage) -> Self { Message { created: legacy.created, blob_hash: legacy.blob_hash, return_path: legacy.return_path.into_boxed_str(), recipients: legacy.recipients.into_iter().map(|r| r.into()).collect(), received_from_ip: legacy.received_from_ip, received_via_port: legacy.received_via_port, flags: legacy.flags, env_id: legacy.env_id.map(|s| s.into_boxed_str()), priority: legacy.priority, size: legacy.size, quota_keys: legacy.quota_keys.into_iter().map(|qk| qk.into()).collect(), } } } impl From for Recipient { fn from(legacy: LegacyRecipient) -> Self { Recipient { address: legacy.address.into_boxed_str(), retry: legacy.retry, notify: legacy.notify, expires: legacy.expires, queue: legacy.queue, status: match legacy.status { Status::Scheduled => Status::Scheduled, Status::Completed(status) => Status::Completed(status.into()), Status::TemporaryFailure(status) => Status::TemporaryFailure(status.into()), Status::PermanentFailure(status) => Status::PermanentFailure(status.into()), }, flags: legacy.flags, orcpt: legacy.orcpt.map(|s| s.into_boxed_str()), } } } impl From for ErrorDetails { fn from(legacy: LegacyErrorDetails) -> Self { ErrorDetails { entity: legacy.entity.into_boxed_str(), details: legacy.details.into(), } } } impl From for QuotaKey { fn from(legacy: LegacyQuotaKey) -> Self { match legacy { LegacyQuotaKey::Size { key, id } => QuotaKey::Size { key: key.into(), id, }, LegacyQuotaKey::Count { key, id } => QuotaKey::Count { key: key.into(), id, }, } } } impl From for Error { fn from(legacy: LegacyError) -> Self { match legacy { LegacyError::DnsError(s) => Error::DnsError(s.into_boxed_str()), LegacyError::UnexpectedResponse(ur) => Error::UnexpectedResponse(ur.into()), LegacyError::ConnectionError(s) => Error::ConnectionError(s.into_boxed_str()), LegacyError::TlsError(s) => Error::TlsError(s.into_boxed_str()), LegacyError::DaneError(s) => Error::DaneError(s.into_boxed_str()), LegacyError::MtaStsError(s) => Error::MtaStsError(s.into_boxed_str()), LegacyError::RateLimited => Error::RateLimited, LegacyError::ConcurrencyLimited => Error::ConcurrencyLimited, LegacyError::Io(s) => Error::Io(s.into_boxed_str()), } } } impl From for UnexpectedResponse { fn from(legacy: LegacyUnexpectedResponse) -> Self { UnexpectedResponse { command: legacy.command.into_boxed_str(), response: Response { code: legacy.response.code, esc: legacy.response.esc, message: legacy.response.message.into_boxed_str(), }, } } } impl From> for HostResponse> { fn from(legacy: LegacyHostResponse) -> Self { HostResponse { hostname: legacy.hostname.into_boxed_str(), response: Response { code: legacy.response.code, esc: legacy.response.esc, message: legacy.response.message.into_boxed_str(), }, } } }