Source: https://cli.nylas.com/guides/send-email-ruby

# 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](https://cli.nylas.com/authors/caleb-geene) Director, Site Reliability Engineering

Reviewed by [Qasim Muhammad](https://cli.nylas.com/authors/qasim-muhammad)

Updated June 9, 2026

> **TL;DR:** Net::SMTP sends a message with zero gems if you have an app password. The mail gem (and ActionMailer in Rails) adds a clean DSL plus attachments and MIME handling. A subprocess call via `Open3.capture3` sends through OAuth with no SMTP credentials at all — the comparison table below shows when each one wins.

> **Disclosure:** Nylas CLI is built by Nylas, Inc. This comparison reflects our testing and product understanding as of June 9, 2026.

Each CLI command used here has a reference page: [`nylas email send`](https://cli.nylas.com/docs/commands/email-send) for the Open3 subprocess pattern, [`nylas auth login`](https://cli.nylas.com/docs/commands/auth-login) for OAuth authentication, and [`nylas auth config`](https://cli.nylas.com/docs/commands/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](https://rubygems.org/gems/mail) 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](https://github.com/ruby/net-smtp) repository. It implements the protocol defined in [RFC 5321](https://datatracker.ietf.org/doc/html/rfc5321), 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](https://support.google.com/accounts/answer/185833), 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.

```ruby
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")
end
```

The 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`.

```ruby
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](https://guides.rubyonrails.org/action_mailer_basics.html) 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](https://cli.nylas.com/guides/getting-started)), 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.

```ruby
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_later` queuing 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](https://cli.nylas.com/guides/send-email-nodejs) runs the same comparison with Nodemailer in the SMTP seat, and the verdicts land in the same places.

## Next steps

- [Send email from Python](https://cli.nylas.com/guides/send-email-python) — the same 3-method comparison with smtplib
- [Send email from Node.js](https://cli.nylas.com/guides/send-email-nodejs) — Nodemailer, Gmail API, and subprocess compared
- [Send email from terminal](https://cli.nylas.com/guides/send-email-from-terminal) — the full `nylas email send` flag reference in practice
- [Twilio SendGrid vs Nylas](https://cli.nylas.com/guides/twilio-vs-nylas) — transactional sending platforms compared
- [EmailEngine vs Nylas](https://cli.nylas.com/guides/emailengine-vs-nylas) — self-hosted email API against a managed one
- [Full command reference](https://cli.nylas.com/docs/commands) — every flag and subcommand documented
