email

package
v0.91.0 Latest Latest
Warning

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

Go to latest
Published: Feb 3, 2026 License: MIT Imports: 8 Imported by: 0

README

Email Plugin

SMTP email sending functionality for self-hosted DivineSense 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/hrygo/divinesense/plugin/email"

config := &email.Config{
    SMTPHost:     "smtp.gmail.com",
    SMTPPort:     587,
    SMTPUsername: "your-email@gmail.com",
    SMTPPassword: "your-app-password",
    FromEmail:    "noreply@yourdomain.com",
    FromName:     "DivineSense",
    UseTLS:       true,
}
2. Create and Send Email
message := &email.Message{
    To:      []string{"user@example.com"},
    Subject: "Welcome to DivineSense!",
    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:     "DivineSense",
    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:     "DivineSense",
    UseSSL:       true,
}
SendGrid
config := &email.Config{
    SMTPHost:     "smtp.sendgrid.net",
    SMTPPort:     587,
    SMTPUsername: "apikey",
    SMTPPassword: "your-sendgrid-api-key",
    FromEmail:    "noreply@yourdomain.com",
    FromName:     "DivineSense",
    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:     "DivineSense",
    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:     "DivineSense",
    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:     "DivineSense",
    UseTLS:       true,
}

HTML Emails

message := &email.Message{
    To:      []string{"user@example.com"},
    Subject: "Welcome to DivineSense!",
    Body: `
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
</head>
<body style="font-family: Arial, sans-serif;">
    <h1 style="color: #333;">Welcome to DivineSense!</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 ./plugin/email/... -v

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

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

Create a simple test program:

package main

import (
    "log"
    "github.com/hrygo/divinesense/plugin/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 DivineSense 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

plugin/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 DivineSense project. See main repository for license details.

Contributing

This plugin follows the DivineSense contribution guidelines. Please ensure:

  1. All code is tested (TDD approach)
  2. Tests pass: go test ./plugin/email/... -v
  3. Code is formatted: go fmt ./plugin/email/...
  4. No linting errors: golangci-lint run ./plugin/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 DivineSense 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:     "DivineSense 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 DivineSense",
    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 DivineSense!</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 spawns a new goroutine to handle the 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     string
	SMTPUsername string
	SMTPPassword string
	FromEmail    string
	FromName     string
	SMTPPort     int
	UseTLS       bool
	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 {
	Subject string
	Body    string
	ReplyTo string
	To      []string
	Cc      []string
	Bcc     []string
	IsHTML  bool
}

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