Overview
Net::SMTP provides Ruby's implementation of the Simple Mail Transfer Protocol for sending emails. The library handles SMTP connection management, authentication, encryption, and message delivery through a clean object-oriented interface. Ruby's Net::SMTP supports multiple authentication methods, TLS/SSL encryption, and connection pooling for production applications.
The core class Net::SMTP
manages connections to SMTP servers and provides methods for authentication, message delivery, and connection lifecycle management. The library integrates with Ruby's standard email libraries and supports both simple text messages and complex MIME multipart emails.
require 'net/smtp'
# Basic SMTP connection and authentication
smtp = Net::SMTP.new('smtp.gmail.com', 587)
smtp.enable_starttls
smtp.start('example.com', 'user@example.com', 'password', :plain)
# => #<Net::SMTP:0x... @started=true>
Net::SMTP handles connection state internally, tracking whether connections are active, authenticated, and encrypted. The library automatically manages protocol-level details like command formatting, response parsing, and error detection while exposing a high-level interface for application code.
# Check connection state
smtp.started?
# => true
# Send a message
message = "Subject: Test\n\nHello World"
smtp.send_message(message, 'sender@example.com', ['recipient@example.com'])
# => #<Net::SMTP::Response:0x... @status="250", @string="250 Message accepted">
The library supports both block-based automatic connection management and manual connection control. Block syntax ensures proper connection cleanup even when exceptions occur, while manual control provides fine-grained connection reuse for bulk operations.
# Automatic connection management
Net::SMTP.start('smtp.example.com', 587, 'localhost',
'user@example.com', 'password', :plain) do |smtp|
smtp.send_message(message, sender, recipients)
end
# Connection automatically closed
Basic Usage
Net::SMTP connections require server hostname, port, authentication credentials, and encryption settings. Most production SMTP servers require authentication and encrypted connections. The library supports common authentication methods including PLAIN, LOGIN, and CRAM-MD5.
require 'net/smtp'
# Configure connection parameters
smtp_server = 'smtp.gmail.com'
port = 587
username = 'your_email@gmail.com'
password = 'your_app_password'
domain = 'yourdomain.com'
# Create and configure SMTP connection
smtp = Net::SMTP.new(smtp_server, port)
smtp.enable_starttls # Enable TLS encryption
smtp.start(domain, username, password, :plain)
Message formatting follows RFC 5322 standards with headers separated from body by a blank line. Net::SMTP accepts messages as strings and handles protocol-level encoding automatically. Recipients can be single addresses or arrays for multiple destinations.
# Format message with headers
message = <<~EMAIL
From: sender@example.com
To: recipient@example.com
Subject: Test Message
Date: #{Time.now.rfc2822}
This is the message body.
It can contain multiple lines.
EMAIL
# Send to single recipient
smtp.send_message(message, 'sender@example.com', 'recipient@example.com')
# Send to multiple recipients
recipients = ['user1@example.com', 'user2@example.com', 'user3@example.com']
smtp.send_message(message, 'sender@example.com', recipients)
The send_mail
method provides an alternative interface that accepts message body and headers as separate parameters. This approach works well when constructing messages programmatically or integrating with email templating systems.
# Using send_mail with separate parameters
from_address = 'noreply@example.com'
to_addresses = ['customer@example.com']
message_body = "Thank you for your order #12345"
smtp.send_mail(message_body, from_address, to_addresses)
# Close connection when finished
smtp.finish
Connection reuse improves performance for bulk email operations. A single SMTP connection can send multiple messages before closing, reducing authentication overhead and network round trips.
# Bulk sending with connection reuse
messages = [
{ body: "Welcome message", to: "new_user1@example.com" },
{ body: "Welcome message", to: "new_user2@example.com" },
{ body: "Welcome message", to: "new_user3@example.com" }
]
Net::SMTP.start('smtp.example.com', 587, 'localhost',
'sender@example.com', 'password', :plain) do |smtp|
messages.each do |msg|
formatted_message = "Subject: Welcome\n\n#{msg[:body]}"
smtp.send_message(formatted_message, 'sender@example.com', msg[:to])
end
end
Error Handling & Debugging
Net::SMTP raises specific exception classes for different failure modes. Connection failures, authentication problems, and server rejections each generate distinct exception types with detailed error information. Applications should handle these exceptions to provide meaningful feedback and implement retry logic.
The exception hierarchy includes Net::SMTPError
as the base class, with specialized subclasses for authentication (Net::SMTPAuthenticationError
), server responses (Net::SMTPServerBusy
, Net::SMTPSyntaxError
), and connection issues (Net::SMTPFatalError
).
require 'net/smtp'
def send_email_with_error_handling(message, sender, recipients)
smtp = Net::SMTP.new('smtp.gmail.com', 587)
smtp.enable_starttls
begin
smtp.start('localhost', 'user@gmail.com', 'password', :plain)
response = smtp.send_message(message, sender, recipients)
puts "Email sent successfully: #{response.message}"
rescue Net::SMTPAuthenticationError => e
puts "Authentication failed: #{e.message}"
puts "Check username and password configuration"
rescue Net::SMTPServerBusy => e
puts "Server temporarily unavailable: #{e.message}"
puts "Consider implementing retry logic with backoff"
rescue Net::SMTPSyntaxError => e
puts "Invalid command or parameter: #{e.message}"
puts "Check message formatting and recipient addresses"
rescue Net::SMTPFatalError => e
puts "Permanent server error: #{e.message}"
puts "Message rejected, do not retry"
rescue StandardError => e
puts "Unexpected error: #{e.class} - #{e.message}"
ensure
smtp.finish if smtp.started?
end
end
Server response codes provide detailed information about delivery status and failure reasons. Net::SMTP exposes these codes through response objects, enabling applications to implement sophisticated error handling and logging strategies.
# Detailed response analysis
def analyze_smtp_response(response)
case response.status.to_i
when 250
puts "Message accepted for delivery"
when 421
puts "Service temporarily unavailable - retry later"
when 450
puts "Mailbox unavailable - temporary failure"
when 550
puts "Mailbox unavailable - permanent failure"
when 552
puts "Message size exceeds limit"
when 553
puts "Invalid sender address"
else
puts "Unexpected response: #{response.status} #{response.string}"
end
end
# Enhanced sending with response analysis
begin
response = smtp.send_message(message, sender, recipients)
analyze_smtp_response(response)
rescue Net::SMTPError => e
puts "SMTP Error: #{e.message}"
puts "Response: #{e.response.status} #{e.response.string}" if e.respond_to?(:response)
end
Debug mode enables detailed protocol logging for troubleshooting connection and authentication issues. The debug output shows raw SMTP commands and server responses, helping identify configuration problems and server behavior.
# Enable debug logging
smtp = Net::SMTP.new('smtp.example.com', 587)
smtp.set_debug_output($stdout) # Log to standard output
smtp.enable_starttls
# Debug output shows protocol exchange:
# opening connection to smtp.example.com:587...
# <- 220 smtp.example.com ESMTP
# -> EHLO localhost
# <- 250-smtp.example.com
# <- 250-STARTTLS
# <- 250 AUTH PLAIN LOGIN
begin
smtp.start('localhost', 'user@example.com', 'password', :plain)
rescue Net::SMTPAuthenticationError => e
puts "Authentication sequence failed:"
puts "Server capabilities: #{smtp.capabilities}"
puts "Attempted auth method: PLAIN"
end
Connection timeout configuration prevents applications from hanging on unresponsive servers. Net::SMTP supports both connection and read timeouts, with different timeout values appropriate for different network conditions.
# Configure connection timeouts
smtp = Net::SMTP.new('smtp.example.com', 587)
smtp.open_timeout = 10 # Connection timeout in seconds
smtp.read_timeout = 30 # Read timeout in seconds
# Timeout handling
begin
smtp.start('localhost', 'user@example.com', 'password', :plain)
rescue Net::OpenTimeout => e
puts "Connection timeout - server unreachable"
rescue Net::ReadTimeout => e
puts "Read timeout - server not responding"
rescue Errno::ECONNREFUSED => e
puts "Connection refused - check server and port"
end
Production Patterns
Production SMTP usage requires connection pooling, retry logic, rate limiting, and monitoring. Applications sending high volumes of email should implement connection reuse, exponential backoff for temporary failures, and comprehensive logging for delivery tracking and debugging.
Connection pooling reduces authentication overhead and improves throughput for bulk operations. A connection pool maintains multiple authenticated SMTP connections and distributes sending operations across available connections.
class SMTPConnectionPool
def initialize(config, pool_size: 5)
@config = config
@pool_size = pool_size
@connections = []
@mutex = Mutex.new
end
def with_connection
connection = acquire_connection
yield connection
ensure
release_connection(connection)
end
private
def acquire_connection
@mutex.synchronize do
if @connections.empty?
create_connection
else
@connections.pop
end
end
end
def release_connection(connection)
@mutex.synchronize do
if connection && connection.started? && @connections.size < @pool_size
@connections.push(connection)
else
connection&.finish rescue nil
end
end
end
def create_connection
smtp = Net::SMTP.new(@config[:host], @config[:port])
smtp.enable_starttls if @config[:tls]
smtp.start(@config[:domain], @config[:username],
@config[:password], @config[:auth_method])
smtp
end
end
# Usage with connection pool
pool = SMTPConnectionPool.new({
host: 'smtp.gmail.com', port: 587, tls: true,
domain: 'example.com', username: 'app@example.com',
password: 'password', auth_method: :plain
})
emails.each do |email|
pool.with_connection do |smtp|
smtp.send_message(email[:message], email[:from], email[:to])
end
end
Retry logic with exponential backoff handles temporary server failures and rate limiting. Production applications should distinguish between retryable errors (server busy, temporary failures) and permanent errors (invalid recipients, authentication failures).
class EmailDeliveryService
MAX_RETRIES = 3
BASE_DELAY = 1
def send_with_retry(message, sender, recipients, retries: 0)
smtp = create_smtp_connection
begin
smtp.start
response = smtp.send_message(message, sender, recipients)
log_success(sender, recipients, response)
rescue Net::SMTPServerBusy, Net::SMTPUnavailable => e
if retries < MAX_RETRIES
delay = BASE_DELAY * (2 ** retries)
log_retry(e, retries, delay)
sleep(delay)
send_with_retry(message, sender, recipients, retries: retries + 1)
else
log_failure(e, sender, recipients, "Max retries exceeded")
raise
end
rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e
log_failure(e, sender, recipients, "Permanent failure")
raise
ensure
smtp.finish if smtp&.started?
end
end
private
def log_success(sender, recipients, response)
Rails.logger.info "Email sent: #{sender} -> #{recipients.join(', ')} " \
"(#{response.status})"
end
def log_retry(error, attempt, delay)
Rails.logger.warn "Email delivery failed (attempt #{attempt + 1}): " \
"#{error.class} - #{error.message}. " \
"Retrying in #{delay} seconds"
end
def log_failure(error, sender, recipients, reason)
Rails.logger.error "Email delivery failed permanently: " \
"#{sender} -> #{recipients.join(', ')} " \
"#{error.class} - #{error.message} (#{reason})"
end
end
Rails integration patterns leverage ActionMailer's delivery methods while providing direct SMTP control for high-volume or specialized sending requirements. Custom delivery methods can implement advanced features like connection pooling, custom retry logic, and detailed delivery tracking.
# Custom ActionMailer delivery method
class PooledSMTPDelivery
def initialize(settings)
@pool = SMTPConnectionPool.new(settings)
end
def deliver!(mail)
message = mail.encoded
sender = mail.from.first
recipients = mail.destinations
@pool.with_connection do |smtp|
response = smtp.send_message(message, sender, recipients)
# Track delivery for analytics
DeliveryLog.create!(
message_id: mail.message_id,
sender: sender,
recipients: recipients,
status: response.status,
response: response.string,
delivered_at: Time.current
)
response
end
end
end
# Configure in Rails application
ActionMailer::Base.add_delivery_method :pooled_smtp, PooledSMTPDelivery,
host: 'smtp.example.com', port: 587, tls: true,
username: 'app@example.com', password: Rails.application.credentials.smtp_password
# Use in mailer classes
class UserMailer < ApplicationMailer
def welcome_email(user)
mail(to: user.email, subject: 'Welcome') do |format|
format.html { render 'welcome' }
end.delivery_method(:pooled_smtp)
end
end
Monitoring and alerting track delivery success rates, response times, and error patterns. Production applications should implement metrics collection for SMTP operations and alert on delivery failures or performance degradation.
class MonitoredSMTPService
include Singleton
def initialize
@metrics = {
sent: 0, failed: 0, retries: 0,
response_times: [], last_error: nil
}
@mutex = Mutex.new
end
def send_email(message, sender, recipients)
start_time = Time.current
begin
response = deliver_message(message, sender, recipients)
record_success(Time.current - start_time)
response
rescue StandardError => e
record_failure(e, Time.current - start_time)
raise
end
end
def health_check
@mutex.synchronize do
success_rate = @metrics[:sent].to_f / [@metrics[:sent] + @metrics[:failed], 1].max
avg_response_time = @metrics[:response_times].sum / [@metrics[:response_times].size, 1].max
{
success_rate: success_rate,
average_response_time: avg_response_time,
total_sent: @metrics[:sent],
total_failed: @metrics[:failed],
total_retries: @metrics[:retries],
last_error: @metrics[:last_error]
}
end
end
private
def record_success(response_time)
@mutex.synchronize do
@metrics[:sent] += 1
@metrics[:response_times] << response_time
@metrics[:response_times] = @metrics[:response_times].last(100) # Keep last 100
end
end
def record_failure(error, response_time)
@mutex.synchronize do
@metrics[:failed] += 1
@metrics[:last_error] = { error: error.class.name, message: error.message, time: Time.current }
end
end
end
Reference
Core Classes
Class | Purpose | Key Methods |
---|---|---|
Net::SMTP |
Main SMTP client | #start , #send_message , #send_mail , #finish |
Net::SMTP::Response |
Server response wrapper | #status , #string , #message |
Connection Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Net::SMTP.new(address, port=25) |
address (String), port (Integer) |
Net::SMTP |
Create SMTP connection object |
Net::SMTP.start(address, port, helo, user, secret, authtype) |
Connection and auth parameters | Net::SMTP |
Create and start SMTP session |
#start(helo='localhost', user=nil, secret=nil, authtype=nil) |
Authentication parameters | self |
Start SMTP session with authentication |
#started? |
None | Boolean |
Check if session is active |
#finish |
None | nil |
Close SMTP session |
Message Sending Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#send_message(msgstr, from_addr, *to_addrs) |
msgstr (String), from_addr (String), to_addrs (Array/String) |
Response |
Send complete RFC 5322 message |
#send_mail(msgstr, from_addr, *to_addrs) |
msgstr (String), from_addr (String), to_addrs (Array/String) |
Response |
Alias for send_message |
#ready(from_addr, *to_addrs) |
from_addr (String), to_addrs (Array/String) |
nil |
Begin message transaction |
#data(msgstr=nil) |
msgstr (String, optional) |
Response |
Send message data |
Security and Encryption
Method | Parameters | Returns | Description |
---|---|---|---|
#enable_starttls(context=nil) |
context (OpenSSL::SSL::SSLContext, optional) |
nil |
Enable STARTTLS encryption |
#enable_starttls_auto(context=nil) |
context (OpenSSL::SSL::SSLContext, optional) |
nil |
Enable STARTTLS if available |
#enable_tls(context=nil) |
context (OpenSSL::SSL::SSLContext, optional) |
nil |
Enable TLS from connection start |
#disable_tls |
None | nil |
Disable TLS encryption |
#tls? |
None | Boolean |
Check if TLS is enabled |
Authentication Methods
Method | Auth Type | Parameters | Description |
---|---|---|---|
:plain |
PLAIN | username, password | Plain text authentication |
:login |
LOGIN | username, password | LOGIN authentication |
:cram_md5 |
CRAM-MD5 | username, password | Challenge-response authentication |
Configuration Properties
Property | Type | Default | Description |
---|---|---|---|
#address |
String |
- | SMTP server hostname |
#port |
Integer |
25 | SMTP server port |
#open_timeout |
Integer |
30 | Connection timeout in seconds |
#read_timeout |
Integer |
60 | Read timeout in seconds |
#esmtp |
Boolean |
true | Use ESMTP extensions |
Exception Hierarchy
Exception | Parent | Condition |
---|---|---|
Net::SMTPError |
StandardError |
Base SMTP error |
Net::SMTPServerBusy |
Net::SMTPError |
Server temporarily unavailable (4xx) |
Net::SMTPSyntaxError |
Net::SMTPError |
Invalid command or syntax (5xx) |
Net::SMTPAuthenticationError |
Net::SMTPError |
Authentication failed |
Net::SMTPFatalError |
Net::SMTPError |
Permanent failure |
Net::SMTPUnavailable |
Net::SMTPError |
Service unavailable |
Common Response Codes
Code | Category | Description |
---|---|---|
220 | Success | Service ready |
221 | Success | Service closing |
250 | Success | Action completed |
354 | Success | Start mail input |
421 | Temporary | Service unavailable |
450 | Temporary | Mailbox unavailable |
451 | Temporary | Local error |
452 | Temporary | Insufficient storage |
500 | Permanent | Syntax error |
501 | Permanent | Invalid parameters |
502 | Permanent | Command not implemented |
503 | Permanent | Bad sequence |
550 | Permanent | Mailbox unavailable |
551 | Permanent | User not local |
552 | Permanent | Storage allocation exceeded |
553 | Permanent | Mailbox name invalid |
Block Usage Pattern
# Automatic connection management
Net::SMTP.start(server, port, helo, user, pass, auth) do |smtp|
# Connection automatically established and closed
smtp.send_message(message, sender, recipients)
end
Manual Connection Pattern
# Manual connection management
smtp = Net::SMTP.new(server, port)
smtp.enable_starttls
smtp.start(helo, user, pass, auth)
begin
smtp.send_message(message, sender, recipients)
ensure
smtp.finish if smtp.started?
end