Epoxi is a high-performance, fault-tolerant Mail Transfer Agent (MTA) and Mail Delivery Agent (MDA) built with Elixir/OTP. Named after NASA's EPOXI mission, it's designed form mission-critical email delivery.
- High-Performance: Built on Broadway pipelines for concurrent email processing
- Fault-Tolerant: Automatic retry logic with exponential backoff
- Distributed: Multi-node cluster support with automatic load balancing
- Durable Queues: Hybrid ETS/DETS storage for fast access with persistence
- Batch Processing: Efficient bulk email delivery with connection pooling
- Observability: Comprehensive telemetry and monitoring
- Smart Routing: Domain-based email routing and IP pool management
- Real-time: HTTP API for immediate email queuing and status monitoring
Add Epoxi to your list of dependencies in mix.exs:
def deps do
[
{:epoxi, "~> 0.1.0"}
]
end# Create an email
email = %Epoxi.Email{
from: "[email protected]",
to: ["[email protected]"],
subject: "Hello from Epoxi!",
html: "<h1>Welcome!</h1><p>This is a test email from Epoxi.</p>",
text: "Welcome! This is a test email from Epoxi."
}
# Send synchronously
{:ok, receipt} = Epoxi.send(email)
# Send asynchronously
:ok = Epoxi.send_async(email)
# Send multiple emails in bulk
emails = [email1, email2, email3]
{:ok, results} = Epoxi.send_bulk(emails)# Configure SMTP settings
opts = [
relay: "smtp.example.com",
port: 587,
username: "[email protected]",
password: "password",
ssl: true,
tls: :always
]
{:ok, receipt} = Epoxi.send(email, opts)# Start a custom pipeline with specific policy
policy = Epoxi.Queue.PipelinePolicy.new(
name: :high_priority,
max_connections: 20,
batch_size: 50,
batch_timeout: 2_000,
max_retries: 3
)
{:ok, pid} = Epoxi.start_pipeline(policy)Epoxi.Email: Email struct with retry logic and delivery trackingEpoxi.Queue.Pipeline: Broadway-based processing pipelineEpoxi.Queue.Producer: Produces emails from durable queuesEpoxi.SmtpClient: SMTP delivery client with batch support
Epoxi.Node: Manages distributed node communication via ERPCEpoxi.NodeRegistry: Tracks cluster nodes and their stateEpoxi.Cluster: Cluster formation and management
Epoxi.Queue: Durable queue with ETS/DETS hybrid storageEpoxi.Queue.PipelinePolicy: Configurable pipeline policiesEpoxi.Queue.PipelineSupervisor: Manages pipeline processes
- Broadway Pipelines: Email processing uses Broadway for concurrent, fault-tolerant batch processing
- Distributed RPC: Node communication uses ERPC for inter-node calls with telemetry
- Durable Queues: Hybrid ETS/DETS storage for fast access with persistence
- Retry Logic: Exponential backoff retry handling in Email struct
- Policy-Based Configuration: Pipeline behavior controlled by configurable policies
# config/config.exs
config :epoxi,
endpoint_options: [
port: 4000,
scheme: :http
]
# config/prod.exs
config :epoxi,
endpoint_options: [
port: 80,
scheme: :http
]# Default pipeline policy
default_policy = Epoxi.Queue.PipelinePolicy.new(
name: :default,
max_connections: 10,
max_retries: 5,
batch_size: 100,
batch_timeout: 1_000,
allowed_messages: 1000,
message_interval: 60_000
)Epoxi provides a RESTful HTTP API for email management:
curl -X POST http://localhost:4000/messages \
-H "Content-Type: application/json" \
-d '{
"message": {
"from": "[email protected]",
"to": ["[email protected]"],
"subject": "Hello from API",
"html": "<p>Hello from Epoxi API!</p>",
"text": "Hello from Epoxi API!"
},
"ip_pool": "default"
}'# Get pipeline statistics
curl http://localhost:4000/admin/pipelines
# Health check all pipelines
curl http://localhost:4000/admin/pipelines/health
# Health check specific routing key
curl http://localhost:4000/admin/pipelines/example.comcurl http://localhost:4000/ping# Initialize cluster
cluster = Epoxi.Cluster.init()
# Get current cluster state
cluster = Epoxi.Cluster.get_current_state()
# Find nodes in specific IP pool
nodes = Epoxi.Cluster.find_nodes_in_pool(cluster, :production)
# Get all IPs in a pool
ips = Epoxi.Cluster.get_pool_ips(cluster, :production)# Route calls to specific nodes
target_node = Epoxi.Node.from_node(:node@host)
# Synchronous call
result = Epoxi.Node.route_call(target_node, Epoxi.Queue, :length, [:mail_queue])
# Asynchronous cast
Epoxi.Node.route_cast(target_node, Epoxi.Queue, :enqueue, [:mail_queue, email])# Start a durable queue
{:ok, pid} = Epoxi.Queue.start_link(name: :mail_queue)
# Enqueue messages
Epoxi.Queue.enqueue(:mail_queue, email, priority: 1)
# Enqueue multiple messages
Epoxi.Queue.enqueue_many(:mail_queue, [email1, email2], priority: 0)
# Dequeue messages
{:ok, email} = Epoxi.Queue.dequeue(:mail_queue)
# Check queue status
length = Epoxi.Queue.length(:mail_queue)
empty? = Epoxi.Queue.empty?(:mail_queue)
# Force sync to disk
Epoxi.Queue.sync(:mail_queue)# Emails are automatically routed based on recipient domain
email = %Epoxi.Email{
from: "[email protected]",
to: ["[email protected]", "[email protected]"],
subject: "Multi-domain test",
text: "This will be routed to gmail.com and yahoo.com pipelines"
}
# The system automatically creates separate pipelines for each domain
Epoxi.send_async(email)# Route emails to specific IP pools
emails = [email1, email2, email3]
{:ok, summary} = Epoxi.Email.Router.route_emails(emails, :production_pool)Epoxi emits comprehensive telemetry events:
# Queue operations
:telemetry.execute([:epoxi, :queue, :sync], %{count: 100}, %{queue: :mail_queue})
:telemetry.execute([:epoxi, :queue, :destroyed], %{}, %{queue: :mail_queue})
# Pipeline operations
:telemetry.execute([:epoxi, :pipeline, :message_processed], %{count: 1}, %{pipeline: :default})
# Node routing
:telemetry.execute([:epoxi, :router, :route, :count], %{count: 1}, %{source_node: :node1, target_node: :node2})# Get cluster statistics
stats = Epoxi.PipelineMonitor.get_cluster_stats()
# Health check all pipelines
health = Epoxi.PipelineMonitor.health_check_all()
# Health check specific routing key
health = Epoxi.PipelineMonitor.health_check_routing_key("example.com")# Run all tests
mix test
# Run tests including distributed cluster tests
mix test --include distributed
# Run specific test file
mix test test/epoxi/email_test.exs
# Run test at specific line
mix test test/epoxi/email_test.exs:123# Format code
mix format
# Run static analysis
mix credo
# Run type checking
mix dialyzer# Start interactive shell with application
iex -S mix
# Run application in foreground
mix run --no-haltFROM elixir:1.18-alpine
WORKDIR /app
COPY . .
RUN mix deps.get && mix compile
EXPOSE 4000
CMD ["mix", "run", "--no-halt"]# SMTP Configuration
export EPOXI_SMTP_RELAY="smtp.example.com"
export EPOXI_SMTP_PORT="587"
export EPOXI_SMTP_USERNAME="[email protected]"
export EPOXI_SMTP_PASSWORD="password"
# Cluster Configuration
export EPOXI_NODE_NAME="[email protected]"
export EPOXI_COOKIE="your-secret-cookie"- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Follow Elixir best practices and avoid anti-patterns
- Add
@specand@docto all public functions - Add
@moduledocto all modules - Write comprehensive tests
- Use conventional commit messages
- Run
mix format,mix test,mix credo, andmix dialyzerbefore submitting
This project is licensed under the MIT License - see the LICENSE file for details.
- Inspired by NASA's EPOXI mission and its mission-critical reliability requirements
- Built with Broadway for robust data processing
- Uses gen_smtp for SMTP functionality
- Powered by Bandit for HTTP serving
Epoxi - Built for mission-critical email delivery. 🚀