Sovite.Core.Config (sovite v0.6.0)

Copy Markdown View Source

Loads, validates, and stores the Sovite configuration file.

The file is TOML. Every key is checked against a schema: unknown keys, wrong types, and invalid values are all reported together, each with the path of the bad key. See docs/configuration.md for the reference.

The running configuration is kept in :persistent_term. Read it with get/0.

Summary

Types

A DNS list for [screen], see Sovite.Abuse.DNSBL.

An [[ip_pool]]: see Sovite.Core.IPPools.

A rate limit: {count, window}, the window in milliseconds.

t()

A [[tenant]]: see Sovite.Core.Tenants.

Functions

Returns whether any listener offers AUTH.

The SASL mechanisms to offer: auth.mechanisms, or by default those the backend supports.

Returns the config file path: $SOVITE_CONFIG if set, otherwise /etc/sovite/sovite.toml.

Forgets the configuration of a server instance that stopped.

Returns the running configuration. Raises if none has been stored.

Returns the running configuration of a server instance, or nil.

Reads, parses, and validates the config file at path.

Like load/1, with shared (TOML) as the shared configuration instead of the file cluster.shared_config names: whether the server could run with a new shared configuration.

Parses and validates TOML config contents.

Stores config as the running configuration.

Stores config as the running configuration of one server instance (Sovite.Core.Supervisor), and as the running configuration. Parts that read it on each use, such as the web server, see reloads at once.

The DNS resolver for config: Sovite.DNS.InetRes with the [dns] nameservers, or the system's.

Returns whether TLS certificates are configured (files or ACME).

Validates a decoded config map (string keys, as produced by a TOML decoder), fills in defaults, and returns the config struct.

Types

dns_list()

@type dns_list() :: %{zone: String.t(), weight: integer(), codes: list()}

A DNS list for [screen], see Sovite.Abuse.DNSBL.

ip_pool()

@type ip_pool() :: %{
  name: String.t(),
  overflow: String.t() | nil,
  address: [
    %{
      address: :inet.ip_address(),
      helo: String.t() | nil,
      node: String.t() | nil,
      warmup_start: Date.t() | nil,
      warmup: [pos_integer()],
      daily_limit: pos_integer() | nil
    }
  ]
}

An [[ip_pool]]: see Sovite.Core.IPPools.

rate()

@type rate() :: {pos_integer(), pos_integer()}

A rate limit: {count, window}, the window in milliseconds.

t()

@type t() :: %Sovite.Core.Config{
  admin: %{
    enabled: boolean(),
    address: :inet.ip_address(),
    port: :inet.port_number(),
    token_file: Path.t(),
    allowed_networks: [Sovite.Net.network()],
    tls: boolean(),
    public_metrics: boolean(),
    web_root: Path.t() | nil
  },
  arc: %{
    verify: boolean(),
    seal: boolean(),
    domain: String.t() | nil,
    selector: String.t() | nil,
    trusted_sealers: [String.t()]
  },
  auth: map(),
  bounce: %{
    double_bounce_recipient: String.t() | nil,
    size_limit: pos_integer()
  },
  cluster: %{
    enabled: boolean(),
    node_name: String.t() | nil,
    address: :inet.ip_address() | nil,
    port: :inet.port_number(),
    seeds: [String.t()],
    cert_file: Path.t() | nil,
    key_file: Path.t() | nil,
    ca_file: Path.t() | nil,
    join_interval: pos_integer(),
    sync_interval: pos_integer(),
    shared_config: Path.t() | nil,
    queue_replicas: 0..2,
    handover_after: pos_integer(),
    destination_affinity: boolean()
  },
  database: %{
    adapter: :sqlite | :postgres | :mysql,
    path: Path.t(),
    url: String.t() | nil,
    pool_size: pos_integer(),
    ssl: boolean()
  },
  delivery: %{
    relayhost:
      %{host: String.t(), port: :inet.port_number(), mx: boolean()} | nil,
    max_deliveries: pos_integer(),
    destination_concurrency: pos_integer(),
    destination_rate_delay: pos_integer() | nil,
    max_recipients: pos_integer(),
    max_addresses: pos_integer(),
    ip_versions: [:ipv6 | :ipv4, ...],
    connect_timeout: pos_integer(),
    tls: tls_level(),
    tls_policy: %{required(String.t()) => tls_level()},
    tls_ca_file: Path.t() | nil,
    relayhost_username: String.t() | nil,
    relayhost_password: String.t() | nil,
    source_address: [:inet.ip_address()],
    ip_pool: String.t() | nil,
    connection_cache: boolean(),
    connection_cache_time: pos_integer(),
    chunking: boolean()
  },
  dkim: %{
    verify: boolean(),
    sign: boolean(),
    headers: [String.t()] | nil,
    expiration: pos_integer() | nil,
    key: [
      %{
        domain: String.t(),
        selector: String.t(),
        file: Path.t(),
        sign: boolean(),
        signing_key: Sovite.DKIM.SigningKey.t() | nil
      }
    ]
  },
  dmarc: %{
    verify: boolean(),
    policy: :report | :enforce,
    reports: boolean(),
    report_interval: pos_integer(),
    report_org: String.t(),
    report_from: String.t()
  },
  dns: %{
    nameservers: [:inet.ip_address()],
    port: :inet.port_number(),
    timeout: pos_integer(),
    dnssec: :auto | :on | :off
  },
  domains: %{
    local: [String.t()],
    relay: [String.t()],
    local_recipients: [String.t()] | nil,
    aliased: [String.t()],
    hosted: [String.t()]
  },
  greylist: %{
    enabled: boolean(),
    delay: pos_integer(),
    retry_window: pos_integer(),
    max_age: pos_integer()
  },
  ip_pool: [ip_pool()],
  listener: [
    %{
      address: :inet.ip_address(),
      port: :inet.port_number(),
      mode: :smtp | :submission | :submissions | :lmtp,
      auth: boolean(),
      require_tls: boolean(),
      require_auth: boolean(),
      screen: boolean(),
      tls_min_version: :"tlsv1.2" | :"tlsv1.3" | nil,
      tls_ciphers: [String.t()] | nil,
      proxy_protocol: boolean(),
      milters: [String.t()],
      content_filter: String.t() | nil,
      reinjection: boolean()
    }
  ],
  log: Sovite.Core.Logging.config(),
  maildir: %{local: String.t() | nil, mailbox: String.t() | nil},
  metrics: %{enabled: boolean()},
  milter: [
    %{
      name: String.t(),
      address: term(),
      default_action: :tempfail | :accept | :reject,
      connect_timeout: pos_integer(),
      command_timeout: pos_integer(),
      content_timeout: pos_integer()
    }
  ],
  mta_sts: %{
    enabled: boolean(),
    fetch_timeout: pos_integer(),
    serve: boolean(),
    address: :inet.ip_address(),
    port: :inet.port_number(),
    mode: :enforce | :testing | :none,
    mx: [String.t()],
    max_age: pos_integer()
  },
  outbound: %{
    enabled: boolean(),
    max_failure_percent: pos_integer(),
    min_failures: pos_integer(),
    window: pos_integer(),
    suspend_time: pos_integer()
  },
  pipe: %{
    required(String.t()) => %{
      command: [String.t(), ...],
      sandbox: [String.t(), ...] | nil,
      timeout: pos_integer(),
      directory: Path.t(),
      env: %{required(String.t()) => String.t()},
      trace_headers: boolean()
    }
  },
  policy: %{timeout: pos_integer(), default_action: String.t()},
  queue: %{
    directory: Path.t(),
    max_lifetime: pos_integer(),
    min_backoff: pos_integer(),
    max_backoff: pos_integer(),
    delay_warning: pos_integer() | nil
  },
  rate_limit: %{
    client_connections: rate() | nil,
    client_messages: rate() | nil,
    client_recipients: rate() | nil,
    user_messages: rate() | nil,
    user_recipients: rate() | nil
  },
  raw: map(),
  restrictions: %{required(atom()) => [String.t()]},
  routing: map(),
  screen: %{
    greet_delay: pos_integer() | nil,
    early_talker_weight: integer(),
    threshold: pos_integer(),
    allow_threshold: integer(),
    lookup_timeout: pos_integer(),
    cache_time: pos_integer(),
    dnsbl: [dns_list()],
    rhsbl: [
      %{
        zone: String.t(),
        weight: integer(),
        codes: list(),
        check: [:helo | :sender]
      }
    ]
  },
  sendmail: %{server: Sovite.Core.Transport.host(), origin: String.t() | nil},
  server: %{
    hostname: String.t(),
    authserv_id: String.t(),
    shutdown_timeout: pos_integer()
  },
  smtp: %{
    max_message_size: pos_integer(),
    max_recipients: pos_integer(),
    max_connections: pos_integer(),
    max_connections_per_ip: pos_integer(),
    max_errors: pos_integer(),
    command_timeout: pos_integer(),
    data_timeout: pos_integer(),
    bare_line_endings: :reject | :normalize,
    vrfy: boolean(),
    trusted_networks: [Sovite.Net.network()],
    max_hops: pos_integer(),
    requiretls: boolean(),
    smtputf8: boolean(),
    chunking: boolean(),
    binarymime: boolean(),
    dsn: boolean(),
    etrn: boolean(),
    mt_priority: boolean(),
    deliverby: boolean(),
    rrvs: boolean(),
    tarpit_after: pos_integer(),
    tarpit_delay: pos_integer(),
    forbid_unauth_pipelining: boolean(),
    content_filter: String.t() | nil,
    xclient_networks: [Sovite.Net.network()],
    xforward_networks: [Sovite.Net.network()],
    proxy_networks: [Sovite.Net.network()] | nil,
    proxy_timeout: pos_integer()
  },
  spf: %{
    verify: boolean(),
    helo: boolean(),
    reject_fail: boolean(),
    timeout: pos_integer()
  },
  srs: %{
    enabled: boolean(),
    domain: String.t(),
    secrets: [String.t()],
    max_age: pos_integer()
  },
  submission: %{
    strip_headers: [String.t()],
    future_release: boolean(),
    max_future_release: pos_integer()
  },
  tenant: [tenant()],
  tls: %{
    certificate: [%{cert_file: Path.t(), key_file: Path.t()}],
    min_version: :"tlsv1.2" | :"tlsv1.3",
    ciphers: [String.t()] | nil,
    reload_interval: pos_integer(),
    acme: map()
  },
  tls_rpt: %{
    reports: boolean(),
    report_interval: pos_integer(),
    report_org: String.t(),
    report_from: String.t(),
    contact_info: String.t()
  },
  tracing: %{
    enabled: boolean(),
    endpoint: String.t(),
    headers: %{optional(String.t()) => String.t()},
    sample_ratio: float(),
    service_name: String.t()
  }
}

tenant()

@type tenant() :: %{
  name: String.t(),
  domains: [String.t()],
  users: [String.t()],
  ip_pool: String.t() | nil,
  messages: rate() | nil,
  recipients: rate() | nil,
  max_message_size: pos_integer() | nil,
  max_recipients: pos_integer() | nil
}

A [[tenant]]: see Sovite.Core.Tenants.

tls_level()

@type tls_level() :: :none | :may | :encrypt | :verify | :dane

Functions

auth_enabled?(config)

@spec auth_enabled?(t() | map()) :: boolean()

Returns whether any listener offers AUTH.

auth_mechanisms(map)

@spec auth_mechanisms(t() | map()) :: [String.t()]

The SASL mechanisms to offer: auth.mechanisms, or by default those the backend supports.

default_path()

@spec default_path() :: Path.t()

Returns the config file path: $SOVITE_CONFIG if set, otherwise /etc/sovite/sovite.toml.

erase(instance)

@spec erase(term()) :: :ok

Forgets the configuration of a server instance that stopped.

get()

@spec get() :: t()

Returns the running configuration. Raises if none has been stored.

get(instance)

@spec get(term()) :: t() | nil

Returns the running configuration of a server instance, or nil.

load(path)

@spec load(Path.t()) :: {:ok, t()} | {:error, [Sovite.Core.Config.Error.t()]}

Reads, parses, and validates the config file at path.

When it sets cluster.shared_config, the settings of that file (the cluster's shared configuration, see Sovite.Core.Cluster.SharedConfig) come first, and those of path override them: tables are merged key by key, other values replaced. A shared file that does not exist yet is skipped.

load_with_shared(path, shared)

@spec load_with_shared(Path.t(), String.t()) ::
  {:ok, t()} | {:error, [Sovite.Core.Config.Error.t()]}

Like load/1, with shared (TOML) as the shared configuration instead of the file cluster.shared_config names: whether the server could run with a new shared configuration.

parse(contents)

@spec parse(String.t()) :: {:ok, t()} | {:error, [Sovite.Core.Config.Error.t()]}

Parses and validates TOML config contents.

put(config)

@spec put(t()) :: :ok

Stores config as the running configuration.

put(config, instance)

@spec put(t(), term()) :: :ok

Stores config as the running configuration of one server instance (Sovite.Core.Supervisor), and as the running configuration. Parts that read it on each use, such as the web server, see reloads at once.

resolver(map)

@spec resolver(t() | map()) :: Sovite.DNS.resolver()

The DNS resolver for config: Sovite.DNS.InetRes with the [dns] nameservers, or the system's.

tls_enabled?(config)

@spec tls_enabled?(t() | map()) :: boolean()

Returns whether TLS certificates are configured (files or ACME).

validate(map)

@spec validate(map()) :: {:ok, t()} | {:error, [Sovite.Core.Config.Error.t()]}

Validates a decoded config map (string keys, as produced by a TOML decoder), fills in defaults, and returns the config struct.