email

package
v0.27.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 19, 2026 License: MIT Imports: 8 Imported by: 0

README

Email Plugin

SMTP email sending functionality for self-hosted Memos instances.

Overview

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.

Features

  • Standard SMTP protocol support
  • TLS/STARTTLS and SSL/TLS encryption
  • HTML and plain text emails
  • Multiple recipients (To, Cc, Bcc)
  • Synchronous and asynchronous sending
  • Detailed error reporting with context
  • Works with all major email providers
  • Reply-To header support
  • RFC 5322 compliant message formatting

Quick Start

1. Configure SMTP Settings
import "github.com/usememos/memos/internal/email"

config := &email.Config{
    SMTPHost:     "smtp.gmail.com",
    SMTPPort:     587,
    SMTPUsername: "your-email@gmail.com",
    SMTPPassword: "your-app-password",
    FromEmail:    "noreply@yourdomain.com",
    FromName:     "Memos",
    UseTLS:       true,
}
2. Create and Send Email
message := &email.Message{
    To:      []string{"user@example.com"},
    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)

Provider Configuration

Gmail

Requires an App Password (2FA must be enabled):

config := &email.Config{
    SMTPHost:     "smtp.gmail.com",
    SMTPPort:     587,
    SMTPUsername: "your-email@gmail.com",
    SMTPPassword: "your-16-char-app-password",
    FromEmail:    "your-email@gmail.com",
    FromName:     "Memos",
    UseTLS:       true,
}

Alternative (SSL):

config := &email.Config{
    SMTPHost:     "smtp.gmail.com",
    SMTPPort:     465,
    SMTPUsername: "your-email@gmail.com",
    SMTPPassword: "your-16-char-app-password",
    FromEmail:    "your-email@gmail.com",
    FromName:     "Memos",
    UseSSL:       true,
}
SendGrid
config := &email.Config{
    SMTPHost:     "smtp.sendgrid.net",
    SMTPPort:     587,
    SMTPUsername: "apikey",
    SMTPPassword: "your-sendgrid-api-key",
    FromEmail:    "noreply@yourdomain.com",
    FromName:     "Memos",
    UseTLS:       true,
}
AWS SES
config := &email.Config{
    SMTPHost:     "email-smtp.us-east-1.amazonaws.com",
    SMTPPort:     587,
    SMTPUsername: "your-smtp-username",
    SMTPPassword: "your-smtp-password",
    FromEmail:    "verified@yourdomain.com",
    FromName:     "Memos",
    UseTLS:       true,
}

Note: Replace us-east-1 with your AWS region. Email must be verified in SES.

Mailgun
config := &email.Config{
    SMTPHost:     "smtp.mailgun.org",
    SMTPPort:     587,
    SMTPUsername: "postmaster@yourdomain.com",
    SMTPPassword: "your-mailgun-smtp-password",
    FromEmail:    "noreply@yourdomain.com",
    FromName:     "Memos",
    UseTLS:       true,
}
Self-Hosted SMTP (Postfix, Exim, etc.)
config := &email.Config{
    SMTPHost:     "mail.yourdomain.com",
    SMTPPort:     587,
    SMTPUsername: "username",
    SMTPPassword: "password",
    FromEmail:    "noreply@yourdomain.com",
    FromName:     "Memos",
    UseTLS:       true,
}

HTML Emails

message := &email.Message{
    To:      []string{"user@example.com"},
    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)

Multiple Recipients

message := &email.Message{
    To:      []string{"user1@example.com", "user2@example.com"},
    Cc:      []string{"manager@example.com"},
    Bcc:     []string{"admin@example.com"},
    Subject: "Team Update",
    Body:    "Important team announcement...",
    ReplyTo: "support@yourdomain.com",
}

email.Send(config, message)

Testing

Run Tests
# All tests
go test ./internal/email/... -v

# With coverage
go test ./internal/email/... -v -cover

# With race detector
go test ./internal/email/... -race
Manual Testing

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: "your-email@gmail.com",
        SMTPPassword: "your-app-password",
        FromEmail:    "your-email@gmail.com",
        FromName:     "Test",
        UseTLS:       true,
    }

    message := &email.Message{
        To:      []string{"recipient@example.com"},
        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!")
}

Security Best Practices

1. Use TLS/SSL Encryption

Always enable encryption in production:

// STARTTLS (port 587) - Recommended
config.UseTLS = true

// SSL/TLS (port 465)
config.UseSSL = true
2. Secure Credential Storage

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,
}
3. Use App-Specific Passwords

For Gmail and similar services, use app passwords instead of your main account password.

4. Validate and Sanitize Input

Always validate email addresses and sanitize content:

// Validate before sending
if err := message.Validate(); err != nil {
    return err
}
5. Implement Rate Limiting

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")
}
6. Monitor and Log

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))
}

Common Ports

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,
}

Error Handling

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)
    }
}
Common Error Messages
❌ "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
Async Error Handling

For async sending, errors are logged automatically:

email.SendAsync(config, message)
// Errors logged as:
// [WARN] Failed to send email asynchronously recipients=user@example.com error=...

Dependencies

Required
  • Go 1.25+
  • Standard library: net/smtp, crypto/tls
  • github.com/pkg/errors - Error wrapping with context
No External SMTP Libraries

This plugin uses Go's standard net/smtp library for maximum compatibility and minimal dependencies.

API Reference

Types
Config
type 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)
}
Message
type 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)
}
Functions
Send(config *Config, message *Message) error

Sends 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) *Client

Creates a new SMTP client for advanced usage.

Client.Send(message *Message) error

Sends email using the client's configuration.

Architecture

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

License

Part of the Memos project. See main repository for license details.

Contributing

This package follows the Memos contribution guidelines. Please ensure:

  1. All code is tested (TDD approach)
  2. Tests pass: go test ./internal/email/... -v
  3. Code is formatted: go fmt ./internal/email/...
  4. No linting errors: golangci-lint run ./internal/email/...

Support

For issues and questions:

Roadmap

Future enhancements may include:

  • Email template system
  • Attachment support
  • Inline image embedding
  • Email queuing system
  • Delivery status tracking
  • Bounce handling

Documentation

Overview

Package email provides SMTP email sending functionality for self-hosted Memos instances.

This package is designed for self-hosted environments where instance administrators configure their own SMTP servers. It follows industry-standard patterns used by platforms like GitHub, GitLab, and Discourse.

Configuration

The package requires SMTP server configuration provided by the instance administrator:

config := &email.Config{
    SMTPHost:     "smtp.gmail.com",
    SMTPPort:     587,
    SMTPUsername: "your-email@gmail.com",
    SMTPPassword: "your-app-password",
    FromEmail:    "noreply@yourdomain.com",
    FromName:     "Memos Notifications",
    UseTLS:       true,
}

Common SMTP Settings

Gmail (requires App Password):

  • Host: smtp.gmail.com
  • Port: 587 (TLS) or 465 (SSL)
  • Username: your-email@gmail.com
  • UseTLS: true (for port 587) or UseSSL: true (for port 465)

SendGrid:

  • Host: smtp.sendgrid.net
  • Port: 587
  • Username: apikey
  • Password: your-sendgrid-api-key
  • UseTLS: true

AWS SES:

  • Host: email-smtp.[region].amazonaws.com
  • Port: 587
  • Username: your-smtp-username
  • Password: your-smtp-password
  • UseTLS: true

Mailgun:

  • Host: smtp.mailgun.org
  • Port: 587
  • Username: your-mailgun-smtp-username
  • Password: your-mailgun-smtp-password
  • UseTLS: true

Sending Email

Synchronous (waits for completion):

message := &email.Message{
    To:      []string{"user@example.com"},
    Subject: "Welcome to Memos",
    Body:    "Thank you for joining!",
    IsHTML:  false,
}

err := email.Send(config, message)
if err != nil {
    // Handle error
}

Asynchronous (returns immediately):

email.SendAsync(config, message)
// Errors are logged but not returned

HTML Email

message := &email.Message{
    To:      []string{"user@example.com"},
    Subject: "Welcome!",
    Body:    "<html><body><h1>Welcome to Memos!</h1></body></html>",
    IsHTML:  true,
}

Security Considerations

  • Always use TLS (port 587) or SSL (port 465) for production
  • Store SMTP credentials securely (environment variables or secrets management)
  • Use app-specific passwords for services like Gmail
  • Validate and sanitize email content to prevent injection attacks
  • Rate limit email sending to prevent abuse

Error Handling

The package returns descriptive errors for common issues:

  • Configuration validation errors (missing host, invalid port, etc.)
  • Message validation errors (missing recipients, subject, or body)
  • Connection errors (cannot reach SMTP server)
  • Authentication errors (invalid credentials)
  • SMTP protocol errors (recipient rejected, etc.)

All errors are wrapped with context using github.com/pkg/errors for better debugging.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Send

func Send(config *Config, message *Message) error

Send sends an email synchronously. Returns an error if the email fails to send.

func SendAsync

func SendAsync(config *Config, message *Message)

SendAsync sends an email asynchronously. It enqueues the message for bounded asynchronous sending and does not wait for the response. Any errors are logged but not returned.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client represents an SMTP email client.

func NewClient

func NewClient(config *Config) *Client

NewClient creates a new email client with the given configuration.

func (*Client) Send

func (c *Client) Send(message *Message) error

Send sends an email message via SMTP.

type Config

type Config struct {
	// SMTPHost is the SMTP server hostname (e.g., "smtp.gmail.com")
	SMTPHost string
	// SMTPPort is the SMTP server port (common: 587 for TLS, 465 for SSL, 25 for unencrypted)
	SMTPPort int
	// SMTPUsername is the SMTP authentication username (usually the email address)
	SMTPUsername string
	// SMTPPassword is the SMTP authentication password or app-specific password
	SMTPPassword string
	// FromEmail is the email address that will appear in the "From" field
	FromEmail string
	// FromName is the display name that will appear in the "From" field
	FromName string
	// UseTLS enables STARTTLS encryption (recommended for port 587)
	UseTLS bool
	// UseSSL enables SSL/TLS encryption (for port 465)
	UseSSL bool
}

Config represents the SMTP configuration for email sending. These settings should be provided by the self-hosted instance administrator.

func (*Config) GetServerAddress

func (c *Config) GetServerAddress() string

GetServerAddress returns the SMTP server address in the format "host:port".

func (*Config) Validate

func (c *Config) Validate() error

Validate checks if the configuration is valid.

type Message

type Message struct {
	To      []string // Required: recipient email addresses
	Cc      []string // Optional: carbon copy recipients
	Bcc     []string // Optional: blind carbon copy recipients
	Subject string   // Required: email subject
	Body    string   // Required: email body content
	IsHTML  bool     // Whether the body is HTML (default: false for plain text)
	ReplyTo string   // Optional: reply-to address
}

Message represents an email message to be sent.

func (*Message) Format

func (m *Message) Format(fromEmail, fromName string) string

Format creates an RFC 5322 formatted email message.

func (*Message) GetAllRecipients

func (m *Message) GetAllRecipients() []string

GetAllRecipients returns all recipients (To, Cc, Bcc) as a single slice.

func (*Message) Validate

func (m *Message) Validate() error

Validate checks that the message has all required fields.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL