mailer

package module
v0.0.5 Latest Latest
Warning

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

Go to latest
Published: May 11, 2025 License: Apache-2.0 Imports: 9 Imported by: 0

README

mailer

Go Reference GitHub go.mod Go version Go Report Card

This package provides a robust and concurrent email sending service for Go applications. It allows queueing emails and sending them asynchronously using a pool of workers via a configurable backend (e.g., SMTP).

Features

  • Concurrent Sending: Uses a worker pool to send emails concurrently.
  • Buffering: Queues emails in a buffered channel, sized according to the worker count.
  • Graceful Shutdown: Supports context cancellation for stopping workers and waits for them to finish processing enqueued items.
  • Pluggable Backend: Uses a MailerService interface, allowing different sending mechanisms (e.g., SMTP, API-based services). An SMTP implementation (MailerSMTP) is included.
  • Content Validation: Includes a builder (MailContentBuilder) for creating validated MailContent with checks for field lengths and allowed MIME types.
  • Context Propagation: Leverages context.Context for cancellation and timeout propagation throughout the sending process.
  • Structured Logging: Uses the standard log/slog package for informative logging.
  • Error Handling: Provides specific error types (MailerError, MailQueueError) for better error management.
  • Customizable Worker Count: Allows configuring the number of concurrent workers within defined limits.
  • MIME Type Support: Supports text/plain and text/html MIME types.
  • Sender and Recipient Details: Allows specifying sender and recipient names along with email addresses.

Installation

To use this library in your project, install it using go get:

go get github.com/p2p-b2b/mailer@latest

Components

  • MailService: The main service that manages the email queue and worker pool.
  • MailContent / MailContentBuilder: Struct and builder for defining email content (sender, recipient, subject, body, MIME type).
  • MailerService: Interface for the actual email sending logic.
  • MailerSMTP: An implementation of MailerService using standard SMTP.

Configuration

MailService

Configure the MailService using MailServiceConfig:

type MailServiceConfig struct {
    Ctx         context.Context // Optional: Parent context for cancellation.
    WorkerCount int             // Number of concurrent sending workers (1-100).
    Timeout     time.Duration   // Optional: Timeout for operations (currently unused in core service logic but available).
    Mailer      MailerService   // The backend mailer implementation (e.g., MailerSMTP).
}
MailerSMTP

Configure the MailerSMTP backend using MailerSMTPConf:

type MailerSMTPConf struct {
    SMTPHost string // SMTP server hostname.
    SMTPPort int    // SMTP server port (e.g., 587, 465, 25).
    Username string // SMTP username for authentication.
    Password string // SMTP password for authentication.
}

Usage Example

package main

import (
  "context"
  "fmt"
  "log/slog"
  "os"
  "os/signal"
  "syscall"
  "time"

  "github.com/p2p-b2b/mailer" // Assuming this is the module path
)

func main() {
  // --- Configuration ---
  smtpConf := mailer.MailerSMTPConf{
    SMTPHost: "smtp.example.com", // Replace with your SMTP host
    SMTPPort: 587,                // Replace with your SMTP port
    Username: "[email protected]", // Replace with your SMTP username
    Password: "your_password",    // Replace with your SMTP password
  }

  smtpMailer, err := mailer.NewMailerSMTP(smtpConf)
  if err != nil {
    slog.Error("Failed to configure SMTP mailer", "error", err)
    os.Exit(1)
  }

  // Create a context that can be cancelled
  appCtx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
  defer cancel()

  mailServiceConf := &mailer.MailServiceConfig{
    Ctx:         appCtx,     // Use the cancellable context
    WorkerCount: 5,          // Number of concurrent workers
    Mailer:      smtpMailer, // Use the configured SMTP mailer
  }

  mailService, err := mailer.NewMailService(mailServiceConf)
  if err != nil {
    slog.Error("Failed to create mail service", "error", err)
    os.Exit(1)
  }

  // --- Start the Service ---
  // Start the service with the application context
  mailService.Start(appCtx)
  slog.Info("Mail service started. Press Ctrl+C to stop.")

  // --- Enqueue Emails ---
  go func() {
    // Example of enqueuing emails
    for i := 0; i < 10; i++ {
      subject := fmt.Sprintf("Test Email %d", i+1)
      body := fmt.Sprintf("This is the body of test email #%d.", i+1)

      content, err := (&mailer.MailContentBuilder{}).
        WithFromName("Sender Name").
        WithFromAddress("[email protected]").
        WithToName("Recipient Name").
        WithToAddress("[email protected]"). // Replace with a valid recipient
        WithMimeType("text/plain").
        WithSubject(subject).
        WithBody(body).
        Build()

      if err != nil {
        slog.Error("Failed to build mail content", "error", err)
        continue // Skip this email
      }

      err = mailService.Enqueue(content)
      if err != nil {
        // This might happen if the context is cancelled while enqueuing
        slog.Error("Failed to enqueue email", "error", err)
        // If context is cancelled, we should probably stop trying to enqueue
        if appCtx.Err() != nil {
          break
        }
      } else {
        slog.Info("Email enqueued", "subject", subject)
      }
      time.Sleep(500 * time.Millisecond) // Simulate some delay between emails
    }
    slog.Info("Finished enqueuing sample emails.")
  }()

  // --- Wait for Shutdown Signal ---
  <-appCtx.Done() // Block until context is cancelled (Ctrl+C)

  slog.Info("Shutdown signal received.")

  // --- Stop the Service Gracefully ---
  // Stop accepting new emails and wait for workers to finish
  // Note: Stop() closes the channel. If context cancellation is the primary
  // shutdown mechanism, workers will stop based on <-ctx.Done().
  // Calling Stop() ensures the channel is closed if not already done by context cancellation propagation.
  // Depending on exact needs, you might just rely on context cancellation and use Wait().
  // Using Stop() here is generally safer for ensuring cleanup.
  mailService.Stop() // This also calls Wait() internally after closing the channel

  slog.Info("Mail service stopped gracefully.")
}

Contributing

Contributions are welcome! Please feel free to submit pull requests or open issues.

License

This project is licensed under the Apache License 2.0. See the LICENSE file for details.

Documentation

Overview

Package mailer provides a robust and concurrent email sending service for Go applications.

It features a queue-based system (`MailService`) that utilizes a pool of worker goroutines to send emails asynchronously. This allows applications to enqueue emails quickly without blocking on the actual sending process.

Key Features:

  • Concurrent email sending via a configurable worker pool.
  • Buffered queue for email content (`MailContent`).
  • Graceful shutdown using context cancellation and wait groups.
  • Pluggable backend architecture via the `MailerService` interface.
  • Includes a standard SMTP implementation (`MailerSMTP`).
  • Provides a builder (`MailContentBuilder`) for validated email content creation.

Usage typically involves:

  1. Configuring a `MailerService` implementation (e.g., `NewMailerSMTP`).
  2. Configuring the main `MailService` with the desired worker count and the chosen mailer backend.
  3. Starting the `MailService` with a context.
  4. Enqueuing `MailContent` instances using the `Enqueue` method.
  5. Stopping the service gracefully or relying on context cancellation for shutdown.

See the `ExampleMailService_Enqueue` function in the tests for a practical usage demonstration.

Index

Examples

Constants

View Source
const (
	ValidMinFromNameLength    = 1
	ValidMaxFromNameLength    = 100
	ValidMinFromAddressLength = 5
	ValidMaxFromAddressLength = 50
	ValidMinToNameLength      = 1
	ValidMaxToNameLength      = 100
	ValidMinToAddressLength   = 5
	ValidMaxToAddressLength   = 50
	ValidMimeType             = "text/plain|text/html"
	ValidMinSubjectLength     = 1
	ValidMaxSubjectLength     = 255
	ValidMinBodyLength        = 1
	ValidMaxBodyLength        = 20000
)
View Source
const (
	ValidMaxWorkerCount = 100
	ValidMinWorkerCount = 1
)
View Source
const (
	ValidMinSMTPHostLength = 5
	ValidMaxSMTPHostLength = 100
	ValidSMTPPorts         = "25|465|587|1025|8025"
	ValidMinUsernameLength = 1
	ValidMaxUsernameLength = 100
	ValidMinPasswordLength = 3
	ValidMaxPasswordLength = 100
)

Variables

This section is empty.

Functions

This section is empty.

Types

type MailContent

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

MailContent represents the content of an email.

type MailContentBuilder

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

func NewMailContentBuilder added in v0.0.3

func NewMailContentBuilder() *MailContentBuilder

func (*MailContentBuilder) Build

func (b *MailContentBuilder) Build() (MailContent, error)

func (*MailContentBuilder) WithBody

func (b *MailContentBuilder) WithBody(body string) *MailContentBuilder

func (*MailContentBuilder) WithFromAddress

func (b *MailContentBuilder) WithFromAddress(address string) *MailContentBuilder

func (*MailContentBuilder) WithFromName

func (b *MailContentBuilder) WithFromName(name string) *MailContentBuilder

func (*MailContentBuilder) WithMimeType

func (b *MailContentBuilder) WithMimeType(mimeType MimeType) *MailContentBuilder

func (*MailContentBuilder) WithMimeTypeAsString added in v0.0.2

func (b *MailContentBuilder) WithMimeTypeAsString(mimeType string) *MailContentBuilder

func (*MailContentBuilder) WithSubject

func (b *MailContentBuilder) WithSubject(subject string) *MailContentBuilder

func (*MailContentBuilder) WithToAddress

func (b *MailContentBuilder) WithToAddress(address string) *MailContentBuilder

func (*MailContentBuilder) WithToName

func (b *MailContentBuilder) WithToName(name string) *MailContentBuilder

type MailQueueError

type MailQueueError struct {
	Message string
}

func (*MailQueueError) Error

func (e *MailQueueError) Error() string

type MailQueueService

type MailQueueService interface {
	Enqueue(content MailContent) error
}

type MailService

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

func NewMailService

func NewMailService(conf *MailServiceConfig) (*MailService, error)

func (*MailService) Enqueue

func (ref *MailService) Enqueue(content MailContent) error

Enqueue adds mail content to the queue. It returns an error if the service's context is cancelled during the attempt.

Example

ExampleMailService_Enqueue demonstrates how to create a MailService, enqueue an email, and have it processed by a mock mailer.

// Use a mock mailer for demonstration purposes
mockMailer := &MockMailerService{}

ctx, cancel := context.WithCancel(context.Background())
defer cancel() // Ensure context is cancelled eventually

// Configure the mail service
config := &MailServiceConfig{
	Ctx:         ctx,
	WorkerCount: 1, // One worker is sufficient for the example
	Mailer:      mockMailer,
}
service, err := NewMailService(config)
if err != nil {
	fmt.Printf("Error creating service: %v\n", err)
	return
}

service.Start()

// Create email content using the builder
content, err := (&MailContentBuilder{}).
	WithFromName("Example Sender").
	WithFromAddress("[email protected]").
	WithToName("Example Recipient").
	WithToAddress("[email protected]").
	WithMimeType("text/plain").
	WithSubject("Example Subject").
	WithBody("This is the example email body.").
	Build()
if err != nil {
	fmt.Printf("Error building content: %v\n", err)
	return
}

// Enqueue the email
err = service.Enqueue(content)
if err != nil {
	fmt.Printf("Error enqueuing email: %v\n", err)
	// Don't return here, allow Stop to run for cleanup
}

// Stop the service gracefully. This ensures the enqueued item is processed
// by the worker before the example finishes.
service.Stop()

// Check if the mock mailer received the email
if mockMailer.Count() > 0 {
	// Accessing unexported fields like subject directly isn't possible here.
	// We'll print a confirmation message based on the count.
	// In a real test, you might check mockMailer.SentMail[0].subject if it were exported
	// or add a method to MockMailerService to get sent subjects.
	fmt.Printf("Mock mailer received %d email(s).\n", mockMailer.Count())
} else {
	fmt.Println("Mock mailer did not receive the email.")
}
Output:

Mock mailer received 1 email(s).

func (*MailService) Start

func (ref *MailService) Start()

Start initializes the worker goroutines that will process the mail queue. Each worker will listen on the content channel for new mail content to process. The workers will run concurrently, and the number of workers is determined by the WorkerCount field. The context provided in the configuration is used to manage the lifecycle of the workers. If the context is cancelled, all workers will stop processing and exit gracefully. The workers will log their status and any errors encountered during the email sending process.

func (*MailService) Stop

func (ref *MailService) Stop()

Stop closes the content channel and waits for all workers to finish processing.

func (*MailService) Wait

func (ref *MailService) Wait()

Wait blocks until all worker goroutines have exited. Useful if you cancel the context and want to ensure workers stopped without necessarily closing the channel via Stop().

type MailServiceConfig

type MailServiceConfig struct {
	Ctx context.Context

	// WorkerCount is the number of concurrent workers to process the mail queue.
	// It must be between 1 and 100.
	WorkerCount int

	// Timeout is the duration for which the service will wait for a worker to finish processing before timing out.
	// This is currently not used in the core service logic but can be implemented in the future.
	Timeout time.Duration

	// Mailer is the service responsible for sending emails.
	// It must not be nil.
	Mailer MailerService
}

type MailerError

type MailerError struct {
	Message string
}

MailerError is a custom error type for mailer-related errors. It implements the error interface.

func (*MailerError) Error

func (e *MailerError) Error() string

Error implements the error interface for MailerError.

type MailerSMTP

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

func NewMailerSMTP

func NewMailerSMTP(conf MailerSMTPConf) (*MailerSMTP, error)

func (*MailerSMTP) Send

func (m *MailerSMTP) Send(ctx context.Context, content MailContent) error

type MailerSMTPConf

type MailerSMTPConf struct {
	// SMTPHost is the hostname of the SMTP server (5-100 chars). *Required*.
	// It must be a valid hostname or IP address.
	SMTPHost string

	// SMTPPort is the port of the SMTP server (must be 25, 465, or 587). *Required*.
	// Valid ports are 25 (SMTP), 465 (SMTPS), 587 (Submission), 1025, and 8025.
	// Ports 1025 and 8025 are less common and may be used in specific configurations, such as testing or non-standard setups.
	SMTPPort int

	// Username is the SMTP username for authentication (1-100 chars). *Required*.
	Username string

	// Password is the SMTP password for authentication (3-100 chars). *Required*.
	// It should be kept secret and not logged or exposed.
	Password string
}

type MailerService

type MailerService interface {
	Send(ctx context.Context, content MailContent) error
}

MailerService is an interface must be implemented by any mailer service that is used to send emails.

type MimeType added in v0.0.2

type MimeType string

MimeType is a custom type for MIME types.

const (
	// MimeTypeTextPlain is the MIME type for plain text emails.
	MimeTypeTextPlain MimeType = "text/plain"

	// MimeTypeTextHTML is the MIME type for HTML emails.
	MimeTypeTextHTML MimeType = "text/html"
)

func (MimeType) IsValid added in v0.0.2

func (m MimeType) IsValid() bool

IsValid checks if the MimeType is valid.

func (MimeType) String added in v0.0.2

func (m MimeType) String() string

String returns the string representation of the MimeType.

Directories

Path Synopsis
filepath: example/main.go
filepath: example/main.go

Jump to

Keyboard shortcuts

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