Skip to content

Commit 069fb9d

Browse files
committed
Improve delivery/bounce info
1 parent d7cbb30 commit 069fb9d

File tree

3 files changed

+115
-26
lines changed

3 files changed

+115
-26
lines changed

cmd/mmailerd/mmailerd.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"crypto/subtle"
7+
"encoding/hex"
78
"encoding/json"
89
"errors"
910
"fmt"
@@ -296,12 +297,32 @@ func loadServices() {
296297
logger.Info(fmt.Sprintf(" - Mandrill: add the following posthook url %s", posthookUrl))
297298
services = append(services, decorate(mandrill.New(parts[1])))
298299
case "mailgun":
299-
if len(parts) != 3 {
300+
if len(parts) != 2 {
300301
logger.Warn("mailgun api string is not valid,", s)
301302
continue
302303
}
304+
apiKeys := slicez.Map(domainApiKeys[service], func(k mmailer.ServiceApiKey) mmailer.ApiKey {
305+
return k.ApiKey
306+
})
307+
for _, k := range domainApiKeys[service] {
308+
logger.Info(fmt.Sprintf(" - Mailgun: key enabled: %s", k.Domain))
309+
for k, v := range k.Props {
310+
logger.Info(fmt.Sprintf(" - Mailgun: property: %s=%s", k, v))
311+
}
312+
}
313+
if len(apiKeys) == 0 {
314+
logger.Warn(" - Mailgun: disabled, no api keys provided")
315+
continue
316+
}
317+
webhookSigningKey := parts[1]
318+
_, err := hex.DecodeString(webhookSigningKey)
319+
if err != nil {
320+
logger.Warn(" - Mailgun: disabled, bad webhook signing key")
321+
continue
322+
}
323+
303324
logger.Info(fmt.Sprintf(" - Mailgun: add the following posthook url %s", posthookUrl))
304-
services = append(services, decorate(mailgun.New(parts[1], parts[2])))
325+
services = append(services, decorate(mailgun.New(apiKeys, webhookSigningKey)))
305326
case "sendgrid":
306327
if len(parts) < 1 || len(parts) > 2 {
307328
logger.Warn("sendgrid api string is not valid,", s)

service.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,10 @@ func KeyByEmailDomain(apiKeys []ApiKey, emailFrom string) (ApiKey, bool) {
3232
domain := ""
3333
if from, err := mail.ParseAddress(emailFrom); err == nil {
3434
parts := strings.Split(from.Address, "@")
35-
if len(parts) == 2 {
36-
d := strings.ToLower(strings.TrimSpace(parts[1]))
37-
if d != "" {
38-
domain = d
39-
}
35+
domain, _ := slicez.Last(parts)
36+
d := strings.ToLower(strings.TrimSpace(domain))
37+
if d != "" {
38+
domain = d
4039
}
4140
}
4241
domainKey, ok := slicez.Find(apiKeys, func(e ApiKey) bool {

services/mailgun/mailgun.go

Lines changed: 88 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"net/mail"
99
"strings"
10+
"time"
1011

1112
jsoniter "github.com/json-iterator/go"
1213
"github.com/mailgun/mailgun-go/v5"
@@ -19,17 +20,17 @@ import (
1920
)
2021

2122
type Mailgun struct {
22-
client *mailgun.Client
23-
confer services.Configurer[*mailgun.PlainMessage]
23+
apiKeys []mmailer.ApiKey
24+
webhookSigningKey string
25+
confer services.Configurer[*mailgun.PlainMessage]
2426
}
2527

26-
func New(apiKey string, webhookSigningKey string) *Mailgun {
28+
func New(apiKeys []mmailer.ApiKey, webhookSigningKey string) *Mailgun {
2729
mg := &Mailgun{
28-
client: mailgun.NewMailgun(apiKey),
29-
confer: configurer{},
30+
apiKeys: apiKeys,
31+
webhookSigningKey: webhookSigningKey,
32+
confer: configurer{},
3033
}
31-
mg.client.SetWebhookSigningKey(webhookSigningKey)
32-
_ = mg.client.SetAPIBase(mailgun.APIBaseEU)
3334
return mg
3435
}
3536

@@ -50,10 +51,23 @@ func (m *Mailgun) CanSend(e mmailer.Email) bool {
5051
return false
5152
}
5253
}
53-
if !strings.HasSuffix(e.From.Email, "strictlog.modfin.se") {
54-
return false
54+
_, ok := mmailer.KeyByEmailDomain(m.apiKeys, e.From.Email)
55+
return ok
56+
}
57+
58+
func (m *Mailgun) newClient(addr string) (*mailgun.Client, error) {
59+
k, ok := mmailer.KeyByEmailDomain(m.apiKeys, addr)
60+
if !ok {
61+
return nil, errors.New("sendgrid: no api key found for " + addr)
62+
}
63+
client := mailgun.NewMailgun(k.Key)
64+
if k.Props != nil && k.Props["region"] == "eu" {
65+
err := client.SetAPIBase(mailgun.APIBaseEU)
66+
if err != nil {
67+
return nil, fmt.Errorf("failed to set EU region")
68+
}
5569
}
56-
return true
70+
return client, nil
5771
}
5872

5973
func (m *Mailgun) Send(ctx context.Context, e mmailer.Email) ([]mmailer.Response, error) {
@@ -66,7 +80,10 @@ func (m *Mailgun) Send(ctx context.Context, e mmailer.Email) ([]mmailer.Response
6680
if domain == "" {
6781
return nil, fmt.Errorf("mailgun: failed to get email domain: %v", from.Address)
6882
}
69-
83+
client, err := m.newClient(domain)
84+
if err != nil {
85+
return nil, fmt.Errorf("mailgun: failed to create client: %w", err)
86+
}
7087
to := slicez.Map(e.To, func(a mmailer.Address) string {
7188
return a.String()
7289
})
@@ -92,7 +109,7 @@ func (m *Mailgun) Send(ctx context.Context, e mmailer.Email) ([]mmailer.Response
92109
msg.AddBufferAttachment(a.Name, b)
93110
}
94111

95-
resp, err := m.client.Send(ctx, msg)
112+
resp, err := client.Send(ctx, msg)
96113
if err != nil {
97114
return nil, fmt.Errorf("mailgun: failed to send email: %w", err)
98115
}
@@ -115,7 +132,9 @@ func (m *Mailgun) UnmarshalPosthook(body []byte) ([]mmailer.Posthook, error) {
115132
if err := jsoniter.Unmarshal(body, &webhook); err != nil {
116133
return nil, err
117134
}
118-
verified, err := m.client.VerifyWebhookSignature(webhook.Signature)
135+
client := mailgun.NewMailgun("") // api key is not used for VerifyWebhookSignature
136+
client.SetWebhookSigningKey(m.webhookSigningKey)
137+
verified, err := client.VerifyWebhookSignature(webhook.Signature)
119138
if err != nil {
120139
return nil, fmt.Errorf("mailgun: failed to verify signature: %w", err)
121140
}
@@ -146,6 +165,7 @@ func (m *Mailgun) UnmarshalPosthook(body []byte) ([]mmailer.Posthook, error) {
146165
h.Event = mmailer.EventDelivered
147166
h.MessageId = e.Message.Headers.MessageID
148167
h.Email = e.Recipient
168+
h.Info = infoString(false, "", "", e.DeliveryStatus)
149169

150170
case *events.Opened:
151171
h.Event = mmailer.EventOpen
@@ -156,17 +176,16 @@ func (m *Mailgun) UnmarshalPosthook(body []byte) ([]mmailer.Posthook, error) {
156176
switch e.Severity {
157177
case "permanent":
158178
h.Event = mmailer.EventBounce
159-
160-
case "temporary":
161-
h.Event = mmailer.EventDeferred
162179
if e.Reason == "suppress-bounce" {
163180
h.Event = mmailer.EventDropped
164181
}
182+
case "temporary":
183+
h.Event = mmailer.EventDeferred
165184
}
166-
167185
h.MessageId = e.Message.Headers.MessageID
168186
h.Email = e.Recipient
169-
h.Info = fmt.Sprintf("%s; %d; %s", e.Reason, e.DeliveryStatus.Code, e.DeliveryStatus.Description)
187+
h.Info = infoString(true, e.Reason, e.Severity, e.DeliveryStatus)
188+
170189
default:
171190
logger.Warn(fmt.Sprintf("received unsupported webhook event: %s", h.Event))
172191
return nil, nil
@@ -175,12 +194,62 @@ func (m *Mailgun) UnmarshalPosthook(body []byte) ([]mmailer.Posthook, error) {
175194
return []mmailer.Posthook{h}, nil
176195
}
177196

197+
func infoString(fail bool, reason, severity string, st events.DeliveryStatus) string {
198+
latency := time.Duration(st.SessionSeconds * float64(time.Second)).Truncate(time.Millisecond)
199+
200+
var words []string
201+
if st.Code != 0 {
202+
words = append(words, fmt.Sprintf("%d", st.Code))
203+
}
204+
if st.EnhancedCode != "" {
205+
words = append(words, st.EnhancedCode)
206+
}
207+
if !fail {
208+
words = append(words, st.Message)
209+
}
210+
if st.MxHost != "" {
211+
words = append(words, st.MxHost)
212+
}
213+
if latency > time.Millisecond {
214+
words = append(words, latency.String())
215+
}
216+
if reason != "" {
217+
words = append(words, reason)
218+
}
219+
if severity != "" {
220+
words = append(words, severity)
221+
}
222+
223+
var flags []string
224+
if st.AttemptNo > 0 {
225+
flags = append(flags, fmt.Sprintf("attempt:%d", st.AttemptNo))
226+
}
227+
if st.Utf8 != nil && *st.Utf8 {
228+
flags = append(flags, "utf8")
229+
}
230+
if st.TLS != nil && *st.TLS {
231+
flags = append(flags, "tls")
232+
}
233+
if st.CertificateVerified != nil && *st.CertificateVerified {
234+
flags = append(flags, "certificate-verified")
235+
}
236+
if len(flags) > 0 {
237+
words = append(words, fmt.Sprintf("[%s]", strings.Join(flags, ", ")))
238+
}
239+
240+
msg := strings.Join(words, " ")
241+
if fail {
242+
msg += ": " + st.Message
243+
}
244+
return msg
245+
}
246+
178247
type configurer struct{}
179248

180249
func (s configurer) SetIpPool(poolId string, message *mailgun.PlainMessage) {
181250
// TODO?
182251
}
183252

184253
func (s configurer) DisableTracking(message *mailgun.PlainMessage) {
185-
message.SetTracking(false)
254+
message.SetTracking(false) // untested
186255
}

0 commit comments

Comments
 (0)