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
fix: address golangci-lint errors
dunkirk.sh
2 months ago
cf8e46b9
5df1b3b8
verified
This commit was signed with the committer's
known signature
.
dunkirk.sh
SSH Key Fingerprint:
SHA256:DqcG0RXYExE26KiWo3VxJnsxswN1QNfTBvB+bdSpk80=
+27
-22
4 changed files
expand all
collapse all
unified
split
scheduler
scheduler.go
store
tracking.go
tracking_test.go
web
handlers.go
+7
-7
scheduler/scheduler.go
···
20
20
emailsPerSecondPerUser = emailsPerMinutePerUser / 60.0
21
21
22
22
// Cleanup intervals
23
23
-
cleanupInterval = 24 * time.Hour
24
24
-
seenItemsRetention = 6 * 30 * 24 * time.Hour // 6 months
25
25
-
itemMaxAge = 3 * 30 * 24 * time.Hour // 3 months
26
26
-
emailSendsRetention = 6 * 30 // 6 months in days
23
23
+
cleanupInterval = 24 * time.Hour
24
24
+
seenItemsRetention = 6 * 30 * 24 * time.Hour // 6 months
25
25
+
itemMaxAge = 3 * 30 * 24 * time.Hour // 3 months
26
26
+
emailSendsRetention = 6 * 30 // 6 months in days
27
27
28
28
// Item limits
29
29
minItemsForDigest = 5
30
30
31
31
// Engagement tracking
32
32
-
inactivityThreshold = 90 // days without opens
32
32
+
inactivityThreshold = 90 // days without opens
33
33
minSendsBeforeDeactivate = 3 // minimum sends before considering deactivation
34
34
)
35
35
···
425
425
s.logger.Warn("failed to record email send", "err", err)
426
426
}
427
427
s.logger.Debug("sendDigestAndMarkSeen: recorded email send")
428
428
-
428
428
+
429
429
// Build keep-alive URL
430
430
keepAliveURL := ""
431
431
if trackingToken != "" {
432
432
keepAliveURL = s.originURL + "/keep-alive/" + trackingToken
433
433
}
434
434
-
434
434
+
435
435
// Send email - if this fails, transaction will rollback
436
436
s.logger.Debug("sendDigestAndMarkSeen: calling mailer.Send", "to", cfg.Email)
437
437
if err := s.mailer.Send(cfg.Email, subject, htmlBody, textBody, unsubToken, dashboardURL, keepAliveURL); err != nil {
+11
-12
store/tracking.go
···
9
9
)
10
10
11
11
type EmailSend struct {
12
12
-
ID int64
13
13
-
ConfigID int64
14
14
-
Recipient string
15
15
-
Subject string
16
16
-
TrackingToken string
17
17
-
SentAt time.Time
18
18
-
Bounced bool
19
19
-
BounceReason sql.NullString
20
20
-
Opened bool
21
21
-
OpenedAt sql.NullTime
12
12
+
ID int64
13
13
+
ConfigID int64
14
14
+
Recipient string
15
15
+
Subject string
16
16
+
TrackingToken string
17
17
+
SentAt time.Time
18
18
+
Bounced bool
19
19
+
BounceReason sql.NullString
20
20
+
Opened bool
21
21
+
OpenedAt sql.NullTime
22
22
}
23
23
24
24
// RecordEmailSend records an email send with optional tracking token
···
111
111
if err != nil {
112
112
return nil, fmt.Errorf("query inactive configs: %w", err)
113
113
}
114
114
-
defer rows.Close()
114
114
+
defer func() { _ = rows.Close() }()
115
115
116
116
var configIDs []int64
117
117
for rows.Next() {
···
220
220
221
221
return nil
222
222
}
223
223
-
+5
-1
store/tracking_test.go
···
8
8
9
9
func TestEmailTracking(t *testing.T) {
10
10
db := setupTestDB(t)
11
11
-
defer db.Close()
11
11
+
defer func() {
12
12
+
if err := db.Close(); err != nil {
13
13
+
t.Errorf("failed to close db: %v", err)
14
14
+
}
15
15
+
}()
12
16
13
17
ctx := context.Background()
14
18
+4
-2
web/handlers.go
···
704
704
// Return success message
705
705
w.Header().Set("Content-Type", "text/html; charset=utf-8")
706
706
w.WriteHeader(http.StatusOK)
707
707
-
fmt.Fprintf(w, `<!DOCTYPE html>
707
707
+
if _, err := fmt.Fprintf(w, `<!DOCTYPE html>
708
708
<html>
709
709
<head>
710
710
<meta charset="utf-8">
···
720
720
<div class="success">✓ Success!</div>
721
721
<div class="details">Your digest will stay active until <strong>%s</strong>.</div>
722
722
</body>
723
723
-
</html>`, expiresAt)
723
723
+
</html>`, expiresAt); err != nil {
724
724
+
s.logger.Error("failed to write response", "error", err)
725
725
+
}
724
726
}
725
727
726
728
func (s *Server) handle404(w http.ResponseWriter, r *http.Request) {