Files
3f5b3cdd98 feat: add configurable SMTP HELO hostname (#2146)
* feat: add configurable SMTP HELO hostname

Allow the SMTP HELO/EHLO hostname to be configured separately from
the SMTP server hostname.

This is useful when the SMTP server requires clients to identify
themselves with a fully qualified hostname different from the server
address.

* chore: remove vendored dependency changes

* Bump go-pkgz/notify to v1.4.0 and document SMTP_HELO_HOST

The HELOHost field lands in go-pkgz/notify v1.4.0, so the branch needs the
bump to compile; v1.3.0 in master has no such field. The example module is
tidied alongside, as any change to backend/go.mod requires.

Documents the parameter in the parameters table and, separately, in the email
setup page: what it does, that leaving it unset keeps the previous `localhost`
greeting, and the case it exists for, a relay refusing the greeting under
Postfix `reject_non_fqdn_helo_hostname`.

Also records the current limit: verification emails for email authentication
go through go-pkgz/auth's own sender, which has no equivalent setting, so the
greeting there is unchanged.

* Bump go-pkgz/auth to v2.2.0 and apply SMTP_HELO_HOST to verification email

The verification email sender had no way to set the greeting, so a relay that
refuses the HELO would accept notifications and still reject sign-in emails.
EmailParams gains HELOHost in go-pkgz/auth v2.2.0, so the same SMTP_HELO_HOST
now drives both paths.

The example module is tidied alongside, as any change to backend/go.mod
requires.

---------

Co-authored-by: oli <someone@somewhere.tld>
Co-authored-by: Dmitry Verkhoturov <paskal.07@gmail.com>
2026-08-19 02:52:39 -05:00

129 lines
3.2 KiB
Go

package stats
import "math"
// TTest performs a one-sample or two-sample (independent) Student's t-test.
//
// For a one-sample t-test, pass the sample data as data1, nil for data2,
// and the expected population mean as populationMean.
//
// For a two-sample independent t-test (assuming equal variance), pass both
// sample datasets. The populationMean parameter is ignored in this case.
//
// Returns the t statistic and the two-tailed p-value.
//
// https://en.wikipedia.org/wiki/Student%27s_t-test
func TTest(data1, data2 Float64Data, populationMean float64) (t float64, pvalue float64, err error) {
n1 := data1.Len()
if n1 == 0 {
return math.NaN(), math.NaN(), ErrEmptyInput
}
mean1, _ := Mean(data1)
// Two-sample independent t-test (equal variance)
if data2 != nil && data2.Len() > 0 {
n2 := data2.Len()
if n1+n2 < 3 {
return math.NaN(), math.NaN(), ErrBounds
}
mean2, _ := Mean(data2)
var1, _ := SampleVariance(data1)
var2, _ := SampleVariance(data2)
df := float64(n1 + n2 - 2)
pooledVar := (float64(n1-1)*var1 + float64(n2-1)*var2) / df
se := math.Sqrt(pooledVar * (1.0/float64(n1) + 1.0/float64(n2)))
t = (mean1 - mean2) / se
pvalue = 2 * tSf(math.Abs(t), df)
} else {
// One-sample t-test
if n1 < 2 {
return math.NaN(), math.NaN(), ErrBounds
}
sd, _ := StandardDeviationSample(data1)
if sd == 0 {
if mean1 == populationMean {
return 0, 1.0, nil
}
return math.NaN(), math.NaN(), ErrBounds
}
se := sd / math.Sqrt(float64(n1))
t = (mean1 - populationMean) / se
df := float64(n1 - 1)
pvalue = 2 * tSf(math.Abs(t), df)
}
return t, pvalue, nil
}
// tSf is the survival function for Student's t-distribution.
// It computes 1 - CDF(t, df) using the regularized incomplete beta function.
func tSf(t float64, df float64) float64 {
x := df / (df + t*t)
return 0.5 * regIncBeta(df/2.0, 0.5, x)
}
// regIncBeta computes the regularized incomplete beta function I_x(a, b)
// using a continued fraction approximation (Lentz's algorithm).
func regIncBeta(a, b, x float64) float64 {
if x == 0 || x == 1 {
return x
}
lbeta := lgammaBeta(a, b)
front := math.Exp(math.Log(x)*a+math.Log(1-x)*b-lbeta) / a
// Use Lentz's continued fraction algorithm
f := 1.0
c := 1.0
d := clampTiny(1.0 - (a+b)*x/(a+1))
d = 1.0 / d
f = d
for i := 1; i <= 200; i++ {
m := float64(i)
// Numerator for even step
num := m * (b - m) * x / ((a + 2*m - 1) * (a + 2*m))
d = clampTiny(1.0 + num*d)
c = clampTiny(1.0 + num/c)
d = 1.0 / d
f *= c * d
// Numerator for odd step
num = -(a + m) * (a + b + m) * x / ((a + 2*m) * (a + 2*m + 1))
d = clampTiny(1.0 + num*d)
c = clampTiny(1.0 + num/c)
d = 1.0 / d
delta := c * d
f *= delta
if math.Abs(delta-1.0) < 1e-10 {
break
}
}
return front * f
}
// clampTiny prevents division by zero in Lentz's continued fraction
// algorithm by replacing near-zero values with a small constant.
func clampTiny(v float64) float64 {
if math.Abs(v) < 1e-30 {
return 1e-30
}
return v
}
// lgammaBeta computes log(Beta(a, b)) = log(Gamma(a)) + log(Gamma(b)) - log(Gamma(a+b))
func lgammaBeta(a, b float64) float64 {
la, _ := math.Lgamma(a)
lb, _ := math.Lgamma(b)
lab, _ := math.Lgamma(a + b)
return la + lb - lab
}