getTaskMailProviders omits open_rate/click_rate when delivered == 0 (1190a940)

Author: runaway518Created Sep 2, 2026Updated Sep 2, 2026

Found while evaluating dev for a downstream fork. Regression introduced by 1190a940 ("Fix open/click rates using wrong denominator") in core/internal/service/batch_mail/stat_service.go.

getTaskMailProviders, lines 365-372:

go
if sends > 0 {
    provider["delivery_rate"] = public.Round(float64(delivered)/float64(sends)*100, 2)
    provider["bounce_rate"] = public.Round(float64(result["bounced"].Int())/float64(sends)*100, 2)
}
if delivered > 0 {
    provider["open_rate"] = public.Round(float64(result["opened"].Int())/float64(delivered)*100, 2)
    provider["click_rate"] = public.Round(float64(result["clicked"].Int())/float64(delivered)*100, 2)
}

open_rate and click_rate are computed in Go, not selected by the query — the field list is mail_provider, sends, delivered, opened, clicked, bounced. So when the if does not fire, the keys are absent from provider entirely rather than reporting zero, and /api/batch_mail/tracking/mail_provider returns provider objects with those fields missing.

Trigger: a provider with sends > 0 and delivered == 0 — every message bounced. That is precisely the situation an operator is looking at the dashboard to diagnose.

Regression: before this commit all four rates were inside if sends > 0, so open_rate was present and equal to opened / sends. Afterwards it can be absent.

The same commit did add else branches setting zero at its two other call sites — getTaskDashboard in the same file and overviewDashboard in maillog_stat/overview.go — so getTaskMailProviders is the odd one out.

Fix: an else on the delivered > 0 branch setting open_rate and click_rate to 0.0, matching the sibling aggregations.

Note the sends > 0 branch has no else either, but sends comes from count(*) with GROUP BY mail_provider, so it is always at least 1 and that branch is unreachable — no change needed there.