mirror of
https://github.com/mjl-/mox.git
synced 2025-07-12 13:04:38 +03:00
in account & admin web api's, differentiate between server errors and user errors, and add a prometheus monitoring rule for server errors
This commit is contained in:
@ -48,6 +48,11 @@ groups:
|
|||||||
annotations:
|
annotations:
|
||||||
summary: webmail submission errors
|
summary: webmail submission errors
|
||||||
|
|
||||||
|
- alert: mox-sherpa-server-errors
|
||||||
|
expr: increase(sherpa_errors_total{api=~"mox.*",code=~"server:.*"}[1h]) > 0
|
||||||
|
annotations:
|
||||||
|
summary: sherpa web api server errors
|
||||||
|
|
||||||
# the alerts below can be used to keep a closer eye or when starting to use mox,
|
# the alerts below can be used to keep a closer eye or when starting to use mox,
|
||||||
# but can be noisy, or you may not be able to prevent them.
|
# but can be noisy, or you may not be able to prevent them.
|
||||||
|
|
||||||
|
@ -77,6 +77,16 @@ func xcheckf(ctx context.Context, err error, format string, args ...any) {
|
|||||||
panic(&sherpa.Error{Code: "server:error", Message: errmsg})
|
panic(&sherpa.Error{Code: "server:error", Message: errmsg})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func xcheckuserf(ctx context.Context, err error, format string, args ...any) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
errmsg := fmt.Sprintf("%s: %s", msg, err)
|
||||||
|
xlog.WithContext(ctx).Errorx(msg, err)
|
||||||
|
panic(&sherpa.Error{Code: "user:error", Message: errmsg})
|
||||||
|
}
|
||||||
|
|
||||||
// Account exports web API functions for the account web interface. All its
|
// Account exports web API functions for the account web interface. All its
|
||||||
// methods are exported under api/. Function calls require valid HTTP
|
// methods are exported under api/. Function calls require valid HTTP
|
||||||
// Authentication credentials of a user.
|
// Authentication credentials of a user.
|
||||||
@ -378,11 +388,11 @@ func (Account) DestinationSave(ctx context.Context, destName string, oldDest, ne
|
|||||||
}
|
}
|
||||||
curDest, ok := accConf.Destinations[destName]
|
curDest, ok := accConf.Destinations[destName]
|
||||||
if !ok {
|
if !ok {
|
||||||
xcheckf(ctx, errors.New("not found"), "looking up destination")
|
xcheckuserf(ctx, errors.New("not found"), "looking up destination")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !curDest.Equal(oldDest) {
|
if !curDest.Equal(oldDest) {
|
||||||
xcheckf(ctx, errors.New("modified"), "checking stored destination")
|
xcheckuserf(ctx, errors.New("modified"), "checking stored destination")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep fields we manage.
|
// Keep fields we manage.
|
||||||
|
@ -211,6 +211,26 @@ func Handle(w http.ResponseWriter, r *http.Request) {
|
|||||||
adminSherpaHandler.ServeHTTP(w, r.WithContext(ctx))
|
adminSherpaHandler.ServeHTTP(w, r.WithContext(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func xcheckf(ctx context.Context, err error, format string, args ...any) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
errmsg := fmt.Sprintf("%s: %s", msg, err)
|
||||||
|
xlog.WithContext(ctx).Errorx(msg, err)
|
||||||
|
panic(&sherpa.Error{Code: "server:error", Message: errmsg})
|
||||||
|
}
|
||||||
|
|
||||||
|
func xcheckuserf(ctx context.Context, err error, format string, args ...any) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg := fmt.Sprintf(format, args...)
|
||||||
|
errmsg := fmt.Sprintf("%s: %s", msg, err)
|
||||||
|
xlog.WithContext(ctx).Errorx(msg, err)
|
||||||
|
panic(&sherpa.Error{Code: "user:error", Message: errmsg})
|
||||||
|
}
|
||||||
|
|
||||||
type Result struct {
|
type Result struct {
|
||||||
Errors []string
|
Errors []string
|
||||||
Warnings []string
|
Warnings []string
|
||||||
@ -372,7 +392,7 @@ func (Admin) CheckDomain(ctx context.Context, domainName string) (r CheckResult)
|
|||||||
|
|
||||||
func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer, domainName string) (r CheckResult) {
|
func checkDomain(ctx context.Context, resolver dns.Resolver, dialer *net.Dialer, domainName string) (r CheckResult) {
|
||||||
domain, err := dns.ParseDomain(domainName)
|
domain, err := dns.ParseDomain(domainName)
|
||||||
xcheckf(ctx, err, "parsing domain")
|
xcheckuserf(ctx, err, "parsing domain")
|
||||||
|
|
||||||
domConf, ok := mox.Conf.Domain(domain)
|
domConf, ok := mox.Conf.Domain(domain)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -1183,10 +1203,10 @@ func (Admin) Domains(ctx context.Context) []dns.Domain {
|
|||||||
// Domain returns the dns domain for a (potentially unicode as IDNA) domain name.
|
// Domain returns the dns domain for a (potentially unicode as IDNA) domain name.
|
||||||
func (Admin) Domain(ctx context.Context, domain string) dns.Domain {
|
func (Admin) Domain(ctx context.Context, domain string) dns.Domain {
|
||||||
d, err := dns.ParseDomain(domain)
|
d, err := dns.ParseDomain(domain)
|
||||||
xcheckf(ctx, err, "parse domain")
|
xcheckuserf(ctx, err, "parse domain")
|
||||||
_, ok := mox.Conf.Domain(d)
|
_, ok := mox.Conf.Domain(d)
|
||||||
if !ok {
|
if !ok {
|
||||||
xcheckf(ctx, errors.New("no such domain"), "looking up domain")
|
xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
|
||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
@ -1194,10 +1214,10 @@ func (Admin) Domain(ctx context.Context, domain string) dns.Domain {
|
|||||||
// DomainLocalparts returns the encoded localparts and accounts configured in domain.
|
// DomainLocalparts returns the encoded localparts and accounts configured in domain.
|
||||||
func (Admin) DomainLocalparts(ctx context.Context, domain string) (localpartAccounts map[string]string) {
|
func (Admin) DomainLocalparts(ctx context.Context, domain string) (localpartAccounts map[string]string) {
|
||||||
d, err := dns.ParseDomain(domain)
|
d, err := dns.ParseDomain(domain)
|
||||||
xcheckf(ctx, err, "parsing domain")
|
xcheckuserf(ctx, err, "parsing domain")
|
||||||
_, ok := mox.Conf.Domain(d)
|
_, ok := mox.Conf.Domain(d)
|
||||||
if !ok {
|
if !ok {
|
||||||
xcheckf(ctx, errors.New("no such domain"), "looking up domain")
|
xcheckuserf(ctx, errors.New("no such domain"), "looking up domain")
|
||||||
}
|
}
|
||||||
return mox.Conf.DomainLocalparts(d)
|
return mox.Conf.DomainLocalparts(d)
|
||||||
}
|
}
|
||||||
@ -1215,7 +1235,7 @@ func (Admin) Accounts(ctx context.Context) []string {
|
|||||||
func (Admin) Account(ctx context.Context, account string) map[string]any {
|
func (Admin) Account(ctx context.Context, account string) map[string]any {
|
||||||
ac, ok := mox.Conf.Account(account)
|
ac, ok := mox.Conf.Account(account)
|
||||||
if !ok {
|
if !ok {
|
||||||
xcheckf(ctx, errors.New("no such account"), "looking up account")
|
xcheckuserf(ctx, errors.New("no such account"), "looking up account")
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: should change sherpa to understand config.Account directly, with its anonymous structs.
|
// todo: should change sherpa to understand config.Account directly, with its anonymous structs.
|
||||||
@ -1237,16 +1257,6 @@ func (Admin) ConfigFiles(ctx context.Context) (staticPath, dynamicPath, static,
|
|||||||
return mox.ConfigStaticPath, mox.ConfigDynamicPath, string(buf0), string(buf1)
|
return mox.ConfigStaticPath, mox.ConfigDynamicPath, string(buf0), string(buf1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func xcheckf(ctx context.Context, err error, format string, args ...any) {
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg := fmt.Sprintf(format, args...)
|
|
||||||
errmsg := fmt.Sprintf("%s: %s", msg, err)
|
|
||||||
xlog.WithContext(ctx).Errorx(msg, err)
|
|
||||||
panic(&sherpa.Error{Code: "server:error", Message: errmsg})
|
|
||||||
}
|
|
||||||
|
|
||||||
// MTASTSPolicies returns all mtasts policies from the cache.
|
// MTASTSPolicies returns all mtasts policies from the cache.
|
||||||
func (Admin) MTASTSPolicies(ctx context.Context) (records []mtastsdb.PolicyRecord) {
|
func (Admin) MTASTSPolicies(ctx context.Context) (records []mtastsdb.PolicyRecord) {
|
||||||
records, err := mtastsdb.PolicyRecords(ctx)
|
records, err := mtastsdb.PolicyRecords(ctx)
|
||||||
@ -1277,6 +1287,9 @@ func (Admin) TLSReportID(ctx context.Context, domain string, reportID int64) tls
|
|||||||
if err == nil && record.Domain != domain {
|
if err == nil && record.Domain != domain {
|
||||||
err = bstore.ErrAbsent
|
err = bstore.ErrAbsent
|
||||||
}
|
}
|
||||||
|
if err == bstore.ErrAbsent {
|
||||||
|
xcheckuserf(ctx, err, "fetching tls report from database")
|
||||||
|
}
|
||||||
xcheckf(ctx, err, "fetching tls report from database")
|
xcheckf(ctx, err, "fetching tls report from database")
|
||||||
return record
|
return record
|
||||||
}
|
}
|
||||||
@ -1345,6 +1358,9 @@ func (Admin) DMARCReportID(ctx context.Context, domain string, reportID int64) (
|
|||||||
if err == nil && report.Domain != domain {
|
if err == nil && report.Domain != domain {
|
||||||
err = bstore.ErrAbsent
|
err = bstore.ErrAbsent
|
||||||
}
|
}
|
||||||
|
if err == bstore.ErrAbsent {
|
||||||
|
xcheckuserf(ctx, err, "fetching dmarc report from database")
|
||||||
|
}
|
||||||
xcheckf(ctx, err, "fetching dmarc report from database")
|
xcheckf(ctx, err, "fetching dmarc report from database")
|
||||||
return report
|
return report
|
||||||
}
|
}
|
||||||
@ -1421,9 +1437,9 @@ type Reverse struct {
|
|||||||
|
|
||||||
// LookupIP does a reverse lookup of ip.
|
// LookupIP does a reverse lookup of ip.
|
||||||
func (Admin) LookupIP(ctx context.Context, ip string) Reverse {
|
func (Admin) LookupIP(ctx context.Context, ip string) Reverse {
|
||||||
resolver := dns.StrictResolver{Pkg: "adminapi"}
|
resolver := dns.StrictResolver{Pkg: "webadmin"}
|
||||||
names, err := resolver.LookupAddr(ctx, ip)
|
names, err := resolver.LookupAddr(ctx, ip)
|
||||||
xcheckf(ctx, err, "looking up ip")
|
xcheckuserf(ctx, err, "looking up ip")
|
||||||
return Reverse{names}
|
return Reverse{names}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1476,10 +1492,10 @@ func dnsblsStatus(ctx context.Context, resolver dns.Resolver) map[string]map[str
|
|||||||
// configured domain.
|
// configured domain.
|
||||||
func (Admin) DomainRecords(ctx context.Context, domain string) []string {
|
func (Admin) DomainRecords(ctx context.Context, domain string) []string {
|
||||||
d, err := dns.ParseDomain(domain)
|
d, err := dns.ParseDomain(domain)
|
||||||
xcheckf(ctx, err, "parsing domain")
|
xcheckuserf(ctx, err, "parsing domain")
|
||||||
dc, ok := mox.Conf.Domain(d)
|
dc, ok := mox.Conf.Domain(d)
|
||||||
if !ok {
|
if !ok {
|
||||||
xcheckf(ctx, errors.New("unknown domain"), "lookup domain")
|
xcheckuserf(ctx, errors.New("unknown domain"), "lookup domain")
|
||||||
}
|
}
|
||||||
records, err := mox.DomainRecords(dc, d)
|
records, err := mox.DomainRecords(dc, d)
|
||||||
xcheckf(ctx, err, "dns records")
|
xcheckf(ctx, err, "dns records")
|
||||||
@ -1489,7 +1505,7 @@ func (Admin) DomainRecords(ctx context.Context, domain string) []string {
|
|||||||
// DomainAdd adds a new domain and reloads the configuration.
|
// DomainAdd adds a new domain and reloads the configuration.
|
||||||
func (Admin) DomainAdd(ctx context.Context, domain, accountName, localpart string) {
|
func (Admin) DomainAdd(ctx context.Context, domain, accountName, localpart string) {
|
||||||
d, err := dns.ParseDomain(domain)
|
d, err := dns.ParseDomain(domain)
|
||||||
xcheckf(ctx, err, "parsing domain")
|
xcheckuserf(ctx, err, "parsing domain")
|
||||||
|
|
||||||
err = mox.DomainAdd(ctx, d, accountName, smtp.Localpart(localpart))
|
err = mox.DomainAdd(ctx, d, accountName, smtp.Localpart(localpart))
|
||||||
xcheckf(ctx, err, "adding domain")
|
xcheckf(ctx, err, "adding domain")
|
||||||
@ -1498,7 +1514,7 @@ func (Admin) DomainAdd(ctx context.Context, domain, accountName, localpart strin
|
|||||||
// DomainRemove removes an existing domain and reloads the configuration.
|
// DomainRemove removes an existing domain and reloads the configuration.
|
||||||
func (Admin) DomainRemove(ctx context.Context, domain string) {
|
func (Admin) DomainRemove(ctx context.Context, domain string) {
|
||||||
d, err := dns.ParseDomain(domain)
|
d, err := dns.ParseDomain(domain)
|
||||||
xcheckf(ctx, err, "parsing domain")
|
xcheckuserf(ctx, err, "parsing domain")
|
||||||
|
|
||||||
err = mox.DomainRemove(ctx, d)
|
err = mox.DomainRemove(ctx, d)
|
||||||
xcheckf(ctx, err, "removing domain")
|
xcheckf(ctx, err, "removing domain")
|
||||||
@ -1555,7 +1571,7 @@ func (Admin) SetAccountLimits(ctx context.Context, accountName string, maxOutgoi
|
|||||||
// Submission (SMTP) for the domain.
|
// Submission (SMTP) for the domain.
|
||||||
func (Admin) ClientConfigDomain(ctx context.Context, domain string) mox.ClientConfig {
|
func (Admin) ClientConfigDomain(ctx context.Context, domain string) mox.ClientConfig {
|
||||||
d, err := dns.ParseDomain(domain)
|
d, err := dns.ParseDomain(domain)
|
||||||
xcheckf(ctx, err, "parsing domain")
|
xcheckuserf(ctx, err, "parsing domain")
|
||||||
|
|
||||||
cc, err := mox.ClientConfigDomain(d)
|
cc, err := mox.ClientConfigDomain(d)
|
||||||
xcheckf(ctx, err, "client config for domain")
|
xcheckf(ctx, err, "client config for domain")
|
||||||
@ -1608,7 +1624,7 @@ func (Admin) LogLevels(ctx context.Context) map[string]string {
|
|||||||
func (Admin) LogLevelSet(ctx context.Context, pkg string, levelStr string) {
|
func (Admin) LogLevelSet(ctx context.Context, pkg string, levelStr string) {
|
||||||
level, ok := mlog.Levels[levelStr]
|
level, ok := mlog.Levels[levelStr]
|
||||||
if !ok {
|
if !ok {
|
||||||
xcheckf(ctx, errors.New("unknown"), "lookup level")
|
xcheckuserf(ctx, errors.New("unknown"), "lookup level")
|
||||||
}
|
}
|
||||||
mox.Conf.LogLevelSet(pkg, level)
|
mox.Conf.LogLevelSet(pkg, level)
|
||||||
}
|
}
|
||||||
@ -1671,7 +1687,7 @@ func (Admin) WebserverConfigSave(ctx context.Context, oldConf, newConf Webserver
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(oldConf.WebDNSDomainRedirects, current.WebDNSDomainRedirects) || !webhandlersEqual() {
|
if !reflect.DeepEqual(oldConf.WebDNSDomainRedirects, current.WebDNSDomainRedirects) || !webhandlersEqual() {
|
||||||
xcheckf(ctx, errors.New("config has changed"), "comparing old/current config")
|
xcheckuserf(ctx, errors.New("config has changed"), "comparing old/current config")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to map, check that there are no duplicates here. The canonicalized
|
// Convert to map, check that there are no duplicates here. The canonicalized
|
||||||
@ -1680,7 +1696,7 @@ func (Admin) WebserverConfigSave(ctx context.Context, oldConf, newConf Webserver
|
|||||||
domainRedirects := map[string]string{}
|
domainRedirects := map[string]string{}
|
||||||
for _, x := range newConf.WebDomainRedirects {
|
for _, x := range newConf.WebDomainRedirects {
|
||||||
if _, ok := domainRedirects[x[0]]; ok {
|
if _, ok := domainRedirects[x[0]]; ok {
|
||||||
xcheckf(ctx, errors.New("already present"), "checking redirect %s", x[0])
|
xcheckuserf(ctx, errors.New("already present"), "checking redirect %s", x[0])
|
||||||
}
|
}
|
||||||
domainRedirects[x[0]] = x[1]
|
domainRedirects[x[0]] = x[1]
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user