aves-tyto 0.5.7

Specialized messaging domain for the Aves async application runtime
use std::sync::Arc;

use async_trait::async_trait;
use aves::utils::uuid::SpectaUuid;
use schemars::{Schema, schema_for};
use tokio::sync::{Mutex, mpsc};
use zeroize::Zeroizing;

use crate::channels::{InboundEvent, MessagingPlugin, MessagingSubscription, MessagingTask};
use crate::messages::ListenerType;
use crate::types::{
    EmailConfig, EmailSendMetadata, MessagingConfig, MessagingCredentials, MessagingError, MessagingInteraction,
    MessagingSendError, MessagingSendErrorCode, MessagingSendReceipt, MessagingSenderConfig,
    MessagingSubscriptionError, NormalizedMessage,
};

use super::connection::{ImapConnection, configure, connect};
use super::send::send;
use super::subscribe::{listen, run_filters};

#[derive(Default)]
pub(crate) struct EmailPlugin {
    connection: Option<ImapConnection>,
    settings: Option<EmailConfig>,
    credentials: Option<(Zeroizing<String>, Zeroizing<String>)>,
}

#[async_trait]
impl MessagingPlugin for EmailPlugin {
    async fn connect(
        &mut self,
        settings: &MessagingConfig,
        credentials: &MessagingCredentials,
    ) -> Result<(), MessagingError> {
        self.connection = None;
        self.credentials = None;

        if let (Some(mut config), MessagingCredentials::Email { username, password }) =
            (settings.email.clone(), credentials)
            && config.imap_server.is_none()
            && config.smtp_server.is_some()
        {
            config.username = Some(username.clone());
            config.password = Some(password.clone());
            self.credentials = Some((username.clone(), password.clone()));
            self.settings = Some(config);
            return Ok(());
        }

        let config = configure(settings, credentials)?;
        let session = connect(&config)
            .await
            .map_err(|error| MessagingError::ChannelConnectionFailed {
                listener_type: ListenerType::Email,
                message: error.to_string(),
            })?;

        self.connection = Some(Arc::new(Mutex::new(Some(session))));
        self.settings = Some(config);
        if let MessagingCredentials::Email { username, password } = credentials {
            self.credentials = Some((username.clone(), password.clone()));
        }
        Ok(())
    }

    async fn listen(
        &mut self,
        tx: mpsc::Sender<InboundEvent>,
        _interaction_tx: mpsc::Sender<MessagingInteraction>,
    ) -> Result<MessagingTask, MessagingSubscriptionError> {
        let settings = self
            .settings
            .as_ref()
            .ok_or_else(|| MessagingSubscriptionError::Configuration {
                listener_type: ListenerType::Email,
                message: "Email plugin is not connected".to_owned(),
            })?;
        if settings.imap_server.is_none() {
            return Err(MessagingSubscriptionError::Configuration {
                listener_type: ListenerType::Email,
                message: "IMAP server is required for inbound email listening".to_owned(),
            });
        }

        let connection = self.connection.as_ref().cloned();
        let connection = match connection {
            Some(connection) if connection.lock().await.is_some() => connection,
            _ => {
                let session = connect(settings).await?;
                let connection = Arc::new(Mutex::new(Some(session)));
                self.connection = Some(connection.clone());
                connection
            }
        };
        listen(connection, tx).await
    }

    async fn run_filters(
        &mut self,
        subscription: MessagingSubscription,
        rx: mpsc::Receiver<NormalizedMessage>,
        tx: mpsc::Sender<NormalizedMessage>,
    ) -> Result<MessagingTask, MessagingSubscriptionError> {
        run_filters(subscription, rx, tx)
    }

    async fn send(
        &mut self,
        sender: &MessagingSenderConfig,
        message: &NormalizedMessage,
    ) -> Result<MessagingSendReceipt, MessagingError> {
        match sender {
            MessagingSenderConfig::Email(sender) => {
                let Some(settings) = self.settings.as_ref() else {
                    return Err(MessagingSendError::Terminal {
                        code: MessagingSendErrorCode::InvalidCredentials,
                        message: "Email settings are not connected".to_owned(),
                    }
                    .into());
                };
                let Some((username, password)) = self.credentials.as_ref() else {
                    return Err(MessagingSendError::Terminal {
                        code: MessagingSendErrorCode::InvalidCredentials,
                        message: "Email credentials are not connected".to_owned(),
                    }
                    .into());
                };
                Ok(send(sender, settings, (username, password), message).await?)
            }
            _ => Err(unsupported_send()),
        }
    }

    async fn send_interactive(
        &mut self,
        _sender: &MessagingSenderConfig,
        _request_id: SpectaUuid,
        _request_text: &str,
    ) -> Result<String, MessagingError> {
        Err(MessagingError::InteractiveUnsupported {
            provider: ListenerType::Email,
        })
    }

    async fn edit_interactive(
        &mut self,
        _sender: &MessagingSenderConfig,
        _provider_message_id: &str,
        _request_text: &str,
        _outcome_label: &str,
    ) -> Result<(), MessagingError> {
        Err(MessagingError::InteractiveUnsupported {
            provider: ListenerType::Email,
        })
    }

    fn send_metadata_schema(&self) -> Schema {
        schema_for!(EmailSendMetadata)
    }
}

fn unsupported_send() -> MessagingError {
    MessagingSendError::Terminal {
        code: MessagingSendErrorCode::Validation,
        message: "Email provider cannot send messages from other channels".to_string(),
    }
    .into()
}