Skip to content
This page was generated and translated with the assistance of AI. If you spot any inaccuracies, feel free to help improve it. Edit on GitHub

SMTP Configuration

PRX-Email sends email through SMTP using the lettre crate with rustls TLS. The outbox pipeline uses an atomic claim-send-finalize workflow to prevent duplicate sends, with exponential backoff retry and deterministic Message-ID idempotency keys.

Basic SMTP Setup

rust
use prx_email::plugin::{SmtpConfig, AuthConfig};

let smtp = SmtpConfig {
    host: "smtp.example.com".to_string(),
    port: 465,
    user: "[email protected]".to_string(),
    auth: AuthConfig {
        password: Some("your-app-password".to_string()),
        oauth_token: None,
    },
};

Configuration Fields

FieldTypeRequiredDescription
hostStringYesSMTP server hostname (must not be empty)
portu16YesSMTP server port (465 for implicit TLS, 587 for STARTTLS)
userStringYesSMTP username (usually the email address)
auth.passwordOption<String>One ofPassword for SMTP AUTH PLAIN/LOGIN
auth.oauth_tokenOption<String>One ofOAuth access token for XOAUTH2

Common Provider Settings

ProviderHostPortAuth Method
Gmailsmtp.gmail.com465App password or XOAUTH2
Outlook / Office 365smtp.office365.com587XOAUTH2
Yahoosmtp.mail.yahoo.com465App password
Fastmailsmtp.fastmail.com465App password

Sending Email

Basic Send

rust
use prx_email::plugin::SendEmailRequest;

let response = plugin.send(SendEmailRequest {
    account_id: 1,
    to: "[email protected]".to_string(),
    subject: "Hello".to_string(),
    body_text: "Message body here.".to_string(),
    now_ts: now,
    attachment: None,
    failure_mode: None,
});

Reply to a Message

rust
use prx_email::plugin::ReplyEmailRequest;

let response = plugin.reply(ReplyEmailRequest {
    account_id: 1,
    in_reply_to_message_id: "<[email protected]>".to_string(),
    body_text: "Thanks for your message!".to_string(),
    now_ts: now,
    attachment: None,
    failure_mode: None,
});

Replies automatically:

  • Set the In-Reply-To header
  • Build the References chain from the parent message
  • Derive the recipient from the parent message's sender
  • Prefix the subject with Re:

Outbox Pipeline

The outbox pipeline ensures reliable email delivery through an atomic state machine:

mermaid
stateDiagram-v2
    [*] --> pending: send() / reply()
    pending --> sending: claim (atomic)
    sending --> sent: finalize (success)
    sending --> failed: finalize (error)
    failed --> sending: retry (claim)
    failed --> sending: retry_outbox()
    pending --> sending: retry_outbox()

State Machine Rules

TransitionConditionGuard
pending -> sendingclaim_outbox_for_send()status IN ('pending','failed') AND next_attempt_at <= now
sending -> sentProvider acceptedupdate_outbox_status_if_current(status='sending')
sending -> failedProvider rejected or network errorupdate_outbox_status_if_current(status='sending')
failed -> sendingretry_outbox()status IN ('pending','failed') AND next_attempt_at <= now

Idempotency

Each outbox message gets a deterministic Message-ID:

<outbox-{id}-{retries}@prx-email.local>

This ensures that retries are distinguishable from the original send, and providers that de-duplicate by Message-ID will accept each retry.

Retry Backoff

Failed sends use exponential backoff:

next_attempt_at = now + base_backoff * 2^retries

With a base backoff of 5 seconds:

RetryBackoff
110s
220s
340s
480s
5160s
6320s
7640s
105,120s (~85 min)

Manual Retry

rust
use prx_email::plugin::RetryOutboxRequest;

let response = plugin.retry_outbox(RetryOutboxRequest {
    outbox_id: 42,
    now_ts: now,
    failure_mode: None,
});

Retry is rejected if:

  • The outbox status is sent or sending (non-retriable)
  • The next_attempt_at has not been reached yet (retry_not_due)

Attachments

Sending with an Attachment

rust
use prx_email::plugin::{SendEmailRequest, AttachmentInput};

let response = plugin.send(SendEmailRequest {
    account_id: 1,
    to: "[email protected]".to_string(),
    subject: "Report attached".to_string(),
    body_text: "Please find the report attached.".to_string(),
    now_ts: now,
    attachment: Some(AttachmentInput {
        filename: "report.pdf".to_string(),
        content_type: "application/pdf".to_string(),
        base64: Some(base64_encoded_content),
        path: None,
    }),
    failure_mode: None,
});

Attachment Policy

The AttachmentPolicy enforces size and MIME type restrictions:

rust
use prx_email::plugin::AttachmentPolicy;

let policy = AttachmentPolicy {
    max_size_bytes: 25 * 1024 * 1024,  // 25 MiB
    allowed_content_types: [
        "application/pdf",
        "image/jpeg",
        "image/png",
        "text/plain",
        "application/zip",
    ].into_iter().map(String::from).collect(),
};
RuleBehavior
Size exceeds max_size_bytesRejected with attachment exceeds size limit
MIME type not in allowed_content_typesRejected with attachment content type is not allowed
Path-based attachment without attachment_storeRejected with attachment store not configured
Path escapes storage root (../ traversal)Rejected with attachment path escapes storage root

Path-Based Attachments

For attachments stored on disk, configure the attachment store:

rust
use prx_email::plugin::AttachmentStoreConfig;

let store = AttachmentStoreConfig {
    enabled: true,
    dir: "/var/lib/prx-email/attachments".to_string(),
};

Path resolution includes directory traversal guards -- any path that resolves outside the configured storage root is rejected, including symlink-based escapes.

API Response Format

All send operations return an ApiResponse<SendResult>:

rust
pub struct SendResult {
    pub outbox_id: i64,
    pub status: String,          // "sent" or "failed"
    pub retries: i64,
    pub provider_message_id: Option<String>,
    pub next_attempt_at: i64,
}

Next Steps

Released under the Apache-2.0 License.