package main import ( "bytes" "encoding/json" "errors" "fmt" "io" "os" "path/filepath" "time" ) const ( smtpDenyActiveStateFile = "active-state.json" smtpDenyRefreshStateFile = "refresh-state.json" ) type smtpDenyActiveState struct { ActivatedAt time.Time `json:"activated_at"` Source string `json:"source"` BundleGeneratedAt time.Time `json:"bundle_generated_at,omitempty"` BundleExpiresAt time.Time `json:"bundle_expires_at,omitempty"` LastRemoteCurrentAt time.Time `json:"last_remote_current_at,omitempty"` } type smtpDenyRefreshState struct { CompletedAt time.Time `json:"completed_at"` Successful bool `json:"successful"` ActiveSource string `json:"active_source"` } func readSMTPDenyActiveState(directory string) (smtpDenyActiveState, error) { state := smtpDenyActiveState{} if err := readStrictJSON(filepath.Join(directory, smtpDenyActiveStateFile), &state); err != nil { return smtpDenyActiveState{}, err } return state, nil } func readSMTPDenyRefreshState(directory string) (smtpDenyRefreshState, error) { state := smtpDenyRefreshState{} if err := readStrictJSON(filepath.Join(directory, smtpDenyRefreshStateFile), &state); err != nil { return smtpDenyRefreshState{}, err } return state, nil } func readStrictJSON(path string, destination any) error { contents, err := os.ReadFile(path) if err != nil { return err } decoder := json.NewDecoder(bytes.NewReader(contents)) decoder.DisallowUnknownFields() if err := decoder.Decode(destination); err != nil { return err } if err := decoder.Decode(&struct{}{}); err != io.EOF { return errors.New("state contains trailing data") } return nil } func (state smtpDenyActiveState) validAt(now time.Time, maximumLifetime time.Duration) bool { if state.Source != "current" && state.Source != "cache" { return false } if state.ActivatedAt.IsZero() || state.ActivatedAt.After(now.Add(5*time.Minute)) { return false } if state.BundleGeneratedAt.IsZero() || state.BundleGeneratedAt.After(now.Add(5*time.Minute)) { return false } if !state.BundleExpiresAt.After(now) || !state.BundleExpiresAt.After(state.BundleGeneratedAt) { return false } if state.BundleExpiresAt.Sub(state.BundleGeneratedAt) > maximumLifetime { return false } if !state.LastRemoteCurrentAt.IsZero() && state.LastRemoteCurrentAt.After(now.Add(5*time.Minute)) { return false } if !state.LastRemoteCurrentAt.IsZero() && state.LastRemoteCurrentAt.After(state.ActivatedAt) { return false } if state.Source == "current" && state.LastRemoteCurrentAt.IsZero() { return false } return true } func completeSMTPDenyRefresh(operationErr error) error { return completeSMTPDenyRefreshAt(smtpDenyStateDirectory, time.Now().UTC(), operationErr) } func completeSMTPDenyRefreshAt(directory string, completedAt time.Time, operationErr error) error { state := smtpDenyRefreshState{ CompletedAt: completedAt, Successful: operationErr == nil, } if active, err := readSMTPDenyActiveState(directory); err == nil { state.ActiveSource = active.Source } contents, err := json.Marshal(state) if err != nil { return errors.Join(operationErr, err) } if err := writeAtomicOwned(filepath.Join(directory, smtpDenyRefreshStateFile), append(contents, '\n'), 0600, os.Getuid(), os.Getgid()); err != nil { return errors.Join(operationErr, fmt.Errorf("write SMTP deny refresh state: %w", err)) } return operationErr }