# How Listmonk Handles POP3 Bounce Processing: A Technical Deep Dive

> Learn how Listmonk processes POP3 email bounces. Discover its methods for connecting to mailboxes, parsing MIME messages, extracting SMTP codes, and managing subscriber status.

- Repository: [Kailash Nadh/listmonk](https://github.com/knadh/listmonk)
- Tags: deep-dive
- Published: 2026-05-19

---

**Listmonk processes email bounces by connecting to a POP3 mailbox at configurable intervals, parsing raw MIME messages to extract SMTP diagnostic codes, and automatically updating subscriber status when bounce thresholds are exceeded.**

Listmonk, the open-source newsletter and mailing list manager, implements a robust **POP3 bounce processing** pipeline that runs continuously in the background to maintain list hygiene. The system fetches raw bounce messages from a configured POP3 mailbox, extracts delivery failure information, and updates subscriber records accordingly. This article examines the complete technical implementation found in the `knadh/listmonk` repository, from mailbox configuration to automatic subscriber disabling.

## Configuring POP3 Bounce Mailboxes

Bounce handling begins with credentials specified in [`config.toml`](https://github.com/knadh/listmonk/blob/main/config.toml). The `bounce.pop3` section defines the mailbox connection parameters:

```toml
[bounce.pop3]
host = "pop.mailprovider.com"
port = 995
username = "bounces@example.com"
password = "********"
tls = true          # Use TLS

interval = "5m"     # Check every 5 minutes

```

The system uses the Go library `github.com/knadh/pop3` to create a `pop.Client` instance. By default, Listmonk checks the mailbox every five minutes, though this interval is configurable via the `interval` setting.

## The POP3 Retrieval Workflow

The bounce retrieval logic resides in [`internal/bounce/mailbox/pop.go`](https://github.com/knadh/listmonk/blob/main/internal/bounce/mailbox/pop.go). This component orchestrates the connection, message fetching, and cleanup operations.

### Connecting and Authenticating

When the bounce worker starts, it initializes a POP3 client using the configured credentials:

```go
import "github.com/knadh/listmonk/internal/bounce/mailbox/pop"

p, err := pop.New(pop.Config{
    Host:     cfg.Bounce.Pop3.Host,
    Port:     cfg.Bounce.Pop3.Port,
    Username: cfg.Bounce.Pop3.Username,
    Password: cfg.Bounce.Pop3.Password,
    TLS:      cfg.Bounce.Pop3.TLS,
})

```

### Fetching and Deleting Messages

The worker repeatedly calls `client.Retr()` to retrieve each message and `client.Dele()` to remove it after processing. This ensures that processed bounces are not re-fetched on subsequent runs:

```go
// Fetch and process every message
for {
    msgs, err := p.RetrieveAll()
    if err != nil { 
        log.Printf("pop fetch error: %v", err) 
    }
    for _, raw := range msgs {
        bounceInfo, err := bounce.Parse(raw)
        if err == nil {
            _ = bounce.Store(bounceInfo)
        }
        p.Delete(raw.ID) // Always delete after attempt
    }
    time.Sleep(cfg.Bounce.Pop3.Interval)
}

```

If network or authentication errors occur, the system logs the failure and retries on the next scheduled run. Messages that cannot be parsed are skipped but still deleted to prevent infinite reprocessing loops.

## Parsing Bounce Data and Storage

Once retrieved, raw MIME messages pass through the generic bounce parser in [`internal/bounce/bounce.go`](https://github.com/knadh/listmonk/blob/main/internal/bounce/bounce.go). This parser extracts critical delivery failure metadata:

- **SMTP response codes** (e.g., 550, 421)
- **Original recipient addresses**
- **Diagnostic reason text** (e.g., "mailbox full", "user unknown")

Parsed data persists to the `bounces` table, defined in [`models/bounces.go`](https://github.com/knadh/listmonk/blob/main/models/bounces.go). Each bounce creates a unique record containing the message ID, timestamp, diagnostic code, and the associated subscriber ID (looked up via email address).

## Subscriber Status Management and Thresholds

After storing bounce records, [`internal/core/bounces.go`](https://github.com/knadh/listmonk/blob/main/internal/core/bounces.go) executes the subscriber update logic. The system increments the `bounce_count` column for the affected subscriber and evaluates it against the threshold defined in [`config.toml`](https://github.com/knadh/listmonk/blob/main/config.toml) (`bounce.threshold`):

```go
if sub.BounceCount >= cfg.Bounce.Threshold {
    sub.Status = models.StatusDisabled
    db.UpdateSubscriber(sub)
}

```

When the bounce count exceeds the configured threshold, Listmonk automatically **disables the subscriber** (soft-bounces them), preventing future campaign sends to that address. This protects sender reputation by maintaining list hygiene automatically.

## Manual Processing and CLI Integration

While the manager runs POP3 bounce processing as a background goroutine alongside campaign sending and queue processing, administrators can manually trigger processing via command line:

```bash
listmonk bounce process

```

This CLI command, implemented in [`cmd/bounce.go`](https://github.com/knadh/listmonk/blob/main/cmd/bounce.go), executes the same retrieval and processing pipeline outside the normal scheduling loop, useful for immediate troubleshooting or initial setup verification.

## Integration with Metrics and Notifications

Bounce statistics surface through the `/api/stats` endpoint, providing visibility into delivery health. Additionally, the system supports webhook notifications for bounces originating from external providers (SendGrid, SES) through handlers in `internal/bounce/webhooks/`, though these operate independently from the POP3 retrieval flow.

## Summary

- **Configuration**: POP3 credentials reside in [`config.toml`](https://github.com/knadh/listmonk/blob/main/config.toml) under `[bounce.pop3]`, supporting TLS and configurable check intervals.
- **Retrieval**: [`internal/bounce/mailbox/pop.go`](https://github.com/knadh/listmonk/blob/main/internal/bounce/mailbox/pop.go) uses `github.com/knadh/pop3` to fetch messages via `client.Retr()` and deletes them with `client.Dele()` after processing.
- **Parsing**: [`internal/bounce/bounce.go`](https://github.com/knadh/listmonk/blob/main/internal/bounce/bounce.go) extracts SMTP codes and diagnostic text from raw MIME messages.
- **Storage**: Bounce records persist to the `bounces` table ([`models/bounces.go`](https://github.com/knadh/listmonk/blob/main/models/bounces.go)) with subscriber correlation.
- **Automation**: [`internal/core/bounces.go`](https://github.com/knadh/listmonk/blob/main/internal/core/bounces.go) increments bounce counts and disables subscribers exceeding the configured threshold.
- **Operations**: Background processing runs via [`internal/manager/manager.go`](https://github.com/knadh/listmonk/blob/main/internal/manager/manager.go), with manual override available via `listmonk bounce process`.

## Frequently Asked Questions

### How do I configure Listmonk to process bounces via POP3?

Add a `[bounce.pop3]` section to your [`config.toml`](https://github.com/knadh/listmonk/blob/main/config.toml) file specifying the host, port, username, password, TLS setting, and check interval. Listmonk will automatically create a POP3 client and begin checking the mailbox at the specified interval (default 5 minutes).

### What happens to emails in the POP3 mailbox after processing?

Listmonk deletes each message from the POP3 server immediately after attempting to parse it, regardless of whether the parse succeeded. This prevents the same bounce from being processed multiple times and keeps the mailbox from filling up.

### How does Listmonk determine when to disable a subscriber?

After recording a bounce in the `bounces` table, Listmonk increments the subscriber's `bounce_count` column. If this count reaches or exceeds the `bounce.threshold` value set in [`config.toml`](https://github.com/knadh/listmonk/blob/main/config.toml), the subscriber's status changes to `disabled` automatically, preventing future campaign deliveries to that address.

### Can I process bounces manually without waiting for the scheduled interval?

Yes. Run the command `listmonk bounce process` from the CLI to immediately trigger the POP3 retrieval and processing workflow. This command, defined in [`cmd/bounce.go`](https://github.com/knadh/listmonk/blob/main/cmd/bounce.go), executes the same logic as the background worker without waiting for the next scheduled tick.