tangled
alpha
login
or
join now
dunkirk.sh
/
herald
1
fork
atom
rss email digests over ssh because you're a cool kid
herald.dunkirk.sh
go
rss
rss-reader
ssh
charm
1
fork
atom
overview
issues
pulls
pipelines
feat: add more fancy headers to email
dunkirk.sh
2 months ago
73a97f1a
d390ee28
verified
This commit was signed with the committer's
known signature
.
dunkirk.sh
SSH Key Fingerprint:
SHA256:DqcG0RXYExE26KiWo3VxJnsxswN1QNfTBvB+bdSpk80=
+45
-1
2 changed files
expand all
collapse all
unified
split
email
send.go
web
handlers.go
+11
-1
email/send.go
···
138
138
headers["MIME-Version"] = "1.0"
139
139
headers["Content-Type"] = fmt.Sprintf("multipart/alternative; boundary=%q", boundary)
140
140
141
141
-
// Add RFC 8058 unsubscribe headers
141
141
+
// RFC 2369 list headers
142
142
+
headers["List-Id"] = fmt.Sprintf("<herald.%s>", m.cfg.Host)
143
143
+
headers["List-Archive"] = fmt.Sprintf("<%s>", dashboardURL)
144
144
+
headers["List-Post"] = "NO"
145
145
+
146
146
+
// RFC 8058 unsubscribe headers
142
147
if unsubToken != "" {
143
148
unsubURL := m.unsubBaseURL + "/unsubscribe/" + unsubToken
144
149
headers["List-Unsubscribe"] = fmt.Sprintf("<%s>", unsubURL)
145
150
headers["List-Unsubscribe-Post"] = "List-Unsubscribe=One-Click"
146
151
}
152
152
+
153
153
+
// Bulk mail and auto-generated headers for better deliverability
154
154
+
headers["Precedence"] = "bulk"
155
155
+
headers["Auto-Submitted"] = "auto-generated"
156
156
+
headers["X-Mailer"] = "Herald"
147
157
148
158
var msg strings.Builder
149
159
for k, v := range headers {
+34
web/handlers.go
···
514
514
return
515
515
}
516
516
517
517
+
// RFC 8058: Check for one-click unsubscribe format
518
518
+
oneClick := r.FormValue("List-Unsubscribe")
519
519
+
if oneClick == "One-Click" {
520
520
+
// One-click unsubscribe: deactivate config without rendering HTML
521
521
+
cfg, err := s.store.GetConfigByToken(ctx, token)
522
522
+
if err != nil {
523
523
+
if errors.Is(err, sql.ErrNoRows) {
524
524
+
http.Error(w, "Invalid token", http.StatusNotFound)
525
525
+
return
526
526
+
}
527
527
+
s.logger.Error("get config by token", "err", err)
528
528
+
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
529
529
+
return
530
530
+
}
531
531
+
532
532
+
if err := s.store.DeactivateConfig(ctx, cfg.ID); err != nil {
533
533
+
s.logger.Error("deactivate config", "err", err)
534
534
+
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
535
535
+
return
536
536
+
}
537
537
+
538
538
+
if err := s.store.DeleteToken(ctx, token); err != nil {
539
539
+
s.logger.Warn("delete token", "err", err)
540
540
+
}
541
541
+
542
542
+
s.logger.Info("config deactivated via one-click", "config_id", cfg.ID, "filename", cfg.Filename)
543
543
+
544
544
+
// RFC 8058: Return success without redirect
545
545
+
w.WriteHeader(http.StatusOK)
546
546
+
_, _ = w.Write([]byte("Unsubscribed"))
547
547
+
return
548
548
+
}
549
549
+
550
550
+
// Manual unsubscribe flow
517
551
action := r.FormValue("action")
518
552
if action != "deactivate" && action != "delete" {
519
553
http.Error(w, "Invalid action", http.StatusBadRequest)