atomicdata-dev / atomic-server

An open source headless CMS / real-time database. Powerful table editor, full-text search, and SDKs for JS / React / Svelte.
https://atomicserver.eu
MIT License
1.07k stars 49 forks source link

E-mail support #276

Open joepio opened 2 years ago

joepio commented 2 years ago

At some point, we'll want to send e-mails. For notifications, registrations, or newsletters, for example.

Usecases for sending mails

Usecases for receiving mail

Implementation

I think it makes sense to offer an e-mail API at the plugin level. Not sure what that means, as at the very least, some sort of server listening to a specific set of ports will need to be started. Maybe atomic-lib may need a runtime, too, at some point, as now only atomic-server does.

Plugins should be able to send e-mails. That means that plugins need access to the mailed functions and SMTP connection state. So that probably means that we need the mailer to be available from Db. We could set a mailer Db::init, which means we need some extra arguments there, or we could set it on a separate function, at Db::set_mailer.

Email for registration / verification

Crates

joepio commented 2 years ago

I've managed to get mail-send working with a local SMTP server (although not properly embedded in Atomic-Server).

But if I use mailin_embedded, the process doesn't exit:

use mailin_embedded::{AuthMechanism, Handler, Server, SslConfig};
use tracing::info;

use crate::errors::AtomicServerResult;

#[derive(Clone)]
struct MailHandler {}
impl Handler for MailHandler {}

const ADDRESS: &str = "127.0.0.1:12041";
const ADDRESS_MAILCATCHER: &str = "127.0.0.1:1025";

pub fn start_mail_server() -> AtomicServerResult<()> {
    let handler = MailHandler {};
    let mut server = Server::new(handler);
    let address = ADDRESS;

    server
        .with_name("example.com")
        .with_auth(AuthMechanism::Plain)
        .with_ssl(SslConfig::None)
        .map_err(|e| format!("Mail SSL error: {e}"))?
        .with_addr(address)
        .map_err(|e| format!("Mail address error: {e}"))?;

    tokio::spawn(async move { send_mail().await });

    tokio::task::spawn_blocking(move || {
        info!("Starting mail server at {address}");
        // TODO: stop this process on ctrl+C
        server.serve().expect("Failed to start mail server");
    });

    Ok(())
}
joepio commented 2 years ago

I've got an SMTP client in atomic-lib working, so we can use it from Endpoint plugins.

However, I'm having doubts on the API to use for send_mail. Specifically, I'm not sure if I should use async (#424)

Since the plugin itself is not async, the code becomes pretty verbose, and we're cloning the Store:

    let store_clone = store.clone();
    tokio::spawn(async move {
        store_clone
            .send_email(message)
            .await
            .unwrap_or_else(|e| tracing::error!("Error sending email: {}", e));
    });

If it was just sync, we'd get something like this, but the response would have to wait for the SMTP server. Slow!

store.send_email(message)?;

But in the future, we should expect plugins to do async stuff without blocking the whole thread.