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

# Send Email in Go: net/smtp, APIs, and CLI

Go ships an SMTP package in the standard library, but the Go team froze it years ago. This guide gives you runnable code for four ways to send email from Go: net/smtp, go-mail, the Gmail REST API, and a CLI subprocess via os/exec.

Written by [Prem Keshari](https://cli.nylas.com/authors/prem-keshari) Senior SRE

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

Updated June 9, 2026

> **TL;DR:** To send email golang-style you have four options: the frozen `net/smtp` stdlib package, the maintained go-mail library, a raw Gmail REST call with `net/http`, or a CLI subprocess via `os/exec`. One of the four needs no email library, no app password, and covers 6 providers — the comparison table below shows where it wins and where it loses.

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

Command docs for the examples below: [`nylas email send`](https://cli.nylas.com/docs/commands/email-send) for the os/exec subprocess pattern, [`nylas auth login`](https://cli.nylas.com/docs/commands/auth-login) for OAuth sign-in, and [`nylas auth config`](https://cli.nylas.com/docs/commands/auth-config) for headless API-key auth.

## Why is Go's net/smtp package frozen?

Go's `net/smtp` package implements RFC 5321 SMTP and has shipped with every release since Go 1.0 in March 2012, but the official package documentation states: “The smtp package is frozen and is not accepting new features.” It still works for basic sends, yet it will never gain modern auth mechanisms or context support.

Frozen means bug fixes only. The [net/smtp docs](https://pkg.go.dev/net/smtp) point developers to external packages for anything beyond PLAIN and CRAM-MD5 authentication. The code below sends a plaintext message through Gmail's SMTP server on port 587 with STARTTLS. It needs a 16-character Google app password: consumer Gmail accounts lost plain-password SMTP when Google shut off Less Secure Apps in May 2022, and Workspace tenants followed when the phase-out completed on May 1, 2025.

```go
package main

import (
	"fmt"
	"net/smtp"
)

func main() {
	auth := smtp.PlainAuth("", "you@gmail.com",
		"your-16-char-app-password", "smtp.gmail.com")

	msg := []byte("To: recipient@example.com
" +
		"Subject: Quarterly report
" +
		"
" +
		"See the attached spreadsheet.
")

	err := smtp.SendMail("smtp.gmail.com:587", auth,
		"you@gmail.com", []string{"recipient@example.com"}, msg)
	if err != nil {
		fmt.Println("send failed:", err)
		return
	}
	fmt.Println("sent")
}
```

Notice what's missing: no HTML body support, no attachments, no timeouts, no connection reuse. You build the raw [RFC 5321](https://datatracker.ietf.org/doc/html/rfc5321) message by hand, including the `\r\n` line endings. For a one-off alert in a 20-line program that's acceptable. For anything more, the Go team itself recommends moving on.

## How do I send email in Go with go-mail?

The [wneessen/go-mail](https://github.com/wneessen/go-mail) library is the actively maintained replacement for `net/smtp`. It's MIT-licensed, relies mainly on the Go standard library, and supports 8 SMTP auth mechanisms including SCRAM-SHA-256 and XOAUTH2, plus attachments, HTML bodies, and context-based timeouts.

Install it with `go get github.com/wneessen/go-mail`. The example below builds a message, dials Gmail on port 587, and sends with PLAIN auth. Unlike the stdlib, go-mail validates addresses against RFC 5322, reuses one SMTP connection across multiple sends, and holds an OpenSSF Best Practices badge for its security posture.

```go
package main

import (
	"fmt"
	"log"

	"github.com/wneessen/go-mail"
)

func main() {
	m := mail.NewMsg()
	if err := m.From("you@gmail.com"); err != nil {
		log.Fatal(err)
	}
	if err := m.To("recipient@example.com"); err != nil {
		log.Fatal(err)
	}
	m.Subject("Quarterly report")
	m.SetBodyString(mail.TypeTextPlain, "See the attached spreadsheet.")

	c, err := mail.NewClient("smtp.gmail.com",
		mail.WithPort(587),
		mail.WithSMTPAuth(mail.SMTPAuthPlain),
		mail.WithUsername("you@gmail.com"),
		mail.WithPassword("your-16-char-app-password"),
	)
	if err != nil {
		log.Fatal(err)
	}
	if err := c.DialAndSend(m); err != nil {
		log.Fatal(err)
	}
	fmt.Println("sent")
}
```

The trade-off is the same as any SMTP client: credentials are provider-specific. Moving from Gmail to Outlook means a new host, and the app password still caps personal Gmail at 500 messages per day (2,000 on Workspace). go-mail solves the library problem, not the authentication problem.

## How do I send email from Go with a provider REST API?

Provider REST APIs replace SMTP entirely: Gmail exposes a `messages.send` endpoint and Microsoft Graph exposes `sendMail`, both over HTTPS with OAuth 2.0 bearer tokens. In Go you can call them with `net/http` alone — no SDK required. Gmail charges 100 quota units per send against a per-project daily budget.

The [users.messages.send reference](https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.messages/send) requires the raw RFC 2822 message base64url-encoded without padding. The example below does that with `base64.URLEncoding.WithPadding(base64.NoPadding)` and POSTs it with an access token from the environment. Tokens expire after 3,600 seconds, so production code needs a refresh step this snippet omits.

```go
package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

func main() {
	raw := "To: recipient@example.com
" +
		"Subject: Quarterly report
" +
		"
" +
		"See the attached spreadsheet."

	encoded := base64.URLEncoding.
		WithPadding(base64.NoPadding).
		EncodeToString([]byte(raw))

	body, _ := json.Marshal(map[string]string{"raw": encoded})
	req, _ := http.NewRequest("POST",
		"https://gmail.googleapis.com/gmail/v1/users/me/messages/send",
		bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("GMAIL_ACCESS_TOKEN"))
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		fmt.Println("request failed:", err)
		return
	}
	defer resp.Body.Close()
	fmt.Println("status:", resp.Status)
}
```

The hidden cost is everything around this snippet: a Google Cloud project, an OAuth consent screen, scope approval for `gmail.send`, and token refresh logic. Budget 15+ minutes for setup. Sending from Outlook instead means rewriting against [Microsoft Graph sendMail](https://learn.microsoft.com/en-us/graph/api/user-sendmail), which uses a different payload shape and a different OAuth tenant model.

## How do I send email from Go with a CLI subprocess?

A subprocess call from `os/exec` delegates SMTP, OAuth token refresh, and MIME encoding to the Nylas CLI binary, which authenticates Gmail, Outlook, Exchange, Yahoo, iCloud, and IMAP accounts from one command. Your Go program imports nothing beyond the standard library and parses JSON from stdout. Setup takes about 2 minutes.

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)), then run `nylas auth login` once. The Go docs describe [os/exec](https://pkg.go.dev/os/exec) as running commands without invoking a shell, which sidesteps injection from untrusted input. The `--yes` flag skips the interactive confirmation, and `--json` returns structured output.

```go
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os/exec"
)

type sendResult struct {
	ID string `json:"id"`
}

func sendEmail(to, subject, body string) (sendResult, error) {
	var res sendResult
	out, err := exec.Command("nylas", "email", "send",
		"--to", to,
		"--subject", subject,
		"--body", body,
		"--yes",
		"--json",
	).Output()
	if err != nil {
		return res, err
	}
	err = json.Unmarshal(out, &res)
	return res, err
}

func main() {
	res, err := sendEmail("recipient@example.com",
		"Quarterly report", "See the attached spreadsheet.")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("sent:", res.ID)
}
```

There's a pleasant symmetry here: the CLI is itself a static Go binary, so your Go program shells out to more Go. Each call spawns a process, adding roughly 100ms of overhead versus an in-process SMTP connection. For cron jobs, CI alerts, and scripts sending dozens of messages, that overhead is invisible; for batches over 1,000, keep a pooled SMTP client.

## Which Go email method should you choose?

Choose by maintenance status, provider count, and setup time. `net/smtp` is frozen but dependency-free. go-mail is the maintained library for SMTP workloads. The Gmail REST API gives scoped OAuth for Gmail-only apps. The CLI subprocess covers 6 providers with zero Go dependencies. The table compares all four across 6 dimensions.

| Dimension | net/smtp | go-mail | Gmail REST API | CLI subprocess |
| --- | --- | --- | --- | --- |
| Maintenance | Frozen (bug fixes only) | Active, MIT license | Active (Google) | Active |
| Setup time | ~5 minutes | ~5 minutes | ~15 minutes | ~2 minutes |
| Auth method | App password (PLAIN) | App password or XOAUTH2 | OAuth 2.0 bearer token | OAuth 2.0 (browser flow) |
| Providers | Any SMTP server | Any SMTP server | Gmail only | Gmail, Outlook, Exchange, Yahoo, iCloud, IMAP |
| Dependencies | None (stdlib) | 1 module | None (stdlib) + token plumbing | None (stdlib + binary) |
| HTML & attachments | Manual MIME by hand | Built in | Manual MIME by hand | Built in (flags) |

- **Throwaway alert in an existing binary:** `net/smtp`. Zero dependencies, ~20 lines, and frozen doesn't mean broken.
- **Production SMTP sending:** go-mail. Connection reuse, context timeouts, and 8 auth mechanisms the stdlib will never get.
- **Gmail-only app needing scoped permissions:** the REST API. Grant `gmail.send` without exposing the inbox.
- **Multi-provider scripts and cron jobs:** CLI subprocess. Switching a user from Gmail to Outlook changes zero lines of Go.

The same four-way trade-off appears in every language: compare the [Node.js version](https://cli.nylas.com/guides/send-email-nodejs), where Nodemailer plays the role go-mail plays here, or the [Python version](https://cli.nylas.com/guides/send-email-python) built around `smtplib`.

## Next steps

- [Send Email in Java: Jakarta Mail and APIs](https://cli.nylas.com/guides/send-email-java) — Four ways to send email in Java
- [Send Email in Rust: lettre and Email APIs](https://cli.nylas.com/guides/send-email-rust) — Three ways to send email in Rust
- [Send email from Node.js](https://cli.nylas.com/guides/send-email-nodejs) — the same comparison for JavaScript with Nodemailer
- [Send email from Python](https://cli.nylas.com/guides/send-email-python) — smtplib, Gmail API, and subprocess patterns
- [Send email from terminal](https://cli.nylas.com/guides/send-email-from-terminal) — the full command walkthrough behind the subprocess pattern
- [Twilio vs Nylas](https://cli.nylas.com/guides/twilio-vs-nylas) — how communication API platforms compare for email sending
- [EmailEngine vs Nylas](https://cli.nylas.com/guides/emailengine-vs-nylas) — self-hosted email API versus a hosted one
- [Full command reference](https://cli.nylas.com/docs/commands) — every flag and subcommand documented
- [net/smtp package docs](https://pkg.go.dev/net/smtp) — the official freeze notice and API reference
- [wneessen/go-mail on GitHub](https://github.com/wneessen/go-mail) — feature list and SMTP auth support matrix
- [RFC 5321](https://datatracker.ietf.org/doc/html/rfc5321) — the SMTP specification both stdlib and go-mail implement
