mirror of
https://github.com/knadh/listmonk.git
synced 2025-01-11 00:38:31 +08:00
1ae98699e7
- Blocklist or unsubscribe subscribers based on a bounce threshold - Add /bounces UI for viewing bounces and in the subscriber view - Add settings UI for managing bounce settings - Add support for scanning POP3 bounce mailboxes - Add a generic webhook for posting custom bounces at /webhooks/bounce - Add SES bounce webhook support at /webhooks/services/ses - Add Sendgrid bounce webhook support at /webhooks/services/sendgrid
119 lines
2.6 KiB
Go
119 lines
2.6 KiB
Go
package mailbox
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/knadh/go-pop3"
|
|
"github.com/knadh/listmonk/models"
|
|
)
|
|
|
|
// POP represents a POP mailbox.
|
|
type POP struct {
|
|
opt Opt
|
|
client *pop3.Client
|
|
}
|
|
|
|
// NewPOP returns a new instance of the POP mailbox client.
|
|
func NewPOP(opt Opt) *POP {
|
|
return &POP{
|
|
opt: opt,
|
|
client: pop3.New(pop3.Opt{
|
|
Host: opt.Host,
|
|
Port: opt.Port,
|
|
TLSEnabled: opt.TLSEnabled,
|
|
TLSSkipVerify: opt.TLSSkipVerify,
|
|
}),
|
|
}
|
|
}
|
|
|
|
// Scan scans the mailbox and pushes the downloaded messages into the given channel.
|
|
// The messages that are downloaded are deleted from the server. If limit > 0,
|
|
// all messages on the server are downloaded and deleted.
|
|
func (p *POP) Scan(limit int, ch chan models.Bounce) error {
|
|
c, err := p.client.NewConn()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer c.Quit()
|
|
|
|
// Authenticate.
|
|
if p.opt.AuthProtocol != "none" {
|
|
if err := c.Auth(p.opt.Username, p.opt.Password); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Get the total number of messages on the server.
|
|
count, _, err := c.Stat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// No messages.
|
|
if count == 0 {
|
|
return nil
|
|
}
|
|
|
|
if limit > 0 && count > limit {
|
|
count = limit
|
|
}
|
|
|
|
// Download messages.
|
|
for id := 1; id <= count; id++ {
|
|
// Download just one line of the body as the body is not required at all.
|
|
m, err := c.Top(id, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var (
|
|
campUUID = m.Header.Get(models.EmailHeaderCampaignUUID)
|
|
subUUID = m.Header.Get(models.EmailHeaderSubscriberUUID)
|
|
date, _ = time.Parse("Mon, 02 Jan 2006 15:04:05 -0700", m.Header.Get("Date"))
|
|
)
|
|
|
|
if campUUID == "" || subUUID == "" {
|
|
continue
|
|
}
|
|
if date.IsZero() {
|
|
date = time.Now()
|
|
}
|
|
|
|
// Additional bounce e-mail metadata.
|
|
meta, _ := json.Marshal(struct {
|
|
From string `json:"from"`
|
|
Subject string `json:"subject"`
|
|
MessageID string `json:"message_id"`
|
|
DeliveredTo string `json:"delivered_to"`
|
|
Received []string `json:"received"`
|
|
}{
|
|
From: m.Header.Get("From"),
|
|
Subject: m.Header.Get("Subject"),
|
|
MessageID: m.Header.Get("Message-Id"),
|
|
DeliveredTo: m.Header.Get("Delivered-To"),
|
|
Received: m.Header.Map()["Received"],
|
|
})
|
|
|
|
select {
|
|
case ch <- models.Bounce{
|
|
Type: "hard",
|
|
CampaignUUID: m.Header.Get(models.EmailHeaderCampaignUUID),
|
|
SubscriberUUID: m.Header.Get(models.EmailHeaderSubscriberUUID),
|
|
Source: p.opt.Host,
|
|
CreatedAt: date,
|
|
Meta: json.RawMessage(meta),
|
|
}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
// Delete the downloaded messages.
|
|
for id := 1; id <= count; id++ {
|
|
if err := c.Dele(id); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|