plexus-comms 0.1.1

Communication integrations for Plexus RPC - email, SMS, messaging platforms
Documentation
use serde::{Deserialize, Serialize};
use sqlx::{sqlite::SqlitePool, sqlite::SqliteConnectOptions, ConnectOptions, Row};
use std::path::PathBuf;

#[derive(Debug, Clone)]
pub struct EmailStorageConfig {
    pub db_path: PathBuf,
}

impl Default for EmailStorageConfig {
    fn default() -> Self {
        Self {
            db_path: PathBuf::from("email_accounts.db"),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailAccount {
    pub name: String,
    pub smtp: Option<SmtpAccountConfig>,
    pub imap: Option<ImapAccountConfig>,
    pub created_at: i64,
    pub updated_at: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct SmtpAccountConfig {
    pub host: String,
    pub port: u16,
    pub username: String,
    pub password: String,
    pub from_email: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ImapAccountConfig {
    pub host: String,
    pub port: u16,
    pub username: String,
    pub password: String,
}

pub struct EmailStorage {
    pool: SqlitePool,
}

impl EmailStorage {
    pub async fn new(config: EmailStorageConfig) -> Result<Self, String> {
        let db_url = format!("sqlite:{}?mode=rwc", config.db_path.display());
        let connect_options: SqliteConnectOptions = db_url
            .parse()
            .map_err(|e| format!("Failed to parse database URL: {}", e))?;
        let connect_options = connect_options.disable_statement_logging();

        let pool = SqlitePool::connect_with(connect_options)
            .await
            .map_err(|e| format!("Failed to connect to database: {}", e))?;

        let storage = Self { pool };
        storage.run_migrations().await?;

        Ok(storage)
    }

    async fn run_migrations(&self) -> Result<(), String> {
        sqlx::query(
            r#"
            CREATE TABLE IF NOT EXISTS email_accounts (
                name TEXT PRIMARY KEY,
                smtp_config TEXT,
                imap_config TEXT,
                created_at INTEGER NOT NULL,
                updated_at INTEGER NOT NULL
            );
            "#,
        )
        .execute(&self.pool)
        .await
        .map_err(|e| format!("Failed to run migrations: {}", e))?;

        Ok(())
    }

    pub async fn register_account(&self, account: EmailAccount) -> Result<(), String> {
        let smtp_json = account
            .smtp
            .as_ref()
            .map(|s| serde_json::to_string(s).unwrap());
        let imap_json = account
            .imap
            .as_ref()
            .map(|i| serde_json::to_string(i).unwrap());

        sqlx::query(
            r#"
            INSERT INTO email_accounts (name, smtp_config, imap_config, created_at, updated_at)
            VALUES (?, ?, ?, ?, ?)
            ON CONFLICT(name) DO UPDATE SET
                smtp_config = excluded.smtp_config,
                imap_config = excluded.imap_config,
                updated_at = excluded.updated_at
            "#,
        )
        .bind(&account.name)
        .bind(smtp_json)
        .bind(imap_json)
        .bind(account.created_at)
        .bind(account.updated_at)
        .execute(&self.pool)
        .await
        .map_err(|e| format!("Failed to register account: {}", e))?;

        Ok(())
    }

    pub async fn get_account(&self, name: &str) -> Result<Option<EmailAccount>, String> {
        let row = sqlx::query(
            r#"
            SELECT name, smtp_config, imap_config, created_at, updated_at
            FROM email_accounts
            WHERE name = ?
            "#,
        )
        .bind(name)
        .fetch_optional(&self.pool)
        .await
        .map_err(|e| format!("Failed to get account: {}", e))?;

        match row {
            Some(row) => {
                let smtp_json: Option<String> = row.get("smtp_config");
                let imap_json: Option<String> = row.get("imap_config");

                let smtp = smtp_json
                    .and_then(|s| serde_json::from_str(&s).ok());
                let imap = imap_json
                    .and_then(|i| serde_json::from_str(&i).ok());

                Ok(Some(EmailAccount {
                    name: row.get("name"),
                    smtp,
                    imap,
                    created_at: row.get("created_at"),
                    updated_at: row.get("updated_at"),
                }))
            }
            None => Ok(None),
        }
    }

    pub async fn list_accounts(&self) -> Result<Vec<EmailAccount>, String> {
        let rows = sqlx::query(
            r#"
            SELECT name, smtp_config, imap_config, created_at, updated_at
            FROM email_accounts
            ORDER BY name
            "#,
        )
        .fetch_all(&self.pool)
        .await
        .map_err(|e| format!("Failed to list accounts: {}", e))?;

        let mut accounts = Vec::new();
        for row in rows {
            let smtp_json: Option<String> = row.get("smtp_config");
            let imap_json: Option<String> = row.get("imap_config");

            let smtp = smtp_json.and_then(|s| serde_json::from_str(&s).ok());
            let imap = imap_json.and_then(|i| serde_json::from_str(&i).ok());

            accounts.push(EmailAccount {
                name: row.get("name"),
                smtp,
                imap,
                created_at: row.get("created_at"),
                updated_at: row.get("updated_at"),
            });
        }

        Ok(accounts)
    }

    pub async fn remove_account(&self, name: &str) -> Result<bool, String> {
        let result = sqlx::query(
            r#"
            DELETE FROM email_accounts WHERE name = ?
            "#,
        )
        .bind(name)
        .execute(&self.pool)
        .await
        .map_err(|e| format!("Failed to remove account: {}", e))?;

        Ok(result.rows_affected() > 0)
    }
}