Go-Guerrilla SMTP Daemon

repository·master·Indexed 41 days ago

https://github.com/phires/go-guerrilla

A lightweight, high-performance SMTP daemon and Go package designed as a Mail Transfer Agent (MTA) for receiving large volumes of email. It focuses on quickly receiving, saving, and disconnecting. It features a modular backend using a producer/consumer structure with chainable Processors, a non-blocking Daemon API for Go integration, and a standalone binary, guerrillad.

Tokens
8.5K
Snippets
35
Records
41
Agent score
53%

What's inside Go-Guerrilla

  1. How the Email Processing Backend and Processors work

    master

    The backend system follows a decorator-like pattern using a producer/consumer structure. The core logic is handled by a chain of Processors configured via the save_process option.

    Key Concepts:

    • Processors: Modular components that perform specific tasks (e.g., parsing, saving, or validating).
    • Chaining: Processors are chained together in a sequence. For example, a configuration like "HeadersParser|Header|Debugger" means the email is first parsed for headers, then delivery headers are added, and finally, debug information is logged.
    • Workers: The backend runs multiple workers in parallel using Go's channels and goroutines to handle high volumes of mail efficiently.
  2. Use Go-Guerrilla as a Go package

    master

    You can integrate Go-Guerrilla directly into your Go application by importing the package.

    Steps:

    1. Import github.com/phires/go-guerrilla.
    2. Use dep ensure or go get ./... to manage dependencies.
    3. Initialize a guerrilla.Daemon and call .Start().

    Note: The d.Start() method is non-blocking. You must ensure your main program remains active (e.g., using a channel or a blocking call) to keep the server running.

    import (
        "fmt"
        "github.com/phires/go-guerrilla"
    )
    
    func main() {
        d := guerrilla.Daemon{}
        err := d.Start()
    
        if err == nil {
            fmt.Println("Server Started!")
        }
        
        // IMPORTANT: d.Start() does not block. 
        // You must keep the program alive here.
        select {}
    }
  3. Build and run Go-Guerrilla as a standalone daemon

    master

    To use Go-Guerrilla as a standalone SMTP daemon, you must first build the guerrillad binary using make. Once built, you can run it using the serve command.

    Setup Steps:

    1. Build the binary: make guerrillad.
    2. Prepare configuration: Copy goguerrilla.conf.sample to goguerrilla.conf.json.
    3. Customize configuration: Adjust pid_file and set tls_always_on to false if you do not have valid TLS certificates configured.
    4. Run the server: ./guerrillad serve.
    $ make guerrillad
    $ cp goguerrilla.conf.sample goguerrilla.conf.json
    $ ./guerrillad serve
  4. Understand ClientState transitions

    master

    The ClientState type tracks the progress of an SMTP transaction. The possible states are:

    • ClientConnected: The client has established a connection.
    • ClientProxy: Awaiting a PROXY header.
    • ClientGreeting: The client is awaiting the server's initial greeting.
    • ClientCmd: The server has responded to the connection and is awaiting an SMTP command.
    • ClientData: Sender and recipient information have been received.
    • ClientStartTLS: The connection has been secured via TLS.
    • ClientShutdown: The server is initiating shutdown; the client should disconnect on the next command turn.
  5. How configuration change events work

    master

    Go-Guerrilla supports dynamic configuration updates. When the AppConfig is updated, the EmitChangeEvents method compares the new configuration against the old one and publishes specific events to the application's event bus.

    Event Types

    • Global App Events: EventConfigBackendConfig, EventConfigNewConfig, EventConfigAllowedHosts, EventConfigPidFile, EventConfigLogFile, EventConfigLogLevel.
    • Server Events:
      • EventConfigServerNew: A new server was added.
      • EventConfigServerRemove: An existing server was removed.
      • EventConfigServerConfig: A specific server's configuration changed.
      • EventConfigServerStart / EventConfigServerStop: The IsEnabled status of a server changed.
      • EventConfigServerLogFile / EventConfigServerLogReopen: The server's log destination changed.
      • EventConfigServerTimeout: The connection timeout changed.
      • EventConfigServerMaxClients: The maximum client limit changed.
      • EventConfigServerTLSConfig: The TLS settings for a server changed.
  6. Gracefully shut down a Pool

    master

    Shutting down a Pool is a two-step process to ensure active clients are notified and allowed to finish their work before the application exits.

    1. ShutdownState(): This method locks the pool to prevent new borrows, signals the shutdown via ShutdownChan, and sets a very low timeout (1 second) on all currently active clients to encourage them to finish quickly.
    2. ShutdownWait(): This method blocks until all active clients have finished their work (tracked via a sync.WaitGroup) and then resets the shutdown flag.

    Use IsShuttingDown() bool to check if the pool is currently in the process of shutting down.

    // Start the shutdown sequence
    pool.ShutdownState()
    
    // Wait for active clients to finish their tasks
    pool.ShutdownWait()
  7. Reference: Included and Available Processors

    master

    Processors define how received emails are handled. Some are included out-of-the-box, while others must be imported and registered via Daemon.AddProcessor.

    | Processor | Description |
    |-----------|-------------|
    | Compressor | Sets a zlib compressor that other processors can use later |
    | Debugger | Logs the email envelope to help with testing |
    | Hasher | Processes each envelope to produce unique hashes to be used for ids later |
    | Header | Add a delivery header to the envelope |
    | HeadersParser | Parses MIME headers and also populates the Subject field of the envelope |
    | MySQL | Saves the emails to MySQL |
    | Redis | Saves the email data to Redis |
    | GuerrillaDbRedis | A 'monolithic' processor used at Guerrilla Mail |
    | [MailDir](https://github.com/flashmob/maildir-processor) | Save emails to a maildir |
    | [FastCGI](https://github.com/flashmob/fastcgi-processor) | Deliver email directly to PHP-FPM or a similar FastCGI backend |
    | [WildcardProcessor](https://github.com/DevelHell/wildcard-processor) | Use wildcards for recipients host validation |
    | [GmailProcessor](https://github.com/greystonedevelopers/gmailprocessor) | Bridge from SMTP to Google API |
  8. Control client connection lifecycle with kill and closeConn

    master

    To manage the connection lifecycle:

    • kill(): Flags the connection to be closed on the next turn of the event loop. It sets the KilledAt timestamp.
    • isAlive(): Returns true if the client is still active (i.e., KilledAt is zero). Use this to check if a connection should be closed.
    • closeConn(): Immediately closes the underlying net.Conn. This method is goroutine-safe.
  9. Upgrade a connection to TLS with upgradeToTLS

    master

    Use upgradeToTLS to wrap the existing net.Conn in a TLS server-side connection. This method performs the TLS handshake immediately and returns an error if the handshake fails. If successful, it updates the client's internal connection and buffers to use the new TLS connection and sets the TLS flag to true.

    err := client.upgradeToTLS(tlsConfig)
    if err != nil {
        // handle handshake error
    }
  10. Configure the Daemon via AppConfig

    master

    You can configure the Daemon in three ways:

    1. From a file: Use LoadConfig(path string) to read a JSON configuration file. This will populate d.Config if it is currently nil.
    2. Directly: Use SetConfig(c AppConfig) to pass an existing AppConfig object. Note that SetConfig does not emit configuration change events; use ReloadConfig for that.
    3. Defaults: If you call Start() without providing a configuration, the Daemon will automatically call configureDefaults(), setting up default values for servers and backend options via d.Config.setDefaults() and d.Config.setBackendDefaults().
    // Loading from file
    d.LoadConfig("path/to/config.json")
    
    // Setting directly
    newCfg := guerrilla.AppConfig{ /* ... */ }
    d.SetConfig(newCfg)
  11. Check OS file limits for client capacity

    master

    Before starting a high-traffic daemon, use CheckFileLimit to ensure the operating system's file descriptor limit (ulimit) is sufficient for the configured number of clients.

    It returns a boolean indicating if the limit is sufficient, the total maxClients requested by all configured servers, and the actual fileLimit detected from the OS.

    // c is your *AppConfig
    passed, requested, limit := guerrilla.CheckFileLimit(c)
    if !passed {
    	fmt.Printf("Warning: Requested %d clients, but OS limit is %d\n", requested, limit)
    }
  12. Unsubscribe from an event

    master

    To stop receiving notifications for a specific event, use the Unsubscribe method. You must provide the same Event topic and the same handler function used during subscription.

    err := handler.Unsubscribe(guerrilla.EventConfigNewConfig, myHandlerFunc)