category_development

How to Receive Email with Ruby: A Practical Guide

Receiving email with Ruby is commonly done by connecting to a mailbox via IMAP and, when needed, parsing message content with a library such as mail. This evergreen guide explai...

Mara Ellison
How to Receive Email with Ruby: A Practical Guide

Receiving email with Ruby is commonly done by connecting to a mailbox via IMAP and, when needed, parsing message content with a library such as mail. This evergreen guide explains how to read and process email in Ruby, covering setup, authentication, searching, fetching headers and bodies, decoding encoded words, and handling attachments. You will find concise examples, common patterns, notes on security and deliverability, and practical advice to keep your inbox processing robust and maintainable over time.

How Ruby Email Receiving Works

At a high level, receiving email in Ruby involves two main approaches: using IMAP to access messages already delivered to a server, or using an SMTP-based approach to act as a mailbox receiver. IMAP is the typical choice for reading and managing existing email, while SMTP-based solutions are useful when you operate an endpoint that accepts mail directly. The main runtime libraries are net/imap from stdlib for IMAP operations, and the mail gem for higher-level parsing and message handling. Understanding how messages flow into your chosen storage (Maildir, mbox, database, or objects in memory) helps you design a durable architecture.

Key IMAP Concepts You Need to Know

IMAP is the backbone for most Ruby applications that read email from providers such as Gmail, Outlook, or self-hosted mail servers. IMAP keeps messages on the server, allowing multiple clients to access them, and provides search, sorting, and flagging capabilities. In Ruby, you use Net::IMAP to connect with TLS, authenticate, select a mailbox (commonly INBOX), and perform operations such as searching for messages or marking them as read. Understanding concepts like UIDVALIDITY, flags, and namespaces helps you write resilient code that handles server differences and recovers from interruptions.

Common IMAP States and Capabilities

  • Connection and TLS: Most providers require STARTTLS on a non-SSL port (e.g., 993) or SSL on an implicit SSL port (e.g., 993).
  • Authentication: LOGIN and PLAIN are widely supported; OAuth2 support depends on the server and the mail gem’s underlying mechanisms.
  • Mailbox selection: SELECT opens a read/write mailbox; EXAMINE opens read-only.
  • Message retrieval: Use UID FETCH to reliably access messages by unique ID across sessions.

Practical IMAP Setup in Ruby

To receive email with Ruby using IMAP, first ensure you can connect to the mail server and authenticate. The standard pattern involves creating a Net::IMAP connection, logging in, selecting a mailbox, searching for messages (for example, by date or from address), then fetching message attributes and body sections. You can fetch just headers to build an index, or fetch full bodies when you need to parse structure and content. Below is a concise example that connects, logs in, counts messages, retrieves the most recent message, and then logs out, demonstrating the essential flow.

Code Example: Basic IMAP Receive Flow

require 'net/imap'

imap = Net::IMAP.new('imap.example.com', 993, usessl=true)
imap.login('user@example.com', 'password')
imap.select('INBOX')

# Search for unseen messages from the last 7 days
one_week_ago = (Date.today - 7).strftime('%d-%b-%Y')
candidates = imap.search([ 'SINCE', one_week_ago, 'FROM', 'sender@example.com' ])
puts "Found #{candidates.size} messages"

# Fetch the most recent message's envelope and body structure
if candidates.any?
  latest = candidates.max
  msg = imap.fetch(latest, 'ENVELOPE BODY.PEEK[]').first
  puts "From: #{msg.attr.envelope.from}"
  puts "Subject: #{msg.attr.envelope.subject}"
end

imap.logout
imap.disconnect

Parsing Messages with the Mail Gem

Raw IMAP payloads are low-level; the mail gem turns them into structured Ruby objects you can easily navigate. After fetching message data via IMAP, you can create a Mail object from the raw string and inspect addresses, parts, headers, and attachments. The mail gem handles content transfer encoding, charset conversion, and MIME parsing so you can focus on extracting headers and body text. The following example fetches a full message via IMAP and parses it, showing how to read plain text and HTML parts, decode recipients, and extract attachment data.

Code Example: Receiving and Parsing with Mail

require 'net/imap'
require 'mail'

imap = Net::IMAP.new('imap.example.com', 993, usessl=true)
imap.login('user@example.com', 'password')
imap.select('INBOX')

raw = imap.fetch(imap.search(['UNSEEN']).first, 'BODY[]').first.attr['BODY[]']
message = Mail.read_from_string(raw)

puts "From: #{message.from}"
puts "To: #{message.to}"
puts "Subject: #{message.subject}"

if message.multipart?
  message.parts.each do |part|
    puts "Part Content-Type: #{part.content_type}"
    puts part.body.decoded if part.body
  end
else
  puts message.body.decoded if message.body
end

imap.logout
imap.disconnect

Handling Attachments and Decoding Messages

Many important emails include attachments, and correctly decoding encoding (quoted-printable, base64) is essential to preserve binary integrity. The mail gem abstracts content transfer encoding so you can work with decoded strings or binary IO. When you encounter multipart messages, iterate over parts and decide which to save or process. For attachments, you can stream decoded content to disk or load it into an ActiveStorage or CarrierWave pipeline. The table below summarizes typical attributes you can rely on across widely used mail-related Ruby libraries and common transports.

Attribute Verified Detail Source Type
Net::IMAP supported auth methods LOGIN, PLAIN, NTLM; OAuth2 possible via mechanisms when server and client support it Ruby stdlib + server docs
Default IMAPS port 993 (SSL/TLS) IANA service names + common practice
Supported message retrieval flags ENVELOPE, BODY, BODY.PEEK, UID IMAP RFC 3501 + net/imap docs
mail gem object model Mail::Message with parts, headers, attachments, decoded body mail gem documentation
Transfer encodings handled 7bit, 8bit, quoted-printable, base64, binary; charset conversion Ruby standard libraries + mail gem source

Error Handling, Retries, and Robustness

Network interruptions, rate limits, and mailbox state changes can break a receive loop. To make your receiver durable, implement reconnect logic, exponential backoff for authentication and command errors, and idempotent processing so that reprocessing a message does not cause duplicates. Use persistent state (a database, a cursor table, or message UIDs) to track which messages you have already handled. Consider processing messages in transactions when you update your application state: mark as read only after you have successfully saved or queued work. For robustness, monitor mailbox size and quota, and plan for edge cases like expunge and flag changes while your script is disconnected.

Deliverability, Security, and Best Practices

When you receive email, provider-side factors such as authentication, reputation, and rate limits affect delivery. Ensure the sending domain has SPF, DKIM, and DMARC records aligned with your infrastructure. On the receiving side, use TLS for all connections, store credentials in environment variables or a secrets manager, and avoid logging sensitive headers or message bodies. Respect mailbox quotas and implement polite polling intervals to avoid triggering provider blocks. If you act as an SMTP receiver, validate sender policies, apply rate limiting, and queue messages to protect downstream processing from bursts.

Design Choices: Maildir vs mbox vs Database

How you store received email influences reliability, concurrency, and operational simplicity. Storing messages as individual files in Maildir makes locking and archival straightforward and works well with tools that expect Maildir layouts. Using mbox simplifies deployment when you prefer a single file per mailbox, but concurrent access can be tricky. For multi-tenant or highly structured applications, persist parsed metadata in a database and keep raw or decoded payloads in object storage or files. The approach you choose should match your throughput, backup, and compliance requirements.

Common Use Cases and Next Steps

Typical Ruby email-receiving scenarios include notification processing, support ticket ingestion, incoming webhook alternatives, and monitoring delivery status. If you are building a consumer, start with a small script that fetches unseen messages, parses them with mail, and writes a record of processed UIDs. Then expand features: add parsing of headers (Message-ID, In-Reply-To), handle subsecond arrival times, and implement archiving. From here, operationalize with scheduling (cron or sidekiq), health checks, and alerting to keep your pipeline reliable over time.