internal/email/README.md
SMTP email sending functionality for self-hosted Memos instances.
This plugin provides a simple, reliable email sending interface following industry-standard SMTP protocols. It's designed for self-hosted environments where instance administrators configure their own email service, similar to platforms like GitHub, GitLab, and Discourse.
import "github.com/usememos/memos/internal/email"
config := &email.Config{
SMTPHost: "smtp.gmail.com",
SMTPPort: 587,
SMTPUsername: "[email protected]",
SMTPPassword: "your-app-password",
FromEmail: "[email protected]",
FromName: "Memos",
UseTLS: true,
}
message := &email.Message{
To: []string{"[email protected]"},
Subject: "Welcome to Memos!",
Body: "Thanks for signing up.",
IsHTML: false,
}
// Synchronous send (waits for result)
err := email.Send(config, message)
if err != nil {
log.Printf("Failed to send email: %v", err)
}
// Asynchronous send (returns immediately)
email.SendAsync(config, message)
Requires an App Password (2FA must be enabled):
config := &email.Config{
SMTPHost: "smtp.gmail.com",
SMTPPort: 587,
SMTPUsername: "[email protected]",
SMTPPassword: "your-16-char-app-password",
FromEmail: "[email protected]",
FromName: "Memos",
UseTLS: true,
}
Alternative (SSL):
config := &email.Config{
SMTPHost: "smtp.gmail.com",
SMTPPort: 465,
SMTPUsername: "[email protected]",
SMTPPassword: "your-16-char-app-password",
FromEmail: "[email protected]",
FromName: "Memos",
UseSSL: true,
}
config := &email.Config{
SMTPHost: "smtp.sendgrid.net",
SMTPPort: 587,
SMTPUsername: "apikey",
SMTPPassword: "your-sendgrid-api-key",
FromEmail: "[email protected]",
FromName: "Memos",
UseTLS: true,
}
config := &email.Config{
SMTPHost: "email-smtp.us-east-1.amazonaws.com",
SMTPPort: 587,
SMTPUsername: "your-smtp-username",
SMTPPassword: "your-smtp-password",
FromEmail: "[email protected]",
FromName: "Memos",
UseTLS: true,
}
Note: Replace us-east-1 with your AWS region. Email must be verified in SES.
config := &email.Config{
SMTPHost: "smtp.mailgun.org",
SMTPPort: 587,
SMTPUsername: "[email protected]",
SMTPPassword: "your-mailgun-smtp-password",
FromEmail: "[email protected]",
FromName: "Memos",
UseTLS: true,
}
config := &email.Config{
SMTPHost: "mail.yourdomain.com",
SMTPPort: 587,
SMTPUsername: "username",
SMTPPassword: "password",
FromEmail: "[email protected]",
FromName: "Memos",
UseTLS: true,
}
message := &email.Message{
To: []string{"[email protected]"},
Subject: "Welcome to Memos!",
Body: `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body style="font-family: Arial, sans-serif;">
<h1 style="color: #333;">Welcome to Memos!</h1>
<p>We're excited to have you on board.</p>
<a href="https://yourdomain.com" style="background-color: #4CAF50; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Get Started</a>
</body>
</html>
`,
IsHTML: true,
}
email.Send(config, message)
message := &email.Message{
To: []string{"[email protected]", "[email protected]"},
Cc: []string{"[email protected]"},
Bcc: []string{"[email protected]"},
Subject: "Team Update",
Body: "Important team announcement...",
ReplyTo: "[email protected]",
}
email.Send(config, message)
# All tests
go test ./internal/email/... -v
# With coverage
go test ./internal/email/... -v -cover
# With race detector
go test ./internal/email/... -race
Create a simple test program:
package main
import (
"log"
"github.com/usememos/memos/internal/email"
)
func main() {
config := &email.Config{
SMTPHost: "smtp.gmail.com",
SMTPPort: 587,
SMTPUsername: "[email protected]",
SMTPPassword: "your-app-password",
FromEmail: "[email protected]",
FromName: "Test",
UseTLS: true,
}
message := &email.Message{
To: []string{"[email protected]"},
Subject: "Test Email",
Body: "This is a test email from Memos email plugin.",
}
if err := email.Send(config, message); err != nil {
log.Fatalf("Failed to send email: %v", err)
}
log.Println("Email sent successfully!")
}
Always enable encryption in production:
// STARTTLS (port 587) - Recommended
config.UseTLS = true
// SSL/TLS (port 465)
config.UseSSL = true
Never hardcode credentials. Use environment variables:
import "os"
config := &email.Config{
SMTPHost: os.Getenv("SMTP_HOST"),
SMTPPort: 587,
SMTPUsername: os.Getenv("SMTP_USERNAME"),
SMTPPassword: os.Getenv("SMTP_PASSWORD"),
FromEmail: os.Getenv("SMTP_FROM_EMAIL"),
UseTLS: true,
}
For Gmail and similar services, use app passwords instead of your main account password.
Always validate email addresses and sanitize content:
// Validate before sending
if err := message.Validate(); err != nil {
return err
}
Prevent abuse by limiting email sending:
// Example using golang.org/x/time/rate
limiter := rate.NewLimiter(rate.Every(time.Second), 10) // 10 emails per second
if !limiter.Allow() {
return errors.New("rate limit exceeded")
}
Log email sending activity for security monitoring:
if err := email.Send(config, message); err != nil {
slog.Error("Email send failed",
slog.String("recipient", message.To[0]),
slog.Any("error", err))
}
| Port | Protocol | Security | Use Case |
|---|---|---|---|
| 587 | SMTP + STARTTLS | Encrypted | Recommended for most providers |
| 465 | SMTP over SSL/TLS | Encrypted | Alternative secure option |
| 25 | SMTP | Unencrypted | Legacy, often blocked by ISPs |
| 2525 | SMTP + STARTTLS | Encrypted | Alternative when 587 is blocked |
Port 587 (STARTTLS) is the recommended standard for modern SMTP:
config := &email.Config{
SMTPPort: 587,
UseTLS: true,
}
Port 465 (SSL/TLS) is the alternative:
config := &email.Config{
SMTPPort: 465,
UseSSL: true,
}
The package provides detailed, contextual errors:
err := email.Send(config, message)
if err != nil {
// Error messages include context:
switch {
case strings.Contains(err.Error(), "invalid email configuration"):
// Configuration error (missing host, invalid port, etc.)
log.Printf("Configuration error: %v", err)
case strings.Contains(err.Error(), "invalid email message"):
// Message validation error (missing recipients, subject, body)
log.Printf("Message error: %v", err)
case strings.Contains(err.Error(), "authentication failed"):
// SMTP authentication failed (wrong credentials)
log.Printf("Auth error: %v", err)
case strings.Contains(err.Error(), "failed to connect"):
// Network/connection error
log.Printf("Connection error: %v", err)
case strings.Contains(err.Error(), "recipient rejected"):
// SMTP server rejected recipient
log.Printf("Recipient error: %v", err)
default:
log.Printf("Unknown error: %v", err)
}
}
❌ "invalid email configuration: SMTP host is required"
→ Fix: Set config.SMTPHost
❌ "invalid email configuration: SMTP port must be between 1 and 65535"
→ Fix: Set valid config.SMTPPort (usually 587 or 465)
❌ "invalid email configuration: from email is required"
→ Fix: Set config.FromEmail
❌ "invalid email message: at least one recipient is required"
→ Fix: Set message.To with at least one email address
❌ "invalid email message: subject is required"
→ Fix: Set message.Subject
❌ "invalid email message: body is required"
→ Fix: Set message.Body
❌ "SMTP authentication failed"
→ Fix: Check credentials (username/password)
❌ "failed to connect to SMTP server"
→ Fix: Verify host/port, check firewall, ensure TLS/SSL settings match server
For async sending, errors are logged automatically:
email.SendAsync(config, message)
// Errors logged as:
// [WARN] Failed to send email asynchronously [email protected] error=...
net/smtp, crypto/tlsgithub.com/pkg/errors - Error wrapping with contextThis plugin uses Go's standard net/smtp library for maximum compatibility and minimal dependencies.
Configtype Config struct {
SMTPHost string // SMTP server hostname
SMTPPort int // SMTP server port
SMTPUsername string // SMTP auth username
SMTPPassword string // SMTP auth password
FromEmail string // From email address
FromName string // From display name (optional)
UseTLS bool // Enable STARTTLS (port 587)
UseSSL bool // Enable SSL/TLS (port 465)
}
Messagetype Message struct {
To []string // Recipients
Cc []string // CC recipients (optional)
Bcc []string // BCC recipients (optional)
Subject string // Email subject
Body string // Email body (plain text or HTML)
IsHTML bool // true for HTML, false for plain text
ReplyTo string // Reply-To address (optional)
}
Send(config *Config, message *Message) errorSends an email synchronously. Blocks until email is sent or error occurs.
SendAsync(config *Config, message *Message)Sends an email asynchronously in a goroutine. Returns immediately. Errors are logged.
NewClient(config *Config) *ClientCreates a new SMTP client for advanced usage.
Client.Send(message *Message) errorSends email using the client's configuration.
internal/email/
├── config.go # SMTP configuration types
├── message.go # Email message types and formatting
├── client.go # SMTP client implementation
├── email.go # High-level Send/SendAsync API
├── doc.go # Package documentation
└── *_test.go # Unit tests
Part of the Memos project. See main repository for license details.
This package follows the Memos contribution guidelines. Please ensure:
go test ./internal/email/... -vgo fmt ./internal/email/...golangci-lint run ./internal/email/...For issues and questions:
Future enhancements may include: