Overview
Net::POP3 provides Ruby's implementation of the Post Office Protocol version 3, defined in RFC 1939. The protocol retrieves email messages from mail servers using a simple request-response mechanism. Ruby's Net::POP3 class handles connection establishment, authentication, message enumeration, and retrieval operations.
The implementation supports both plain and secure connections through POP3S, handles various authentication mechanisms including APOP, and manages the stateful nature of POP3 sessions. The protocol operates in distinct phases: authorization, transaction, and update, with Ruby abstracting much of the state management.
require 'net/pop'
# Basic connection and authentication
Net::POP3.start('mail.example.com', 110, 'username', 'password') do |pop|
pop.mails.each do |mail|
puts mail.header
mail.delete
end
end
Net::POP3 returns Net::POPMail
objects representing individual messages. Each mail object provides methods for header inspection, body retrieval, and deletion operations. The class manages connection pooling, automatic reconnection on certain failures, and proper session termination.
# Message count and size information
Net::POP3.start('mail.example.com', 110, 'user', 'pass') do |pop|
puts "#{pop.mails.length} messages (#{pop.n_bytes} bytes total)"
pop.each_mail do |mail|
puts "Message #{mail.number}: #{mail.length} bytes"
content = mail.pop
puts content[0..100] # First 100 characters
end
end
The protocol's stateful nature means operations affect server state. Deleted messages remain marked for deletion until the session ends successfully. Connection interruptions or errors during the update phase can result in unexpected message states.
# Headers-only retrieval for efficiency
Net::POP3.start('mail.example.com', 110, 'user', 'pass') do |pop|
pop.mails.each do |mail|
header = mail.header
if header =~ /Subject: .*urgent/i
puts mail.pop # Only retrieve urgent messages
mail.delete
end
end
end
Basic Usage
Net::POP3 requires establishing a connection, authenticating, and then performing operations within a session context. The start
class method handles connection setup and teardown automatically, executing the provided block within an active session.
require 'net/pop'
# Standard connection with automatic cleanup
Net::POP3.start('mail.server.com', 110, 'username', 'password') do |pop|
if pop.mails.empty?
puts "No messages"
else
puts "Retrieved #{pop.mails.length} messages"
pop.mails.each_with_index do |mail, index|
puts "Message #{index + 1}: #{mail.length} bytes"
puts "From: #{mail.header[/^From: .*/]}"
puts "Subject: #{mail.header[/^Subject: .*/]}"
puts "---"
end
end
end
Manual connection management provides more control over the session lifecycle. This approach requires explicit connection establishment, authentication, and cleanup operations.
# Manual connection management
pop = Net::POP3.new('mail.server.com', 110)
begin
pop.start('username', 'password')
# Check for messages
message_count = pop.n_mails
total_size = pop.n_bytes
puts "#{message_count} messages, #{total_size} bytes total"
# Process messages
pop.mails.each do |mail|
content = mail.pop
# Process message content
puts "Processing message #{mail.number}"
# Mark for deletion
mail.delete
end
ensure
pop.finish if pop.started?
end
Message retrieval operates through Net::POPMail
objects. Each object represents a single message on the server and provides methods for content access and management operations.
Net::POP3.start('mail.server.com', 110, 'user', 'pass') do |pop|
pop.mails.each do |mail|
# Get message size before retrieval
size_bytes = mail.length
puts "Message size: #{size_bytes} bytes"
# Retrieve headers only for filtering
headers = mail.header
subject = headers[/^Subject: (.*)$/m, 1]
from = headers[/^From: (.*)$/m, 1]
# Conditional retrieval based on headers
if subject && subject.include?('Important')
full_content = mail.pop
puts "Retrieved important message from #{from}"
# Message deletion
mail.delete
puts "Message marked for deletion"
end
end
end
The each_mail
method provides an alternative iteration approach that yields mail objects directly. This method handles the enumeration details while providing access to individual message operations.
# Processing with each_mail iterator
Net::POP3.start('mail.server.com', 110, 'user', 'pass') do |pop|
processed_count = 0
pop.each_mail do |mail|
# Header analysis
header_data = mail.header
content_type = header_data[/^Content-Type: (.*)$/i, 1]
# Size-based processing decisions
if mail.length < 10_000 # Small messages only
message_body = mail.pop
# Simple text extraction
if content_type&.include?('text/plain')
puts "Text message: #{message_body[0..200]}"
end
mail.delete
processed_count += 1
else
puts "Skipping large message (#{mail.length} bytes)"
end
end
puts "Processed #{processed_count} messages"
end
Advanced Usage
Secure connections through POP3S require SSL/TLS configuration. Net::POP3 supports encrypted connections using the enable_ssl
method or by specifying SSL parameters during connection establishment.
require 'net/pop'
require 'openssl'
# POP3S with SSL verification
Net::POP3.enable_ssl(OpenSSL::SSL::VERIFY_PEER)
Net::POP3.start('mail.server.com', 995, 'username', 'password') do |pop|
puts "Secure connection established"
puts "SSL cipher: #{pop.instance_variable_get(:@socket).cipher[0]}"
pop.mails.each do |mail|
# Process securely retrieved messages
content = mail.pop
puts "Secure message retrieval: #{content.length} bytes"
end
end
Connection timeout and retry logic handles network reliability issues. Custom timeout values and retry strategies can improve robustness in unstable network conditions.
# Custom timeout and retry configuration
def robust_pop_retrieval(server, port, username, password, retries = 3)
attempt = 0
begin
attempt += 1
# Custom timeout settings
pop = Net::POP3.new(server, port)
pop.read_timeout = 30 # Read timeout
pop.open_timeout = 10 # Connection timeout
pop.start(username, password) do |session|
yield session
end
rescue Net::TimeoutError, Errno::ECONNRESET => e
if attempt < retries
puts "Connection failed (attempt #{attempt}), retrying..."
sleep(2 ** attempt) # Exponential backoff
retry
else
raise "Failed after #{retries} attempts: #{e.message}"
end
end
end
# Usage with retry logic
robust_pop_retrieval('mail.server.com', 110, 'user', 'pass') do |pop|
pop.mails.each { |mail| mail.delete if mail.header.include?('spam') }
end
APOP authentication provides enhanced security by avoiding plain-text password transmission. This method uses MD5 hashing of the password combined with a server timestamp.
# APOP authentication
Net::POP3.APOP('mail.server.com', 110, 'username', 'password') do |pop|
puts "APOP authentication successful"
# Secure message processing
pop.mails.select { |mail| mail.length < 5000 }.each do |mail|
content = mail.pop
# Process smaller messages with APOP security
# Conditional deletion based on content analysis
if content.include?('unsubscribe')
mail.delete
puts "Deleted newsletter message"
end
end
end
Partial message retrieval optimizes bandwidth usage for large messages. The top
method retrieves message headers plus a specified number of body lines without downloading complete messages.
Net::POP3.start('mail.server.com', 110, 'user', 'pass') do |pop|
pop.mails.each do |mail|
# Get headers plus first 10 lines of body
partial_content = mail.top(10)
lines = partial_content.split("\n")
header_end = lines.index("") || 0
preview_lines = lines[(header_end + 1)..(header_end + 10)]
puts "Message preview:"
puts preview_lines.join("\n")
# Decision to retrieve full message
if preview_lines.any? { |line| line.include?('attachment') }
puts "Message has attachments, retrieving full content"
full_content = mail.pop
# Process attachments
process_attachments(full_content)
mail.delete
else
puts "Text-only message, skipping full retrieval"
end
end
end
def process_attachments(message_content)
# Attachment processing logic
boundary_match = message_content[/boundary="([^"]+)"/]
return unless boundary_match
boundary = boundary_match[1]
parts = message_content.split("--#{boundary}")
parts.each_with_index do |part, index|
next if index == 0 # Skip preamble
if part.include?('Content-Disposition: attachment')
filename = part[/filename="([^"]+)"/, 1]
puts "Found attachment: #{filename}" if filename
end
end
end
Bulk operations and message filtering implement efficient processing patterns for high-volume email handling. These patterns minimize server round-trips while maintaining processing flexibility.
# Efficient bulk processing with filtering
Net::POP3.start('mail.server.com', 110, 'user', 'pass') do |pop|
# Pre-filter by size to avoid large downloads
small_messages = pop.mails.select { |mail| mail.length < 50_000 }
large_messages = pop.mails.select { |mail| mail.length >= 50_000 }
puts "Processing #{small_messages.length} small messages"
puts "Deferring #{large_messages.length} large messages"
# Batch header analysis
headers_batch = small_messages.map do |mail|
{
mail: mail,
headers: mail.header,
number: mail.number
}
end
# Filter by header content
urgent_messages = headers_batch.select do |msg|
msg[:headers].match?(/Priority: (high|urgent)/i) ||
msg[:headers].match?(/X-Priority: [12]/i)
end
# Process urgent messages first
urgent_messages.each do |msg|
content = msg[:mail].pop
puts "Processing urgent message #{msg[:number]}"
# Custom urgent processing logic
process_urgent_email(content)
msg[:mail].delete
end
# Handle remaining small messages
(small_messages - urgent_messages.map { |m| m[:mail] }).each do |mail|
if mail.header.include?('newsletter')
mail.delete # Delete without retrieval
else
content = mail.pop
process_regular_email(content)
mail.delete
end
end
end
def process_urgent_email(content)
# Urgent message processing
puts "URGENT: #{content[/Subject: (.*)/, 1]}"
end
def process_regular_email(content)
# Regular message processing
puts "Regular: #{content[/Subject: (.*)/, 1]}"
end
Error Handling & Debugging
POP3 operations generate various exception types corresponding to protocol errors, network issues, and authentication failures. Understanding these exceptions enables robust error handling and appropriate recovery strategies.
require 'net/pop'
def handle_pop_errors(server, port, username, password)
begin
Net::POP3.start(server, port, username, password) do |pop|
pop.mails.each do |mail|
content = mail.pop
process_message(content)
mail.delete
end
end
rescue Net::POPAuthenticationError => e
puts "Authentication failed: #{e.message}"
puts "Check username and password"
# Log authentication failure for security monitoring
log_auth_failure(username, server)
rescue Net::POPServerBusy => e
puts "Server busy: #{e.message}"
puts "Retry after delay"
return :retry_later
rescue Net::POPError => e
puts "POP protocol error: #{e.message}"
puts "Response: #{e.response}" if e.respond_to?(:response)
# Handle protocol-specific errors
rescue Net::TimeoutError => e
puts "Connection timeout: #{e.message}"
puts "Check network connectivity and server status"
return :network_error
rescue Errno::ECONNREFUSED => e
puts "Connection refused: #{e.message}"
puts "Server may be down or port blocked"
return :connection_refused
rescue Errno::EHOSTUNREACH => e
puts "Host unreachable: #{e.message}"
puts "Check DNS resolution and routing"
return :host_unreachable
rescue StandardError => e
puts "Unexpected error: #{e.class} - #{e.message}"
puts e.backtrace.first(5).join("\n")
return :unknown_error
end
:success
end
def log_auth_failure(username, server)
timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
puts "AUTH_FAIL: #{timestamp} - User: #{username}, Server: #{server}"
end
def process_message(content)
# Message processing with error handling
puts "Processing: #{content[/Subject: (.*)/, 1] || 'No subject'}"
end
Connection state debugging requires understanding POP3 session phases and server responses. The protocol's stateful nature means connection interruptions can leave messages in unexpected states.
class DebugPOPSession
def initialize(server, port, username, password)
@server = server
@port = port
@username = username
@password = password
@debug_mode = true
end
def retrieve_with_debugging
pop = Net::POP3.new(@server, @port)
# Enable protocol debugging
pop.set_debug_output($stderr) if @debug_mode
begin
puts "Connecting to #{@server}:#{@port}"
pop.start(@username, @password)
puts "Connection established, entering transaction state"
# Check server capabilities
puts "Server greeting: #{pop.greeting}" if pop.respond_to?(:greeting)
# Message enumeration
message_count = pop.n_mails
total_size = pop.n_bytes
puts "Messages available: #{message_count} (#{total_size} bytes)"
pop.mails.each_with_index do |mail, index|
puts "\n--- Message #{index + 1} ---"
puts "Number: #{mail.number}"
puts "Size: #{mail.length} bytes"
begin
# Test header retrieval
header = mail.header
puts "Headers retrieved: #{header.split("\n").length} lines"
# Test partial retrieval
preview = mail.top(5)
puts "Preview retrieved: #{preview.length} characters"
# Full retrieval only for small messages
if mail.length < 1000
content = mail.pop
puts "Full content retrieved: #{content.length} characters"
# Test deletion
mail.delete
puts "Message marked for deletion"
else
puts "Skipping large message retrieval"
end
rescue Net::POPError => e
puts "Error processing message #{mail.number}: #{e.message}"
next
end
end
puts "\nEntering update state (committing deletions)"
rescue Net::POPError => e
puts "POP error during session: #{e.class} - #{e.message}"
puts "Session state may be inconsistent"
ensure
if pop.started?
puts "Closing connection and committing changes"
pop.finish
else
puts "Connection was not established or already closed"
end
end
end
end
# Usage with debugging
debug_session = DebugPOPSession.new('mail.server.com', 110, 'user', 'pass')
debug_session.retrieve_with_debugging
Message parsing errors occur when handling malformed or corrupted messages. Implementing defensive parsing prevents individual message errors from disrupting entire sessions.
def safe_message_processing(pop_session)
successful_processed = 0
failed_messages = []
pop_session.mails.each do |mail|
begin
# Validate message before processing
if mail.length == 0
puts "Warning: Zero-length message #{mail.number}"
next
end
# Safe header parsing
headers = parse_headers_safely(mail.header)
# Content validation before full retrieval
if headers[:content_length] && headers[:content_length] > 10_000_000
puts "Skipping oversized message #{mail.number}"
next
end
# Retrieve with encoding handling
content = mail.pop
# Validate encoding
unless content.valid_encoding?
puts "Invalid encoding in message #{mail.number}, attempting repair"
content = content.encode('UTF-8', invalid: :replace, undef: :replace)
end
# Process successfully validated message
process_validated_message(content, headers)
mail.delete
successful_processed += 1
rescue StandardError => e
error_info = {
number: mail.number,
error: e.class.name,
message: e.message,
size: mail.length
}
failed_messages << error_info
puts "Failed to process message #{mail.number}: #{e.message}"
# Continue processing other messages
next
end
end
puts "\nProcessing summary:"
puts "Successfully processed: #{successful_processed}"
puts "Failed messages: #{failed_messages.length}"
failed_messages.each do |failure|
puts " Message #{failure[:number]}: #{failure[:error]} - #{failure[:message]}"
end
end
def parse_headers_safely(header_text)
headers = {}
begin
header_text.split("\n").each do |line|
next if line.strip.empty?
if line.match(/^([^:]+):\s*(.*)$/)
key = $1.downcase.gsub('-', '_').to_sym
value = $2.strip
headers[key] = value
end
end
# Parse specific headers with validation
headers[:content_length] = headers[:content_length].to_i if headers[:content_length]
rescue StandardError => e
puts "Header parsing error: #{e.message}"
headers[:parse_error] = e.message
end
headers
end
def process_validated_message(content, headers)
subject = headers[:subject] || "No subject"
from = headers[:from] || "Unknown sender"
puts "Processing: #{subject} from #{from}"
puts "Content length: #{content.length} characters"
end
Production Patterns
Production email processing requires robust connection management, efficient resource usage, and comprehensive error handling. These patterns address scalability, reliability, and monitoring requirements in production environments.
require 'net/pop'
require 'logger'
require 'timeout'
class ProductionEmailProcessor
def initialize(config)
@config = config
@logger = Logger.new(@config[:log_file] || STDOUT)
@logger.level = Logger::INFO
@stats = {
processed: 0,
deleted: 0,
errors: 0,
bytes_processed: 0,
start_time: Time.now
}
end
def process_mailbox
@logger.info "Starting email processing for #{@config[:username]}@#{@config[:server]}"
retry_count = 0
max_retries = @config[:max_retries] || 3
begin
# Connection with comprehensive timeout handling
Timeout::timeout(@config[:session_timeout] || 300) do
establish_connection do |pop|
process_messages(pop)
end
end
log_completion_stats
rescue Net::POPAuthenticationError => e
@logger.error "Authentication failed: #{e.message}"
raise # Don't retry auth failures
rescue Net::TimeoutError, Errno::ECONNRESET, Errno::ECONNREFUSED => e
retry_count += 1
@logger.warn "Connection error (attempt #{retry_count}): #{e.message}"
if retry_count < max_retries
delay = [2 ** retry_count, 60].min # Exponential backoff, max 60s
@logger.info "Retrying in #{delay} seconds..."
sleep(delay)
retry
else
@logger.error "Max retries exceeded, failing"
raise
end
rescue StandardError => e
@logger.error "Unexpected error: #{e.class} - #{e.message}"
@logger.error e.backtrace.first(10).join("\n")
raise
end
end
private
def establish_connection
if @config[:ssl]
Net::POP3.enable_ssl
port = @config[:port] || 995
else
port = @config[:port] || 110
end
pop = Net::POP3.new(@config[:server], port)
pop.read_timeout = @config[:read_timeout] || 30
pop.open_timeout = @config[:open_timeout] || 10
pop.start(@config[:username], @config[:password]) do |session|
@logger.info "Connected successfully, #{session.n_mails} messages available"
yield session
end
end
def process_messages(pop)
# Sort messages by size for efficient processing
messages = pop.mails.sort_by(&:length)
# Process in batches to manage memory
batch_size = @config[:batch_size] || 50
messages.each_slice(batch_size) do |batch|
process_message_batch(batch)
# Memory cleanup between batches
GC.start if @config[:gc_between_batches]
end
end
def process_message_batch(messages)
@logger.info "Processing batch of #{messages.length} messages"
messages.each do |mail|
begin
process_single_message(mail)
rescue StandardError => e
@stats[:errors] += 1
@logger.error "Error processing message #{mail.number}: #{e.message}"
# Continue with next message
end
end
end
def process_single_message(mail)
# Size-based processing decisions
if mail.length > (@config[:max_message_size] || 10_000_000)
@logger.warn "Skipping oversized message #{mail.number} (#{mail.length} bytes)"
return
end
# Header-only filtering for efficiency
headers = mail.header
if should_skip_message?(headers)
@logger.debug "Skipping filtered message #{mail.number}"
mail.delete if @config[:delete_filtered]
return
end
# Retrieve and process
content = mail.pop
@stats[:bytes_processed] += content.length
# Custom message processing
result = process_message_content(content, headers)
if result[:delete]
mail.delete
@stats[:deleted] += 1
@logger.debug "Deleted message #{mail.number}"
end
@stats[:processed] += 1
# Progress logging
if @stats[:processed] % 100 == 0
@logger.info "Processed #{@stats[:processed]} messages"
end
end
def should_skip_message?(headers)
# Example filtering logic
spam_indicators = ['X-Spam-Flag: YES', 'X-Spam-Status: Yes']
headers.match?(Regexp.union(spam_indicators))
end
def process_message_content(content, headers)
# Implement custom business logic
subject = headers[/^Subject: (.*)$/m, 1] || 'No subject'
# Example: Archive important messages
if subject.match?(/\b(urgent|important|critical)\b/i)
archive_message(content, subject)
return { delete: false } # Keep important messages
end
# Example: Process and delete routine messages
extract_data_from_message(content)
return { delete: true }
end
def archive_message(content, subject)
# Implementation for archiving important messages
@logger.info "Archiving important message: #{subject}"
end
def extract_data_from_message(content)
# Implementation for data extraction
@logger.debug "Extracting data from message"
end
def log_completion_stats
duration = Time.now - @stats[:start_time]
@logger.info "Processing completed in #{duration.round(2)} seconds"
@logger.info "Messages processed: #{@stats[:processed]}"
@logger.info "Messages deleted: #{@stats[:deleted]}"
@logger.info "Processing errors: #{@stats[:errors]}"
@logger.info "Bytes processed: #{@stats[:bytes_processed]}"
@logger.info "Average rate: #{(@stats[:processed] / duration).round(2)} messages/second"
end
end
# Production configuration and usage
config = {
server: 'mail.company.com',
port: 995,
ssl: true,
username: 'processor@company.com',
password: ENV['EMAIL_PASSWORD'],
max_retries: 5,
session_timeout: 600,
read_timeout: 45,
batch_size: 100,
max_message_size: 50_000_000,
delete_filtered: true,
gc_between_batches: true,
log_file: '/var/log/email_processor.log'
}
processor = ProductionEmailProcessor.new(config)
processor.process_mailbox
Monitoring and alerting integration provides visibility into email processing operations and enables proactive issue detection.
require 'net/pop'
require 'json'
require 'net/http'
class MonitoredEmailProcessor
def initialize(config, metrics_config = {})
@config = config
@metrics_config = metrics_config
@metrics = {
connection_attempts: 0,
connection_failures: 0,
messages_processed: 0,
processing_errors: 0,
session_duration: 0,
last_successful_run: nil
}
end
def process_with_monitoring
start_time = Time.now
begin
@metrics[:connection_attempts] += 1
Net::POP3.start(@config[:server], @config[:port],
@config[:username], @config[:password]) do |pop|
@metrics[:session_duration] = Time.now - start_time
message_count = pop.n_mails
# Alert on unusual message volume
check_message_volume_alert(message_count)
pop.mails.each do |mail|
begin
process_message_with_metrics(mail)
@metrics[:messages_processed] += 1
rescue StandardError => e
@metrics[:processing_errors] += 1
send_error_alert(e, mail.number)
end
end
@metrics[:last_successful_run] = Time.now
end
send_success_metrics
rescue Net::POPError, Net::TimeoutError => e
@metrics[:connection_failures] += 1
send_connection_alert(e)
raise
ensure
@metrics[:session_duration] = Time.now - start_time
end
end
private
def process_message_with_metrics(mail)
message_start = Time.now
# Message processing logic
content = mail.pop
# Track message processing time
processing_time = Time.now - message_start
# Alert on slow processing
if processing_time > 30 # 30 seconds threshold
send_performance_alert(mail.number, processing_time)
end
# Example processing
if content.include?('order confirmation')
process_order_confirmation(content)
elsif content.include?('error report')
process_error_report(content)
end
mail.delete
end
def check_message_volume_alert(count)
expected_range = @config[:expected_message_range] || (0..1000)
unless expected_range.include?(count)
alert_data = {
alert_type: 'unusual_volume',
message_count: count,
expected_range: expected_range,
timestamp: Time.now.iso8601
}
send_alert(alert_data)
end
end
def send_error_alert(error, message_number)
alert_data = {
alert_type: 'processing_error',
error_class: error.class.name,
error_message: error.message,
message_number: message_number,
timestamp: Time.now.iso8601,
error_count: @metrics[:processing_errors]
}
send_alert(alert_data)
end
def send_connection_alert(error)
alert_data = {
alert_type: 'connection_failure',
error_class: error.class.name,
error_message: error.message,
server: @config[:server],
failure_count: @metrics[:connection_failures],
timestamp: Time.now.iso8601
}
send_alert(alert_data)
end
def send_performance_alert(message_number, processing_time)
alert_data = {
alert_type: 'slow_processing',
message_number: message_number,
processing_time: processing_time,
threshold: 30,
timestamp: Time.now.iso8601
}
send_alert(alert_data)
end
def send_success_metrics
return unless @metrics_config[:webhook_url]
metrics_data = {
type: 'success_metrics',
server: @config[:server],
metrics: @metrics,
timestamp: Time.now.iso8601
}
send_webhook(metrics_data)
end
def send_alert(alert_data)
puts "ALERT: #{alert_data[:alert_type]} - #{alert_data}"
# Send to monitoring system
send_webhook(alert_data) if @metrics_config[:webhook_url]
# Log to file
if @metrics_config[:alert_log_file]
File.open(@metrics_config[:alert_log_file], 'a') do |f|
f.puts "#{Time.now.iso8601}: #{alert_data.to_json}"
end
end
end
def send_webhook(data)
uri = URI(@metrics_config[:webhook_url])
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request.body = data.to_json
response = http.request(request)
puts "Webhook response: #{response.code}" unless response.code == '200'
rescue StandardError => e
puts "Failed to send webhook: #{e.message}"
end
def process_order_confirmation(content)
# Business logic for order confirmations
puts "Processing order confirmation"
end
def process_error_report(content)
# Business logic for error reports
puts "Processing error report - escalating"
end
end
Common Pitfalls
Message deletion timing represents a significant pitfall in POP3 operations. Messages marked for deletion during a session remain on the server until the connection closes successfully. Connection interruptions or exceptions can prevent deletions from being committed.
require 'net/pop'
# PROBLEMATIC: Deletion state inconsistency
def unreliable_deletion
Net::POP3.start('mail.server.com', 110, 'user', 'pass') do |pop|
pop.mails.each do |mail|
content = mail.pop
if content.include?('delete me')
mail.delete # Marked for deletion
puts "Message #{mail.number} marked for deletion"
# DANGER: If an exception occurs here, deletion is lost
risky_processing(content) # May raise exception
end
end
# Deletions committed only if block completes successfully
end
rescue StandardError => e
# Connection closed abnormally - deletions are NOT committed
puts "Error occurred: #{e.message}"
puts "Marked deletions were NOT committed to server"
end
def risky_processing(content)
# Simulated processing that might fail
raise "Processing failed" if content.include?('bad data')
end
# BETTER: Explicit deletion control
def reliable_deletion
pop = Net::POP3.new('mail.server.com', 110)
deletion_queue = []
begin
pop.start('user', 'pass')
pop.mails.each do |mail|
content = mail.pop
# Process first, mark for deletion after success
begin
process_message(content)
# Only add to deletion queue after successful processing
if content.include?('delete me')
deletion_queue << mail
puts "Message #{mail.number} queued for deletion"
end
rescue StandardError => e
puts "Processing failed for message #{mail.number}: #{e.message}"
# Don't add to deletion queue on processing failure
end
end
# Batch deletions after all processing
deletion_queue.each do |mail|
mail.delete
puts "Deleting message #{mail.number}"
end
puts "Committing #{deletion_queue.length} deletions"
ensure
pop.finish if pop.started? # Commits deletions
end
end
def process_message(content)
# Safe message processing
puts "Processing: #{content[0..100]}"
end
Character encoding issues frequently occur with international email content. POP3 messages may contain various encodings, and Ruby's string handling requires explicit encoding management.
# PROBLEMATIC: Encoding assumptions
def naive_message_processing
Net::POP3.start('mail.server.com', 110, 'user', 'pass') do |pop|
pop.mails.each do |mail|
content = mail.pop
# DANGER: Assumes UTF-8 encoding
if content.include?('résumé') # May fail with encoding errors
puts "Found résumé in message"
end
# DANGER: String operations on mixed encodings
subject = content[/^Subject: (.*)$/m, 1]
puts "Subject: #{subject.upcase}" # May raise encoding error
end
end
rescue Encoding::CompatibilityError => e
puts "Encoding error: #{e.message}"
end
# BETTER: Proper encoding handling
def encoding_aware_processing
Net::POP3.start('mail.server.com', 110, 'user', 'pass') do |pop|
pop.mails.each do |mail|
content = mail.pop
# Detect and handle encoding
unless content.valid_encoding?
puts "Invalid encoding detected, attempting repair"
# Try common encodings
['UTF-8', 'ISO-8859-1', 'Windows-1252'].each do |encoding|
begin
repaired = content.force_encoding(encoding)
if repaired.valid_encoding?
content = repaired.encode('UTF-8')
puts "Repaired encoding using #{encoding}"
break
end
rescue Encoding::UndefinedConversionError
next
end
end
# Fallback: Replace invalid characters
unless content.valid_encoding?
content = content.encode('UTF-8', invalid: :replace, undef: :replace)
puts "Used replacement characters for invalid bytes"
end
end
# Safe string operations
begin
if content.include?('résumé')
puts "Found résumé in message"
end
subject = extract_subject_safely(content)
puts "Subject: #{subject}" if subject
rescue Encoding::CompatibilityError => e
puts "Encoding compatibility issue: #{e.message}"
puts "Content encoding: #{content.encoding}"
end
end
end
end
def extract_subject_safely(content)
# Extract subject with encoding awareness
match = content.match(/^Subject: (.*)$/m)
return nil unless match
subject = match[1].strip
# Handle encoded-word format (RFC 2047)
if subject.match(/=\?([^?]+)\?([BQ])\?([^?]+)\?=/)
encoding = $1
transfer_encoding = $2
encoded_text = $3
begin
if transfer_encoding.upcase == 'B'
decoded = encoded_text.unpack1('m') # Base64
elsif transfer_encoding.upcase == 'Q'
decoded = encoded_text.tr('_', ' ').unpack1('M') # Quoted-printable
end
subject = decoded.force_encoding(encoding).encode('UTF-8')
rescue StandardError => e
puts "Failed to decode subject: #{e.message}"
# Return original subject
end
end
subject
end
Connection timeout handling requires understanding both Ruby's timeout mechanisms and POP3 protocol behavior. Improper timeout handling can leave connections in undefined states.
require 'timeout'
# PROBLEMATIC: Improper timeout handling
def problematic_timeouts
# DANGER: Global timeout can interrupt protocol mid-command
Timeout::timeout(30) do
Net::POP3.start('mail.server.com', 110, 'user', 'pass') do |pop|
pop.mails.each do |mail|
# Timeout may occur during message retrieval
content = mail.pop # May be interrupted
mail.delete # Server state becomes inconsistent
end
end
end
rescue Timeout::Error
puts "Operation timed out - connection state unknown"
end
# BETTER: Granular timeout management
def proper_timeout_handling
pop = Net::POP3.new('mail.server.com', 110)
# Set appropriate timeouts for different operations
pop.open_timeout = 10 # Connection establishment
pop.read_timeout = 60 # Individual read operations
begin
pop.start('user', 'pass')
# Check server responsiveness
message_count = pop.n_mails
puts "Retrieved message count: #{message_count}"
pop.mails.each_with_index do |mail, index|
operation_start = Time.now
begin
# Monitor individual message processing time
Timeout::timeout(120) do # Per-message timeout
content = mail.pop
processing_time = Time.now - operation_start
if processing_time > 30
puts "Slow message retrieval: #{processing_time}s for message #{index + 1}"
end
# Process with its own timeout
process_with_timeout(content, 30)
# Safe deletion after successful processing
mail.delete
end
rescue Timeout::Error
puts "Timeout processing message #{index + 1}, skipping"
# Don't delete message on timeout
next
rescue Net::ReadTimeout
puts "Network read timeout for message #{index + 1}"
# Connection may still be valid, continue with next message
next
end
end
rescue Net::OpenTimeout
puts "Failed to establish connection within timeout"
raise
rescue Net::ReadTimeout
puts "Network read timeout during session"
# Connection is likely broken
raise
ensure
# Ensure clean connection closure
if pop.started?
begin
Timeout::timeout(10) do
pop.finish
end
rescue Timeout::Error
puts "Timeout during connection closure"
# Force close if normal closure times out
end
end
end
end
def process_with_timeout(content, timeout_seconds)
Timeout::timeout(timeout_seconds) do
# Message processing logic
lines = content.split("\n")
puts "Processing #{lines.length} lines"
# Simulate processing work
sleep(0.1) if lines.length > 1000 # Simulate heavy processing
end
rescue Timeout::Error
puts "Message processing timed out"
raise
end
Memory management becomes critical when processing large volumes of messages or individual large messages. Ruby's garbage collection behavior and string handling patterns affect memory usage patterns.
# PROBLEMATIC: Memory accumulation
def memory_inefficient_processing
all_messages = []
Net::POP3.start('mail.server.com', 110, 'user', 'pass') do |pop|
pop.mails.each do |mail|
# DANGER: Accumulating all message content in memory
content = mail.pop
all_messages << {
number: mail.number,
content: content, # Large strings retained
headers: parse_headers(content),
processed: false
}
end
# DANGER: All messages in memory simultaneously
all_messages.each do |msg_data|
process_large_message(msg_data[:content])
msg_data[:processed] = true
# Original content string still in memory
end
end
# Memory pressure continues until method exit
puts "Processed #{all_messages.length} messages"
end
# BETTER: Streaming and memory-conscious processing
def memory_efficient_processing
processed_count = 0
Net::POP3.start('mail.server.com', 110, 'user', 'pass') do |pop|
# Process messages in batches to control memory usage
batch_size = 10
pop.mails.each_slice(batch_size) do |batch|
batch.each do |mail|
# Process one message at a time
process_single_message_efficiently(mail)
processed_count += 1
# Explicit cleanup for large messages
if mail.length > 1_000_000
GC.start # Force garbage collection
end
end
# Progress reporting without retaining data
puts "Processed #{processed_count} messages" if processed_count % 50 == 0
# Batch-level memory cleanup
GC.start
end
end
end
def process_single_message_efficiently(mail)
# Check size before retrieval
if mail.length > 50_000_000 # 50MB threshold
puts "Skipping oversized message #{mail.number} (#{mail.length} bytes)"
return
end
# Stream headers for filtering without full retrieval
headers = mail.header
# Filter before expensive full retrieval
return if headers.include?('X-Spam-Flag: YES')
# Retrieve content only when needed
content = mail.pop
begin
# Process in chunks for large messages
if content.length > 1_000_000
process_large_content_in_chunks(content)
else
process_normal_content(content)
end
mail.delete
ensure
# Explicit cleanup for large strings
content = nil # Allow immediate GC
end
end
def process_large_content_in_chunks(content)
chunk_size = 100_000 # 100KB chunks
offset = 0
while offset < content.length
chunk_end = [offset + chunk_size, content.length].min
chunk = content[offset...chunk_end]
# Process chunk
process_content_chunk(chunk, offset)
# Clear chunk reference
chunk = nil
offset = chunk_end
end
end
def process_content_chunk(chunk, offset)
puts "Processing chunk at offset #{offset}: #{chunk.length} bytes"
# Chunk processing logic
end
def process_normal_content(content)
puts "Processing normal message: #{content.length} bytes"
# Regular processing logic
end
def parse_headers(content)
# Simple header extraction
header_section = content[/\A(.*?)\n\n/m, 1]
return {} unless header_section
headers = {}
header_section.split("\n").each do |line|
if line.match(/^([^:]+):\s*(.*)$/)
headers[$1.downcase] = $2
end
end
headers
end
def process_large_message(content)
# Simulated processing
puts "Processing large message: #{content.length} characters"
end
Reference
Core Classes and Methods
Class/Method | Parameters | Returns | Description |
---|---|---|---|
Net::POP3.new(address, port) |
address (String), port (Integer) |
Net::POP3 |
Creates POP3 connection object |
Net::POP3.start(address, port, account, password, &block) |
Connection params, credentials, block | Object or nil |
Establishes session with automatic cleanup |
Net::POP3.APOP(address, port, account, password, &block) |
Connection params, credentials, block | Object or nil |
APOP authentication session |
Net::POP3.enable_ssl(*args) |
SSL verification options | nil |
Enables SSL for subsequent connections |
#start(account, password) |
account (String), password (String) |
self |
Authenticates and enters transaction state |
#finish |
None | nil |
Closes connection and commits deletions |
#started? |
None | Boolean |
Checks if session is active |
#n_mails |
None | Integer |
Returns number of messages |
#n_bytes |
None | Integer |
Returns total mailbox size in bytes |
#mails |
None | Array<Net::POPMail> |
Returns array of mail objects |
#each_mail(&block) |
Block | nil |
Iterates over mail objects |
#delete_all |
None | nil |
Marks all messages for deletion |
#reset |
None | nil |
Unmarks messages marked for deletion |
POPMail Object Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#number |
None | Integer |
Message sequence number |
#length |
None | Integer |
Message size in bytes |
#header |
None | String |
Retrieves message headers |
#top(lines) |
lines (Integer) |
String |
Headers plus specified body lines |
#pop |
None | String |
Retrieves complete message |
#delete |
None | nil |
Marks message for deletion |
#deleted? |
None | Boolean |
Checks deletion status |
#unique_id |
None | String |
Server-assigned unique identifier |
Configuration Attributes
Attribute | Type | Default | Description |
---|---|---|---|
open_timeout |
Integer |
30 |
Connection timeout in seconds |
read_timeout |
Integer |
60 |
Read operation timeout in seconds |
port |
Integer |
110 |
Server port (995 for SSL) |
use_ssl |
Boolean |
false |
Enable SSL/TLS encryption |
Exception Hierarchy
Exception | Inherits From | Description |
---|---|---|
Net::POPError |
Net::ProtocolError |
Base class for POP3 errors |
Net::POPAuthenticationError |
Net::POPError |
Authentication failures |
Net::POPServerBusy |
Net::POPError |
Server temporarily unavailable |
Net::POPBadResponse |
Net::POPError |
Invalid server response |
Net::TimeoutError |
Timeout::Error |
Operation timeout |
SSL Configuration Options
Option | Type | Description |
---|---|---|
OpenSSL::SSL::VERIFY_NONE |
Integer |
Disable certificate verification |
OpenSSL::SSL::VERIFY_PEER |
Integer |
Verify server certificate |
ca_file |
String |
Path to CA certificate file |
ca_path |
String |
Path to CA certificate directory |
cert |
OpenSSL::X509::Certificate |
Client certificate |
key |
OpenSSL::PKey |
Client private key |
Common Response Codes
Code | Meaning | Context |
---|---|---|
+OK |
Success | Command completed successfully |
-ERR |
Error | Command failed or invalid |
+OK message follows |
Data transfer | Message content follows |
-ERR no such message |
Invalid message | Message number doesn't exist |
-ERR login failure |
Authentication | Invalid credentials |
Protocol States
State | Description | Available Commands |
---|---|---|
Authorization | Initial state after connection | USER , PASS , APOP , QUIT |
Transaction | After successful authentication | STAT , LIST , RETR , DELE , RSET , QUIT |
Update | During connection closure | Automatic deletion processing |
Performance Considerations
Operation | Typical Speed | Memory Usage | Network I/O |
---|---|---|---|
Connection establishment | 1-3 seconds | Low | 2-3 round trips |
Authentication | < 1 second | Low | 1-2 round trips |
Message enumeration | < 1 second | Low | 1 round trip |
Header retrieval | < 1 second per message | Medium | 1 round trip per message |
Full message retrieval | Variable by size | High | 1 round trip per message |
Message deletion | < 1 second | Low | 1 round trip per message |
Security Considerations
Aspect | Standard POP3 | POP3S | Recommendations |
---|---|---|---|
Authentication | Plain text | Encrypted | Use POP3S or APOP |
Data transmission | Plain text | Encrypted | Always use POP3S in production |
Password storage | Server responsibility | Server responsibility | Use application passwords when available |
Connection security | None | TLS/SSL | Verify certificates in production |