mirror of
https://github.com/mjl-/mox.git
synced 2024-12-26 16:33:47 +03:00
960a51242d
the members must currently all be addresses of local accounts. a message sent to an alias is accepted if at least one of the members accepts it. if no members accepts it (e.g. due to bad reputation of sender), the message is rejected. if a message is submitted to both an alias addresses and to recipients that are members of the alias in an smtp transaction, the message will be delivered to such members only once. the same applies if the address in the message from-header is the address of a member: that member won't receive the message (they sent it). this prevents duplicate messages. aliases have three configuration options: - PostPublic: whether anyone can send through the alias, or only members. members-only lists can be useful inside organizations for internal communication. public lists can be useful for support addresses. - ListMembers: whether members can see the addresses of other members. this can be seen in the account web interface. in the future, we could export this in other ways, so clients can expand the list. - AllowMsgFrom: whether messages can be sent through the alias with the alias address used in the message from-header. the webmail knows it can use that address, and will use it as from-address when replying to a message sent to that address. ideas for the future: - allow external addresses as members. still with some restrictions, such as requiring a valid dkim-signature so delivery has a chance to succeed. will also need configuration of an admin that can receive any bounces. - allow specifying specific members who can sent through the list (instead of all members). for github issue #57 by hmfaysal. also relevant for #99 by naturalethic. thanks to damir & marin from sartura for discussing requirements/features.
434 lines
18 KiB
Go
434 lines
18 KiB
Go
package webadmin
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"runtime/debug"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"github.com/mjl-/sherpa"
|
|
|
|
"github.com/mjl-/mox/config"
|
|
"github.com/mjl-/mox/dns"
|
|
"github.com/mjl-/mox/mlog"
|
|
"github.com/mjl-/mox/mox-"
|
|
"github.com/mjl-/mox/mtasts"
|
|
"github.com/mjl-/mox/queue"
|
|
"github.com/mjl-/mox/store"
|
|
"github.com/mjl-/mox/webauth"
|
|
)
|
|
|
|
var ctxbg = context.Background()
|
|
|
|
func init() {
|
|
mox.LimitersInit()
|
|
webauth.BadAuthDelay = 0
|
|
}
|
|
|
|
func tneedErrorCode(t *testing.T, code string, fn func()) {
|
|
t.Helper()
|
|
defer func() {
|
|
t.Helper()
|
|
x := recover()
|
|
if x == nil {
|
|
debug.PrintStack()
|
|
t.Fatalf("expected sherpa user error, saw success")
|
|
}
|
|
if err, ok := x.(*sherpa.Error); !ok {
|
|
debug.PrintStack()
|
|
t.Fatalf("expected sherpa error, saw %#v", x)
|
|
} else if err.Code != code {
|
|
debug.PrintStack()
|
|
t.Fatalf("expected sherpa error code %q, saw other sherpa error %#v", code, err)
|
|
}
|
|
}()
|
|
|
|
fn()
|
|
}
|
|
|
|
func tcheck(t *testing.T, err error, msg string) {
|
|
t.Helper()
|
|
if err != nil {
|
|
t.Fatalf("%s: %s", msg, err)
|
|
}
|
|
}
|
|
|
|
func tcompare(t *testing.T, got, expect any) {
|
|
t.Helper()
|
|
if !reflect.DeepEqual(got, expect) {
|
|
t.Fatalf("got:\n%#v\nexpected:\n%#v", got, expect)
|
|
}
|
|
}
|
|
|
|
func readBody(r io.Reader) string {
|
|
buf, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return fmt.Sprintf("read error: %s", err)
|
|
}
|
|
return fmt.Sprintf("data: %q", buf)
|
|
}
|
|
|
|
func TestAdminAuth(t *testing.T) {
|
|
os.RemoveAll("../testdata/webadmin/data")
|
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/webadmin/mox.conf")
|
|
mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
|
|
mox.MustLoadConfig(true, false)
|
|
|
|
adminpwhash, err := bcrypt.GenerateFromPassword([]byte("moxtest123"), bcrypt.DefaultCost)
|
|
tcheck(t, err, "generate bcrypt hash")
|
|
|
|
path := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
|
|
err = os.WriteFile(path, adminpwhash, 0660)
|
|
tcheck(t, err, "write password file")
|
|
defer os.Remove(path)
|
|
|
|
api := Admin{cookiePath: "/admin/"}
|
|
apiHandler, err := makeSherpaHandler(api.cookiePath, false)
|
|
tcheck(t, err, "sherpa handler")
|
|
|
|
respRec := httptest.NewRecorder()
|
|
reqInfo := requestInfo{"", respRec, &http.Request{RemoteAddr: "127.0.0.1:1234"}}
|
|
ctx := context.WithValue(ctxbg, requestInfoCtxKey, reqInfo)
|
|
|
|
// Missing login token.
|
|
tneedErrorCode(t, "user:error", func() { api.Login(ctx, "", "moxtest123") })
|
|
|
|
// Login with loginToken.
|
|
loginCookie := &http.Cookie{Name: "webadminlogin"}
|
|
loginCookie.Value = api.LoginPrep(ctx)
|
|
reqInfo.Request.Header = http.Header{"Cookie": []string{loginCookie.String()}}
|
|
|
|
csrfToken := api.Login(ctx, loginCookie.Value, "moxtest123")
|
|
var sessionCookie *http.Cookie
|
|
for _, c := range respRec.Result().Cookies() {
|
|
if c.Name == "webadminsession" {
|
|
sessionCookie = c
|
|
break
|
|
}
|
|
}
|
|
if sessionCookie == nil {
|
|
t.Fatalf("missing session cookie")
|
|
}
|
|
|
|
// Valid loginToken, but bad credentials.
|
|
loginCookie.Value = api.LoginPrep(ctx)
|
|
reqInfo.Request.Header = http.Header{"Cookie": []string{loginCookie.String()}}
|
|
tneedErrorCode(t, "user:loginFailed", func() { api.Login(ctx, loginCookie.Value, "badauth") })
|
|
|
|
type httpHeaders [][2]string
|
|
ctJSON := [2]string{"Content-Type", "application/json; charset=utf-8"}
|
|
|
|
cookieOK := &http.Cookie{Name: "webadminsession", Value: sessionCookie.Value}
|
|
cookieBad := &http.Cookie{Name: "webadminsession", Value: "AAAAAAAAAAAAAAAAAAAAAA"}
|
|
hdrSessionOK := [2]string{"Cookie", cookieOK.String()}
|
|
hdrSessionBad := [2]string{"Cookie", cookieBad.String()}
|
|
hdrCSRFOK := [2]string{"x-mox-csrf", string(csrfToken)}
|
|
hdrCSRFBad := [2]string{"x-mox-csrf", "AAAAAAAAAAAAAAAAAAAAAA"}
|
|
|
|
testHTTP := func(method, path string, headers httpHeaders, expStatusCode int, expHeaders httpHeaders, check func(resp *http.Response)) {
|
|
t.Helper()
|
|
|
|
req := httptest.NewRequest(method, path, nil)
|
|
for _, kv := range headers {
|
|
req.Header.Add(kv[0], kv[1])
|
|
}
|
|
rr := httptest.NewRecorder()
|
|
rr.Body = &bytes.Buffer{}
|
|
handle(apiHandler, false, rr, req)
|
|
if rr.Code != expStatusCode {
|
|
t.Fatalf("got status %d, expected %d (%s)", rr.Code, expStatusCode, readBody(rr.Body))
|
|
}
|
|
|
|
resp := rr.Result()
|
|
for _, h := range expHeaders {
|
|
if resp.Header.Get(h[0]) != h[1] {
|
|
t.Fatalf("for header %q got value %q, expected %q", h[0], resp.Header.Get(h[0]), h[1])
|
|
}
|
|
}
|
|
|
|
if check != nil {
|
|
check(resp)
|
|
}
|
|
}
|
|
testHTTPAuthAPI := func(method, path string, expStatusCode int, expHeaders httpHeaders, check func(resp *http.Response)) {
|
|
t.Helper()
|
|
testHTTP(method, path, httpHeaders{hdrCSRFOK, hdrSessionOK}, expStatusCode, expHeaders, check)
|
|
}
|
|
|
|
userAuthError := func(resp *http.Response, expCode string) {
|
|
t.Helper()
|
|
|
|
var response struct {
|
|
Error *sherpa.Error `json:"error"`
|
|
}
|
|
err := json.NewDecoder(resp.Body).Decode(&response)
|
|
tcheck(t, err, "parsing response as json")
|
|
if response.Error == nil {
|
|
t.Fatalf("expected sherpa error with code %s, no error", expCode)
|
|
}
|
|
if response.Error.Code != expCode {
|
|
t.Fatalf("got sherpa error code %q, expected %s", response.Error.Code, expCode)
|
|
}
|
|
}
|
|
badAuth := func(resp *http.Response) {
|
|
t.Helper()
|
|
userAuthError(resp, "user:badAuth")
|
|
}
|
|
noAuth := func(resp *http.Response) {
|
|
t.Helper()
|
|
userAuthError(resp, "user:noAuth")
|
|
}
|
|
|
|
testHTTP("POST", "/api/Bogus", httpHeaders{}, http.StatusOK, nil, noAuth)
|
|
testHTTP("POST", "/api/Bogus", httpHeaders{hdrCSRFBad}, http.StatusOK, nil, noAuth)
|
|
testHTTP("POST", "/api/Bogus", httpHeaders{hdrSessionBad}, http.StatusOK, nil, noAuth)
|
|
testHTTP("POST", "/api/Bogus", httpHeaders{hdrCSRFBad, hdrSessionBad}, http.StatusOK, nil, badAuth)
|
|
testHTTP("POST", "/api/Bogus", httpHeaders{hdrCSRFOK}, http.StatusOK, nil, noAuth)
|
|
testHTTP("POST", "/api/Bogus", httpHeaders{hdrSessionOK}, http.StatusOK, nil, noAuth)
|
|
testHTTP("POST", "/api/Bogus", httpHeaders{hdrCSRFBad, hdrSessionOK}, http.StatusOK, nil, badAuth)
|
|
testHTTP("POST", "/api/Bogus", httpHeaders{hdrCSRFOK, hdrSessionBad}, http.StatusOK, nil, badAuth)
|
|
testHTTPAuthAPI("GET", "/api/Transports", http.StatusMethodNotAllowed, nil, nil)
|
|
testHTTPAuthAPI("POST", "/api/Transports", http.StatusOK, httpHeaders{ctJSON}, nil)
|
|
|
|
// Logout needs session token.
|
|
reqInfo.SessionToken = store.SessionToken(strings.SplitN(sessionCookie.Value, " ", 2)[0])
|
|
ctx = context.WithValue(ctxbg, requestInfoCtxKey, reqInfo)
|
|
|
|
api.Logout(ctx)
|
|
tneedErrorCode(t, "server:error", func() { api.Logout(ctx) })
|
|
}
|
|
|
|
func TestAdmin(t *testing.T) {
|
|
os.RemoveAll("../testdata/webadmin/data")
|
|
defer os.RemoveAll("../testdata/webadmin/dkim")
|
|
mox.ConfigStaticPath = filepath.FromSlash("../testdata/webadmin/mox.conf")
|
|
mox.ConfigDynamicPath = filepath.Join(filepath.Dir(mox.ConfigStaticPath), "domains.conf")
|
|
mox.MustLoadConfig(true, false)
|
|
err := queue.Init()
|
|
tcheck(t, err, "queue init")
|
|
|
|
api := Admin{}
|
|
|
|
mrl := api.RetiredList(ctxbg, queue.RetiredFilter{}, queue.RetiredSort{})
|
|
tcompare(t, len(mrl), 0)
|
|
|
|
n := api.HookQueueSize(ctxbg)
|
|
tcompare(t, n, 0)
|
|
|
|
hl := api.HookList(ctxbg, queue.HookFilter{}, queue.HookSort{})
|
|
tcompare(t, len(hl), 0)
|
|
|
|
n = api.HookNextAttemptSet(ctxbg, queue.HookFilter{}, 0)
|
|
tcompare(t, n, 0)
|
|
|
|
n = api.HookNextAttemptAdd(ctxbg, queue.HookFilter{}, 0)
|
|
tcompare(t, n, 0)
|
|
|
|
hrl := api.HookRetiredList(ctxbg, queue.HookRetiredFilter{}, queue.HookRetiredSort{})
|
|
tcompare(t, len(hrl), 0)
|
|
|
|
n = api.HookCancel(ctxbg, queue.HookFilter{})
|
|
tcompare(t, n, 0)
|
|
|
|
api.Config(ctxbg)
|
|
api.DomainConfig(ctxbg, "mox.example")
|
|
tneedErrorCode(t, "user:error", func() { api.DomainConfig(ctxbg, "bogus.example") })
|
|
|
|
api.AccountRoutesSave(ctxbg, "mjl", []config.Route{{Transport: "direct"}})
|
|
tneedErrorCode(t, "user:error", func() { api.AccountRoutesSave(ctxbg, "mjl", []config.Route{{Transport: "bogus"}}) })
|
|
api.AccountRoutesSave(ctxbg, "mjl", nil)
|
|
|
|
api.DomainRoutesSave(ctxbg, "mox.example", []config.Route{{Transport: "direct"}})
|
|
tneedErrorCode(t, "user:error", func() { api.DomainRoutesSave(ctxbg, "mox.example", []config.Route{{Transport: "bogus"}}) })
|
|
api.DomainRoutesSave(ctxbg, "mox.example", nil)
|
|
|
|
api.RoutesSave(ctxbg, []config.Route{{Transport: "direct"}})
|
|
tneedErrorCode(t, "user:error", func() { api.RoutesSave(ctxbg, []config.Route{{Transport: "bogus"}}) })
|
|
api.RoutesSave(ctxbg, nil)
|
|
|
|
api.DomainDescriptionSave(ctxbg, "mox.example", "description")
|
|
tneedErrorCode(t, "server:error", func() { api.DomainDescriptionSave(ctxbg, "mox.example", "newline not ok\n") }) // todo: user error
|
|
tneedErrorCode(t, "user:error", func() { api.DomainDescriptionSave(ctxbg, "bogus.example", "unknown domain") })
|
|
api.DomainDescriptionSave(ctxbg, "mox.example", "") // Restore.
|
|
|
|
api.DomainClientSettingsDomainSave(ctxbg, "mox.example", "mail.mox.example")
|
|
tneedErrorCode(t, "user:error", func() { api.DomainClientSettingsDomainSave(ctxbg, "mox.example", "bogus domain") })
|
|
tneedErrorCode(t, "user:error", func() { api.DomainClientSettingsDomainSave(ctxbg, "bogus.example", "unknown.example") })
|
|
api.DomainClientSettingsDomainSave(ctxbg, "mox.example", "") // Restore.
|
|
|
|
api.DomainLocalpartConfigSave(ctxbg, "mox.example", "-", true)
|
|
tneedErrorCode(t, "user:error", func() { api.DomainLocalpartConfigSave(ctxbg, "bogus.example", "", false) })
|
|
api.DomainLocalpartConfigSave(ctxbg, "mox.example", "", false) // Restore.
|
|
|
|
api.DomainDMARCAddressSave(ctxbg, "mox.example", "dmarc-reports", "", "mjl", "DMARC")
|
|
tneedErrorCode(t, "user:error", func() { api.DomainDMARCAddressSave(ctxbg, "bogus.example", "dmarc-reports", "", "mjl", "DMARC") })
|
|
tneedErrorCode(t, "user:error", func() { api.DomainDMARCAddressSave(ctxbg, "mox.example", "dmarc-reports", "", "bogus", "DMARC") })
|
|
api.DomainDMARCAddressSave(ctxbg, "mox.example", "", "", "", "") // Restore.
|
|
|
|
api.DomainTLSRPTAddressSave(ctxbg, "mox.example", "tls-reports", "", "mjl", "TLSRPT")
|
|
tneedErrorCode(t, "user:error", func() { api.DomainTLSRPTAddressSave(ctxbg, "bogus.example", "tls-reports", "", "mjl", "TLSRPT") })
|
|
tneedErrorCode(t, "user:error", func() { api.DomainTLSRPTAddressSave(ctxbg, "mox.example", "tls-reports", "", "bogus", "TLSRPT") })
|
|
api.DomainTLSRPTAddressSave(ctxbg, "mox.example", "", "", "", "") // Restore.
|
|
|
|
// todo: cannot enable mta-sts because we have no listener, which would require a tls cert for the domain.
|
|
// api.DomainMTASTSSave(ctxbg, "mox.example", "id0", mtasts.ModeEnforce, time.Hour, []string{"mail.mox.example"})
|
|
tneedErrorCode(t, "user:error", func() {
|
|
api.DomainMTASTSSave(ctxbg, "bogus.example", "id0", mtasts.ModeEnforce, time.Hour, []string{"mail.mox.example"})
|
|
})
|
|
tneedErrorCode(t, "user:error", func() {
|
|
api.DomainMTASTSSave(ctxbg, "mox.example", "invalid id", mtasts.ModeEnforce, time.Hour, []string{"mail.mox.example"})
|
|
})
|
|
tneedErrorCode(t, "user:error", func() {
|
|
api.DomainMTASTSSave(ctxbg, "mox.example", "id0", mtasts.Mode("bogus"), time.Hour, []string{"mail.mox.example"})
|
|
})
|
|
tneedErrorCode(t, "user:error", func() {
|
|
api.DomainMTASTSSave(ctxbg, "mox.example", "id0", mtasts.ModeEnforce, time.Hour, []string{"*.*.mail.mox.example"})
|
|
})
|
|
api.DomainMTASTSSave(ctxbg, "mox.example", "", mtasts.ModeNone, 0, nil) // Restore.
|
|
|
|
api.DomainDKIMAdd(ctxbg, "mox.example", "testsel", "ed25519", "sha256", true, true, true, nil, 24*time.Hour)
|
|
tneedErrorCode(t, "user:error", func() {
|
|
api.DomainDKIMAdd(ctxbg, "mox.example", "testsel", "ed25519", "sha256", true, true, true, nil, 24*time.Hour)
|
|
}) // Duplicate selector.
|
|
tneedErrorCode(t, "user:error", func() {
|
|
api.DomainDKIMAdd(ctxbg, "bogus.example", "testsel", "ed25519", "sha256", true, true, true, nil, 24*time.Hour)
|
|
})
|
|
conf := api.DomainConfig(ctxbg, "mox.example")
|
|
api.DomainDKIMSave(ctxbg, "mox.example", conf.DKIM.Selectors, conf.DKIM.Sign)
|
|
api.DomainDKIMSave(ctxbg, "mox.example", conf.DKIM.Selectors, []string{"testsel"})
|
|
tneedErrorCode(t, "user:error", func() { api.DomainDKIMSave(ctxbg, "mox.example", conf.DKIM.Selectors, []string{"bogus"}) })
|
|
tneedErrorCode(t, "user:error", func() { api.DomainDKIMSave(ctxbg, "mox.example", nil, []string{}) }) // Cannot remove selectors with save.
|
|
tneedErrorCode(t, "user:error", func() { api.DomainDKIMSave(ctxbg, "bogus.example", nil, []string{}) })
|
|
moreSel := map[string]config.Selector{
|
|
"testsel": conf.DKIM.Selectors["testsel"],
|
|
"testsel2": conf.DKIM.Selectors["testsel2"],
|
|
}
|
|
tneedErrorCode(t, "user:error", func() { api.DomainDKIMSave(ctxbg, "mox.example", moreSel, []string{}) }) // Cannot add selectors with save.
|
|
api.DomainDKIMRemove(ctxbg, "mox.example", "testsel")
|
|
tneedErrorCode(t, "user:error", func() { api.DomainDKIMRemove(ctxbg, "mox.example", "testsel") }) // Already removed.
|
|
tneedErrorCode(t, "user:error", func() { api.DomainDKIMRemove(ctxbg, "bogus.example", "testsel") })
|
|
|
|
// Aliases
|
|
alias := config.Alias{Addresses: []string{"mjl@mox.example"}}
|
|
api.AliasAdd(ctxbg, "support", "mox.example", alias)
|
|
tneedErrorCode(t, "user:error", func() { api.AliasAdd(ctxbg, "support", "mox.example", alias) }) // Already present.
|
|
tneedErrorCode(t, "user:error", func() { api.AliasAdd(ctxbg, "Support", "mox.example", alias) }) // Duplicate, canonical.
|
|
tneedErrorCode(t, "user:error", func() { api.AliasAdd(ctxbg, "support", "bogus.example", alias) }) // Unknown domain.
|
|
tneedErrorCode(t, "user:error", func() { api.AliasAdd(ctxbg, "support2", "mox.example", config.Alias{}) }) // No addresses.
|
|
|
|
api.AliasUpdate(ctxbg, "support", "mox.example", true, true, true)
|
|
tneedErrorCode(t, "user:error", func() { api.AliasUpdate(ctxbg, "bogus", "mox.example", true, true, true) }) // Unknown alias localpart.
|
|
tneedErrorCode(t, "user:error", func() { api.AliasUpdate(ctxbg, "support", "bogus.example", true, true, true) }) // Unknown alias domain.
|
|
|
|
tneedErrorCode(t, "user:error", func() {
|
|
api.AliasAddressesAdd(ctxbg, "support", "mox.example", []string{"mjl2@mox.example", "mjl2@mox.example"})
|
|
}) // Cannot add twice.
|
|
api.AliasAddressesAdd(ctxbg, "support", "mox.example", []string{"mjl2@mox.example"})
|
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesAdd(ctxbg, "support", "mox.example", []string{"mjl2@mox.example"}) }) // Already present.
|
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesAdd(ctxbg, "support", "mox.example", []string{"bogus@mox.example"}) }) // Unknown dest localpart.
|
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesAdd(ctxbg, "support", "mox.example", []string{"bogus@bogus.example"}) }) // Unknown dest domain.
|
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesAdd(ctxbg, "support2", "mox.example", []string{"mjl@mox.example"}) }) // Unknown alias localpart.
|
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesAdd(ctxbg, "support", "bogus.example", []string{"mjl@mox.example"}) }) // Unknown alias localpart.
|
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesAdd(ctxbg, "support", "mox.example", []string{"support@mox.example"}) }) // Alias cannot be destination.
|
|
|
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesRemove(ctxbg, "support", "mox.example", []string{}) }) // Need at least 1 address.
|
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesRemove(ctxbg, "support", "mox.example", []string{"bogus@mox.example"}) }) // Not a member.
|
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesRemove(ctxbg, "support", "mox.example", []string{"bogus@bogus.example"}) }) // Not member, unknown domain.
|
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesRemove(ctxbg, "support2", "mox.example", []string{"mjl@mox.example"}) }) // Unknown alias localpart.
|
|
tneedErrorCode(t, "user:error", func() { api.AliasAddressesRemove(ctxbg, "support", "bogus.example", []string{"mjl@mox.example"}) }) // Unknown alias domain.
|
|
tneedErrorCode(t, "user:error", func() {
|
|
api.AliasAddressesRemove(ctxbg, "support", "mox.example", []string{"mjl@mox.example", "mjl2@mox.example"})
|
|
}) // Cannot leave zero addresses.
|
|
api.AliasAddressesRemove(ctxbg, "support", "mox.example", []string{"mjl@mox.example"})
|
|
|
|
api.AliasRemove(ctxbg, "support", "mox.example") // Restore.
|
|
tneedErrorCode(t, "user:error", func() { api.AliasRemove(ctxbg, "support", "mox.example") }) // No longer exists.
|
|
tneedErrorCode(t, "user:error", func() { api.AliasRemove(ctxbg, "support", "bogus.example") }) // Unknown alias domain.
|
|
|
|
}
|
|
|
|
func TestCheckDomain(t *testing.T) {
|
|
// NOTE: we aren't currently looking at the results, having the code paths executed is better than nothing.
|
|
|
|
log := mlog.New("webadmin", nil)
|
|
|
|
resolver := dns.MockResolver{
|
|
MX: map[string][]*net.MX{
|
|
"mox.example.": {{Host: "mail.mox.example.", Pref: 10}},
|
|
},
|
|
A: map[string][]string{
|
|
"mail.mox.example.": {"127.0.0.2"},
|
|
},
|
|
AAAA: map[string][]string{
|
|
"mail.mox.example.": {"127.0.0.2"},
|
|
},
|
|
TXT: map[string][]string{
|
|
"mox.example.": {"v=spf1 mx -all"},
|
|
"test._domainkey.mox.example.": {"v=DKIM1;h=sha256;k=ed25519;p=ln5zd/JEX4Jy60WAhUOv33IYm2YZMyTQAdr9stML504="},
|
|
"_dmarc.mox.example.": {"v=DMARC1; p=reject; rua=mailto:mjl@mox.example"},
|
|
"_smtp._tls.mox.example": {"v=TLSRPTv1; rua=mailto:tlsrpt@mox.example;"},
|
|
"_mta-sts.mox.example": {"v=STSv1; id=20160831085700Z"},
|
|
},
|
|
CNAME: map[string]string{},
|
|
}
|
|
|
|
listener := config.Listener{
|
|
IPs: []string{"127.0.0.2"},
|
|
Hostname: "mox.example",
|
|
HostnameDomain: dns.Domain{ASCII: "mox.example"},
|
|
}
|
|
listener.SMTP.Enabled = true
|
|
listener.AutoconfigHTTPS.Enabled = true
|
|
listener.MTASTSHTTPS.Enabled = true
|
|
|
|
mox.Conf.Static.Listeners = map[string]config.Listener{
|
|
"public": listener,
|
|
}
|
|
domain := config.Domain{
|
|
DKIM: config.DKIM{
|
|
Selectors: map[string]config.Selector{
|
|
"test": {
|
|
HashEffective: "sha256",
|
|
HeadersEffective: []string{"From", "Date", "Subject"},
|
|
Key: ed25519.NewKeyFromSeed(make([]byte, 32)), // warning: fake zero key, do not copy this code.
|
|
Domain: dns.Domain{ASCII: "test"},
|
|
},
|
|
"missing": {
|
|
HashEffective: "sha256",
|
|
HeadersEffective: []string{"From", "Date", "Subject"},
|
|
Key: ed25519.NewKeyFromSeed(make([]byte, 32)), // warning: fake zero key, do not copy this code.
|
|
Domain: dns.Domain{ASCII: "missing"},
|
|
},
|
|
},
|
|
Sign: []string{"test", "test2"},
|
|
},
|
|
}
|
|
mox.Conf.Dynamic.Domains = map[string]config.Domain{
|
|
"mox.example": domain,
|
|
}
|
|
|
|
// Make a dialer that fails immediately before actually connecting.
|
|
done := make(chan struct{})
|
|
close(done)
|
|
dialer := &net.Dialer{Deadline: time.Now().Add(-time.Second), Cancel: done}
|
|
|
|
checkDomain(ctxbg, resolver, dialer, "mox.example")
|
|
// todo: check returned data
|
|
|
|
Admin{}.Domains(ctxbg) // todo: check results
|
|
dnsblsStatus(ctxbg, log, resolver) // todo: check results
|
|
}
|