Agreed by the owner on 2026-10-13 (the local day, no "UTC" beside a
time); the zone is the container's TZ in .env and Settings only shows it,
his "Да" of 2026-10-17. Mockups c205106.
Every time the panel shows is formatted in time.Local and says no zone:
the logs' Time and the DMARC Received columns, a message's timeline,
Overview's "Updated", Health's certificate and renewal, the filter lists,
DMARC reports and windows, the auto limit's last recalculation. Stored
instants stay UTC. The days counted are the zone's: delivery_log_days and
sender_check_days key their rows by the local date, the sender check cuts
downtime at local midnights (byDay), the log filters' days run from local
midnight to local midnight. Notification mail and the test message write
the zone's abbreviation after a time - they are read away from the panel.
DMARC's one-day window label stays the reporter's (UTC) day.
Settings box 05, Time zone, read-only: the name (UTC without TZ), its
offset, and a TZ the image cannot load said to leave the times in UTC
(time/zoneinfo_unix.go, initLocal) rather than named. TZ is passed by the
compose file ("${TZ:-}"), in .env.example and the guide's table; tzdata is
named in the image (supervisor's python3.13 brings it today). Postfix
takes TZ through import_environment, so mail.log's stamps are the same
zone, which closes the trap the roadmap wrote down: the tailer has always
read them in time.Local.
Tests: a TestMain runs view, handlers, store and cmd/panel in UTC whatever
the machine's zone (as logtail's); TestSettingsShowsTheTimeZone,
TestTimesAreTheContainersZone, TestByDayInTheContainersZone, a Moscow day
in TestQueryInboundSpamFilters; envdoc's pattern takes two-letter names.
e2e time_zone_is_the_containers recreates the stand with
TZ=Europe/Moscow: date says MSK, Settings names the zone and UTC+03:00, a
test message's page gives its time and its mail.log lines in Moscow's.
go vet + go test (26 packages) and make e2e on the test server for this
tree.
Docs: guide (§ Settings, the TZ row, the days of Health and the logs),
architecture, schema-migrations, help (word for word with the mockup),
CHANGELOG [Unreleased] § Added, roadmap row done with the decision.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
123 lines
3.7 KiB
Go
123 lines
3.7 KiB
Go
// Package handlers implements the panel's authenticated HTTP handlers.
|
|
package handlers
|
|
|
|
import (
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/mixeme/selfpost/internal/app"
|
|
"github.com/mixeme/selfpost/internal/dmarc"
|
|
"github.com/mixeme/selfpost/internal/dnscheck"
|
|
"github.com/mixeme/selfpost/internal/domain"
|
|
"github.com/mixeme/selfpost/internal/health"
|
|
"github.com/mixeme/selfpost/internal/inbound"
|
|
"github.com/mixeme/selfpost/internal/notify"
|
|
"github.com/mixeme/selfpost/internal/postfix"
|
|
"github.com/mixeme/selfpost/internal/store"
|
|
"github.com/mixeme/selfpost/internal/web/auth"
|
|
"github.com/mixeme/selfpost/internal/web/view"
|
|
)
|
|
|
|
// Config holds handler-specific panel configuration.
|
|
type Config struct {
|
|
Hostname string
|
|
SubmissionEnabled bool
|
|
MailLogPath string
|
|
DataDir string
|
|
DBPath string
|
|
DeployRoot string
|
|
Version string
|
|
TLSCertFile string
|
|
TLSServedCertFile string
|
|
TLSReloadInterval time.Duration
|
|
SMTPBanner string
|
|
TimeZone string
|
|
SignSocket string
|
|
SignLocalSocket string
|
|
JournalSocket string
|
|
SenderSocket string
|
|
// Level-1 Postfix anvil backstop (env RATE_LIMIT_*), shown in the panel
|
|
// and used to cap domain/app level-2 ceilings (guide § Rate limiting).
|
|
RateLimitMessagesPerIP int
|
|
RateLimitWindowSeconds int
|
|
// RetryPolicy is this Postfix's deferred-mail timings, snapshotted once
|
|
// when the HTTP role starts (architecture.md). The Mail queue card and
|
|
// delivery history read it from here; they never call postconf.
|
|
RetryPolicy postfix.RetryPolicy
|
|
// InboundEnabled mirrors INBOUND_RELAY_ENABLE: the inbound panel and
|
|
// routes exist only when this is true.
|
|
InboundEnabled bool
|
|
// InboundAntispamMilter mirrors INBOUND_ANTISPAM_MILTER and
|
|
// InboundAntispamAction INBOUND_ANTISPAM_MILTER_ACTION ("accept",
|
|
// "tempfail", or "" for a value build/postfix-config.sh would refuse). The
|
|
// filter is on only when the inbound relay is on and the milter is set.
|
|
InboundAntispamMilter string
|
|
InboundAntispamAction string
|
|
// DMARCEnabled mirrors DMARC_REPORTS_ENABLE.
|
|
DMARCEnabled bool
|
|
// SendLogRetentionEnvDefault is SEND_LOG_RETENTION_DAYS at panel start; used
|
|
// as bootstrap and fallback when the settings row is missing or invalid.
|
|
SendLogRetentionEnvDefault int
|
|
// InboundSpamLogRetentionEnvDefault is INBOUND_SPAM_LOG_RETENTION_DAYS at
|
|
// panel start, the same bootstrap and fallback for the inbound journal.
|
|
InboundSpamLogRetentionEnvDefault int
|
|
}
|
|
|
|
// Handlers holds dependencies for authenticated panel routes.
|
|
type Handlers struct {
|
|
store *store.Store
|
|
domains *domain.Service
|
|
apps *app.Service
|
|
inbound *inbound.Service
|
|
lists *inbound.Lists
|
|
dmarc *dmarc.Service
|
|
cfg Config
|
|
view *view.Engine
|
|
dns *dnscheck.Checker
|
|
machine *health.MachineSampler
|
|
auth *auth.Module
|
|
// testMail sends the panel's test message (notify.Mailer; tests
|
|
// substitute one that records).
|
|
testMail testMailer
|
|
}
|
|
|
|
// testMailer is what sends the test message: notify.Mailer.SendWithID.
|
|
type testMailer interface {
|
|
SendWithID(to, subject, body, id string) error
|
|
}
|
|
|
|
// New builds authenticated panel handlers.
|
|
func New(
|
|
st *store.Store,
|
|
domains *domain.Service,
|
|
apps *app.Service,
|
|
inboundSvc *inbound.Service,
|
|
lists *inbound.Lists,
|
|
dmarcSvc *dmarc.Service,
|
|
cfg Config,
|
|
v *view.Engine,
|
|
dns *dnscheck.Checker,
|
|
machine *health.MachineSampler,
|
|
a *auth.Module,
|
|
) *Handlers {
|
|
return &Handlers{
|
|
store: st,
|
|
domains: domains,
|
|
apps: apps,
|
|
inbound: inboundSvc,
|
|
lists: lists,
|
|
dmarc: dmarcSvc,
|
|
cfg: cfg,
|
|
view: v,
|
|
dns: dns,
|
|
machine: machine,
|
|
auth: a,
|
|
// The test message goes as notifications go (test-email).
|
|
testMail: notify.NewMailer(cfg.Hostname),
|
|
}
|
|
}
|
|
|
|
func logf(format string, args ...any) {
|
|
log.Printf(format, args...)
|
|
}
|