1package webadmin
2
3import (
4 "bytes"
5 "context"
6 "crypto/ed25519"
7 "encoding/json"
8 "fmt"
9 "io"
10 "net"
11 "net/http"
12 "net/http/httptest"
13 "os"
14 "path/filepath"
15 "reflect"
16 "runtime/debug"
17 "strings"
18 "testing"
19 "time"
20
21 "golang.org/x/crypto/bcrypt"
22
23 "github.com/mjl-/sherpa"
24
25 "github.com/mjl-/mox/config"
26 "github.com/mjl-/mox/dns"
27 "github.com/mjl-/mox/mlog"
28 "github.com/mjl-/mox/mox-"
29 "github.com/mjl-/mox/mtasts"
30 "github.com/mjl-/mox/queue"
31 "github.com/mjl-/mox/store"
32 "github.com/mjl-/mox/webauth"
33)
34
35var ctxbg = context.Background()
36
37func init() {
38 mox.LimitersInit()
39 webauth.BadAuthDelay = 0
40}
41
42func tneedErrorCode(t *testing.T, code string, fn func()) {
43 t.Helper()
44 defer func() {
45 t.Helper()
46 x := recover()
47 if x == nil {
48 debug.PrintStack()
49 t.Fatalf("expected sherpa user error, saw success")
50 }
51 if err, ok := x.(*sherpa.Error); !ok {
52 debug.PrintStack()
53 t.Fatalf("expected sherpa error, saw %#v", x)
54 } else if err.Code != code {
55 debug.PrintStack()
56 t.Fatalf("expected sherpa error code %q, saw other sherpa error %#v", code, err)
57 }
58 }()
59
60 fn()
61}
62
63func tcheck(t *testing.T, err error, msg string) {
64 t.Helper()
65 if err != nil {
66 t.Fatalf("%s: %s", msg, err)
67 }
68}
69
70func tcompare(t *testing.T, got, expect any) {
71 t.Helper()
72 if !reflect.DeepEqual(got, expect) {
73 t.Fatalf("got:\n%#v\nexpected:\n%#v", got, expect)
74 }
75}
76
77func readBody(r io.Reader) string {
78 buf, err := io.ReadAll(r)
79 if err != nil {
80 return fmt.Sprintf("read error: %s", err)
81 }
82 return fmt.Sprintf("data: %q", buf)
83}
84
85func TestAdminAuth(t *testing.T) {
86 os.RemoveAll("../testdata/webadmin/data")
87 mox.ConfigStaticPath = filepath.FromSlash("../testdata/webadmin/mox.conf")
88 mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
89 mox.MustLoadConfig(true, false)
90
91 adminpwhash, err := bcrypt.GenerateFromPassword([]byte("moxtest123"), bcrypt.DefaultCost)
92 tcheck(t, err, "generate bcrypt hash")
93
94 path := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
95 err = os.WriteFile(path, adminpwhash, 0660)
96 tcheck(t, err, "write password file")
97 defer os.Remove(path)
98
99 api := Admin{cookiePath: "/admin/"}
100 apiHandler, err := makeSherpaHandler(api.cookiePath, false)
101 tcheck(t, err, "sherpa handler")
102
103 respRec := httptest.NewRecorder()
104 reqInfo := requestInfo{"", respRec, &http.Request{RemoteAddr: "127.0.0.1:1234"}}
105 ctx := context.WithValue(ctxbg, requestInfoCtxKey, reqInfo)
106
107 // Missing login token.
108 tneedErrorCode(t, "user:error", func() { api.Login(ctx, "", "moxtest123") })
109
110 // Login with loginToken.
111 loginCookie := &http.Cookie{Name: "webadminlogin"}
112 loginCookie.Value = api.LoginPrep(ctx)
113 reqInfo.Request.Header = http.Header{"Cookie": []string{loginCookie.String()}}
114
115 csrfToken := api.Login(ctx, loginCookie.Value, "moxtest123")
116 var sessionCookie *http.Cookie
117 for _, c := range respRec.Result().Cookies() {
118 if c.Name == "webadminsession" {
119 sessionCookie = c
120 break
121 }
122 }
123 if sessionCookie == nil {
124 t.Fatalf("missing session cookie")
125 }
126
127 // Valid loginToken, but bad credentials.
128 loginCookie.Value = api.LoginPrep(ctx)
129 reqInfo.Request.Header = http.Header{"Cookie": []string{loginCookie.String()}}
130 tneedErrorCode(t, "user:loginFailed", func() { api.Login(ctx, loginCookie.Value, "badauth") })
131
132 type httpHeaders [][2]string
133 ctJSON := [2]string{"Content-Type", "application/json; charset=utf-8"}
134
135 cookieOK := &http.Cookie{Name: "webadminsession", Value: sessionCookie.Value}
136 cookieBad := &http.Cookie{Name: "webadminsession", Value: "AAAAAAAAAAAAAAAAAAAAAA"}
137 hdrSessionOK := [2]string{"Cookie", cookieOK.String()}
138 hdrSessionBad := [2]string{"Cookie", cookieBad.String()}
139 hdrCSRFOK := [2]string{"x-mox-csrf", string(csrfToken)}
140 hdrCSRFBad := [2]string{"x-mox-csrf", "AAAAAAAAAAAAAAAAAAAAAA"}
141
142 testHTTP := func(method, path string, headers httpHeaders, expStatusCode int, expHeaders httpHeaders, check func(resp *http.Response)) {
143 t.Helper()
144
145 req := httptest.NewRequest(method, path, nil)
146 for _, kv := range headers {
147 req.Header.Add(kv[0], kv[1])
148 }
149 rr := httptest.NewRecorder()
150 rr.Body = &bytes.Buffer{}
151 handle(apiHandler, false, rr, req)
152 if rr.Code != expStatusCode {
153 t.Fatalf("got status %d, expected %d (%s)", rr.Code, expStatusCode, readBody(rr.Body))
154 }
155
156 resp := rr.Result()
157 for _, h := range expHeaders {
158 if resp.Header.Get(h[0]) != h[1] {
159 t.Fatalf("for header %q got value %q, expected %q", h[0], resp.Header.Get(h[0]), h[1])
160 }
161 }
162
163 if check != nil {
164 check(resp)
165 }
166 }
167 testHTTPAuthAPI := func(method, path string, expStatusCode int, expHeaders httpHeaders, check func(resp *http.Response)) {
168 t.Helper()
169 testHTTP(method, path, httpHeaders{hdrCSRFOK, hdrSessionOK}, expStatusCode, expHeaders, check)
170 }
171
172 userAuthError := func(resp *http.Response, expCode string) {
173 t.Helper()
174
175 var response struct {
176 Error *sherpa.Error `json:"error"`
177 }
178 err := json.NewDecoder(resp.Body).Decode(&response)
179 tcheck(t, err, "parsing response as json")
180 if response.Error == nil {
181 t.Fatalf("expected sherpa error with code %s, no error", expCode)
182 }
183 if response.Error.Code != expCode {
184 t.Fatalf("got sherpa error code %q, expected %s", response.Error.Code, expCode)
185 }
186 }
187 badAuth := func(resp *http.Response) {
188 t.Helper()
189 userAuthError(resp, "user:badAuth")
190 }
191 noAuth := func(resp *http.Response) {
192 t.Helper()
193 userAuthError(resp, "user:noAuth")
194 }
195
196 testHTTP("POST", "/api/Bogus", httpHeaders{}, http.StatusOK, nil, noAuth)
197 testHTTP("POST", "/api/Bogus", httpHeaders{hdrCSRFBad}, http.StatusOK, nil, noAuth)
198 testHTTP("POST", "/api/Bogus", httpHeaders{hdrSessionBad}, http.StatusOK, nil, noAuth)
199 testHTTP("POST", "/api/Bogus", httpHeaders{hdrCSRFBad, hdrSessionBad}, http.StatusOK, nil, badAuth)
200 testHTTP("POST", "/api/Bogus", httpHeaders{hdrCSRFOK}, http.StatusOK, nil, noAuth)
201 testHTTP("POST", "/api/Bogus", httpHeaders{hdrSessionOK}, http.StatusOK, nil, noAuth)
202 testHTTP("POST", "/api/Bogus", httpHeaders{hdrCSRFBad, hdrSessionOK}, http.StatusOK, nil, badAuth)
203 testHTTP("POST", "/api/Bogus", httpHeaders{hdrCSRFOK, hdrSessionBad}, http.StatusOK, nil, badAuth)
204 testHTTPAuthAPI("GET", "/api/Transports", http.StatusMethodNotAllowed, nil, nil)
205 testHTTPAuthAPI("POST", "/api/Transports", http.StatusOK, httpHeaders{ctJSON}, nil)
206
207 // Logout needs session token.
208 reqInfo.SessionToken = store.SessionToken(strings.SplitN(sessionCookie.Value, " ", 2)[0])
209 ctx = context.WithValue(ctxbg, requestInfoCtxKey, reqInfo)
210
211 api.Logout(ctx)
212 tneedErrorCode(t, "server:error", func() { api.Logout(ctx) })
213}
214
215func TestAdmin(t *testing.T) {
216 os.RemoveAll("../testdata/webadmin/data")
217 defer os.RemoveAll("../testdata/webadmin/dkim")
218 mox.ConfigStaticPath = filepath.FromSlash("../testdata/webadmin/mox.conf")
219 mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
220 mox.MustLoadConfig(true, false)
221 err := queue.Init()
222 tcheck(t, err, "queue init")
223 defer queue.Shutdown()
224
225 api := Admin{}
226
227 mrl := api.RetiredList(ctxbg, queue.RetiredFilter{}, queue.RetiredSort{})
228 tcompare(t, len(mrl), 0)
229
230 n := api.HookQueueSize(ctxbg)
231 tcompare(t, n, 0)
232
233 hl := api.HookList(ctxbg, queue.HookFilter{}, queue.HookSort{})
234 tcompare(t, len(hl), 0)
235
236 n = api.HookNextAttemptSet(ctxbg, queue.HookFilter{}, 0)
237 tcompare(t, n, 0)
238
239 n = api.HookNextAttemptAdd(ctxbg, queue.HookFilter{}, 0)
240 tcompare(t, n, 0)
241
242 hrl := api.HookRetiredList(ctxbg, queue.HookRetiredFilter{}, queue.HookRetiredSort{})
243 tcompare(t, len(hrl), 0)
244
245 n = api.HookCancel(ctxbg, queue.HookFilter{})
246 tcompare(t, n, 0)
247
248 api.Config(ctxbg)
249 api.DomainConfig(ctxbg, "mox.example")
250 tneedErrorCode(t, "user:error", func() { api.DomainConfig(ctxbg, "bogus.example") })
251
252 api.AccountRoutesSave(ctxbg, "mjl", []config.Route{{Transport: "direct"}})
253 tneedErrorCode(t, "user:error", func() { api.AccountRoutesSave(ctxbg, "mjl", []config.Route{{Transport: "bogus"}}) })
254 api.AccountRoutesSave(ctxbg, "mjl", nil)
255
256 api.DomainRoutesSave(ctxbg, "mox.example", []config.Route{{Transport: "direct"}})
257 tneedErrorCode(t, "user:error", func() { api.DomainRoutesSave(ctxbg, "mox.example", []config.Route{{Transport: "bogus"}}) })
258 api.DomainRoutesSave(ctxbg, "mox.example", nil)
259
260 api.RoutesSave(ctxbg, []config.Route{{Transport: "direct"}})
261 tneedErrorCode(t, "user:error", func() { api.RoutesSave(ctxbg, []config.Route{{Transport: "bogus"}}) })
262 api.RoutesSave(ctxbg, nil)
263
264 api.DomainDescriptionSave(ctxbg, "mox.example", "description")
265 tneedErrorCode(t, "server:error", func() { api.DomainDescriptionSave(ctxbg, "mox.example", "newline not ok\n") }) // todo: user error
266 tneedErrorCode(t, "user:error", func() { api.DomainDescriptionSave(ctxbg, "bogus.example", "unknown domain") })
267 api.DomainDescriptionSave(ctxbg, "mox.example", "") // Restore.
268
269 api.DomainClientSettingsDomainSave(ctxbg, "mox.example", "mail.mox.example")
270 tneedErrorCode(t, "user:error", func() { api.DomainClientSettingsDomainSave(ctxbg, "mox.example", "bogus domain") })
271 tneedErrorCode(t, "user:error", func() { api.DomainClientSettingsDomainSave(ctxbg, "bogus.example", "unknown.example") })
272 api.DomainClientSettingsDomainSave(ctxbg, "mox.example", "") // Restore.
273
274 api.DomainLocalpartConfigSave(ctxbg, "mox.example", "-", true)
275 tneedErrorCode(t, "user:error", func() { api.DomainLocalpartConfigSave(ctxbg, "bogus.example", "", false) })
276 api.DomainLocalpartConfigSave(ctxbg, "mox.example", "", false) // Restore.
277
278 api.DomainDMARCAddressSave(ctxbg, "mox.example", "dmarc-reports", "", "mjl", "DMARC")
279 tneedErrorCode(t, "user:error", func() { api.DomainDMARCAddressSave(ctxbg, "bogus.example", "dmarc-reports", "", "mjl", "DMARC") })
280 tneedErrorCode(t, "user:error", func() { api.DomainDMARCAddressSave(ctxbg, "mox.example", "dmarc-reports", "", "bogus", "DMARC") })
281 api.DomainDMARCAddressSave(ctxbg, "mox.example", "", "", "", "") // Restore.
282
283 api.DomainTLSRPTAddressSave(ctxbg, "mox.example", "tls-reports", "", "mjl", "TLSRPT")
284 tneedErrorCode(t, "user:error", func() { api.DomainTLSRPTAddressSave(ctxbg, "bogus.example", "tls-reports", "", "mjl", "TLSRPT") })
285 tneedErrorCode(t, "user:error", func() { api.DomainTLSRPTAddressSave(ctxbg, "mox.example", "tls-reports", "", "bogus", "TLSRPT") })
286 api.DomainTLSRPTAddressSave(ctxbg, "mox.example", "", "", "", "") // Restore.
287
288 // todo: cannot enable mta-sts because we have no listener, which would require a tls cert for the domain.
289 // api.DomainMTASTSSave(ctxbg, "mox.example", "id0", mtasts.ModeEnforce, time.Hour, []string{"mail.mox.example"})
290 tneedErrorCode(t, "user:error", func() {
291 api.DomainMTASTSSave(ctxbg, "bogus.example", "id0", mtasts.ModeEnforce, time.Hour, []string{"mail.mox.example"})
292 })
293 tneedErrorCode(t, "user:error", func() {
294 api.DomainMTASTSSave(ctxbg, "mox.example", "invalid id", mtasts.ModeEnforce, time.Hour, []string{"mail.mox.example"})
295 })
296 tneedErrorCode(t, "user:error", func() {
297 api.DomainMTASTSSave(ctxbg, "mox.example", "id0", mtasts.Mode("bogus"), time.Hour, []string{"mail.mox.example"})
298 })
299 tneedErrorCode(t, "user:error", func() {
300 api.DomainMTASTSSave(ctxbg, "mox.example", "id0", mtasts.ModeEnforce, time.Hour, []string{"*.*.mail.mox.example"})
301 })
302 api.DomainMTASTSSave(ctxbg, "mox.example", "", mtasts.ModeNone, 0, nil) // Restore.
303
304 api.DomainDKIMAdd(ctxbg, "mox.example", "testsel", "ed25519", "sha256", true, true, true, nil, 24*time.Hour)
305 tneedErrorCode(t, "user:error", func() {
306 api.DomainDKIMAdd(ctxbg, "mox.example", "testsel", "ed25519", "sha256", true, true, true, nil, 24*time.Hour)
307 }) // Duplicate selector.
308 tneedErrorCode(t, "user:error", func() {
309 api.DomainDKIMAdd(ctxbg, "bogus.example", "testsel", "ed25519", "sha256", true, true, true, nil, 24*time.Hour)
310 })
311 conf := api.DomainConfig(ctxbg, "mox.example")
312 api.DomainDKIMSave(ctxbg, "mox.example", conf.DKIM.Selectors, conf.DKIM.Sign)
313 api.DomainDKIMSave(ctxbg, "mox.example", conf.DKIM.Selectors, []string{"testsel"})
314 tneedErrorCode(t, "user:error", func() { api.DomainDKIMSave(ctxbg, "mox.example", conf.DKIM.Selectors, []string{"bogus"}) })
315 tneedErrorCode(t, "user:error", func() { api.DomainDKIMSave(ctxbg, "mox.example", nil, []string{}) }) // Cannot remove selectors with save.
316 tneedErrorCode(t, "user:error", func() { api.DomainDKIMSave(ctxbg, "bogus.example", nil, []string{}) })
317 moreSel := map[string]config.Selector{
318 "testsel": conf.DKIM.Selectors["testsel"],
319 "testsel2": conf.DKIM.Selectors["testsel2"],
320 }
321 tneedErrorCode(t, "user:error", func() { api.DomainDKIMSave(ctxbg, "mox.example", moreSel, []string{}) }) // Cannot add selectors with save.
322 api.DomainDKIMRemove(ctxbg, "mox.example", "testsel")
323 tneedErrorCode(t, "user:error", func() { api.DomainDKIMRemove(ctxbg, "mox.example", "testsel") }) // Already removed.
324 tneedErrorCode(t, "user:error", func() { api.DomainDKIMRemove(ctxbg, "bogus.example", "testsel") })
325
326 // Aliases
327 alias := config.Alias{Addresses: []string{"mjl@mox.example"}}
328 api.AliasAdd(ctxbg, "support", "mox.example", alias)
329 tneedErrorCode(t, "user:error", func() { api.AliasAdd(ctxbg, "support", "mox.example", alias) }) // Already present.
330 tneedErrorCode(t, "user:error", func() { api.AliasAdd(ctxbg, "Support", "mox.example", alias) }) // Duplicate, canonical.
331 tneedErrorCode(t, "user:error", func() { api.AliasAdd(ctxbg, "support", "bogus.example", alias) }) // Unknown domain.
332 tneedErrorCode(t, "user:error", func() { api.AliasAdd(ctxbg, "support2", "mox.example", config.Alias{}) }) // No addresses.
333
334 api.AliasUpdate(ctxbg, "support", "mox.example", true, true, true)
335 tneedErrorCode(t, "user:error", func() { api.AliasUpdate(ctxbg, "bogus", "mox.example", true, true, true) }) // Unknown alias localpart.
336 tneedErrorCode(t, "user:error", func() { api.AliasUpdate(ctxbg, "support", "bogus.example", true, true, true) }) // Unknown alias domain.
337
338 tneedErrorCode(t, "user:error", func() {
339 api.AliasAddressesAdd(ctxbg, "support", "mox.example", []string{"mjl2@mox.example", "mjl2@mox.example"})
340 }) // Cannot add twice.
341 api.AliasAddressesAdd(ctxbg, "support", "mox.example", []string{"mjl2@mox.example"})
342 tneedErrorCode(t, "user:error", func() { api.AliasAddressesAdd(ctxbg, "support", "mox.example", []string{"mjl2@mox.example"}) }) // Already present.
343 tneedErrorCode(t, "user:error", func() { api.AliasAddressesAdd(ctxbg, "support", "mox.example", []string{"bogus@mox.example"}) }) // Unknown dest localpart.
344 tneedErrorCode(t, "user:error", func() { api.AliasAddressesAdd(ctxbg, "support", "mox.example", []string{"bogus@bogus.example"}) }) // Unknown dest domain.
345 tneedErrorCode(t, "user:error", func() { api.AliasAddressesAdd(ctxbg, "support2", "mox.example", []string{"mjl@mox.example"}) }) // Unknown alias localpart.
346 tneedErrorCode(t, "user:error", func() { api.AliasAddressesAdd(ctxbg, "support", "bogus.example", []string{"mjl@mox.example"}) }) // Unknown alias localpart.
347 tneedErrorCode(t, "user:error", func() { api.AliasAddressesAdd(ctxbg, "support", "mox.example", []string{"support@mox.example"}) }) // Alias cannot be destination.
348
349 tneedErrorCode(t, "user:error", func() { api.AliasAddressesRemove(ctxbg, "support", "mox.example", []string{}) }) // Need at least 1 address.
350 tneedErrorCode(t, "user:error", func() { api.AliasAddressesRemove(ctxbg, "support", "mox.example", []string{"bogus@mox.example"}) }) // Not a member.
351 tneedErrorCode(t, "user:error", func() { api.AliasAddressesRemove(ctxbg, "support", "mox.example", []string{"bogus@bogus.example"}) }) // Not member, unknown domain.
352 tneedErrorCode(t, "user:error", func() { api.AliasAddressesRemove(ctxbg, "support2", "mox.example", []string{"mjl@mox.example"}) }) // Unknown alias localpart.
353 tneedErrorCode(t, "user:error", func() { api.AliasAddressesRemove(ctxbg, "support", "bogus.example", []string{"mjl@mox.example"}) }) // Unknown alias domain.
354 tneedErrorCode(t, "user:error", func() {
355 api.AliasAddressesRemove(ctxbg, "support", "mox.example", []string{"mjl@mox.example", "mjl2@mox.example"})
356 }) // Cannot leave zero addresses.
357 api.AliasAddressesRemove(ctxbg, "support", "mox.example", []string{"mjl@mox.example"})
358
359 api.AliasRemove(ctxbg, "support", "mox.example") // Restore.
360 tneedErrorCode(t, "user:error", func() { api.AliasRemove(ctxbg, "support", "mox.example") }) // No longer exists.
361 tneedErrorCode(t, "user:error", func() { api.AliasRemove(ctxbg, "support", "bogus.example") }) // Unknown alias domain.
362
363}
364
365func TestCheckDomain(t *testing.T) {
366 // NOTE: we aren't currently looking at the results, having the code paths executed is better than nothing.
367
368 log := mlog.New("webadmin", nil)
369
370 resolver := dns.MockResolver{
371 MX: map[string][]*net.MX{
372 "mox.example.": {{Host: "mail.mox.example.", Pref: 10}},
373 },
374 A: map[string][]string{
375 "mail.mox.example.": {"127.0.0.2"},
376 },
377 AAAA: map[string][]string{
378 "mail.mox.example.": {"127.0.0.2"},
379 },
380 TXT: map[string][]string{
381 "mox.example.": {"v=spf1 mx -all"},
382 "test._domainkey.mox.example.": {"v=DKIM1;h=sha256;k=ed25519;p=ln5zd/JEX4Jy60WAhUOv33IYm2YZMyTQAdr9stML504="},
383 "_dmarc.mox.example.": {"v=DMARC1; p=reject; rua=mailto:mjl@mox.example"},
384 "_smtp._tls.mox.example": {"v=TLSRPTv1; rua=mailto:tlsrpt@mox.example;"},
385 "_mta-sts.mox.example": {"v=STSv1; id=20160831085700Z"},
386 },
387 CNAME: map[string]string{},
388 }
389
390 listener := config.Listener{
391 IPs: []string{"127.0.0.2"},
392 Hostname: "mox.example",
393 HostnameDomain: dns.Domain{ASCII: "mox.example"},
394 }
395 listener.SMTP.Enabled = true
396 listener.AutoconfigHTTPS.Enabled = true
397 listener.MTASTSHTTPS.Enabled = true
398
399 mox.Conf.Static.Listeners = map[string]config.Listener{
400 "public": listener,
401 }
402 domain := config.Domain{
403 DKIM: config.DKIM{
404 Selectors: map[string]config.Selector{
405 "test": {
406 HashEffective: "sha256",
407 HeadersEffective: []string{"From", "Date", "Subject"},
408 Key: ed25519.NewKeyFromSeed(make([]byte, 32)), // warning: fake zero key, do not copy this code.
409 Domain: dns.Domain{ASCII: "test"},
410 },
411 "missing": {
412 HashEffective: "sha256",
413 HeadersEffective: []string{"From", "Date", "Subject"},
414 Key: ed25519.NewKeyFromSeed(make([]byte, 32)), // warning: fake zero key, do not copy this code.
415 Domain: dns.Domain{ASCII: "missing"},
416 },
417 },
418 Sign: []string{"test", "test2"},
419 },
420 }
421 mox.Conf.Dynamic.Domains = map[string]config.Domain{
422 "mox.example": domain,
423 }
424
425 // Make a dialer that fails immediately before actually connecting.
426 done := make(chan struct{})
427 close(done)
428 dialer := &net.Dialer{Deadline: time.Now().Add(-time.Second), Cancel: done}
429
430 checkDomain(ctxbg, resolver, dialer, "mox.example")
431 // todo: check returned data
432
433 Admin{}.Domains(ctxbg) // todo: check results
434 dnsblsStatus(ctxbg, log, resolver) // todo: check results
435}
436