CrackedRuby logo

CrackedRuby

Net::SMTP

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