# 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

```go
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

```go
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](https://support.google.com/accounts/answer/185833) (2FA must be enabled):

```go
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):**
```go
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

```go
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

```go
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

```go
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.)

```go
config := &email.Config{
    SMTPHost:     "mail.yourdomain.com",
    SMTPPort:     587,
    SMTPUsername: "username",
    SMTPPassword: "password",
    FromEmail:    "noreply@yourdomain.com",
    FromName:     "Memos",
    UseTLS:       true,
}
```

## HTML Emails

```go
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

```go
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

```bash
# 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:

```go
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:

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

// SSL/TLS (port 465)
config.UseSSL = true
```

### 2. Secure Credential Storage

Never hardcode credentials. Use environment variables:

```go
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:

```go
// Validate before sending
if err := message.Validate(); err != nil {
    return err
}
```

### 5. Implement Rate Limiting

Prevent abuse by limiting email sending:

```go
// 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:

```go
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:
```go
config := &email.Config{
    SMTPPort: 587,
    UseTLS:   true,
}
```

**Port 465 (SSL/TLS)** is the alternative:
```go
config := &email.Config{
    SMTPPort: 465,
    UseSSL:   true,
}
```

## Error Handling

The package provides detailed, contextual errors:

```go
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:

```go
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`
```go
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`
```go
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:

- Memos GitHub Issues: https://github.com/usememos/memos/issues
- Memos Documentation: https://usememos.com/docs

## Roadmap

Future enhancements may include:

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