How to Extend Listmonk with Custom Messenger Backends: A Complete Implementation Guide
To extend Listmonk with custom messenger backends, implement the Messenger interface defined in internal/manager/manager.go and register your implementation with the Manager using AddMessenger().
Listmonk’s architecture supports pluggable messenger backends that enable delivery through any channel implementing the standard interface. This guide explains how to extend listmonk with custom messenger backends by examining the core Messenger contract, walking through a complete implementation example, and registering it within the initialization flow of the knadh/listmonk repository.
Understanding the Messenger Interface Architecture
Listmonk abstracts all messaging logic behind a minimal interface that the Manager uses to route campaigns uniformly. This design allows any delivery mechanism—from Slack webhooks to SMS gateways—to function as a first-class citizen alongside the built-in email backend.
The Messenger Interface Contract
The Messenger interface in internal/manager/manager.go#L47-L54 defines the contract every backend must satisfy:
Name() string– Returns a unique identifier for the messenger (e.g.,"slack"or"sms").Push(models.Message) error– Receives a message struct and handles delivery.Flush() error– Signals the end of a batch for backends that implement buffering.Close() error– Cleans up resources like HTTP clients or database connections.
The Manager’s Role
The Manager struct maintains a registry of available messengers in internal/manager/manager.go#L84-L92 via a messengers map[string]Messenger. When a campaign sends messages, the manager looks up the requested backend by name and delegates to its Push method. This central coordination handles scheduling, rate-limiting, and retries uniformly regardless of the underlying transport.
Implementing a Custom Messenger Backend
Create a new package under internal/messenger/ and implement the four required methods. The following example demonstrates a Slack webhook backend that posts campaign content to a channel.
Step 1: Define the Struct and Constructor
Create internal/messenger/slack/slack.go with a struct that holds configuration and an HTTP client:
package slack
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"github.com/knadh/listmonk/internal/manager"
"github.com/knadh/listmonk/models"
)
// SlackConfig holds messenger-specific settings from the listmonk config file.
type SlackConfig struct {
WebhookURL string `json:"webhook_url"`
}
// SlackMessenger implements manager.Messenger.
type SlackMessenger struct {
name string
cfg SlackConfig
client *http.Client
}
// New validates the configuration and returns an initialized SlackMessenger.
func New(name string, cfg SlackConfig) (*SlackMessenger, error) {
if cfg.WebhookURL == "" {
return nil, fmt.Errorf("missing webhook_url in slack messenger config")
}
return &SlackMessenger{
name: name,
cfg: cfg,
client: &http.Client{},
}, nil
}
This constructor pattern mirrors email.New in internal/messenger/email/email.go#L51-L59, which validates SMTP parameters before returning an instance.
Step 2: Implement the Interface Methods
Add the required methods to satisfy the Messenger interface:
// Name returns the messenger identifier used in campaign settings.
func (s *SlackMessenger) Name() string { return s.name }
// Push sends a single Listmonk message to Slack via webhook.
func (s *SlackMessenger) Push(m models.Message) error {
payload := map[string]string{
"text": fmt.Sprintf("*%s*\n%s", m.Subject, m.Body),
}
b, _ := json.Marshal(payload)
resp, err := s.client.Post(s.cfg.WebhookURL, "application/json", bytes.NewReader(b))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("slack webhook returned status %d", resp.StatusCode)
}
return nil
}
// Flush is a no-op for Slack since messages send immediately.
func (s *SlackMessenger) Flush() error { return nil }
// Close releases resources (none required for this HTTP client).
func (s *SlackMessenger) Close() error { return nil }
The Push method receives a models.Message containing fields like From, To, Subject, Body, and Headers, which you can map to your target API’s payload format.
Registering Your Custom Messenger
After implementation, register your messenger during application startup. The initialization logic in cmd/init.go#L689-L698 demonstrates how the built-in email messenger is constructed and added to the manager.
Modify cmd/init.go
Import your package and add registration logic where other messengers initialize:
// Import your custom messenger
import "github.com/knadh/listmonk/internal/messenger/slack"
// Inside the initialization function, after loading configuration:
if cfg.Slack.Enabled {
slackCfg := slack.SlackConfig{
WebhookURL: cfg.Slack.WebhookURL,
}
slackMsgr, err := slack.New("slack", slackCfg)
if err != nil {
log.Fatalf("error initializing slack messenger: %v", err)
}
if err = mgr.AddMessenger(slackMsgr); err != nil {
log.Fatalf("error adding slack messenger: %v", err)
}
}
The AddMessenger method validates that the messenger’s name is unique and stores it in the manager’s internal map.
Configuration Schema
Define your configuration structure in the global settings parsed by cmd/settings.go. Add a corresponding section to your config.toml or config.json:
[slack]
enabled = true
webhook_url = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
The settings loader unmarshals this into the cfg object passed to your constructor.
Using Custom Messengers in Campaigns
Once registered, specify your custom backend by its Name() return value when creating campaigns. In the Listmonk admin UI or API payload, set the messenger field to "slack" (or whatever identifier you passed to New()). The manager routes these messages to your Push method while applying the same rate-limiting and retry logic used for email.
Summary
- Implement the interface: Create a package under
internal/messenger/that satisfies the four methods defined ininternal/manager/manager.go. - Validate in constructor: Follow the pattern in
internal/messenger/email/email.goby validating configuration in aNew()function before returning an instance. - Register during init: Call
mgr.AddMessenger()incmd/init.gousing the same pattern shown at lines 689-698 for the email backend. - Configure properly: Extend
cmd/settings.goto parse your custom config section and pass it to your constructor. - Use by name: Reference your messenger’s identifier in campaign configurations to route messages through your custom backend.
Frequently Asked Questions
What methods must I implement to create a custom messenger backend in Listmonk?
You must implement four methods defined in internal/manager/manager.go#L47-L54: Name() returning a unique string identifier, Push(models.Message) error handling single message delivery, Flush() error for batch completion (can be a no-op), and Close() error for resource cleanup. These methods enable the Manager to treat your backend identically to the built-in email messenger.
How do I configure a custom messenger backend in the Listmonk settings file?
Define a configuration struct matching your YAML or JSON schema (like SlackConfig in the example above) and parse it in cmd/settings.go. Pass this struct to your constructor in cmd/init.go, following the pattern used by email.New() which receives email.Server configurations from the global settings object at internal/messenger/email/email.go.
Can I use multiple messenger backends simultaneously in Listmonk?
Yes. The Manager maintains a map of messengers map[string]Messenger and can route different campaigns to different backends simultaneously. Register each messenger instance with a unique name using AddMessenger(), then specify the desired backend when creating campaigns. The built-in email messenger and your custom implementations can operate concurrently.
Where should I place my custom messenger code in the Listmonk repository?
Create a new subpackage under internal/messenger/ (e.g., internal/messenger/slack) alongside the existing email directory. This keeps your implementation consistent with the project structure, allows clean imports into cmd/init.go, and follows the established convention where each transport mechanism maintains its own isolated configuration and logic.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →