Overview
SMTP, POP3, and IMAP form the core protocol suite for email communication on the internet. SMTP (Simple Mail Transfer Protocol) handles message transmission between mail servers and from clients to servers. POP3 (Post Office Protocol version 3) downloads messages from a mail server to a local client, typically removing them from the server. IMAP (Internet Message Access Protocol) provides remote access to messages stored on a server, maintaining server-side state and folder hierarchies.
These protocols operate at the application layer of the TCP/IP stack. SMTP uses port 25 for server-to-server communication, port 587 for client submission (often with STARTTLS), and port 465 for implicit TLS. POP3 uses port 110 for plain connections and port 995 for TLS. IMAP uses port 143 for plain connections and port 993 for TLS.
The relationship between these protocols reflects different aspects of email workflow. A typical email journey begins with SMTP transmitting the message from sender to recipient's mail server. The recipient then uses either POP3 to download and store messages locally, or IMAP to access messages that remain on the server. This separation of concerns allows each protocol to specialize in its specific function.
# Basic protocol interaction flow
require 'net/smtp'
require 'net/imap'
# Sending via SMTP
Net::SMTP.start('smtp.example.com', 587) do |smtp|
smtp.send_message(message, 'from@example.com', 'to@example.com')
end
# Receiving via IMAP
imap = Net::IMAP.new('imap.example.com', 993, true)
imap.login('user@example.com', 'password')
imap.select('INBOX')
# => Messages remain on server
The protocols differ fundamentally in their operational models. SMTP operates as a push protocol, actively delivering messages to destination servers. POP3 and IMAP operate as pull protocols, with clients initiating connections to retrieve messages. SMTP uses a command-response model with text-based commands like HELO, MAIL FROM, RCPT TO, and DATA. POP3 provides simple commands (USER, PASS, STAT, LIST, RETR, DELE) for session-based message retrieval. IMAP offers extensive capabilities including folder management, server-side search, partial message fetching, and persistent message flags.
Key Principles
SMTP establishes connections through a multi-step handshake process. The client initiates with HELO or EHLO, identifying itself to the server. The server responds with its capabilities, particularly SMTP extensions supported through ESMTP (Extended SMTP). The client then specifies the envelope sender with MAIL FROM, followed by one or more RCPT TO commands for recipients. The DATA command introduces the message content, terminated by a line containing only a period. The server accepts or rejects the message based on policy rules, recipient validity, and content analysis.
Message envelopes exist separately from message headers. The envelope, established through SMTP commands, controls routing and delivery. Headers within the message content provide metadata visible to recipients. This separation allows for forwarding, mailing lists, and blind carbon copies where envelope addresses differ from header addresses. Bounce messages return to the envelope sender, not the From header address.
# Envelope vs. headers demonstration
message = <<~MESSAGE
From: display@example.com
To: recipient@example.com
Subject: Envelope Example
Message body
MESSAGE
Net::SMTP.start('localhost', 25) do |smtp|
# Envelope sender differs from From header
smtp.send_message(
message,
'bounce-handler@example.com', # Envelope sender
['actual-recipient@example.com'] # Envelope recipients
)
end
POP3 operates through distinct session phases. The authorization phase authenticates the user through USER/PASS commands or APOP for digest authentication. The transaction phase allows message retrieval and deletion marking through RETR and DELE commands. STAT returns message count and total size. LIST provides per-message sizes. The update phase, triggered by QUIT, permanently removes messages marked for deletion. POP3 maintains exclusive locks on mailboxes during sessions, preventing concurrent access.
IMAP maintains persistent server-side state across sessions. Messages retain flags (Seen, Answered, Flagged, Deleted, Draft) that synchronize between clients. The protocol supports multiple simultaneous connections to the same mailbox, with servers broadcasting changes to all connected clients. Commands operate on sequence numbers or unique identifiers (UIDs). Sequence numbers change as messages are added or removed; UIDs remain constant for each message's lifetime.
# IMAP state management
imap = Net::IMAP.new('imap.example.com', 993, true)
imap.login('user', 'password')
imap.select('INBOX')
# Search returns sequence numbers
messages = imap.search(['UNSEEN'])
# => [1, 3, 5]
# UIDs provide stable references
uids = imap.uid_search(['UNSEEN'])
# => [1001, 1003, 1005]
# Fetch by UID instead of sequence number
imap.uid_fetch(1001, 'BODY[]')
IMAP folder hierarchies use mailbox paths separated by delimiters, typically period or slash characters. The NAMESPACE command reveals the server's folder structure conventions. Personal mailboxes, shared mailboxes, and public folders may exist in different namespaces with different access permissions. The LSUB command lists subscribed folders, while LIST shows all available folders.
Message formats follow RFC 5322 structure with headers and body separated by a blank line. Headers use field-name colon field-value format. Multiline headers indent continuation lines with whitespace. MIME (Multipurpose Internet Mail Extensions) encodes non-text content through Content-Type and Content-Transfer-Encoding headers. Multipart messages contain multiple MIME parts with boundary delimiters.
Ruby Implementation
Ruby's standard library provides Net::SMTP, Net::POP3, and Net::IMAP classes in the net-smtp, net-pop, and net-imap gems respectively. These libraries implement the protocol specifications with Ruby's object-oriented patterns and exception handling conventions.
Net::SMTP supports both simple send operations and session-based interactions. The class method start creates a connection, yields to a block for message transmission, then closes the connection. The instance maintains connection state and provides access to SMTP extensions through the capabilities method after EHLO.
require 'net/smtp'
# Simple send with automatic connection management
Net::SMTP.start(
'smtp.example.com',
587,
'localhost', # HELO domain
'user',
'password',
:login # Authentication method
) do |smtp|
smtp.send_message(message_string, from, to_addresses)
end
# Manual session management for multiple messages
smtp = Net::SMTP.new('smtp.example.com', 587)
smtp.enable_starttls_auto
smtp.start('localhost', 'user', 'password', :login) do
recipients.each do |recipient|
message = build_message(recipient)
smtp.send_message(message, from, recipient)
end
end
The send_message method accepts message content as a string, envelope sender as a string, and recipients as a string or array. The method constructs proper SMTP commands internally. For more control, use the data method with a block that yields a socket-like object for writing message content line by line.
# Low-level message sending
smtp.start('localhost') do
smtp.mailfrom('sender@example.com')
smtp.rcptto('recipient1@example.com')
smtp.rcptto('recipient2@example.com')
smtp.data do |data_stream|
data_stream.puts "From: sender@example.com"
data_stream.puts "To: recipient1@example.com"
data_stream.puts "Subject: Test"
data_stream.puts ""
data_stream.puts "Message body"
end
end
Net::POP3 provides session management through the start class method or explicit connection handling. The class supports both plain authentication and APOP digest authentication. Sessions lock the mailbox, requiring proper cleanup through finish or using blocks that automatically close connections.
require 'net/pop'
# Retrieve all messages
Net::POP3.start('pop.example.com', 995, 'user', 'password') do |pop|
if pop.mails.empty?
puts "No messages"
else
pop.mails.each do |msg|
puts "Message #{msg.number}:"
puts msg.pop # Retrieves and returns message content
msg.delete # Marks for deletion
end
end
end
# Check messages without retrieval
pop = Net::POP3.new('pop.example.com', 995)
pop.enable_ssl
pop.start('user', 'password')
message_count = pop.n_mails
total_size = pop.n_bytes
pop.each do |msg|
puts "Message #{msg.number}: #{msg.length} bytes"
header = msg.header # Retrieves only headers
puts header
end
pop.finish
The POPMail objects returned by mails or each represent individual messages. The pop method retrieves the full message content. The header method fetches only headers, reducing bandwidth. The top method retrieves headers plus a specified number of body lines. The delete method marks messages for deletion, with actual removal occurring when the session ends normally through finish or block exit.
Net::IMAP provides comprehensive protocol support through explicit method calls matching IMAP commands. The library exposes raw protocol operations while handling response parsing. Methods return structured data objects containing server responses.
require 'net/imap'
# Connect with implicit TLS
imap = Net::IMAP.new('imap.example.com', 993, true)
# Authenticate
imap.login('user@example.com', 'password')
# List mailboxes
mailboxes = imap.list('', '*')
mailboxes.each do |mb|
puts "#{mb.name}: #{mb.attr.join(', ')}"
end
# Select mailbox
imap.select('INBOX')
# Returns mailbox data: message count, recent count, flags
# Search for messages
unseen_ids = imap.search(['UNSEEN'])
from_ids = imap.search(['FROM', 'boss@example.com'])
recent_ids = imap.search(['SINCE', '1-Jan-2024'])
# Fetch message data
messages = imap.fetch(unseen_ids, ['ENVELOPE', 'BODY[HEADER.FIELDS (FROM TO SUBJECT)]'])
messages.each do |msg_data|
envelope = msg_data.attr['ENVELOPE']
puts "From: #{envelope.from[0].mailbox}@#{envelope.from[0].host}"
puts "Subject: #{envelope.subject}"
headers = msg_data.attr['BODY[HEADER.FIELDS (FROM TO SUBJECT)]']
puts headers
end
IMAP search criteria use array syntax where the first element specifies the search key and subsequent elements provide parameters. Complex searches combine multiple criteria. The NOT operator inverts criteria. The OR operator combines alternatives. Sequences of criteria are implicitly ANDed together.
# Complex search examples
flagged_and_unseen = imap.search(['UNFLAGGED', 'UNSEEN'])
large_recent = imap.search(['RECENT', 'LARGER', 100000])
from_or_to = imap.search(['OR', ['FROM', 'alice'], ['TO', 'alice']])
complex = imap.search([
'SINCE', '1-Jan-2024',
'NOT', ['SEEN'],
'OR', ['FROM', 'important'], ['FLAGGED']
])
The fetch method retrieves message attributes specified by name. Common attributes include ENVELOPE (structured header data), BODY (message structure), BODYSTRUCTURE (detailed structure), BODY[section] (message content or parts), FLAGS (message flags), INTERNALDATE (server date), and RFC822.SIZE (message size). Partial fetches retrieve byte ranges for large messages or attachments.
# Fetch specific message parts
msg_data = imap.fetch(message_id, [
'ENVELOPE',
'BODY[HEADER]',
'BODY[TEXT]',
'BODY[1]', # First MIME part
'FLAGS',
'INTERNALDATE'
]).first
# Partial fetch for large attachments
partial = imap.fetch(message_id, ['BODY[2]<0.8192>'])
# Fetches first 8192 bytes of second MIME part
Security Implications
Email protocols transmit authentication credentials and message content that require protection against interception and tampering. Unencrypted connections expose passwords to network sniffing and messages to inspection. SMTP originally provided no encryption, sending all data in plaintext. Modern implementations use STARTTLS to upgrade connections to TLS after initial handshake, or implicit TLS where encryption begins immediately.
STARTTLS operates through a two-phase connection. The client connects on the plain port and issues the STARTTLS command. The server confirms support, then both parties negotiate TLS encryption. All subsequent traffic flows through the encrypted channel. This approach maintains compatibility with legacy servers that lack encryption support, but creates an opportunity for downgrade attacks where an attacker strips the STARTTLS command to force plaintext communication.
# STARTTLS for SMTP
smtp = Net::SMTP.new('smtp.example.com', 587)
smtp.enable_starttls_auto # Upgrades if server supports it
smtp.start('localhost', 'user', 'password', :login)
# Implicit TLS for SMTP
smtp = Net::SMTP.new('smtp.example.com', 465)
smtp.enable_tls # Connects with immediate encryption
smtp.start('localhost', 'user', 'password', :login)
# Strict STARTTLS enforcement
smtp = Net::SMTP.new('smtp.example.com', 587)
smtp.enable_starttls # Raises exception if STARTTLS unavailable
smtp.start('localhost', 'user', 'password', :login)
POP3 and IMAP follow similar encryption patterns. Port 110 (POP3) and port 143 (IMAP) traditionally used STARTTLS through STLS and STARTTLS commands respectively. Ports 995 (POP3S) and 993 (IMAPS) provide implicit TLS. Modern deployments disable plaintext ports entirely, accepting only encrypted connections.
Authentication mechanisms range from plaintext to challenge-response to token-based. PLAIN authentication sends username and password in Base64 encoding, providing no security without TLS. LOGIN authentication splits credentials across multiple exchanges but remains equivalent to PLAIN. CRAM-MD5 uses challenge-response with MD5 hashing, avoiding password transmission but vulnerable to rainbow tables. DIGEST-MD5 improves on CRAM-MD5 but has implementation inconsistencies. OAuth2 provides token-based authentication suitable for third-party applications accessing user accounts without handling passwords.
# Different authentication methods
# PLAIN (default, requires TLS)
smtp.start('localhost', 'user', 'password', :plain)
# LOGIN (equivalent to PLAIN)
smtp.start('localhost', 'user', 'password', :login)
# CRAM-MD5 (challenge-response)
smtp.start('localhost', 'user', 'password', :cram_md5)
# OAuth2 (requires external token acquisition)
imap = Net::IMAP.new('imap.gmail.com', 993, true)
imap.authenticate('XOAUTH2', 'user@gmail.com', access_token)
Certificate validation prevents man-in-the-middle attacks where an attacker intercepts connections by presenting fraudulent certificates. Ruby's Net libraries use OpenSSL for TLS, with certificate validation enabled by default. The validation checks certificate signatures, expiration dates, and hostname matching. Self-signed certificates or certificates with hostname mismatches cause connection failures unless explicitly allowed.
# Strict certificate validation (default)
imap = Net::IMAP.new('imap.example.com', 993, true)
# Allow self-signed certificates (unsafe for production)
imap = Net::IMAP.new('imap.example.com', 993, ssl: {
verify_mode: OpenSSL::SSL::VERIFY_NONE
})
# Custom certificate verification
imap = Net::IMAP.new('imap.example.com', 993, ssl: {
verify_mode: OpenSSL::SSL::VERIFY_PEER,
ca_file: '/path/to/ca-bundle.crt',
verify_hostname: true
})
Message content security extends beyond transport encryption. Email headers can be forged, making sender addresses unreliable for security decisions. SPF (Sender Policy Framework) verifies that sending servers are authorized by the domain owner. DKIM (DomainKeys Identified Mail) cryptographically signs messages to prove authenticity and detect tampering. DMARC (Domain-based Message Authentication, Reporting, and Conformance) specifies policies for handling messages that fail SPF or DKIM checks.
Rate limiting protects against abuse through excessive connection attempts or message sending. SMTP servers implement limits on messages per connection, recipients per message, and total messages per time period. Exceeding limits triggers temporary failures or permanent blocks. Legitimate bulk senders require IP address warming and reputation management.
Practical Examples
Sending multipart MIME messages requires constructing proper headers and boundaries. The Mail gem simplifies this process compared to manual MIME assembly, but understanding the underlying structure helps diagnose issues and implement custom behavior.
require 'mail'
Mail.defaults do
delivery_method :smtp, {
address: 'smtp.example.com',
port: 587,
user_name: 'user',
password: 'password',
authentication: :login,
enable_starttls_auto: true
}
end
mail = Mail.new do
from 'sender@example.com'
to 'recipient@example.com'
subject 'Report with Attachment'
text_part do
body 'Please review the attached report.'
end
html_part do
content_type 'text/html; charset=UTF-8'
body '<p>Please review the <strong>attached report</strong>.</p>'
end
add_file filename: 'report.pdf', content: File.read('report.pdf')
end
mail.deliver
Retrieving messages with IMAP and processing MIME structure demonstrates parsing multipart content. Messages contain nested MIME parts that require recursive traversal. Each part has its own content type and encoding.
require 'net/imap'
require 'mail'
imap = Net::IMAP.new('imap.example.com', 993, true)
imap.login('user', 'password')
imap.select('INBOX')
# Fetch recent messages
message_ids = imap.search(['SINCE', '1-Jan-2024'])
message_ids.first(10).each do |msg_id|
# Fetch full message content
fetch_data = imap.fetch(msg_id, 'RFC822').first
raw_message = fetch_data.attr['RFC822']
# Parse with Mail gem
mail = Mail.read_from_string(raw_message)
puts "Subject: #{mail.subject}"
puts "From: #{mail.from.first}"
puts "Date: #{mail.date}"
# Extract text content
if mail.multipart?
text_part = mail.text_part
html_part = mail.html_part
puts "Text body:" if text_part
puts text_part.decoded if text_part
puts "HTML body:" if html_part
puts html_part.decoded if html_part
# Process attachments
mail.attachments.each do |attachment|
filename = attachment.filename
puts "Attachment: #{filename} (#{attachment.content_type})"
File.write("downloads/#{filename}", attachment.decoded)
end
else
puts mail.decoded
end
# Mark as read
imap.store(msg_id, '+FLAGS', [:Seen])
end
imap.logout
Implementing a mail queue processor with POP3 demonstrates production patterns for reliable message handling. The processor retrieves messages, processes them asynchronously, and removes them only after successful handling.
require 'net/pop'
class MailQueueProcessor
def initialize(host, port, username, password)
@host = host
@port = port
@username = username
@password = password
end
def process_queue
Net::POP3.start(@host, @port, @username, @password) do |pop|
pop.mails.each do |message|
begin
raw_content = message.pop
mail = Mail.read_from_string(raw_content)
# Process message
handle_message(mail)
# Archive before deletion
archive_message(mail)
# Mark for deletion only after successful processing
message.delete
rescue StandardError => e
# Log error but don't delete message
log_error(message.number, e)
end
end
end
end
private
def handle_message(mail)
case mail.subject
when /\[SUPPORT\]/
create_support_ticket(mail)
when /\[ORDER\]/
process_order(mail)
else
route_to_default(mail)
end
end
def archive_message(mail)
Archive.create!(
message_id: mail.message_id,
from: mail.from.first,
subject: mail.subject,
received_at: mail.date,
raw_content: mail.to_s
)
end
def log_error(message_number, error)
logger.error("Failed processing message #{message_number}: #{error.message}")
logger.error(error.backtrace.join("\n"))
end
end
Managing IMAP folders and organizing messages demonstrates server-side mail management. This approach keeps messages on the server while organizing them into folders based on content or sender.
require 'net/imap'
class ImapOrganizer
def initialize(host, username, password)
@imap = Net::IMAP.new(host, 993, true)
@imap.login(username, password)
end
def organize_inbox
@imap.select('INBOX')
# Create folder structure if needed
ensure_folders(['Work', 'Personal', 'Receipts', 'Newsletters'])
# Get all unprocessed messages
message_ids = @imap.search(['UNKEYWORD', 'Processed'])
message_ids.each do |msg_id|
envelope = @imap.fetch(msg_id, 'ENVELOPE').first.attr['ENVELOPE']
from = "#{envelope.from[0].mailbox}@#{envelope.from[0].host}"
subject = envelope.subject
destination = categorize(from, subject)
if destination
@imap.copy(msg_id, destination)
@imap.store(msg_id, '+FLAGS', [:Deleted])
end
# Mark as processed regardless
@imap.store(msg_id, '+FLAGS', ['Processed'])
end
# Expunge deleted messages
@imap.expunge
end
private
def ensure_folders(folders)
existing = @imap.list('', '*').map(&:name)
folders.each do |folder|
unless existing.include?(folder)
@imap.create(folder)
@imap.subscribe(folder)
end
end
end
def categorize(from, subject)
return 'Receipts' if subject =~ /receipt|invoice|order confirmation/i
return 'Newsletters' if from =~ /@newsletter|@marketing/
return 'Work' if from =~ /@company\.com$/
return 'Personal' if from =~ /family|friend/i
nil
end
def close
@imap.logout
end
end
Tools & Ecosystem
The Mail gem provides high-level abstractions over the Net libraries, handling MIME multipart construction, encoding, and parsing. It offers a delivery method abstraction that supports SMTP, Sendmail, and test modes. The gem integrates with Rails as ActionMailer's underlying engine.
# Mail gem with custom delivery settings
require 'mail'
mail = Mail.new do
from 'sender@example.com'
to 'recipient@example.com'
subject 'Test Message'
body 'Message content'
delivery_method :smtp, {
address: 'smtp.example.com',
port: 587,
domain: 'example.com',
user_name: 'user',
password: 'password',
authentication: :login,
enable_starttls_auto: true,
openssl_verify_mode: OpenSSL::SSL::VERIFY_PEER
}
end
mail.deliver!
Mailman processes incoming mail through Rack-compatible handlers or direct POP3/IMAP polling. The gem routes messages to Ruby methods based on sender, recipient, or content patterns. It handles background polling and automatic reconnection.
require 'mailman'
Mailman.config.pop3 = {
server: 'pop.example.com',
port: 995,
ssl: true,
username: 'user',
password: 'password'
}
Mailman::Application.run do
to 'support@example.com' do
SupportTicket.create!(
from: message.from.first,
subject: message.subject,
body: message.body.decoded
)
end
from 'boss@example.com' do
# High priority routing
UrgentQueue.push(message)
end
subject /\[ORDER-(\d+)\]/ do
order_id = params['captures'].first
Order.find(order_id).process_email(message)
end
default do
DefaultHandler.process(message)
end
end
The gmail gem wraps Gmail's IMAP extensions, providing simplified access to labels, threads, and Gmail-specific features. It handles OAuth2 authentication and Gmail's unique folder structure.
require 'gmail'
gmail = Gmail.connect('user@gmail.com', 'password')
# Use Gmail labels
unread = gmail.inbox.emails(:unread)
unread.each do |email|
puts email.subject
email.label!('Processed')
email.mark(:read)
end
# Search with Gmail syntax
important = gmail.mailbox('[Gmail]/Important').emails
starred = gmail.mailbox('[Gmail]/Starred').emails
# Thread handling
gmail.inbox.emails.each do |email|
thread = email.thread
puts "Thread with #{thread.count} messages"
end
gmail.logout
Pony provides a minimalist interface for sending mail with automatic SMTP configuration detection. It suits simple use cases where full Mail gem features are unnecessary.
require 'pony'
Pony.mail(
to: 'recipient@example.com',
from: 'sender@example.com',
subject: 'Quick Message',
body: 'Message content',
via: :smtp,
via_options: {
address: 'smtp.example.com',
port: 587,
enable_starttls_auto: true,
user_name: 'user',
password: 'password',
authentication: :login
}
)
ActionMailer integrates email functionality into Rails applications, providing mailer classes, view templates, and delivery configuration. It uses the Mail gem internally while adding Rails conventions and features.
Premailer inlines CSS styles for email HTML, converting stylesheet rules to inline style attributes. Email clients have inconsistent CSS support, requiring inline styles for reliable rendering.
LetterOpener intercepts outgoing mail in development environments, opening messages in the browser instead of sending them. This prevents accidental message delivery during testing and development.
Common Pitfalls
SMTP delivery failures often occur silently. The Net::SMTP methods return successfully after the receiving server accepts the message, but subsequent delivery failures generate bounce messages sent to the envelope sender. Applications must monitor bounce addresses and process delivery status notifications.
# Setting a bounce handler address
smtp.mailfrom('sender@example.com', 'ENVID=uniqueid123')
smtp.rcptto('recipient@example.com', 'NOTIFY=SUCCESS,FAILURE')
# But bounces arrive asynchronously as email messages
# Requires separate processing of bounce address
Line ending conventions differ between platforms and protocols. Email uses CRLF (carriage return, line feed) line endings per RFC 5322. Ruby's string literals use LF (line feed) on Unix systems. Mixing line endings breaks message parsing and causes protocol violations. The Net::SMTP library handles conversion automatically, but manual message construction requires attention to line endings.
# Incorrect: Unix line endings
message = "From: sender@example.com\nTo: recipient@example.com\n\nBody"
# Correct: CRLF line endings
message = "From: sender@example.com\r\nTo: recipient@example.com\r\n\r\nBody"
# Or use heredoc with explicit conversion
message = <<~MSG.gsub("\n", "\r\n")
From: sender@example.com
To: recipient@example.com
Body
MSG
POP3's exclusive locking prevents concurrent access to mailboxes. Multiple processes attempting to connect to the same POP3 account generate errors. This limitation makes POP3 unsuitable for distributed processing or web applications with multiple workers. IMAP supports concurrent access, making it preferable for multi-client scenarios.
IMAP sequence numbers change as messages are deleted or moved. Code that stores sequence numbers between operations experiences incorrect message references. UIDs remain stable and should be used for persistent references. However, UIDs become invalid if the mailbox's UIDVALIDITY value changes, which occurs during mailbox reconstruction.
# Wrong: Sequence numbers change
imap.select('INBOX')
messages = imap.search(['ALL'])
# => [1, 2, 3, 4, 5]
# Later, after some messages deleted
imap.fetch(3, 'BODY[]') # Fetches wrong message!
# Correct: Use UIDs
messages = imap.uid_search(['ALL'])
# => [101, 102, 103, 104, 105]
# UIDs remain stable
imap.uid_fetch(103, 'BODY[]') # Always correct message
Character encoding issues arise from legacy email systems using various character sets. Headers should use ASCII with encoded-words for non-ASCII characters. Message bodies declare encoding through Content-Type charset parameters. The Mail gem handles encoding automatically, but manual processing requires explicit encoding conversion.
Large message handling causes memory issues with naive implementations. Loading entire mailboxes into memory fails with large archives. IMAP partial fetch retrieves byte ranges, enabling streaming or selective content access. POP3 lacks partial fetch, requiring full message retrieval.
# Memory-intensive approach
imap.select('Archive')
all_messages = imap.search(['ALL'])
all_messages.each do |msg_id|
full_message = imap.fetch(msg_id, 'RFC822').first
# Loads all messages into memory
end
# Memory-efficient approach
imap.select('Archive')
batch_size = 100
offset = 0
loop do
batch = imap.search(['ALL']).slice(offset, batch_size)
break if batch.empty?
batch.each do |msg_id|
# Process one at a time
message_data = imap.fetch(msg_id, 'RFC822').first
process_message(message_data)
end
offset += batch_size
end
Connection timeouts occur with slow servers or network issues. The Net libraries use Ruby's default socket timeout, which may be too short for slow mail servers. Setting explicit timeouts prevents hanging connections but requires handling timeout exceptions.
Authentication failures stem from incorrect credentials, disabled accounts, or security restrictions. Gmail and other providers require application-specific passwords or OAuth2 for third-party applications. Regular password authentication may be disabled for security reasons.
SMTP relay restrictions prevent unauthorized mail sending. Most SMTP servers require authentication before accepting mail for non-local domains. Attempting to send without authentication results in relay access denied errors. Port 25 blocks are common on residential ISPs to prevent spam, requiring use of submission ports 587 or 465.
Reference
Protocol Comparison
| Protocol | Purpose | Default Ports | State Location | Multi-Client |
|---|---|---|---|---|
| SMTP | Send messages | 25, 587, 465 | Stateless | N/A |
| POP3 | Download messages | 110, 995 | Server | Exclusive lock |
| IMAP | Access messages | 143, 993 | Server | Concurrent |
SMTP Commands
| Command | Purpose | Syntax |
|---|---|---|
| HELO | Identify client | HELO domain |
| EHLO | Extended hello | EHLO domain |
| MAIL FROM | Set envelope sender | MAIL FROM: |
| RCPT TO | Add recipient | RCPT TO: |
| DATA | Begin message content | DATA |
| RSET | Reset transaction | RSET |
| QUIT | End session | QUIT |
| STARTTLS | Upgrade to TLS | STARTTLS |
POP3 Commands
| Command | Purpose | Syntax |
|---|---|---|
| USER | Specify username | USER name |
| PASS | Provide password | PASS string |
| STAT | Get message count | STAT |
| LIST | List message sizes | LIST [msg] |
| RETR | Retrieve message | RETR msg |
| DELE | Mark for deletion | DELE msg |
| RSET | Unmark deletions | RSET |
| QUIT | End session | QUIT |
| TOP | Get headers + lines | TOP msg n |
IMAP Common Commands
| Command | Purpose | Syntax |
|---|---|---|
| LOGIN | Authenticate | LOGIN user pass |
| SELECT | Open mailbox | SELECT mailbox |
| EXAMINE | Open read-only | EXAMINE mailbox |
| CREATE | Create mailbox | CREATE mailbox |
| DELETE | Delete mailbox | DELETE mailbox |
| RENAME | Rename mailbox | RENAME old new |
| LIST | List mailboxes | LIST reference name |
| LSUB | List subscribed | LSUB reference name |
| SEARCH | Find messages | SEARCH criteria |
| FETCH | Get message data | FETCH set items |
| STORE | Set flags | STORE set +FLAGS flags |
| COPY | Copy messages | COPY set mailbox |
| EXPUNGE | Delete marked | EXPUNGE |
| CLOSE | Close mailbox | CLOSE |
| LOGOUT | End session | LOGOUT |
IMAP Search Criteria
| Criterion | Matches | Example |
|---|---|---|
| ALL | All messages | SEARCH ALL |
| ANSWERED | Has Answered flag | SEARCH ANSWERED |
| DELETED | Has Deleted flag | SEARCH DELETED |
| FLAGGED | Has Flagged flag | SEARCH FLAGGED |
| NEW | Recent and unseen | SEARCH NEW |
| OLD | Not recent | SEARCH OLD |
| RECENT | Has Recent flag | SEARCH RECENT |
| SEEN | Has Seen flag | SEARCH SEEN |
| UNANSWERED | No Answered flag | SEARCH UNANSWERED |
| UNDELETED | No Deleted flag | SEARCH UNDELETED |
| UNFLAGGED | No Flagged flag | SEARCH UNFLAGGED |
| UNSEEN | No Seen flag | SEARCH UNSEEN |
| FROM | From address | SEARCH FROM alice |
| TO | To address | SEARCH TO bob |
| SUBJECT | Subject text | SEARCH SUBJECT report |
| BODY | Body text | SEARCH BODY urgent |
| BEFORE | Internal date before | SEARCH BEFORE 1-Jan-2024 |
| SINCE | Internal date since | SEARCH SINCE 1-Jan-2024 |
| LARGER | Size larger than | SEARCH LARGER 10000 |
| SMALLER | Size smaller than | SEARCH SMALLER 1000 |
| OR | Either criterion | SEARCH OR FROM alice TO bob |
| NOT | Inverted criterion | SEARCH NOT SEEN |
Ruby Net::SMTP Methods
| Method | Purpose | Returns |
|---|---|---|
| new(address, port) | Create instance | Net::SMTP |
| start(helo, user, pass, authtype) | Begin session | Block result or nil |
| send_message(msg, from, to) | Send complete message | Response |
| mailfrom(address) | Set sender | Response |
| rcptto(address) | Add recipient | Response |
| data | Transmit message | Block result |
| finish | Close session | Response |
| enable_starttls_auto | Enable STARTTLS | nil |
| enable_tls | Enable implicit TLS | nil |
Ruby Net::POP3 Methods
| Method | Purpose | Returns |
|---|---|---|
| new(address, port) | Create instance | Net::POP3 |
| start(user, pass) | Begin session | Block result or nil |
| mails | Get message list | Array of POPMail |
| n_mails | Message count | Integer |
| n_bytes | Total bytes | Integer |
| finish | Close session | nil |
| enable_ssl | Enable TLS | nil |
Ruby Net::IMAP Methods
| Method | Purpose | Returns |
|---|---|---|
| new(host, port, ssl) | Create connection | Net::IMAP |
| login(user, pass) | Authenticate | Response |
| select(mailbox) | Open mailbox | MailboxData |
| search(criteria) | Find messages | Array of integers |
| uid_search(criteria) | Find by UID | Array of integers |
| fetch(set, attr) | Get message data | Array of FetchData |
| uid_fetch(set, attr) | Fetch by UID | Array of FetchData |
| store(set, attr, flags) | Set flags | Array of FetchData |
| copy(set, mailbox) | Copy messages | Response |
| expunge | Permanently delete | Response |
| logout | End session | Response |
Message Flags
| Flag | Meaning | Set By |
|---|---|---|
| Seen | Message read | Client marking read |
| Answered | Reply sent | Client sending reply |
| Flagged | Marked important | User action |
| Deleted | Marked for deletion | User or filter |
| Draft | Incomplete message | Message composition |
| Recent | Newly arrived | Server on delivery |
Common MIME Types
| Type | Description | Usage |
|---|---|---|
| text/plain | Plain text | Message bodies |
| text/html | HTML content | Rich message bodies |
| multipart/alternative | Alternative versions | Text and HTML together |
| multipart/mixed | Mixed content | Messages with attachments |
| multipart/related | Related parts | HTML with embedded images |
| application/pdf | PDF document | Document attachments |
| image/jpeg | JPEG image | Photo attachments |
| application/octet-stream | Binary data | Generic files |
TLS Configuration
| Setting | Purpose | Values |
|---|---|---|
| enable_starttls_auto | Upgrade if available | Boolean |
| enable_starttls | Required upgrade | Boolean |
| enable_tls | Implicit TLS | Boolean |
| verify_mode | Certificate validation | VERIFY_NONE, VERIFY_PEER |
| ca_file | CA certificate path | File path |
| cert | Client certificate | OpenSSL::X509::Certificate |
| key | Client private key | OpenSSL::PKey::RSA |