add comment about the sconf config file format at the top of the config files

hopefully this helps admins editing the file and prevent mistakes about config files.

for issue #56 by kikoreis, thanks!
This commit is contained in:
Mechiel Lukkien 2023-09-21 08:59:10 +02:00
parent 0d8603f9e1
commit 9534e464f9
No known key found for this signature in database
8 changed files with 65 additions and 25 deletions

View file

@ -34,7 +34,7 @@ func Port(port, fallback int) int {
// Static is a parsed form of the mox.conf configuration file, before converting it // Static is a parsed form of the mox.conf configuration file, before converting it
// into a mox.Config after additional processing. // into a mox.Config after additional processing.
type Static struct { type Static struct {
DataDir string `sconf-doc:"Directory where all data is stored, e.g. queue, accounts and messages, ACME TLS certs/keys. If this is a relative path, it is relative to the directory of mox.conf."` DataDir string `sconf-doc:"NOTE: This config file is in 'sconf' format. Indent with tabs. Comments must be on their own line, they don't end a line. Do not escape or quote strings. Details: https://pkg.go.dev/github.com/mjl-/sconf.\n\n\nDirectory where all data is stored, e.g. queue, accounts and messages, ACME TLS certs/keys. If this is a relative path, it is relative to the directory of mox.conf."`
LogLevel string `sconf-doc:"Default log level, one of: error, info, debug, trace, traceauth, tracedata. Trace logs SMTP and IMAP protocol transcripts, with traceauth also messages with passwords, and tracedata on top of that also the full data exchanges (full messages), which can be a large amount of data."` LogLevel string `sconf-doc:"Default log level, one of: error, info, debug, trace, traceauth, tracedata. Trace logs SMTP and IMAP protocol transcripts, with traceauth also messages with passwords, and tracedata on top of that also the full data exchanges (full messages), which can be a large amount of data."`
PackageLogLevels map[string]string `sconf:"optional" sconf-doc:"Overrides of log level per package (e.g. queue, smtpclient, smtpserver, imapserver, spf, dkim, dmarc, dmarcdb, autotls, junk, mtasts, tlsrpt)."` PackageLogLevels map[string]string `sconf:"optional" sconf-doc:"Overrides of log level per package (e.g. queue, smtpclient, smtpserver, imapserver, spf, dkim, dmarc, dmarcdb, autotls, junk, mtasts, tlsrpt)."`
User string `sconf:"optional" sconf-doc:"User to switch to after binding to all sockets as root. Default: mox. If the value is not a known user, it is parsed as integer and used as uid and gid."` User string `sconf:"optional" sconf-doc:"User to switch to after binding to all sockets as root. Default: mox. If the value is not a known user, it is parsed as integer and used as uid and gid."`
@ -94,7 +94,7 @@ type SpecialUseMailboxes struct {
// Dynamic is the parsed form of domains.conf, and is automatically reloaded when changed. // Dynamic is the parsed form of domains.conf, and is automatically reloaded when changed.
type Dynamic struct { type Dynamic struct {
Domains map[string]Domain `sconf-doc:"Domains for which email is accepted. For internationalized domains, use their IDNA names in UTF-8."` Domains map[string]Domain `sconf-doc:"NOTE: This config file is in 'sconf' format. Indent with tabs. Comments must be on their own line, they don't end a line. Do not escape or quote strings. Details: https://pkg.go.dev/github.com/mjl-/sconf.\n\n\nDomains for which email is accepted. For internationalized domains, use their IDNA names in UTF-8."`
Accounts map[string]Account `sconf-doc:"Accounts to which email can be delivered. An account can accept email for multiple domains, for multiple localparts, and deliver to multiple mailboxes."` Accounts map[string]Account `sconf-doc:"Accounts to which email can be delivered. An account can accept email for multiple domains, for multiple localparts, and deliver to multiple mailboxes."`
WebDomainRedirects map[string]string `sconf:"optional" sconf-doc:"Redirect all requests from domain (key) to domain (value). Always redirects to HTTPS. For plain HTTP redirects, use a WebHandler with a WebRedirect."` WebDomainRedirects map[string]string `sconf:"optional" sconf-doc:"Redirect all requests from domain (key) to domain (value). Always redirects to HTTPS. For plain HTTP redirects, use a WebHandler with a WebRedirect."`
WebHandlers []WebHandler `sconf:"optional" sconf-doc:"Handle webserver requests by serving static files, redirecting or reverse-proxying HTTP(s). The first matching WebHandler will handle the request. Built-in handlers, e.g. for account, admin, autoconfig and mta-sts always run first. If no handler matches, the response status code is file not found (404). If functionality you need is missng, simply forward the requests to an application that can provide the needed functionality."` WebHandlers []WebHandler `sconf:"optional" sconf-doc:"Handle webserver requests by serving static files, redirecting or reverse-proxying HTTP(s). The first matching WebHandler will handle the request. Built-in handlers, e.g. for account, admin, autoconfig and mta-sts always run first. If no handler matches, the response status code is file not found (404). If functionality you need is missng, simply forward the requests to an application that can provide the needed functionality."`

View file

@ -13,6 +13,11 @@ describe-static" and "mox config describe-domains":
# mox.conf # mox.conf
# NOTE: This config file is in 'sconf' format. Indent with tabs. Comments must be
# on their own line, they don't end a line. Do not escape or quote strings.
# Details: https://pkg.go.dev/github.com/mjl-/sconf.
# Directory where all data is stored, e.g. queue, accounts and messages, ACME TLS # Directory where all data is stored, e.g. queue, accounts and messages, ACME TLS
# certs/keys. If this is a relative path, it is relative to the directory of # certs/keys. If this is a relative path, it is relative to the directory of
# mox.conf. # mox.conf.
@ -526,6 +531,11 @@ describe-static" and "mox config describe-domains":
# domains.conf # domains.conf
# NOTE: This config file is in 'sconf' format. Indent with tabs. Comments must be
# on their own line, they don't end a line. Do not escape or quote strings.
# Details: https://pkg.go.dev/github.com/mjl-/sconf.
# Domains for which email is accepted. For internationalized domains, use their # Domains for which email is accepted. For internationalized domains, use their
# IDNA names in UTF-8. # IDNA names in UTF-8.
Domains: Domains:

4
go.mod
View file

@ -4,7 +4,7 @@ go 1.18
require ( require (
github.com/mjl-/bstore v0.0.2 github.com/mjl-/bstore v0.0.2
github.com/mjl-/sconf v0.0.4 github.com/mjl-/sconf v0.0.5
github.com/mjl-/sherpa v0.6.6 github.com/mjl-/sherpa v0.6.6
github.com/mjl-/sherpadoc v0.0.12 github.com/mjl-/sherpadoc v0.0.12
github.com/mjl-/sherpaprom v0.0.2 github.com/mjl-/sherpaprom v0.0.2
@ -22,7 +22,7 @@ require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mjl-/xfmt v0.0.0-20190521151243-39d9c00752ce // indirect github.com/mjl-/xfmt v0.0.2 // indirect
github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect

8
go.sum
View file

@ -147,8 +147,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mjl-/bstore v0.0.2 h1:4fdpIOY/+Dv1dBHyzdqa4PD90p8Mz86FeyRpI4qcehw= github.com/mjl-/bstore v0.0.2 h1:4fdpIOY/+Dv1dBHyzdqa4PD90p8Mz86FeyRpI4qcehw=
github.com/mjl-/bstore v0.0.2/go.mod h1:/cD25FNBaDfvL/plFRxI3Ba3E+wcB0XVOS8nJDqndg0= github.com/mjl-/bstore v0.0.2/go.mod h1:/cD25FNBaDfvL/plFRxI3Ba3E+wcB0XVOS8nJDqndg0=
github.com/mjl-/sconf v0.0.4 h1:uyfn4vv5qOULSgiwQsPbbgkiONKnMFMsSOhsHfAiYwI= github.com/mjl-/sconf v0.0.5 h1:4CMUTENpSnaeP2g6RKtrs8udTxnJgjX2MCCovxGId6s=
github.com/mjl-/sconf v0.0.4/go.mod h1:ezf7YOn7gtClo8y71SqgZKaEkyMQ5Te7vkv4PmTTfwM= github.com/mjl-/sconf v0.0.5/go.mod h1:uF8OdWtLT8La3i4ln176i1pB0ps9pXGCaABEU55ZkE0=
github.com/mjl-/sherpa v0.6.6 h1:4Xc4/s12W2I/C1genIL8l4ZCLMsTo8498cPSjQcIHGc= github.com/mjl-/sherpa v0.6.6 h1:4Xc4/s12W2I/C1genIL8l4ZCLMsTo8498cPSjQcIHGc=
github.com/mjl-/sherpa v0.6.6/go.mod h1:dSpAOdgpwdqQZ72O4n3EHo/tR68eKyan8tYYraUMPNc= github.com/mjl-/sherpa v0.6.6/go.mod h1:dSpAOdgpwdqQZ72O4n3EHo/tR68eKyan8tYYraUMPNc=
github.com/mjl-/sherpadoc v0.0.0-20190505200843-c0a7f43f5f1d/go.mod h1:5khTKxoKKNXcB8bkVUO6GlzC7PFtMmkHq578lPbmnok= github.com/mjl-/sherpadoc v0.0.0-20190505200843-c0a7f43f5f1d/go.mod h1:5khTKxoKKNXcB8bkVUO6GlzC7PFtMmkHq578lPbmnok=
@ -158,8 +158,8 @@ github.com/mjl-/sherpaprom v0.0.2 h1:1dlbkScsNafM5jURI44uiWrZMSwfZtcOFEEq7vx2C1Y
github.com/mjl-/sherpaprom v0.0.2/go.mod h1:cl5nMNOvqhzMiQJ2FzccQ9ReivjHXe53JhOVkPfSvw4= github.com/mjl-/sherpaprom v0.0.2/go.mod h1:cl5nMNOvqhzMiQJ2FzccQ9ReivjHXe53JhOVkPfSvw4=
github.com/mjl-/sherpats v0.0.4 h1:rZkJO4YV4MfuCi3E4ifzbhpY6VgZgsQoOcL04ABEib4= github.com/mjl-/sherpats v0.0.4 h1:rZkJO4YV4MfuCi3E4ifzbhpY6VgZgsQoOcL04ABEib4=
github.com/mjl-/sherpats v0.0.4/go.mod h1:MoNZJtLmu8oCZ4Ocv5vZksENN4pp6/SJMlg9uTII4KA= github.com/mjl-/sherpats v0.0.4/go.mod h1:MoNZJtLmu8oCZ4Ocv5vZksENN4pp6/SJMlg9uTII4KA=
github.com/mjl-/xfmt v0.0.0-20190521151243-39d9c00752ce h1:oyFmIHo3GLWZzb0odAzN9QUy0MTW6P8JaNRnNVGCBCk= github.com/mjl-/xfmt v0.0.2 h1:6dLgd6U3bmDJKtTxsaSYYyMaORoO4hKBAJo4XKkPRko=
github.com/mjl-/xfmt v0.0.0-20190521151243-39d9c00752ce/go.mod h1:DIEOLmETMQHHr4OgwPG7iC37rDiN9MaZIZxNm5hBtL8= github.com/mjl-/xfmt v0.0.2/go.mod h1:DIEOLmETMQHHr4OgwPG7iC37rDiN9MaZIZxNm5hBtL8=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=

View file

@ -17,6 +17,7 @@ type writeError struct{ error }
type writer struct { type writer struct {
out *bufio.Writer out *bufio.Writer
wrote int
prefix string prefix string
keepZero bool // If set, we also write zero values. keepZero bool // If set, we also write zero values.
docs bool // If set, we write comments. docs bool // If set, we write comments.
@ -33,8 +34,9 @@ func (w *writer) check(err error) {
} }
func (w *writer) write(s string) { func (w *writer) write(s string) {
_, err := w.out.WriteString(s) n, err := w.out.WriteString(s)
w.check(err) w.check(err)
w.wrote += n
} }
func (w *writer) flush() { func (w *writer) flush() {
@ -108,7 +110,7 @@ func isEmptyStruct(v reflect.Value) bool {
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
ft := t.Field(i) ft := t.Field(i)
tag := ft.Tag.Get("sconf") tag := ft.Tag.Get("sconf")
if isIgnore(tag) { if !ft.IsExported() || isIgnore(tag) {
continue continue
} }
if !isOptional(tag) { if !isOptional(tag) {
@ -134,7 +136,7 @@ func isZeroIgnored(v reflect.Value) bool {
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
ft := t.Field(i) ft := t.Field(i)
tag := ft.Tag.Get("sconf") tag := ft.Tag.Get("sconf")
if isIgnore(tag) { if !ft.IsExported() || isIgnore(tag) {
continue continue
} }
if !isZeroIgnored(v.Field(i)) { if !isZeroIgnored(v.Field(i)) {
@ -153,7 +155,7 @@ func (w *writer) describeStruct(v reflect.Value) {
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
f := t.Field(i) f := t.Field(i)
fv := v.Field(i) fv := v.Field(i)
if isIgnore(f.Tag.Get("sconf")) { if !f.IsExported() || isIgnore(f.Tag.Get("sconf")) {
continue continue
} }
if !w.keepZero && isOptional(f.Tag.Get("sconf")) && isZeroIgnored(fv) { if !w.keepZero && isOptional(f.Tag.Get("sconf")) && isZeroIgnored(fv) {
@ -163,11 +165,33 @@ func (w *writer) describeStruct(v reflect.Value) {
doc := f.Tag.Get("sconf-doc") doc := f.Tag.Get("sconf-doc")
optional := isOptional(f.Tag.Get("sconf")) optional := isOptional(f.Tag.Get("sconf"))
if doc != "" || optional { if doc != "" || optional {
s := "\n" + w.prefix + "# " + doc s := "\n"
if w.wrote == 0 {
// No empty line at start of file.
s = ""
}
// Treat two blank lines as section separator: the comments are not joined with
// lines with just "#", but instead with empty lines. To allow a hack where the
// first field of a config struct gives some context about the file.
sections := strings.Split(doc, "\n\n\n")
for si, section := range sections {
if si > 0 {
s += "\n\n\n"
}
for i, line := range strings.Split(section, "\n") {
if i > 0 {
s += "\n"
}
s += w.prefix + "#"
if line != "" {
s += " " + line
}
}
}
if optional { if optional {
opt := "(optional)" opt := "(optional)"
if doc != "" { if !strings.HasSuffix(doc, " ") {
opt = " " + opt s += " "
} }
s += opt s += opt
} }

View file

@ -245,8 +245,8 @@ func (p *parser) parseStruct0(v reflect.Value) {
if vv == zeroValue { if vv == zeroValue {
p.stop(fmt.Sprintf("unknown key %q", k)) p.stop(fmt.Sprintf("unknown key %q", k))
} }
if ft, _ := t.FieldByName(k); isIgnore(ft.Tag.Get("sconf")) { if ft, _ := t.FieldByName(k); !ft.IsExported() || isIgnore(ft.Tag.Get("sconf")) {
p.stop(fmt.Sprintf("unknown key %q (has ignore tag)", k)) p.stop(fmt.Sprintf("unknown key %q (has ignore tag or not exported)", k))
} }
vv.Set(p.parseValue(vv)) vv.Set(p.parseValue(vv))
} }
@ -254,7 +254,7 @@ func (p *parser) parseStruct0(v reflect.Value) {
n := t.NumField() n := t.NumField()
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
f := t.Field(i) f := t.Field(i)
if isIgnore(f.Tag.Get("sconf")) || isOptional(f.Tag.Get("sconf")) { if !f.IsExported() || isIgnore(f.Tag.Get("sconf")) || isOptional(f.Tag.Get("sconf")) {
continue continue
} }
if _, ok := seen[f.Name]; !ok { if _, ok := seen[f.Name]; !ok {

16
vendor/github.com/mjl-/xfmt/xfmt.go generated vendored
View file

@ -10,8 +10,12 @@ import (
// Config tells format how to reformat text. // Config tells format how to reformat text.
type Config struct { type Config struct {
MaxWidth int // Max width of content (excluding indenting), after which lines are wrapped. // Max width of content (excluding indenting), after which lines are wrapped.
BreakPrefixes []string // String prefixes that cause a line to break, instead of being merged into the previous line. MaxWidth int
// String prefixes that cause a line to break, instead of being merged into the
// previous line.
BreakPrefixes []string
} }
// Format reads text from r and writes reformatted text to w, according to // Format reads text from r and writes reformatted text to w, according to
@ -33,7 +37,7 @@ type formatter struct {
curLineend string curLineend string
} }
type parseError error type parseError struct{ error }
func (f *formatter) format() (rerr error) { func (f *formatter) format() (rerr error) {
defer func() { defer func() {
@ -65,7 +69,7 @@ func (f *formatter) format() (rerr error) {
func (f *formatter) check(err error, action string) { func (f *formatter) check(err error, action string) {
if err != nil { if err != nil {
panic(parseError(fmt.Errorf("%s: %s", action, err))) panic(parseError{fmt.Errorf("%s: %s", action, err)})
} }
} }
@ -108,6 +112,7 @@ func (f *formatter) gatherLine() (string, string) {
var curLine, curLineend string var curLine, curLineend string
var curPrefix string var curPrefix string
n := 0
for { for {
line, end := f.peekLine() line, end := f.peekLine()
if line == "" && end == "" { if line == "" && end == "" {
@ -123,7 +128,7 @@ func (f *formatter) gatherLine() (string, string) {
} }
break break
} }
if curLine != "" && (curPrefix != prefix || rem == "" || f.causeBreak(rem)) { if n > 0 && (curPrefix != prefix || rem == "" || f.causeBreak(rem)) {
break break
} }
curPrefix = prefix curPrefix = prefix
@ -136,6 +141,7 @@ func (f *formatter) gatherLine() (string, string) {
if curLine != "" && curLine[len(curLine)-1] < 0x20 { if curLine != "" && curLine[len(curLine)-1] < 0x20 {
break break
} }
n++
} }
return curPrefix + curLine, curLineend return curPrefix + curLine, curLineend

4
vendor/modules.txt vendored
View file

@ -14,7 +14,7 @@ github.com/matttproud/golang_protobuf_extensions/pbutil
# github.com/mjl-/bstore v0.0.2 # github.com/mjl-/bstore v0.0.2
## explicit; go 1.19 ## explicit; go 1.19
github.com/mjl-/bstore github.com/mjl-/bstore
# github.com/mjl-/sconf v0.0.4 # github.com/mjl-/sconf v0.0.5
## explicit; go 1.12 ## explicit; go 1.12
github.com/mjl-/sconf github.com/mjl-/sconf
# github.com/mjl-/sherpa v0.6.6 # github.com/mjl-/sherpa v0.6.6
@ -31,7 +31,7 @@ github.com/mjl-/sherpaprom
## explicit; go 1.12 ## explicit; go 1.12
github.com/mjl-/sherpats github.com/mjl-/sherpats
github.com/mjl-/sherpats/cmd/sherpats github.com/mjl-/sherpats/cmd/sherpats
# github.com/mjl-/xfmt v0.0.0-20190521151243-39d9c00752ce # github.com/mjl-/xfmt v0.0.2
## explicit; go 1.12 ## explicit; go 1.12
github.com/mjl-/xfmt github.com/mjl-/xfmt
# github.com/prometheus/client_golang v1.14.0 # github.com/prometheus/client_golang v1.14.0