Guide
Send Email in Ruby: Net::SMTP, Mail Gem, CLI
Ruby gives you three practical ways to send email: Net::SMTP from the standard library, the mail gem (which powers ActionMailer in Rails), and a CLI subprocess that handles OAuth and delivery in one shell command. This guide gives you runnable code for each and a table to pick the right one.
Written by Caleb Geene Director, Site Reliability Engineering
Reviewed by Qasim Muhammad
Each CLI command used here has a reference page: nylas email send for the Open3 subprocess pattern, nylas auth login for OAuth authentication, and nylas auth config for headless API-key configuration.
Why send email from Ruby?
To send email in Ruby you pick one of three layers: the net-smtp standard-library gem for raw protocol access, the mail gem for a full MIME toolkit, or an external CLI binary that owns authentication. Each layer trades convenience for control, and the right pick depends on whether you're in a Rails app or a 30-line script.
The decision got sharper in Ruby 3.1 (December 2021), when net-smtp moved from a default gem to a bundled gem — Bundler-managed projects now declare it in the Gemfile explicitly. Meanwhile the mail gem has passed 726 million downloads on RubyGems, making it one of the most-installed Ruby packages ever.
How do you send email with Net::SMTP?
Net::SMTP is Ruby's standard-library SMTP client, maintained in the ruby/net-smtp repository. It implements the protocol defined in RFC 5321, speaks STARTTLS on port 587, and needs zero third-party gems. You build the RFC 822 message string yourself, headers included.
The script below connects to Gmail's SMTP server on port 587, authenticates with a 16-character app password, and sends one plaintext message. Regular account passwords stopped working for personal Gmail SMTP on May 30, 2022, when Less Secure Apps went away; Google finished the Workspace phase-out on May 1, 2025. Sending limits: 500 messages a day on a personal account, 2,000 on Workspace.
require "net/smtp"
message = <<~MESSAGE
From: Your Name <you@gmail.com>
To: recipient@example.com
Subject: Quarterly report
See the attached spreadsheet.
MESSAGE
Net::SMTP.start("smtp.gmail.com", 587,
user: "you@gmail.com",
secret: "your-16-char-app-password",
authtype: :plain) do |smtp|
smtp.send_message(message, "you@gmail.com", "recipient@example.com")
endThe catch: you're hand-assembling headers. Get the blank line between headers and body wrong and the subject silently becomes body text. Attachments mean writing your own multipart MIME boundaries, which is exactly the work the mail gem exists to eliminate.
How do you send email with the mail gem and ActionMailer?
The mail gem is Ruby's standard MIME library: a DSL for building messages plus delivery over SMTP, sendmail, or a test transport. It sits underneath ActionMailer, so every Rails app since Rails 3 (2010) already ships it. Version 2.9.0 handles encoding, attachments, and multipart bodies that Net::SMTP leaves to you.
Install with gem install mail, configure a delivery method once, then build messages declaratively. The script below sends the same Gmail message as the Net::SMTP example but in a block DSL — swapping to Outlook means changing only the address value to smtp.office365.com.
require "mail"
Mail.defaults do
delivery_method :smtp, {
address: "smtp.gmail.com",
port: 587,
user_name: "you@gmail.com",
password: "your-16-char-app-password",
authentication: :plain,
enable_starttls_auto: true
}
end
mail = Mail.new do
from "you@gmail.com"
to "recipient@example.com"
subject "Quarterly report"
body "See the attached spreadsheet."
end
mail.deliver!In Rails, you don't call the gem directly: ActionMailer wraps it with mailer classes, views, and deliver_later queuing through ActiveJob. The Action Mailer Basics guide documents the same smtp_settings hash shown above. Either way you still manage SMTP credentials, and app passwords grant full mailbox access rather than scoped send permission.
How do you send email from Ruby with a CLI subprocess?
A subprocess call hands SMTP configuration, OAuth 2.0 token refresh, and MIME encoding to the Nylas CLI binary. Ruby's Open3.capture3 runs the command and captures stdout, stderr, and exit status separately. One authentication covers Gmail, Outlook, and 4 other providers, and your Ruby code imports nothing but the standard library.
Install the binary with brew install nylas/nylas-cli/nylas (other methods are in the getting started guide), authenticate once with nylas auth login, and the wrapper below sends in about 15 lines. The --json flag returns structured output and --yes skips the interactive confirmation, so it runs cleanly inside cron jobs and Sidekiq workers. On CI runners with no browser, nylas auth config --api-key authenticates headlessly instead.
require "open3"
require "json"
def send_email(to, subject, body)
stdout, stderr, status = Open3.capture3(
"nylas", "email", "send",
"--to", to,
"--subject", subject,
"--body", body,
"--json", "--yes"
)
raise "send failed: #{stderr}" unless status.success?
JSON.parse(stdout)
end
response = send_email(
"recipient@example.com",
"Quarterly report",
"See the attached spreadsheet."
)
puts "Sent: #{response["id"]}"Because Open3.capture3 passes arguments as an array, no shell interpolation happens — a subject containing quotes or semicolons can't inject commands. The trade-off is roughly 100ms of process-spawn overhead per send, which matters for bulk jobs but not for the password resets and report mails most scripts handle.
How do the three Ruby methods compare?
The table below compares the three approaches across 6 dimensions. Setup time assumes a Gmail account with 2-Step Verification already enabled. Lines of code counts the minimum to send one plaintext message, including requires and configuration. Provider coverage means support without changing libraries or hosts.
| Dimension | Net::SMTP | Mail gem / ActionMailer | CLI subprocess |
|---|---|---|---|
| Setup time | ~5 minutes | ~5 minutes | ~2 minutes |
| Auth method | App password / SMTP creds | App password / SMTP creds | OAuth 2.0 (browser flow) |
| Providers | Any SMTP server | Any SMTP server | Gmail, Outlook, Exchange, Yahoo, iCloud, IMAP |
| Lines of code | ~15 | ~22 | ~15 |
| Dependencies | net-smtp (bundled gem) | mail (1 gem) | None (stdlib Open3) |
| MIME / attachments | Manual boundaries | Built-in DSL | Handled by the binary |
- Dependency-free scripts: Net::SMTP. It ships with Ruby, talks to any server on port 587, and a single heredoc covers the whole message.
- Rails applications: ActionMailer on the mail gem. Mailer classes, ERB views, attachments, and
deliver_laterqueuing through ActiveJob come built in. - No app passwords allowed: CLI subprocess. The binary refreshes OAuth tokens (which expire every 3,600 seconds) on its own, so no SMTP credential ever touches your code or your environment variables.
- Multi-provider scripts: CLI subprocess. Switching a script from Gmail to Outlook needs zero code changes, where both SMTP approaches need new hosts and credentials.
Pick Net::SMTP for infrastructure you control, the mail gem inside Rails, and the subprocess when OAuth is non-negotiable. All three run fine under cron and CI. The Node.js guide runs the same comparison with Nodemailer in the SMTP seat, and the verdicts land in the same places.
Next steps
- Send email from Python — the same 3-method comparison with smtplib
- Send email from Node.js — Nodemailer, Gmail API, and subprocess compared
- Send email from terminal — the full
nylas email sendflag reference in practice - Twilio SendGrid vs Nylas — transactional sending platforms compared
- EmailEngine vs Nylas — self-hosted email API against a managed one
- Full command reference — every flag and subcommand documented