missive 0.7.0

Compose, deliver, preview, and test emails in Rust - pluggable providers with zero configuration code
Documentation
#![cfg(feature = "local")]

use missive::providers::LocalMailer;
use missive::{configure, deliver, deliver_with, reset, Email};
use std::sync::{Arc, Mutex};
use tracing::field::{Field, Visit};
use tracing::span::{Attributes, Id, Record};
use tracing::{Event, Level, Metadata, Subscriber};

#[derive(Clone, Debug)]
struct CapturedTelemetry {
    name: String,
    level: Option<Level>,
    fields: Vec<(String, String)>,
}

#[derive(Clone, Default)]
struct CapturingSubscriber {
    spans: Arc<Mutex<Vec<CapturedTelemetry>>>,
    events: Arc<Mutex<Vec<CapturedTelemetry>>>,
}

impl Subscriber for CapturingSubscriber {
    fn enabled(&self, _metadata: &Metadata<'_>) -> bool {
        true
    }

    fn new_span(&self, attrs: &Attributes<'_>) -> Id {
        let mut visitor = FieldVisitor::default();
        attrs.record(&mut visitor);

        let mut spans = self.spans.lock().unwrap();
        spans.push(CapturedTelemetry {
            name: attrs.metadata().name().to_string(),
            level: Some(*attrs.metadata().level()),
            fields: visitor.fields,
        });

        Id::from_u64(spans.len() as u64)
    }

    fn record(&self, span: &Id, values: &Record<'_>) {
        let mut visitor = FieldVisitor::default();
        values.record(&mut visitor);

        let span_index = span.clone().into_u64() as usize - 1;
        if let Some(captured) = self.spans.lock().unwrap().get_mut(span_index) {
            captured.fields.extend(visitor.fields);
        }
    }

    fn record_follows_from(&self, _span: &Id, _follows: &Id) {}

    fn event(&self, event: &Event<'_>) {
        let mut visitor = FieldVisitor::default();
        event.record(&mut visitor);

        self.events.lock().unwrap().push(CapturedTelemetry {
            name: event.metadata().name().to_string(),
            level: Some(*event.metadata().level()),
            fields: visitor.fields,
        });
    }

    fn enter(&self, _span: &Id) {}

    fn exit(&self, _span: &Id) {}
}

#[derive(Default)]
struct FieldVisitor {
    fields: Vec<(String, String)>,
}

impl Visit for FieldVisitor {
    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
        self.fields
            .push((field.name().to_string(), format!("{value:?}")));
    }
}

struct ResetGlobalMailer;

impl Drop for ResetGlobalMailer {
    fn drop(&mut self) {
        reset();
    }
}

#[tokio::test(flavor = "current_thread")]
async fn delivery_spans_do_not_record_recipient_or_subject() {
    let subscriber = CapturingSubscriber::default();
    let spans = Arc::clone(&subscriber.spans);
    let events = Arc::clone(&subscriber.events);
    let _guard = tracing::subscriber::set_default(subscriber);

    let mailer = LocalMailer::new();
    let email = Email::new()
        .from("noreply@example.com")
        .to("steve.rogers@example.com")
        .cc("natasha.romanova@example.com")
        .subject("Sensitive password reset")
        .text_body("Hello");

    deliver_with(&email, &mailer).await.unwrap();
    configure(LocalMailer::new());
    let _reset = ResetGlobalMailer;
    deliver(&email).await.unwrap();

    let spans = spans.lock().unwrap();
    let delivery_spans: Vec<_> = spans
        .iter()
        .filter(|span| span.name == "missive.deliver")
        .collect();
    assert_eq!(delivery_spans.len(), 2);

    for span in delivery_spans {
        let field_names: Vec<&str> = span
            .fields
            .iter()
            .map(|(name, _value)| name.as_str())
            .collect();
        assert!(field_names.contains(&"provider"));
        assert!(field_names.contains(&"recipient_count"));
        assert!(field_names.contains(&"attachment_count"));
        assert!(field_names.contains(&"status"));
        assert!(field_names.contains(&"duration_ms"));
        assert!(!field_names.contains(&"to"));
        assert!(!field_names.contains(&"subject"));
        assert!(!field_names.contains(&"message_id"));

        let rendered_fields = span
            .fields
            .iter()
            .map(|(name, value)| format!("{name}={value}"))
            .collect::<Vec<_>>()
            .join(" ");
        assert!(!rendered_fields.contains("steve.rogers@example.com"));
        assert!(!rendered_fields.contains("natasha.romanova@example.com"));
        assert!(!rendered_fields.contains("Sensitive password reset"));
    }

    let events = events.lock().unwrap();
    let default_level_events: Vec<_> = events
        .iter()
        .filter(|event| matches!(event.level, Some(Level::INFO | Level::ERROR)))
        .collect();

    for event in &default_level_events {
        let field_names: Vec<&str> = event
            .fields
            .iter()
            .map(|(name, _value)| name.as_str())
            .collect();
        assert!(!field_names.contains(&"to"));
        assert!(!field_names.contains(&"subject"));
        assert!(!field_names.contains(&"message_id"));

        let rendered_fields = event
            .fields
            .iter()
            .map(|(name, value)| format!("{name}={value}"))
            .collect::<Vec<_>>()
            .join(" ");
        assert!(!rendered_fields.contains("steve.rogers@example.com"));
        assert!(!rendered_fields.contains("natasha.romanova@example.com"));
        assert!(!rendered_fields.contains("Sensitive password reset"));
    }

    let delivery_events: Vec<_> = default_level_events
        .iter()
        .filter(|event| {
            let field_names: Vec<&str> = event
                .fields
                .iter()
                .map(|(name, _value)| name.as_str())
                .collect();
            field_names.contains(&"provider")
                && field_names.contains(&"status")
                && field_names.contains(&"recipient_count")
                && field_names.contains(&"attachment_count")
                && field_names.contains(&"duration_ms")
        })
        .collect();
    assert_eq!(delivery_events.len(), 2);
}