113 lines
3.4 KiB
Go
113 lines
3.4 KiB
Go
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
|
|
}
|