From 8690f7c6cb6dbbf5e878471cf5b20f3a74af5f68 Mon Sep 17 00:00:00 2001 From: Pascal Bleser Date: Fri, 5 Jun 2026 18:59:40 +0200 Subject: [PATCH] groupware: port tests to Stalwart 0.16.7 --- go.mod | 4 +- go.sum | 4 - pkg/jmap/export_integration_test.go | 1135 +++++++++++++---- pkg/jmap/http.go | 1 + pkg/jmap/integration_addressbook_test.go | 16 +- pkg/jmap/integration_calendar_test.go | 12 +- pkg/jmap/integration_email_test.go | 30 +- pkg/jmap/integration_ws_test.go | 4 +- pkg/jmap/model_examples.go | 26 +- pkg/jmap/tools.go | 10 +- vendor/github.com/go-crypt/crypt/LICENSE | 21 - .../go-crypt/crypt/algorithm/const.go | 26 - .../go-crypt/crypt/algorithm/doc.go | 3 - .../go-crypt/crypt/algorithm/errors.go | 66 - .../crypt/algorithm/shacrypt/const.go | 46 - .../crypt/algorithm/shacrypt/decoder.go | 136 -- .../crypt/algorithm/shacrypt/digest.go | 83 -- .../go-crypt/crypt/algorithm/shacrypt/doc.go | 7 - .../crypt/algorithm/shacrypt/hasher.go | 156 --- .../go-crypt/crypt/algorithm/shacrypt/opts.go | 98 -- .../crypt/algorithm/shacrypt/variant.go | 92 -- .../go-crypt/crypt/algorithm/types.go | 62 - .../crypt/internal/encoding/base64adapted.go | 14 - .../go-crypt/crypt/internal/encoding/const.go | 9 - .../crypt/internal/encoding/digest.go | 10 - .../go-crypt/crypt/internal/encoding/doc.go | 2 - .../crypt/internal/encoding/parameters.go | 54 - .../go-crypt/crypt/internal/random/bytes.go | 32 - .../go-crypt/crypt/internal/random/doc.go | 2 - vendor/github.com/go-crypt/x/LICENSE | 27 - vendor/github.com/go-crypt/x/base64/base64.go | 51 - vendor/github.com/go-crypt/x/base64/const.go | 7 - vendor/github.com/go-crypt/x/crypt/const.go | 223 ---- vendor/github.com/go-crypt/x/crypt/crypt.go | 300 ----- vendor/github.com/go-crypt/x/crypt/util.go | 54 - .../testcontainers-go/network/network.go | 196 +++ vendor/modules.txt | 11 +- 37 files changed, 1108 insertions(+), 1922 deletions(-) delete mode 100644 vendor/github.com/go-crypt/crypt/LICENSE delete mode 100644 vendor/github.com/go-crypt/crypt/algorithm/const.go delete mode 100644 vendor/github.com/go-crypt/crypt/algorithm/doc.go delete mode 100644 vendor/github.com/go-crypt/crypt/algorithm/errors.go delete mode 100644 vendor/github.com/go-crypt/crypt/algorithm/shacrypt/const.go delete mode 100644 vendor/github.com/go-crypt/crypt/algorithm/shacrypt/decoder.go delete mode 100644 vendor/github.com/go-crypt/crypt/algorithm/shacrypt/digest.go delete mode 100644 vendor/github.com/go-crypt/crypt/algorithm/shacrypt/doc.go delete mode 100644 vendor/github.com/go-crypt/crypt/algorithm/shacrypt/hasher.go delete mode 100644 vendor/github.com/go-crypt/crypt/algorithm/shacrypt/opts.go delete mode 100644 vendor/github.com/go-crypt/crypt/algorithm/shacrypt/variant.go delete mode 100644 vendor/github.com/go-crypt/crypt/algorithm/types.go delete mode 100644 vendor/github.com/go-crypt/crypt/internal/encoding/base64adapted.go delete mode 100644 vendor/github.com/go-crypt/crypt/internal/encoding/const.go delete mode 100644 vendor/github.com/go-crypt/crypt/internal/encoding/digest.go delete mode 100644 vendor/github.com/go-crypt/crypt/internal/encoding/doc.go delete mode 100644 vendor/github.com/go-crypt/crypt/internal/encoding/parameters.go delete mode 100644 vendor/github.com/go-crypt/crypt/internal/random/bytes.go delete mode 100644 vendor/github.com/go-crypt/crypt/internal/random/doc.go delete mode 100644 vendor/github.com/go-crypt/x/LICENSE delete mode 100644 vendor/github.com/go-crypt/x/base64/base64.go delete mode 100644 vendor/github.com/go-crypt/x/base64/const.go delete mode 100644 vendor/github.com/go-crypt/x/crypt/const.go delete mode 100644 vendor/github.com/go-crypt/x/crypt/crypt.go delete mode 100644 vendor/github.com/go-crypt/x/crypt/util.go create mode 100644 vendor/github.com/testcontainers/testcontainers-go/network/network.go diff --git a/go.mod b/go.mod index 0e4717e8b5..a6ac5213bf 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,6 @@ require ( github.com/ggwhite/go-masker v1.1.0 github.com/go-chi/chi/v5 v5.3.0 github.com/go-chi/render v1.0.3 - github.com/go-crypt/crypt v0.4.5 github.com/go-jose/go-jose/v3 v3.0.5 github.com/go-ldap/ldap/v3 v3.4.13 github.com/go-ldap/ldif v0.0.0-20200320164324-fd88d9b715b3 @@ -62,6 +61,7 @@ require ( github.com/microcosm-cc/bluemonday v1.0.27 github.com/miekg/dns v1.1.68 github.com/mna/pigeon v1.3.0 + github.com/moby/moby/api v1.54.1 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/nats-io/nats-server/v2 v2.14.2 github.com/nats-io/nats.go v1.51.0 @@ -220,7 +220,6 @@ require ( github.com/gdexlab/go-render v1.0.1 // indirect github.com/go-acme/lego/v4 v4.4.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect - github.com/go-crypt/x v0.4.7 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.9.0 // indirect github.com/go-git/go-git/v5 v5.19.1 // indirect @@ -310,7 +309,6 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.2.0 // indirect - github.com/moby/moby/api v1.54.1 // indirect github.com/moby/moby/client v0.4.0 // indirect github.com/moby/patternmatcher v0.6.1 // indirect github.com/moby/sys/sequential v0.6.0 // indirect diff --git a/go.sum b/go.sum index 92276a95d6..2101d70e21 100644 --- a/go.sum +++ b/go.sum @@ -392,10 +392,6 @@ github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutV github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s= -github.com/go-crypt/crypt v0.4.5 h1:cCR5vVejGk1kurwoGfkLxGORY+Pc9GiE7xKCpyHZ3n4= -github.com/go-crypt/crypt v0.4.5/go.mod h1:cQijpCkqavdF52J1bE0PObWwqKKjQCHASHQ2dtLzOJs= -github.com/go-crypt/x v0.4.7 h1:hObjW67nhq/GI1jaD7XCv5RoiVKzF46XIbULgzH71oU= -github.com/go-crypt/x v0.4.7/go.mod h1:K3q7VmLC0U1QFAPn0SQvXjkAtu6FJuH0rN9LNqobX6k= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= diff --git a/pkg/jmap/export_integration_test.go b/pkg/jmap/export_integration_test.go index ae596bbfd1..b1b72ef3b3 100644 --- a/pkg/jmap/export_integration_test.go +++ b/pkg/jmap/export_integration_test.go @@ -1,9 +1,11 @@ package jmap import ( + "archive/tar" "bytes" "context" "crypto/tls" + "encoding/base64" "encoding/json" "fmt" "io" @@ -16,6 +18,7 @@ import ( "os" "reflect" "regexp" + "runtime" "slices" "strconv" "strings" @@ -24,22 +27,22 @@ import ( "time" "github.com/google/go-cmp/cmp" + dockernetwork "github.com/moby/moby/api/types/network" "github.com/stretchr/testify/require" "github.com/gorilla/websocket" "github.com/tidwall/pretty" "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/network" "github.com/testcontainers/testcontainers-go/wait" "github.com/brianvoe/gofakeit/v7" pw "github.com/sethvargo/go-password/password" "github.com/opencloud-eu/opencloud/pkg/jscontact" - clog "github.com/opencloud-eu/opencloud/pkg/log" + oclog "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/pkg/structs" - - "github.com/go-crypt/crypt/algorithm/shacrypt" ) const ( @@ -52,87 +55,228 @@ const ( type User struct { name string description string - email string + alias string password string + email string } func userpassword() string { - password, err := pw.Generate(4+rand.Intn(28), 2, 0, false, true) + password, err := pw.Generate(10+rand.Intn(28), 2, 0, false, true) if err != nil { panic(err) } return password } +func mkuser(name string, description string, alias string) User { + parts := strings.Split(alias, "@") + return User{name: name, description: description, alias: alias, email: name + "@" + parts[1], password: userpassword()} +} + var ( - domains = [...]string{"earth.gov", "mars.mil", "opa.org"} - users = [...]User{ - {"cdrummer", "Camina Drummer", "camina.drummer@opa.org", userpassword()}, - {"aburton", "Amos Burton", "amos.burton@earth.gov", userpassword()}, - {"jholden", "James Holden", "james.holden@earth.gov", userpassword()}, - {"adawes", "Anderson Dawes", "anderson.dawes@opa.org", userpassword()}, - {"nnagata", "Naomi Nagata", "naomi.nagata@opa.org", userpassword()}, - {"kashford", "Klaes Ashford", "klaes.ashford@opa.org", userpassword()}, - {"fjohnson", "Fred Johnson", "fred.johnson@opa.org", userpassword()}, - {"cavasarala", "Chrisjen Avasarala}", "chrissy@earth.gov", userpassword()}, - {"bdraper", "Roberta Draper", "bobby@mars.mil", userpassword()}, + users = [...]User{ + mkuser("cdrummer", "Camina Drummer", "camina.drummer@opa.org"), + mkuser("aburton", "Amos Burton", "amos.burton@earth.gov"), + mkuser("jholden", "James Holden", "james.holden@earth.gov"), + mkuser("adawes", "Anderson Dawes", "anderson.dawes@opa.org"), + mkuser("nnagata", "Naomi Nagata", "naomi.nagata@opa.org"), + mkuser("kashford", "Klaes Ashford", "klaes.ashford@opa.org"), + mkuser("fjohnson", "Fred Johnson", "fred.johnson@opa.org"), + mkuser("cavasarala", "Chrisjen Avasarala}", "chrissy@earth.gov"), + mkuser("bdraper", "Roberta Draper", "bobby@mars.mil"), } ) const ( - stalwartImage = "ghcr.io/stalwartlabs/stalwart:v0.15.5-alpine" - httpPort = "8080" - imapsPort = "993" - configTemplate = ` -authentication.fallback-admin.secret = "secret" -authentication.fallback-admin.user = "mailadmin" -authentication.master.secret = "{{.masterpassword}}" -authentication.master.user = "{{.masterusername}}" -directory.test.bind.auth.method = "default" -directory.test.cache.size = 1048576 -directory.test.cache.ttl.negative = "10m" -directory.test.cache.ttl.positive = "1h" -directory.test.store = "rocksdb" -directory.test.type = "internal" -metrics.prometheus.enable = false -server.listener.http.bind = "[::]:{{.httpPort}}" -server.listener.http.protocol = "http" -server.listener.imaptls.bind = "[::]:{{.imapsPort}}" -server.listener.imaptls.protocol = "imap" -server.listener.imaptls.tls.implicit = true -server.hostname = "{{.hostname}}" -server.max-connections = 8192 -server.socket.backlog = 1024 -server.socket.nodelay = true -server.socket.reuse-addr = true -server.socket.reuse-port = true -storage.blob = "rocksdb" -storage.data = "rocksdb" -storage.directory = "test" -storage.fts = "rocksdb" -storage.lookup = "rocksdb" -store.rocksdb.compression = "lz4" -store.rocksdb.path = "/opt/stalwart/data" -store.rocksdb.type = "rocksdb" -tracer.log.ansi = false -tracer.log.buffered = false -tracer.log.enable = true -tracer.log.level = "trace" -tracer.log.lossy = false -tracer.log.multiline = false -tracer.log.type = "stdout" -sharing.allow-directory-query = {{.dirquery}} -auth.dkim.sign = false -auth.dkim.verify = "disable" -auth.spf.verify.ehlo = "disable" -auth.spf.verify.mail-from = "disable" -auth.arc.verify = "disable" -auth.arc.seal = false -auth.dmarc.verify = "disable" -auth.iprev.verify = "disable" + stalwartImage = "ghcr.io/stalwartlabs/stalwart:v0.16.7-alpine" + httpPort = "8080" + imapsPort = "993" + jsonConfigTemplate = `{"@type":"RocksDb","path":"/var/lib/stalwart/","blobSize":16834,"bufferSize":134217728,"poolWorkers":null}` + cliDockerfile = ` +FROM alpine:latest +ARG VERSION="1.0.8" +ARG ARCH="x86_64" +RUN apk add --no-cache curl tar xz && curl --proto '=https' --tlsv1.2 -LsSf https://github.com/stalwartlabs/cli/releases/download/v${VERSION}/stalwart-cli-${ARCH}-unknown-linux-musl.tar.xz | tar xJf - --strip-components=1 -C /usr/local/bin/ && rm /usr/local/bin/*.md +CMD ["/usr/local/bin/stalwart-cli"] ` ) +var ( + dumpTemplate = []string{ + `{"@type":"destroy","object":"Tracer"}`, + `{"@type":"destroy","object":"NetworkListener"}`, + `{"@type":"destroy","object":"DnsServer"}`, + `{"@type":"destroy","object":"Certificate"}`, + `{"@type":"destroy","object":"Tenant"}`, + `{"@type":"destroy","object":"Role"}`, + `{"@type":"destroy","object":"AcmeProvider"}`, + `{"@type":"destroy","object":"Directory"}`, + `{"@type":"destroy","object":"Domain"}`, + `{"@type":"destroy","object":"Account"}`, + `{"@type":"destroy","object":"DkimSignature"}`, + `{"@type":"create","object":"Tracer","value":{"tracer-iugg9zgwaaaa":{"@type":"Stdout","eventsPolicy":"exclude","enable":true,"multiline":false,"level":"trace","lossy":false,"buffered":true,"ansi":false,"events":{"store.data-write":true,"eval.result":true,"store.cache-hit":true,"store.cache-miss":true,"store.cache-stale":true,"store.cache-update":true,"store.data-iterate":true,"spam.rules-updated":true,"resource.download-external":true,"task-manager.task-acquired":true,"task-manager.scheduler-started":true,"task-manager.manager-started":true}}}}`, + `{"@type":"create","object":"NetworkListener","value":{"networklistener-iughfwjyahqb":{"socketNoDelay":true,"socketReusePort":true,"tlsDisableCipherSuites":{},"socketReuseAddress":true,"name":"http","bind":{"[::]:8080":true},"socketTosV4":null,"socketTtl":null,"maxConnections":8192,"socketBacklog":1024,"socketReceiveBufferSize":null,"overrideProxyTrustedNetworks":{},"socketSendBufferSize":null,"tlsIgnoreClientOrder":true,"tlsImplicit":false,"tlsTimeout":60000,"useTls":true,"tlsDisableProtocols":{},"protocol":"http"},"networklistener-iughfwjyahab":{"socketNoDelay":true,"socketReusePort":true,"tlsDisableCipherSuites":{},"socketReuseAddress":true,"name":"https","bind":{"[::]:443":true},"socketTosV4":null,"socketTtl":null,"maxConnections":8192,"socketBacklog":1024,"socketReceiveBufferSize":null,"overrideProxyTrustedNetworks":{},"socketSendBufferSize":null,"tlsIgnoreClientOrder":true,"tlsImplicit":true,"tlsTimeout":60000,"useTls":true,"tlsDisableProtocols":{},"protocol":"http"},"networklistener-iughfwjyagqb":{"socketNoDelay":true,"socketReusePort":true,"tlsDisableCipherSuites":{},"socketReuseAddress":true,"name":"sieve","bind":{"[::]:4190":true},"socketTosV4":null,"socketTtl":null,"maxConnections":8192,"socketBacklog":1024,"socketReceiveBufferSize":null,"overrideProxyTrustedNetworks":{},"socketSendBufferSize":null,"tlsIgnoreClientOrder":true,"tlsImplicit":false,"tlsTimeout":60000,"useTls":true,"tlsDisableProtocols":{},"protocol":"manageSieve"},"networklistener-iughfwjyagab":{"socketNoDelay":true,"socketReusePort":true,"tlsDisableCipherSuites":{},"socketReuseAddress":true,"name":"pop3s","bind":{"[::]:995":true},"socketTosV4":null,"socketTtl":null,"maxConnections":8192,"socketBacklog":1024,"socketReceiveBufferSize":null,"overrideProxyTrustedNetworks":{},"socketSendBufferSize":null,"tlsIgnoreClientOrder":true,"tlsImplicit":true,"tlsTimeout":60000,"useTls":true,"tlsDisableProtocols":{},"protocol":"pop3"},"networklistener-iughfwjyafqb":{"socketNoDelay":true,"socketReusePort":true,"tlsDisableCipherSuites":{},"socketReuseAddress":true,"name":"imaps","bind":{"[::]:993":true},"socketTosV4":null,"socketTtl":null,"maxConnections":8192,"socketBacklog":1024,"socketReceiveBufferSize":null,"overrideProxyTrustedNetworks":{},"socketSendBufferSize":null,"tlsIgnoreClientOrder":true,"tlsImplicit":true,"tlsTimeout":60000,"useTls":true,"tlsDisableProtocols":{},"protocol":"imap"},"networklistener-iughfwjwafab":{"socketNoDelay":true,"socketReusePort":true,"tlsDisableCipherSuites":{},"socketReuseAddress":true,"name":"submissions","bind":{"[::]:465":true},"socketTosV4":null,"socketTtl":null,"maxConnections":8192,"socketBacklog":1024,"socketReceiveBufferSize":null,"overrideProxyTrustedNetworks":{},"socketSendBufferSize":null,"tlsIgnoreClientOrder":true,"tlsImplicit":true,"tlsTimeout":60000,"useTls":true,"tlsDisableProtocols":{},"protocol":"smtp"},"networklistener-iughfwjwaeqb":{"socketNoDelay":true,"socketReusePort":true,"tlsDisableCipherSuites":{},"socketReuseAddress":true,"name":"smtp","bind":{"[::]:25":true},"socketTosV4":null,"socketTtl":null,"maxConnections":8192,"socketBacklog":1024,"socketReceiveBufferSize":null,"overrideProxyTrustedNetworks":{},"socketSendBufferSize":null,"tlsIgnoreClientOrder":true,"tlsImplicit":false,"tlsTimeout":60000,"useTls":true,"tlsDisableProtocols":{},"protocol":"smtp"}}}`, + `{"@type":"create","object":"Role","value":{"role-e":{"disabledPermissions":{},"memberTenantId":null,"enabledPermissions":{"authenticate":true,"authenticateWithAlias":true,"interactAi":true,"impersonate":true,"unlimitedRequests":true,"unlimitedUploads":true,"fetchAnyBlob":true,"oAuthClientRegistration":true,"oAuthClientOverride":true,"liveTracing":true,"liveMetrics":true,"liveDeliveryTest":true,"sysAccountGet":true,"sysAccountCreate":true,"sysAccountUpdate":true,"sysAccountDestroy":true,"sysAccountQuery":true,"sysAccountPasswordGet":true,"sysAccountPasswordUpdate":true,"sysAccountSettingsGet":true,"sysAccountSettingsUpdate":true,"sysAcmeProviderGet":true,"sysAcmeProviderCreate":true,"sysAcmeProviderUpdate":true,"sysAcmeProviderDestroy":true,"sysAcmeProviderQuery":true,"actionReloadSettings":true,"actionReloadTlsCertificates":true,"actionReloadLookupStores":true,"actionReloadBlockedIps":true,"actionUpdateApps":true,"actionTroubleshootDmarc":true,"actionClassifySpam":true,"actionInvalidateCaches":true,"actionInvalidateNegativeCaches":true,"actionPauseMtaQueue":true,"actionResumeMtaQueue":true,"sysActionGet":true,"sysActionCreate":true,"sysActionUpdate":true,"sysActionDestroy":true,"sysActionQuery":true,"sysAddressBookGet":true,"sysAddressBookUpdate":true,"sysAiModelGet":true,"sysAiModelCreate":true,"sysAiModelUpdate":true,"sysAiModelDestroy":true,"sysAiModelQuery":true,"sysAlertGet":true,"sysAlertCreate":true,"sysAlertUpdate":true,"sysAlertDestroy":true,"sysAlertQuery":true,"sysAllowedIpGet":true,"sysAllowedIpCreate":true,"sysAllowedIpUpdate":true,"sysAllowedIpDestroy":true,"sysAllowedIpQuery":true,"sysApiKeyGet":true,"sysApiKeyCreate":true,"sysApiKeyUpdate":true,"sysApiKeyDestroy":true,"sysApiKeyQuery":true,"sysAppPasswordGet":true,"sysAppPasswordCreate":true,"sysAppPasswordUpdate":true,"sysAppPasswordDestroy":true,"sysAppPasswordQuery":true,"sysApplicationGet":true,"sysApplicationCreate":true,"sysApplicationUpdate":true,"sysApplicationDestroy":true,"sysApplicationQuery":true,"sysArchivedItemGet":true,"sysArchivedItemCreate":true,"sysArchivedItemUpdate":true,"sysArchivedItemDestroy":true,"sysArchivedItemQuery":true,"sysArfExternalReportGet":true,"sysArfExternalReportCreate":true,"sysArfExternalReportUpdate":true,"sysArfExternalReportDestroy":true,"sysArfExternalReportQuery":true,"sysAsnGet":true,"sysAsnUpdate":true,"sysAuthenticationGet":true,"sysAuthenticationUpdate":true,"sysBlobStoreGet":true,"sysBlobStoreUpdate":true,"sysBlockedIpGet":true,"sysBlockedIpCreate":true,"sysBlockedIpUpdate":true,"sysBlockedIpDestroy":true,"sysBlockedIpQuery":true,"sysBootstrapGet":true,"sysBootstrapUpdate":true,"sysCacheGet":true,"sysCacheUpdate":true,"sysCalendarGet":true,"sysCalendarUpdate":true,"sysCalendarAlarmGet":true,"sysCalendarAlarmUpdate":true,"sysCalendarSchedulingGet":true,"sysCalendarSchedulingUpdate":true,"sysCertificateGet":true,"sysCertificateCreate":true,"sysCertificateUpdate":true,"sysCertificateDestroy":true,"sysCertificateQuery":true,"sysClusterNodeGet":true,"sysClusterNodeCreate":true,"sysClusterNodeUpdate":true,"sysClusterNodeDestroy":true,"sysClusterNodeQuery":true,"sysClusterRoleGet":true,"sysClusterRoleCreate":true,"sysClusterRoleUpdate":true,"sysClusterRoleDestroy":true,"sysClusterRoleQuery":true,"sysCoordinatorGet":true,"sysCoordinatorUpdate":true,"sysDataRetentionGet":true,"sysDataRetentionUpdate":true,"sysDataStoreGet":true,"sysDataStoreUpdate":true,"sysDirectoryGet":true,"sysDirectoryCreate":true,"sysDirectoryUpdate":true,"sysDirectoryDestroy":true,"sysDirectoryQuery":true,"sysDkimReportSettingsGet":true,"sysDkimReportSettingsUpdate":true,"sysDkimSignatureGet":true,"sysDkimSignatureCreate":true,"sysDkimSignatureUpdate":true,"sysDkimSignatureDestroy":true,"sysDkimSignatureQuery":true,"sysDmarcExternalReportGet":true,"sysDmarcExternalReportCreate":true,"sysDmarcExternalReportUpdate":true,"sysDmarcExternalReportDestroy":true,"sysDmarcExternalReportQuery":true,"sysDmarcInternalReportGet":true,"sysDmarcInternalReportCreate":true,"sysDmarcInternalReportUpdate":true,"sysDmarcInternalReportDestroy":true,"sysDmarcInternalReportQuery":true,"sysDmarcReportSettingsGet":true,"sysDmarcReportSettingsUpdate":true,"sysDnsResolverGet":true,"sysDnsResolverUpdate":true,"sysDnsServerGet":true,"sysDnsServerCreate":true,"sysDnsServerUpdate":true,"sysDnsServerDestroy":true,"sysDnsServerQuery":true,"sysDomainGet":true,"sysDomainCreate":true,"sysDomainUpdate":true,"sysDomainDestroy":true,"sysDomainQuery":true,"sysDsnReportSettingsGet":true,"sysDsnReportSettingsUpdate":true,"sysEmailGet":true,"sysEmailUpdate":true,"sysEnterpriseGet":true,"sysEnterpriseUpdate":true,"sysEventTracingLevelGet":true,"sysEventTracingLevelCreate":true,"sysEventTracingLevelUpdate":true,"sysEventTracingLevelDestroy":true,"sysEventTracingLevelQuery":true,"sysFileStorageGet":true,"sysFileStorageUpdate":true,"sysHttpGet":true,"sysHttpUpdate":true,"sysHttpFormGet":true,"sysHttpFormUpdate":true,"sysHttpLookupGet":true,"sysHttpLookupCreate":true,"sysHttpLookupUpdate":true,"sysHttpLookupDestroy":true,"sysHttpLookupQuery":true,"sysImapGet":true,"sysImapUpdate":true,"sysInMemoryStoreGet":true,"sysInMemoryStoreUpdate":true,"sysJmapGet":true,"sysJmapUpdate":true,"sysLogGet":true,"sysLogCreate":true,"sysLogUpdate":true,"sysLogDestroy":true,"sysLogQuery":true,"sysMailingListGet":true,"sysMailingListCreate":true,"sysMailingListUpdate":true,"sysMailingListDestroy":true,"sysMailingListQuery":true,"sysMaskedEmailGet":true,"sysMaskedEmailCreate":true,"sysMaskedEmailUpdate":true,"sysMaskedEmailDestroy":true,"sysMaskedEmailQuery":true,"sysMemoryLookupKeyGet":true,"sysMemoryLookupKeyCreate":true,"sysMemoryLookupKeyUpdate":true,"sysMemoryLookupKeyDestroy":true,"sysMemoryLookupKeyQuery":true,"sysMemoryLookupKeyValueGet":true,"sysMemoryLookupKeyValueCreate":true,"sysMemoryLookupKeyValueUpdate":true,"sysMemoryLookupKeyValueDestroy":true,"sysMemoryLookupKeyValueQuery":true,"sysMetricGet":true,"sysMetricCreate":true,"sysMetricUpdate":true,"sysMetricDestroy":true,"sysMetricQuery":true,"sysMetricsGet":true,"sysMetricsUpdate":true,"sysMetricsStoreGet":true,"sysMetricsStoreUpdate":true,"sysMtaConnectionStrategyGet":true,"sysMtaConnectionStrategyCreate":true,"sysMtaConnectionStrategyUpdate":true,"sysMtaConnectionStrategyDestroy":true,"sysMtaConnectionStrategyQuery":true,"sysMtaDeliveryScheduleGet":true,"sysMtaDeliveryScheduleCreate":true,"sysMtaDeliveryScheduleUpdate":true,"sysMtaDeliveryScheduleDestroy":true,"sysMtaDeliveryScheduleQuery":true,"sysMtaExtensionsGet":true,"sysMtaExtensionsUpdate":true,"sysMtaHookGet":true,"sysMtaHookCreate":true,"sysMtaHookUpdate":true,"sysMtaHookDestroy":true,"sysMtaHookQuery":true,"sysMtaInboundSessionGet":true,"sysMtaInboundSessionUpdate":true,"sysMtaInboundThrottleGet":true,"sysMtaInboundThrottleCreate":true,"sysMtaInboundThrottleUpdate":true,"sysMtaInboundThrottleDestroy":true,"sysMtaInboundThrottleQuery":true,"sysMtaMilterGet":true,"sysMtaMilterCreate":true,"sysMtaMilterUpdate":true,"sysMtaMilterDestroy":true,"sysMtaMilterQuery":true,"sysMtaOutboundStrategyGet":true,"sysMtaOutboundStrategyUpdate":true,"sysMtaOutboundThrottleGet":true,"sysMtaOutboundThrottleCreate":true,"sysMtaOutboundThrottleUpdate":true,"sysMtaOutboundThrottleDestroy":true,"sysMtaOutboundThrottleQuery":true,"sysMtaQueueQuotaGet":true,"sysMtaQueueQuotaCreate":true,"sysMtaQueueQuotaUpdate":true,"sysMtaQueueQuotaDestroy":true,"sysMtaQueueQuotaQuery":true,"sysMtaRouteGet":true,"sysMtaRouteCreate":true,"sysMtaRouteUpdate":true,"sysMtaRouteDestroy":true,"sysMtaRouteQuery":true,"sysMtaStageAuthGet":true,"sysMtaStageAuthUpdate":true,"sysMtaStageConnectGet":true,"sysMtaStageConnectUpdate":true,"sysMtaStageDataGet":true,"sysMtaStageDataUpdate":true,"sysMtaStageEhloGet":true,"sysMtaStageEhloUpdate":true,"sysMtaStageMailGet":true,"sysMtaStageMailUpdate":true,"sysMtaStageRcptGet":true,"sysMtaStageRcptUpdate":true,"sysMtaStsGet":true,"sysMtaStsUpdate":true,"sysMtaTlsStrategyGet":true,"sysMtaTlsStrategyCreate":true,"sysMtaTlsStrategyUpdate":true,"sysMtaTlsStrategyDestroy":true,"sysMtaTlsStrategyQuery":true,"sysMtaVirtualQueueGet":true,"sysMtaVirtualQueueCreate":true,"sysMtaVirtualQueueUpdate":true,"sysMtaVirtualQueueDestroy":true,"sysMtaVirtualQueueQuery":true,"sysNetworkListenerGet":true,"sysNetworkListenerCreate":true,"sysNetworkListenerUpdate":true,"sysNetworkListenerDestroy":true,"sysNetworkListenerQuery":true,"sysOAuthClientGet":true,"sysOAuthClientCreate":true,"sysOAuthClientUpdate":true,"sysOAuthClientDestroy":true,"sysOAuthClientQuery":true,"sysOidcProviderGet":true,"sysOidcProviderUpdate":true,"sysPublicKeyGet":true,"sysPublicKeyCreate":true,"sysPublicKeyUpdate":true,"sysPublicKeyDestroy":true,"sysPublicKeyQuery":true,"sysQueuedMessageGet":true,"sysQueuedMessageCreate":true,"sysQueuedMessageUpdate":true,"sysQueuedMessageDestroy":true,"sysQueuedMessageQuery":true,"sysReportSettingsGet":true,"sysReportSettingsUpdate":true,"sysRoleGet":true,"sysRoleCreate":true,"sysRoleUpdate":true,"sysRoleDestroy":true,"sysRoleQuery":true,"sysSearchGet":true,"sysSearchUpdate":true,"sysSearchStoreGet":true,"sysSearchStoreUpdate":true,"sysSecurityGet":true,"sysSecurityUpdate":true,"sysSenderAuthGet":true,"sysSenderAuthUpdate":true,"sysSharingGet":true,"sysSharingUpdate":true,"sysSieveSystemInterpreterGet":true,"sysSieveSystemInterpreterUpdate":true,"sysSieveSystemScriptGet":true,"sysSieveSystemScriptCreate":true,"sysSieveSystemScriptUpdate":true,"sysSieveSystemScriptDestroy":true,"sysSieveSystemScriptQuery":true,"sysSieveUserInterpreterGet":true,"sysSieveUserInterpreterUpdate":true,"sysSieveUserScriptGet":true,"sysSieveUserScriptCreate":true,"sysSieveUserScriptUpdate":true,"sysSieveUserScriptDestroy":true,"sysSieveUserScriptQuery":true,"sysSpamClassifierGet":true,"sysSpamClassifierUpdate":true,"sysSpamDnsblServerGet":true,"sysSpamDnsblServerCreate":true,"sysSpamDnsblServerUpdate":true,"sysSpamDnsblServerDestroy":true,"sysSpamDnsblServerQuery":true,"sysSpamDnsblSettingsGet":true,"sysSpamDnsblSettingsUpdate":true,"sysSpamFileExtensionGet":true,"sysSpamFileExtensionCreate":true,"sysSpamFileExtensionUpdate":true,"sysSpamFileExtensionDestroy":true,"sysSpamFileExtensionQuery":true,"sysSpamLlmGet":true,"sysSpamLlmUpdate":true,"sysSpamPyzorGet":true,"sysSpamPyzorUpdate":true,"sysSpamRuleGet":true,"sysSpamRuleCreate":true,"sysSpamRuleUpdate":true,"sysSpamRuleDestroy":true,"sysSpamRuleQuery":true,"sysSpamSettingsGet":true,"sysSpamSettingsUpdate":true,"sysSpamTagGet":true,"sysSpamTagCreate":true,"sysSpamTagUpdate":true,"sysSpamTagDestroy":true,"sysSpamTagQuery":true,"sysSpamTrainingSampleGet":true,"sysSpamTrainingSampleCreate":true,"sysSpamTrainingSampleUpdate":true,"sysSpamTrainingSampleDestroy":true,"sysSpamTrainingSampleQuery":true,"sysSpfReportSettingsGet":true,"sysSpfReportSettingsUpdate":true,"sysStoreLookupGet":true,"sysStoreLookupCreate":true,"sysStoreLookupUpdate":true,"sysStoreLookupDestroy":true,"sysStoreLookupQuery":true,"sysSystemSettingsGet":true,"sysSystemSettingsUpdate":true,"taskIndexDocument":true,"taskUnindexDocument":true,"taskIndexTrace":true,"taskCalendarAlarmEmail":true,"taskCalendarAlarmNotification":true,"taskCalendarItipMessage":true,"taskMergeThreads":true,"taskDmarcReport":true,"taskTlsReport":true,"taskRestoreArchivedItem":true,"taskDestroyAccount":true,"taskAccountMaintenance":true,"taskTenantMaintenance":true,"taskStoreMaintenance":true,"taskSpamFilterMaintenance":true,"taskAcmeRenewal":true,"taskDkimManagement":true,"taskDnsManagement":true,"sysTaskGet":true,"sysTaskCreate":true,"sysTaskUpdate":true,"sysTaskDestroy":true,"sysTaskQuery":true,"sysTaskManagerGet":true,"sysTaskManagerUpdate":true,"sysTenantGet":true,"sysTenantCreate":true,"sysTenantUpdate":true,"sysTenantDestroy":true,"sysTenantQuery":true,"sysTlsExternalReportGet":true,"sysTlsExternalReportCreate":true,"sysTlsExternalReportUpdate":true,"sysTlsExternalReportDestroy":true,"sysTlsExternalReportQuery":true,"sysTlsInternalReportGet":true,"sysTlsInternalReportCreate":true,"sysTlsInternalReportUpdate":true,"sysTlsInternalReportDestroy":true,"sysTlsInternalReportQuery":true,"sysTlsReportSettingsGet":true,"sysTlsReportSettingsUpdate":true,"sysTraceGet":true,"sysTraceCreate":true,"sysTraceUpdate":true,"sysTraceDestroy":true,"sysTraceQuery":true,"sysTracerGet":true,"sysTracerCreate":true,"sysTracerUpdate":true,"sysTracerDestroy":true,"sysTracerQuery":true,"sysTracingStoreGet":true,"sysTracingStoreUpdate":true,"sysWebDavGet":true,"sysWebDavUpdate":true,"sysWebHookGet":true,"sysWebHookCreate":true,"sysWebHookUpdate":true,"sysWebHookDestroy":true,"sysWebHookQuery":true},"description":"System Administrator","roleIds":{}},"role-d":{"disabledPermissions":{},"memberTenantId":null,"enabledPermissions":{"authenticate":true,"authenticateWithAlias":true,"interactAi":true,"fetchAnyBlob":true,"liveDeliveryTest":true,"sysAccountGet":true,"sysAccountCreate":true,"sysAccountUpdate":true,"sysAccountDestroy":true,"sysAccountQuery":true,"sysAcmeProviderGet":true,"sysAcmeProviderCreate":true,"sysAcmeProviderUpdate":true,"sysAcmeProviderDestroy":true,"sysAcmeProviderQuery":true,"sysDkimSignatureGet":true,"sysDkimSignatureCreate":true,"sysDkimSignatureUpdate":true,"sysDkimSignatureDestroy":true,"sysDkimSignatureQuery":true,"sysDnsServerGet":true,"sysDnsServerCreate":true,"sysDnsServerUpdate":true,"sysDnsServerDestroy":true,"sysDnsServerQuery":true,"sysDomainGet":true,"sysDomainCreate":true,"sysDomainUpdate":true,"sysDomainDestroy":true,"sysDomainQuery":true,"sysMailingListGet":true,"sysMailingListCreate":true,"sysMailingListUpdate":true,"sysMailingListDestroy":true,"sysMailingListQuery":true,"sysOAuthClientGet":true,"sysOAuthClientCreate":true,"sysOAuthClientUpdate":true,"sysOAuthClientDestroy":true,"sysOAuthClientQuery":true,"sysQueuedMessageGet":true,"sysQueuedMessageCreate":true,"sysQueuedMessageUpdate":true,"sysQueuedMessageDestroy":true,"sysQueuedMessageQuery":true,"sysRoleGet":true,"sysRoleCreate":true,"sysRoleUpdate":true,"sysRoleDestroy":true,"sysRoleQuery":true},"description":"Tenant Administrator","roleIds":{}},"role-c":{"disabledPermissions":{},"memberTenantId":null,"enabledPermissions":{"emailSend":true,"emailReceive":true,"calendarAlarmsSend":true,"calendarSchedulingSend":true,"calendarSchedulingReceive":true,"jmapPushSubscriptionGet":true,"jmapPushSubscriptionCreate":true,"jmapPushSubscriptionUpdate":true,"jmapPushSubscriptionDestroy":true,"jmapMailboxGet":true,"jmapMailboxChanges":true,"jmapMailboxQuery":true,"jmapMailboxQueryChanges":true,"jmapMailboxCreate":true,"jmapMailboxUpdate":true,"jmapMailboxDestroy":true,"jmapThreadGet":true,"jmapThreadChanges":true,"jmapEmailGet":true,"jmapEmailChanges":true,"jmapEmailQuery":true,"jmapEmailQueryChanges":true,"jmapEmailCreate":true,"jmapEmailUpdate":true,"jmapEmailDestroy":true,"jmapEmailCopy":true,"jmapEmailImport":true,"jmapEmailParse":true,"jmapSearchSnippetGet":true,"jmapIdentityGet":true,"jmapIdentityChanges":true,"jmapIdentityCreate":true,"jmapIdentityUpdate":true,"jmapIdentityDestroy":true,"jmapEmailSubmissionGet":true,"jmapEmailSubmissionChanges":true,"jmapEmailSubmissionQuery":true,"jmapEmailSubmissionQueryChanges":true,"jmapEmailSubmissionCreate":true,"jmapEmailSubmissionUpdate":true,"jmapEmailSubmissionDestroy":true,"jmapVacationResponseGet":true,"jmapVacationResponseCreate":true,"jmapVacationResponseUpdate":true,"jmapVacationResponseDestroy":true,"jmapSieveScriptGet":true,"jmapSieveScriptQuery":true,"jmapSieveScriptValidate":true,"jmapSieveScriptCreate":true,"jmapSieveScriptUpdate":true,"jmapSieveScriptDestroy":true,"jmapPrincipalGet":true,"jmapPrincipalQuery":true,"jmapPrincipalChanges":true,"jmapPrincipalQueryChanges":true,"jmapPrincipalGetAvailability":true,"jmapPrincipalCreate":true,"jmapPrincipalUpdate":true,"jmapPrincipalDestroy":true,"jmapQuotaGet":true,"jmapQuotaChanges":true,"jmapQuotaQuery":true,"jmapQuotaQueryChanges":true,"jmapBlobGet":true,"jmapBlobCopy":true,"jmapBlobLookup":true,"jmapBlobUpload":true,"jmapAddressBookGet":true,"jmapAddressBookChanges":true,"jmapAddressBookCreate":true,"jmapAddressBookUpdate":true,"jmapAddressBookDestroy":true,"jmapContactCardGet":true,"jmapContactCardChanges":true,"jmapContactCardQuery":true,"jmapContactCardQueryChanges":true,"jmapContactCardCreate":true,"jmapContactCardUpdate":true,"jmapContactCardDestroy":true,"jmapContactCardCopy":true,"jmapContactCardParse":true,"jmapFileNodeGet":true,"jmapFileNodeChanges":true,"jmapFileNodeQuery":true,"jmapFileNodeQueryChanges":true,"jmapFileNodeCreate":true,"jmapFileNodeUpdate":true,"jmapFileNodeDestroy":true,"jmapShareNotificationGet":true,"jmapShareNotificationChanges":true,"jmapShareNotificationQuery":true,"jmapShareNotificationQueryChanges":true,"jmapShareNotificationCreate":true,"jmapShareNotificationUpdate":true,"jmapShareNotificationDestroy":true,"jmapCalendarGet":true,"jmapCalendarChanges":true,"jmapCalendarCreate":true,"jmapCalendarUpdate":true,"jmapCalendarDestroy":true,"jmapCalendarEventGet":true,"jmapCalendarEventChanges":true,"jmapCalendarEventQuery":true,"jmapCalendarEventQueryChanges":true,"jmapCalendarEventCreate":true,"jmapCalendarEventUpdate":true,"jmapCalendarEventDestroy":true,"jmapCalendarEventCopy":true,"jmapCalendarEventParse":true,"jmapCalendarEventNotificationGet":true,"jmapCalendarEventNotificationChanges":true,"jmapCalendarEventNotificationQuery":true,"jmapCalendarEventNotificationQueryChanges":true,"jmapCalendarEventNotificationCreate":true,"jmapCalendarEventNotificationUpdate":true,"jmapCalendarEventNotificationDestroy":true,"jmapParticipantIdentityGet":true,"jmapParticipantIdentityChanges":true,"jmapParticipantIdentityCreate":true,"jmapParticipantIdentityUpdate":true,"jmapParticipantIdentityDestroy":true,"jmapCoreEcho":true,"imapAuthenticate":true,"imapAclGet":true,"imapAclSet":true,"imapMyRights":true,"imapListRights":true,"imapAppend":true,"imapCapability":true,"imapId":true,"imapCopy":true,"imapMove":true,"imapCreate":true,"imapDelete":true,"imapEnable":true,"imapExpunge":true,"imapFetch":true,"imapIdle":true,"imapList":true,"imapLsub":true,"imapNamespace":true,"imapRename":true,"imapSearch":true,"imapSort":true,"imapSelect":true,"imapExamine":true,"imapStatus":true,"imapStore":true,"imapSubscribe":true,"imapThread":true,"pop3Authenticate":true,"pop3List":true,"pop3Uidl":true,"pop3Stat":true,"pop3Retr":true,"pop3Dele":true,"sieveAuthenticate":true,"sieveListScripts":true,"sieveSetActive":true,"sieveGetScript":true,"sievePutScript":true,"sieveDeleteScript":true,"sieveRenameScript":true,"sieveCheckScript":true,"sieveHaveSpace":true,"davSyncCollection":true,"davExpandProperty":true,"davPrincipalAcl":true,"davPrincipalList":true,"davPrincipalMatch":true,"davPrincipalSearch":true,"davPrincipalSearchPropSet":true,"davFilePropFind":true,"davFilePropPatch":true,"davFileGet":true,"davFileMkCol":true,"davFileDelete":true,"davFilePut":true,"davFileCopy":true,"davFileMove":true,"davFileLock":true,"davFileAcl":true,"davCardPropFind":true,"davCardPropPatch":true,"davCardGet":true,"davCardMkCol":true,"davCardDelete":true,"davCardPut":true,"davCardCopy":true,"davCardMove":true,"davCardLock":true,"davCardAcl":true,"davCardQuery":true,"davCardMultiGet":true,"davCalPropFind":true,"davCalPropPatch":true,"davCalGet":true,"davCalMkCol":true,"davCalDelete":true,"davCalPut":true,"davCalCopy":true,"davCalMove":true,"davCalLock":true,"davCalAcl":true,"davCalQuery":true,"davCalMultiGet":true,"davCalFreeBusyQuery":true,"sysAccountSettingsGet":true,"sysAccountSettingsUpdate":true,"sysArchivedItemGet":true,"sysArchivedItemCreate":true,"sysArchivedItemUpdate":true,"sysArchivedItemDestroy":true,"sysArchivedItemQuery":true,"sysMaskedEmailGet":true,"sysMaskedEmailCreate":true,"sysMaskedEmailUpdate":true,"sysMaskedEmailDestroy":true,"sysMaskedEmailQuery":true,"sysPublicKeyGet":true,"sysPublicKeyCreate":true,"sysPublicKeyUpdate":true,"sysPublicKeyDestroy":true,"sysPublicKeyQuery":true,"sysSpamTrainingSampleGet":true,"sysSpamTrainingSampleUpdate":true,"sysSpamTrainingSampleDestroy":true,"sysSpamTrainingSampleQuery":true,"jmapFileNodeCopy":true},"description":"Group","roleIds":{}},"role-b":{"disabledPermissions":{},"memberTenantId":null,"enabledPermissions":{"authenticate":true,"authenticateWithAlias":true,"interactAi":true,"emailSend":true,"emailReceive":true,"calendarAlarmsSend":true,"calendarSchedulingSend":true,"calendarSchedulingReceive":true,"jmapPushSubscriptionGet":true,"jmapPushSubscriptionCreate":true,"jmapPushSubscriptionUpdate":true,"jmapPushSubscriptionDestroy":true,"jmapMailboxGet":true,"jmapMailboxChanges":true,"jmapMailboxQuery":true,"jmapMailboxQueryChanges":true,"jmapMailboxCreate":true,"jmapMailboxUpdate":true,"jmapMailboxDestroy":true,"jmapThreadGet":true,"jmapThreadChanges":true,"jmapEmailGet":true,"jmapEmailChanges":true,"jmapEmailQuery":true,"jmapEmailQueryChanges":true,"jmapEmailCreate":true,"jmapEmailUpdate":true,"jmapEmailDestroy":true,"jmapEmailCopy":true,"jmapEmailImport":true,"jmapEmailParse":true,"jmapSearchSnippetGet":true,"jmapIdentityGet":true,"jmapIdentityChanges":true,"jmapIdentityCreate":true,"jmapIdentityUpdate":true,"jmapIdentityDestroy":true,"jmapEmailSubmissionGet":true,"jmapEmailSubmissionChanges":true,"jmapEmailSubmissionQuery":true,"jmapEmailSubmissionQueryChanges":true,"jmapEmailSubmissionCreate":true,"jmapEmailSubmissionUpdate":true,"jmapEmailSubmissionDestroy":true,"jmapVacationResponseGet":true,"jmapVacationResponseCreate":true,"jmapVacationResponseUpdate":true,"jmapVacationResponseDestroy":true,"jmapSieveScriptGet":true,"jmapSieveScriptQuery":true,"jmapSieveScriptValidate":true,"jmapSieveScriptCreate":true,"jmapSieveScriptUpdate":true,"jmapSieveScriptDestroy":true,"jmapPrincipalGet":true,"jmapPrincipalQuery":true,"jmapPrincipalChanges":true,"jmapPrincipalQueryChanges":true,"jmapPrincipalGetAvailability":true,"jmapPrincipalCreate":true,"jmapPrincipalUpdate":true,"jmapPrincipalDestroy":true,"jmapQuotaGet":true,"jmapQuotaChanges":true,"jmapQuotaQuery":true,"jmapQuotaQueryChanges":true,"jmapBlobGet":true,"jmapBlobCopy":true,"jmapBlobLookup":true,"jmapBlobUpload":true,"jmapAddressBookGet":true,"jmapAddressBookChanges":true,"jmapAddressBookCreate":true,"jmapAddressBookUpdate":true,"jmapAddressBookDestroy":true,"jmapContactCardGet":true,"jmapContactCardChanges":true,"jmapContactCardQuery":true,"jmapContactCardQueryChanges":true,"jmapContactCardCreate":true,"jmapContactCardUpdate":true,"jmapContactCardDestroy":true,"jmapContactCardCopy":true,"jmapContactCardParse":true,"jmapFileNodeGet":true,"jmapFileNodeChanges":true,"jmapFileNodeQuery":true,"jmapFileNodeQueryChanges":true,"jmapFileNodeCreate":true,"jmapFileNodeUpdate":true,"jmapFileNodeDestroy":true,"jmapShareNotificationGet":true,"jmapShareNotificationChanges":true,"jmapShareNotificationQuery":true,"jmapShareNotificationQueryChanges":true,"jmapShareNotificationCreate":true,"jmapShareNotificationUpdate":true,"jmapShareNotificationDestroy":true,"jmapCalendarGet":true,"jmapCalendarChanges":true,"jmapCalendarCreate":true,"jmapCalendarUpdate":true,"jmapCalendarDestroy":true,"jmapCalendarEventGet":true,"jmapCalendarEventChanges":true,"jmapCalendarEventQuery":true,"jmapCalendarEventQueryChanges":true,"jmapCalendarEventCreate":true,"jmapCalendarEventUpdate":true,"jmapCalendarEventDestroy":true,"jmapCalendarEventCopy":true,"jmapCalendarEventParse":true,"jmapCalendarEventNotificationGet":true,"jmapCalendarEventNotificationChanges":true,"jmapCalendarEventNotificationQuery":true,"jmapCalendarEventNotificationQueryChanges":true,"jmapCalendarEventNotificationCreate":true,"jmapCalendarEventNotificationUpdate":true,"jmapCalendarEventNotificationDestroy":true,"jmapParticipantIdentityGet":true,"jmapParticipantIdentityChanges":true,"jmapParticipantIdentityCreate":true,"jmapParticipantIdentityUpdate":true,"jmapParticipantIdentityDestroy":true,"jmapCoreEcho":true,"imapAuthenticate":true,"imapAclGet":true,"imapAclSet":true,"imapMyRights":true,"imapListRights":true,"imapAppend":true,"imapCapability":true,"imapId":true,"imapCopy":true,"imapMove":true,"imapCreate":true,"imapDelete":true,"imapEnable":true,"imapExpunge":true,"imapFetch":true,"imapIdle":true,"imapList":true,"imapLsub":true,"imapNamespace":true,"imapRename":true,"imapSearch":true,"imapSort":true,"imapSelect":true,"imapExamine":true,"imapStatus":true,"imapStore":true,"imapSubscribe":true,"imapThread":true,"pop3Authenticate":true,"pop3List":true,"pop3Uidl":true,"pop3Stat":true,"pop3Retr":true,"pop3Dele":true,"sieveAuthenticate":true,"sieveListScripts":true,"sieveSetActive":true,"sieveGetScript":true,"sievePutScript":true,"sieveDeleteScript":true,"sieveRenameScript":true,"sieveCheckScript":true,"sieveHaveSpace":true,"davSyncCollection":true,"davExpandProperty":true,"davPrincipalAcl":true,"davPrincipalList":true,"davPrincipalMatch":true,"davPrincipalSearch":true,"davPrincipalSearchPropSet":true,"davFilePropFind":true,"davFilePropPatch":true,"davFileGet":true,"davFileMkCol":true,"davFileDelete":true,"davFilePut":true,"davFileCopy":true,"davFileMove":true,"davFileLock":true,"davFileAcl":true,"davCardPropFind":true,"davCardPropPatch":true,"davCardGet":true,"davCardMkCol":true,"davCardDelete":true,"davCardPut":true,"davCardCopy":true,"davCardMove":true,"davCardLock":true,"davCardAcl":true,"davCardQuery":true,"davCardMultiGet":true,"davCalPropFind":true,"davCalPropPatch":true,"davCalGet":true,"davCalMkCol":true,"davCalDelete":true,"davCalPut":true,"davCalCopy":true,"davCalMove":true,"davCalLock":true,"davCalAcl":true,"davCalQuery":true,"davCalMultiGet":true,"davCalFreeBusyQuery":true,"sysAccountPasswordGet":true,"sysAccountPasswordUpdate":true,"sysAccountSettingsGet":true,"sysAccountSettingsUpdate":true,"sysApiKeyGet":true,"sysApiKeyCreate":true,"sysApiKeyUpdate":true,"sysApiKeyDestroy":true,"sysApiKeyQuery":true,"sysAppPasswordGet":true,"sysAppPasswordCreate":true,"sysAppPasswordUpdate":true,"sysAppPasswordDestroy":true,"sysAppPasswordQuery":true,"sysArchivedItemGet":true,"sysArchivedItemCreate":true,"sysArchivedItemUpdate":true,"sysArchivedItemDestroy":true,"sysArchivedItemQuery":true,"sysMaskedEmailGet":true,"sysMaskedEmailCreate":true,"sysMaskedEmailUpdate":true,"sysMaskedEmailDestroy":true,"sysMaskedEmailQuery":true,"sysPublicKeyGet":true,"sysPublicKeyCreate":true,"sysPublicKeyUpdate":true,"sysPublicKeyDestroy":true,"sysPublicKeyQuery":true,"sysSpamTrainingSampleGet":true,"sysSpamTrainingSampleUpdate":true,"sysSpamTrainingSampleDestroy":true,"sysSpamTrainingSampleQuery":true,"jmapFileNodeCopy":true},"description":"User","roleIds":{}}}}`, + `{ + "@type": "create", + "object": "Domain", + "value": { + "domain-b": { + "aliases": {}, + "catchAllAddress": null, + "certificateManagement": { + "@type": "Manual" + }, + "dkimManagement": { + "@type": "Manual" + }, + "subAddressing": { + "@type": "Enabled" + }, + "logo": null, + "isEnabled": true, + "memberTenantId": null, + "description": null, + "name": "<<.domain>>", + "directoryId": null, + "allowRelaying": false, + "dnsManagement": { + "@type": "Manual" + }, + "reportAddressUri": "mailto:postmaster" + } + } + }`, + `{ + "@type": "create", + "object": "Account", + "value": { + "account-d": { + "@type": "User", + "name": "<<.masterusername>>", + "memberGroupIds": {}, + "locale": "en_US", + "quotas": {}, + "aliases": {}, + "domainId": "#domain-b", + "memberTenantId": null, + "permissions": { + "@type": "Merge", + "enabledPermissions": { + "impersonate": true + }, + "disabledPermissions": {} + }, + "encryptionAtRest": { + "@type": "Disabled" + }, + "credentials": { + "0": { + "@type": "Password", + "expiresAt": null, + "secret": "<<.masterpassword>>", + "allowedIps": {} + } + }, + "description": "Master", + "timeZone": null, + "roles": { + "@type": "User" + } + }, + "account-b": { + "@type": "User", + "name": "<<.adminusername>>", + "memberGroupIds": {}, + "locale": "en_US", + "quotas": {}, + "aliases": {}, + "domainId": "#domain-b", + "memberTenantId": null, + "permissions": { + "@type": "Inherit" + }, + "encryptionAtRest": { + "@type": "Disabled" + }, + "credentials": { + "0": { + "@type": "Password", + "expiresAt": null, + "secret": "<<.adminpassword>>", + "allowedIps": {} + } + }, + "description": "System administrator", + "timeZone": null, + "roles": { + "@type": "Admin" + } + } + } + }`, + `{"@type":"update","object":"Authentication","value":{"defaultGroupRoleIds":{"#role-c":true},"defaultAdminRoleIds":{"#role-e":true,"#role-b":true},"defaultUserRoleIds":{"#role-b":true},"maxAppPasswords":5,"defaultTenantRoleIds":{"#role-d":true,"#role-b":true},"directoryId":null,"passwordMinLength":1,"passwordDefaultExpiry":null,"passwordHashAlgorithm":"argon2id","passwordMinStrength":"zero","passwordMaxLength":128,"maxApiKeys":5}}`, + `{"@type":"update","object":"Sharing","value":{"allowDirectoryQueries":<<.dirquery>>,"maxShares":10}}`, + `{ + "@type": "update", + "object": "SystemSettings", + "value": { + "defaultHostname": "<<.hostname>>", + "threadPoolSize": null, + "defaultCertificateId": null, + "proxyTrustedNetworks": {}, + "defaultDomainId": "#domain-b", + "services": { + "caldav": { + "hostname": null, + "cleartext": false + }, + "carddav": { + "hostname": null, + "cleartext": false + }, + "imap": { + "hostname": null, + "cleartext": false + }, + "jmap": { + "hostname": null, + "cleartext": false + }, + "managesieve": { + "hostname": null, + "cleartext": false + }, + "pop3": { + "hostname": null, + "cleartext": false + }, + "smtp": { + "hostname": null, + "cleartext": false + }, + "webdav": { + "hostname": null, + "cleartext": false + } + }, + "mailExchangers": { + "0": { + "priority": 10, + "hostname": null + } + }, + "maxConnections": 8192, + "providerInfo": {} + } + }`, + `{"@type":"update","object":"DataRetention","value":{"archiveDeletedAccountsFor":null,"blobCleanupSchedule":{"@type":"Daily","hour":4,"minute":0},"expungeSchedule":{"@type":"Daily","hour":0,"minute":0},"dataCleanupSchedule":{"@type":"Daily","hour":2,"minute":0},"expungeTrashAfter":2592000000,"holdMtaReportsFor":2592000000,"metricsCollectionInterval":{"@type":"Hourly","minute":0},"holdMetricsFor":7776000000,"holdTracesFor":2592000000,"expungeShareNotifyAfter":2592000000,"expungeSchedulingInboxAfter":2592000000,"maxChangesHistory":10000,"expungeSubmissionsAfter":259200000,"archiveDeletedItemsFor":null}}`, + `{"@type":"update","object":"BlobStore","value":{"@type":"Default"}}`, + `{"@type":"update","object":"InMemoryStore","value":{"@type":"Default"}}`, + `{"@type":"update","object":"SearchStore","value":{"@type":"Default"}}`, + } +) + func skip(t *testing.T) bool { if os.Getenv("CI") == "woodpecker" { t.Skip("Skipping tests because CI==woodpecker") @@ -157,7 +301,7 @@ type StalwartTest struct { ctx context.Context cancelCtx context.CancelFunc client *Client - logger *clog.Logger + logger *oclog.Logger jmapBaseUrl *url.URL sessionUrl *url.URL @@ -196,24 +340,58 @@ func (s *StalwartTest) Session(username string) *Session { // "localhost" as we defined the hostname in the Stalwart configuration, // and we also need to overwrite the port number as its not mapped session.JmapUrl.Host = s.jmapBaseUrl.Host + session.JmapUrl.Scheme = "http" // replace https with http session.WebsocketUrl.Host = s.jmapBaseUrl.Host - var err error - session.ApiUrl, err = replaceHost(session.ApiUrl, s.jmapBaseUrl.Host) - require.NoError(s.t, err) - session.DownloadUrl, err = replaceHost(session.DownloadUrl, s.jmapBaseUrl.Host) - require.NoError(s.t, err) - session.UploadUrl, err = replaceHost(session.UploadUrl, s.jmapBaseUrl.Host) - require.NoError(s.t, err) - session.EventSourceUrl, err = replaceHost(session.EventSourceUrl, s.jmapBaseUrl.Host) - require.NoError(s.t, err) + session.WebsocketUrl.Scheme = "ws" // replace wss with ws + + if v, err := replaceHostProto(session.ApiUrl, "http", s.jmapBaseUrl.Host); err != nil { + require.NoError(s.t, err) + } else { + session.ApiUrl = v + } + if v, err := replaceHostProto(session.DownloadUrl, "http", s.jmapBaseUrl.Host); err != nil { + require.NoError(s.t, err) + } else { + session.DownloadUrl = v + } + if v, err := replaceHostProto(session.UploadUrl, "http", s.jmapBaseUrl.Host); err != nil { + require.NoError(s.t, err) + } else { + session.UploadUrl = v + } + if v, err := replaceHostProto(session.EventSourceUrl, "http", s.jmapBaseUrl.Host); err != nil { + require.NoError(s.t, err) + } else { + session.EventSourceUrl = v + } return &session } -type stalwartTestLogConsumer struct{} +type cliLogConsumer struct { + lines *[]string +} -func (lc *stalwartTestLogConsumer) Accept(l testcontainers.Log) { - fmt.Print("STALWART: " + string(l.Content)) +func (c *cliLogConsumer) Accept(l testcontainers.Log) { + s := string(l.Content) + log.Printf("CLI: %s", s) + *c.lines = append(*c.lines, s) +} + +type printingLogConsumer struct { + prefix string +} + +// 2026/06/05 16:18:36 STALWART: 2026-06-05T14:18:36Z INFO Shutting down Stalwart Server (server.shutdown) causedBy = "SIGTERM" +var printingLogConsumerRegex = regexp.MustCompile(`^(\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ)\s+(\S+)\s+(.+)$`) + +func (c *printingLogConsumer) Accept(l testcontainers.Log) { + str := string(l.Content) + if m := printingLogConsumerRegex.FindAllStringSubmatch(str, 3); m != nil { + log.Printf("\x1b[32;1m%s\x1b[0m \x1b[34m%s\x1b[0m \x1b[31;1m%s\x1b[0m %s", c.prefix, m[0][1], m[0][2], m[0][3]) + } else { + log.Printf("%s: %s", c.prefix, str) + } } func withDirectoryQueries(allowDirectoryQueries bool) func(map[string]any) { @@ -222,153 +400,494 @@ func withDirectoryQueries(allowDirectoryQueries bool) func(map[string]any) { } } +func applySnapshot(ctx context.Context, net *testcontainers.DockerNetwork, uri, user, password string, content *strings.Reader) ([]string, error) { + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + hdr := &tar.Header{ + Name: "Dockerfile", + Mode: 0600, + Size: int64(len(cliDockerfile)), + } + if err := tw.WriteHeader(hdr); err != nil { + return nil, err + } + if _, err := tw.Write([]byte(cliDockerfile)); err != nil { + return nil, err + } + if err := tw.Close(); err != nil { + return nil, err + } + + output := []string{} + + cliArch := "x86_64" + switch runtime.GOARCH { + case "amd64": + cliArch = "x86_64" + case "arm64", "arm64be": + cliArch = "aarch64" + case "arm", "armbe": + cliArch = "armv7" + default: + return nil, fmt.Errorf("unsupported architecture: '%s'", runtime.GOARCH) + } + + opts := []testcontainers.ContainerCustomizer{ + testcontainers.WithDockerfile(testcontainers.FromDockerfile{ + ContextArchive: bytes.NewReader(buf.Bytes()), + KeepImage: true, // speeds up subsequent test runs by using the Docker cache + BuildArgs: map[string]*string{ + "ARCH": &cliArch, + }, + }), + testcontainers.WithCmdArgs("/usr/local/bin/stalwart-cli", "apply", "--json", "--no-color", "--file", "/snapshot.json"), + testcontainers.WithEnv(map[string]string{ + "STALWART_URL": uri, + "STALWART_USER": user, + "STALWART_PASSWORD": password, + }), + testcontainers.WithFiles(testcontainers.ContainerFile{ + Reader: content, + ContainerFilePath: "/snapshot.json", + FileMode: 0o700, + }), + testcontainers.WithLogConsumers(&cliLogConsumer{lines: &output}), + testcontainers.WithWaitStrategy(wait.ForExit()), + } + + if net != nil { + opts = append(opts, network.WithNetwork([]string{"cli"}, net)) + } + + container, err := testcontainers.Run(ctx, "", opts...) + if err != nil { + return nil, err + } + + rc := 0 + s, stateErr := container.State(ctx) + if stateErr == nil { + rc = s.ExitCode + } + + if err := container.Terminate(ctx); err != nil { + return nil, err + } + + if stateErr != nil { + return nil, stateErr + } + if rc != 0 { + return nil, fmt.Errorf("cli returned exit code %d", rc) + } + return output, nil +} + +func postJmap(ctx context.Context, h http.Client, url string, username string, password string, body map[string]any) (map[string]any, error) { + bb, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(bb)) + if err != nil { + return nil, err + } + req.SetBasicAuth(username, password) + resp, err := h.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.Status != "200 OK" { + return nil, fmt.Errorf("failed POST onto the JMAP API endpoint at %s: %s", url, resp.Status) + } + + result := map[string]any{} + err = json.NewDecoder(resp.Body).Decode(&result) + if err != nil { + return nil, err + } + + return result, nil +} + +func createManagementObject(ctx context.Context, h http.Client, url string, adminUsername string, adminPassword string, objectType string, obj map[string]any) (string, error) { + result, err := postJmap(ctx, h, url, adminUsername, adminPassword, map[string]any{ + "using": []string{"urn:ietf:params:jmap:core", "urn:stalwart:jmap"}, + "methodCalls": []any{ + []any{ + fmt.Sprintf("x:%s/set", objectType), + map[string]any{ + "create": map[string]any{ + "n": obj, + }, + }, + "c", + }, + }, + }) + if err != nil { + return "", err + } + + text := "" + { + if b, err := json.MarshalIndent(result, "", " "); err == nil { + text = string(b) + } + } + + if methodResponses, ok := result["methodResponses"]; !ok { + return "", fmt.Errorf("response has no methodResponses: %s", text) + } else { + methodResponses := methodResponses.([]any) + if len(methodResponses) != 1 { + return "", fmt.Errorf("response has a methodResponses that doesn't have a single item: %s", text) + + } + firstMethodResponse := methodResponses[0].([]any) + if len(firstMethodResponse) != 3 { + return "", fmt.Errorf("response has a methodResponses with one item that is a JMAP response that does not have 3 elements: %s", text) + } + payload := firstMethodResponse[1] + if payload == nil { + return "", fmt.Errorf("response has a first methodResponse without any payload: %s", text) + } + if created, ok := (payload.(map[string]any))["created"]; !ok { + return "", fmt.Errorf("response has a first methodResponse with a payload that has no 'created': %s", text) + } else { + created := created.(map[string]any) + if specifically, ok := created["n"]; !ok { + return "", fmt.Errorf("response has a first methodResponse with a payload that has no 'n': %s", text) + } else { + specifically := specifically.(map[string]any) + if id, ok := specifically["id"]; !ok { + return "", fmt.Errorf("response has a first methodResponse with a payload with a created with an 'n' that has no 'id': %s", text) + } else { + return id.(string), nil + } + } + } + } +} + +func createJmapClient(container *testcontainers.DockerContainer, ctx context.Context, auth HttpJmapClientAuthenticator, logger *oclog.Logger) (Client, *url.URL, *url.URL, error) { + ip, err := container.Host(ctx) + if err != nil { + return Client{}, nil, nil, err + } + + tlsConfig := &tls.Config{InsecureSkipVerify: true} + + tr := http.DefaultTransport.(*http.Transport).Clone() + tr.ResponseHeaderTimeout = time.Duration(30 * time.Second) + tr.TLSClientConfig = tlsConfig + jh := *http.DefaultClient + jh.Transport = tr + + wsd := &websocket.Dialer{ + TLSClientConfig: tlsConfig, + HandshakeTimeout: time.Duration(10) * time.Second, + } + + jmapPort, err := container.MappedPort(ctx, httpPort) + if err != nil { + return Client{}, nil, nil, err + } + jmapBaseUrl := &url.URL{ + Scheme: "http", + Host: ip + ":" + jmapPort.Port(), + Path: "/", + } + sessionUrl := jmapBaseUrl.JoinPath(".well-known", "jmap") + + if Wireshark != "" { + fmt.Printf("\x1b[45;37;1m Starting Wireshark on port %v \x1b[0m\n", jmapPort) + attr := os.ProcAttr{ + Dir: ".", + Env: os.Environ(), + Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, + } + cmd := []string{Wireshark, "-pkSl", "-i", "lo", "-f", fmt.Sprintf("port %d", jmapPort.Num()), "-Y", "http||websocket"} + process, err := os.StartProcess(Wireshark, cmd, &attr) + if err != nil { + return Client{}, nil, nil, err + } + err = process.Release() + if err != nil { + return Client{}, nil, nil, err + } + + time.Sleep(10 * time.Second) + } + + eventListener := nullHttpJmapApiClientEventListener{} + + api := NewHttpJmapClient(&jh, auth, eventListener) + + wscf, err := NewHttpWsClientFactory(wsd, auth, logger, eventListener) + if err != nil { + return Client{}, nil, nil, err + } + + return NewClient(api, api, api, wscf), jmapBaseUrl, sessionUrl, nil +} + +type ContextPasswordAuthHttpJmapClientAuthenticator struct { + key string +} + +var _ HttpJmapClientAuthenticator = &ContextPasswordAuthHttpJmapClientAuthenticator{} + +func (h *ContextPasswordAuthHttpJmapClientAuthenticator) Authenticate(ctx context.Context, username string, _ *oclog.Logger, req *http.Request) Error { + password := ctx.Value(h.key).(string) + req.SetBasicAuth(username, password) + return nil +} + +func (h *ContextPasswordAuthHttpJmapClientAuthenticator) AuthenticateWS(ctx context.Context, username string, _ *oclog.Logger, headers http.Header) Error { + password := ctx.Value(h.key).(string) + headers.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(username+":"+password))) + return nil +} + +const useNetwork = false + func newStalwartTest(t *testing.T, options ...func(map[string]any)) (*StalwartTest, error) { //NOSONAR - ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + //ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + ctx := t.Context() + cancel := func() {} var _ context.CancelFunc = cancel // ignore context leak warning: it is passed in the struct and called in Close() // A master user name different from "master" does not seem to work as of the current Stalwart version //masterUsernameSuffix, err := pw.Generate(4+rand.Intn(28), 2, 0, false, true) //require.NoError(err) masterUsername := "master" //"master_" + masterUsernameSuffix - - masterPassword, err := pw.Generate(4+rand.Intn(28), 2, 0, false, true) + masterPassword, err := pw.Generate(10+rand.Intn(28), 2, 0, false, true) if err != nil { return nil, err } - masterPasswordHash := "" - { - hasher, err := shacrypt.New(shacrypt.WithSHA512(), shacrypt.WithIterations(shacrypt.IterationsDefaultOmitted)) - if err != nil { - return nil, err - } - digest, err := hasher.Hash(masterPassword) - if err != nil { - return nil, err - } - masterPasswordHash = digest.Encode() + adminUsername := "admin" + adminPassword, err := pw.Generate(10+rand.Intn(28), 2, 0, false, true) + if err != nil { + return nil, err } - hostname := "localhost" + recoveryAlias := "recovery" + containerAlias := "stalwart" + volumeName := "stalwart-test-data-volume" + { + str, err := pw.Generate(32, 12, 0, true, true) + if err != nil { + return nil, err + } + volumeName += "-" + str + } + hostname := "127.0.0.1" settings := map[string]any{ - "hostname": hostname, - "masterusername": masterUsername, - "masterpassword": masterPasswordHash, - "httpPort": httpPort, - "imapsPort": imapsPort, - "dirquery": "false", + "hostname": hostname, + "masterusername": masterUsername, + "masterpassword": masterPassword, //Hash, + "adminusername": adminUsername, + "adminpassword": adminPassword, + "httpPort": httpPort, + "httpsPort": 10443, + "popsPort": 10995, + "imapsPort": imapsPort, + "submissionsPort": 10465, + "smtpPort": 10025, + "dirquery": "false", + "domain": "example.org", } for _, option := range options { option(settings) } - configBuf := bytes.NewBufferString("") - template.Must(template.New("").Parse(configTemplate)).Execute(configBuf, settings) - config := configBuf.String() - configReader := strings.NewReader(config) - - container, err := testcontainers.Run( - ctx, - stalwartImage, - testcontainers.WithLogConsumers(&stalwartTestLogConsumer{}), - testcontainers.WithExposedPorts(httpPort+"/tcp", imapsPort+"/tcp"), - testcontainers.WithFiles(testcontainers.ContainerFile{ - Reader: configReader, - ContainerFilePath: "/opt/stalwart/etc/config.toml", - FileMode: 0o700, - }), - testcontainers.WithWaitStrategyAndDeadline( - 30*time.Second, - wait.ForLog(`Network listener started (network.listen-start) listenerId = "imaptls"`), - wait.ForLog(`Network listener started (network.listen-start) listenerId = "http"`), - ), - ) - - success := false - defer func() { - if !success { - testcontainers.CleanupContainer(t, container) + var net *testcontainers.DockerNetwork + { + if useNetwork { + if n, err := network.New(ctx); err != nil { + return nil, err + } else { + testcontainers.CleanupNetwork(t, n) + net = n + } + } else { + net = nil } - }() + } + + httpWait := wait.ForHTTP("/.well-known/jmap") + httpWait.Port = dockernetwork.MustParsePort(httpPort) + + configBuf := bytes.NewBufferString("") + template.Must(template.New("config").Delims("<<", ">>").Parse(jsonConfigTemplate)).Execute(configBuf, settings) + config := configBuf.String() + { + var recovery *testcontainers.DockerContainer + { + opts := []testcontainers.ContainerCustomizer{ + testcontainers.WithLogConsumers(&printingLogConsumer{prefix: "RECOVERY"}), + testcontainers.WithExposedPorts(httpPort + "/tcp"), + testcontainers.WithEnv(map[string]string{ + "STALWART_RECOVERY_ADMIN": strings.Join([]string{adminUsername, adminPassword}, ":"), + "STALWART_RECOVERY_MODE": "1", + }), + testcontainers.WithFiles(testcontainers.ContainerFile{ + Reader: strings.NewReader(config), + ContainerFilePath: "/etc/stalwart/config.json", + FileMode: 0o666, + }), + testcontainers.WithWaitStrategyAndDeadline( + 30*time.Second, + wait.ForMappedPort(httpPort), + httpWait, + ), + testcontainers.WithName(recoveryAlias), + testcontainers.WithMounts( + testcontainers.ContainerMount{ + Source: testcontainers.GenericVolumeMountSource{ + Name: volumeName, + }, + Target: "/var/lib/stalwart", + ReadOnly: false, + }, + ), + } + if net != nil { + opts = append(opts, network.WithNetwork([]string{recoveryAlias}, net)) + } + + if c, err := testcontainers.Run(ctx, stalwartImage, opts...); err != nil { + return nil, err + } else { + testcontainers.CleanupContainer(t, c) + recovery = c + } + } + + // import config using the cli + { + uri := "" + if net != nil { + uri = (&url.URL{Scheme: "http", Host: recoveryAlias + ":" + httpPort, Path: "/"}).String() + } else { + if ir, err := recovery.Inspect(ctx); err != nil { + return nil, err + } else { + id := ir.Config.Hostname + for _, network := range ir.NetworkSettings.Networks { + id = network.IPAddress.String() + } + uri = (&url.URL{Scheme: "http", Host: id + ":" + httpPort, Path: "/"}).String() + } + } + + snapshot := []string{} + { + t := template.New("snapshot").Delims("<<", ">>") + for _, line := range dumpTemplate { + buf := bytes.NewBufferString("") + p, err := t.Parse(line) + if err != nil { + return nil, err + } + err = p.Execute(buf, settings) + if err != nil { + return nil, err + } + + var m map[string]any + err = json.Unmarshal(buf.Bytes(), &m) + if err != nil { + return nil, err + } + b, err := json.Marshal(m) + if err != nil { + return nil, err + } + snapshot = append(snapshot, strings.TrimSpace(string(b))) + } + } + text := strings.Join(snapshot, "\n") + + t.Logf("Snapshot:\n%s", text) + output, err := applySnapshot(ctx, net, uri, adminUsername, adminPassword, strings.NewReader(text)) + if err != nil { + return nil, err + } + t.Logf("Output of applying configuration:\n%s", strings.Join(output, "")) + } + + if err := recovery.Terminate(ctx, testcontainers.RemoveVolumes()); err != nil { + return nil, err + } + } + + var container *testcontainers.DockerContainer + { + opts := []testcontainers.ContainerCustomizer{ + testcontainers.WithLogConsumers(&printingLogConsumer{prefix: "STALWART"}), + testcontainers.WithExposedPorts(httpPort+"/tcp", imapsPort+"/tcp"), + testcontainers.WithFiles(testcontainers.ContainerFile{ + Reader: strings.NewReader(config), + ContainerFilePath: "/etc/stalwart/config.json", + FileMode: 0o666, + }), + testcontainers.WithWaitStrategyAndDeadline( + 30*time.Second, + wait.ForMappedPort(httpPort), + httpWait, + ), + testcontainers.WithName(containerAlias), + testcontainers.WithMounts( + testcontainers.ContainerMount{ + Source: testcontainers.GenericVolumeMountSource{ + Name: volumeName, + }, + Target: "/var/lib/stalwart", + ReadOnly: false, + }, + ), + } + + if net != nil { + opts = append(opts, network.WithNetwork([]string{containerAlias}, net)) + } + + if c, err := testcontainers.Run(ctx, stalwartImage, opts...); err != nil { + return nil, err + } else { + testcontainers.CleanupContainer(t, c, testcontainers.RemoveVolumes(volumeName)) + container = c + } + } ip, err := container.Host(ctx) if err != nil { return nil, err } - imapPort, err := container.MappedPort(ctx, "993") + imapPort, err := container.MappedPort(ctx, imapsPort) if err != nil { return nil, err } - tlsConfig := &tls.Config{InsecureSkipVerify: true} + domains := structs.Uniq(structs.Map(users[:], func(u User) string { + parts := strings.Split(u.email, "@") + return parts[1] + })) + slices.Sort(domains) - loggerImpl := clog.NewLogger(clog.Level("trace")) - logger := &loggerImpl - var j Client - var jmapBaseUrl *url.URL - var sessionUrl *url.URL - { - tr := http.DefaultTransport.(*http.Transport).Clone() - tr.ResponseHeaderTimeout = time.Duration(30 * time.Second) - tr.TLSClientConfig = tlsConfig - jh := *http.DefaultClient - jh.Transport = tr - - wsd := &websocket.Dialer{ - TLSClientConfig: tlsConfig, - HandshakeTimeout: time.Duration(10) * time.Second, - } - - jmapPort, err := container.MappedPort(ctx, httpPort) - if err != nil { - return nil, err - } - jmapBaseUrl = &url.URL{ - Scheme: "http", - Host: ip + ":" + jmapPort.Port(), - Path: "/", - } - sessionUrl = jmapBaseUrl.JoinPath(".well-known", "jmap") - - if Wireshark != "" { - fmt.Printf("\x1b[45;37;1m Starting Wireshark on port %v \x1b[0m\n", jmapPort) - attr := os.ProcAttr{ - Dir: ".", - Env: os.Environ(), - Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, - } - cmd := []string{Wireshark, "-pkSl", "-i", "lo", "-f", fmt.Sprintf("port %d", jmapPort.Num()), "-Y", "http||websocket"} - process, err := os.StartProcess(Wireshark, cmd, &attr) - require.NoError(t, err) - err = process.Release() - require.NoError(t, err) - - time.Sleep(10 * time.Second) - } - - eventListener := nullHttpJmapApiClientEventListener{} - - auth := NewMasterAuthHttpJmapClientAuthenticator(masterUsername, masterPassword) - - api := NewHttpJmapClient(&jh, auth, eventListener) - - wscf, err := NewHttpWsClientFactory(wsd, auth, logger, eventListener) - if err != nil { - return nil, err - } - - j = NewClient(api, api, api, wscf) - } - - // provision some things using Stalwart's Management API + // provision some things using Stalwart's JMAP based Management API { var h http.Client { tr := http.DefaultTransport.(*http.Transport).Clone() tr.ResponseHeaderTimeout = time.Duration(30 * time.Second) - tr.TLSClientConfig = tlsConfig + tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} h = *http.DefaultClient h.Transport = tr } @@ -376,112 +895,178 @@ func newStalwartTest(t *testing.T, options ...func(map[string]any)) (*StalwartTe apiPort, err := container.MappedPort(ctx, httpPort) require.NoError(t, err) - url := fmt.Sprintf("http://%s:%d/api/principal", ip, apiPort.Num()) + apiPath := "" + { + // fetch JMAP session first + session := map[string]any{} + { + url := fmt.Sprintf("http://%s:%d/.well-known/jmap", ip, apiPort.Num()) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + require.NoError(t, err) + req.SetBasicAuth(adminUsername, adminPassword) + resp, err := h.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, "200 OK", resp.Status) + err = json.NewDecoder(resp.Body).Decode(&session) + require.NoError(t, err) + } + if u, ok := session["apiUrl"]; ok { + p, err := url.Parse(u.(string)) + require.NoError(t, err) + apiPath = strings.TrimLeft(p.Path, "/") + } else { + require.FailNow(t, "failed to find apiUrl in JMAP Session") + } + } + url := fmt.Sprintf("http://%s:%d/%s", ip, apiPort.Num(), apiPath) + domainIds := map[string]string{} for _, domain := range domains { fmt.Printf("Creating domain '%v'\n", domain) - bb, err := json.Marshal(map[string]any{ - "type": "domain", - "name": domain, - "description": domain, + id, err := createManagementObject(ctx, h, url, adminUsername, adminPassword, "Domain", map[string]any{ + "name": domain, + "aliases": map[string]any{}, + "certificateManagement": map[string]any{"@type": "Manual"}, + "dkimManagement": map[string]any{"@type": "Manual"}, + "dnsManagement": map[string]any{"@type": "Manual"}, + "subAddressing": map[string]any{"@type": "Enabled"}, }) require.NoError(t, err) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(bb)) - require.NoError(t, err) - req.SetBasicAuth("mailadmin", "secret") - resp, err := h.Do(req) - require.NoError(t, err) - require.Equal(t, "200 OK", resp.Status) //NOSONAR + domainIds[domain] = id } for _, user := range users { fmt.Printf("Creating individual '%v'\n", user.name) - bb, err := json.Marshal(map[string]any{ - "type": "individual", - "name": user.name, - "description": user.description, - "emails": user.email, - "roles": []string{"user"}, - "secrets": user.password, - "quota": 20000000000, - }) - require.NoError(t, err) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(bb)) - require.NoError(t, err) - req.SetBasicAuth("mailadmin", "secret") - resp, err := h.Do(req) - require.NoError(t, err) - require.Equal(t, "200 OK", resp.Status) - // fetch the user once with the superadmin credentials to "activate" it, - // it is unclear why that is needed, but without that, we get errors back - // that we are not allowed to access that resource + localPart := "" + domain := "" + domainId := "" { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - require.NoError(t, err) - req.SetBasicAuth("mailadmin", "secret") - resp, err := h.Do(req) - require.NoError(t, err) - require.Equal(t, "200 OK", resp.Status) - } - } - - { - require.NoError(t, err) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - require.NoError(t, err) - req.SetBasicAuth("mailadmin", "secret") - resp, err := h.Do(req) - require.NoError(t, err) - require.Equal(t, "200 OK", resp.Status) - var list struct { - Data struct { - Total int `json:"total"` - Items []struct { - Type string `json:"type"` - Id int `json:"id"` - Name string `json:"name"` - Emails []string `json:"emails"` - Roles []string `json:"roles"` - } `json:"items"` - } `json:"data"` - } - bb, err := io.ReadAll(resp.Body) - require.NoError(t, err) - defer resp.Body.Close() - err = json.Unmarshal(bb, &list) - require.NoError(t, err) - individuals := []struct { - Id int - Name string - Emails []string - Roles []string - }{} - for _, p := range list.Data.Items { - if p.Type == "individual" { - individuals = append(individuals, struct { - Id int - Name string - Emails []string - Roles []string - }{p.Id, p.Name, p.Emails, p.Roles}) + parts := strings.Split(user.alias, "@") + localPart = parts[0] + domain = parts[1] + if v, ok := domainIds[domain]; ok { + domainId = v + } else { + require.FailNow(t, "failed to find id for domain '%s' in user email address '%s'", domain, user.email) } } - require.Equal(t, len(users), len(individuals)) + _, err := createManagementObject(ctx, h, url, adminUsername, adminPassword, "Account", map[string]any{ + "@type": "User", + "name": user.name, + "description": user.description, + "aliases": map[string]any{ + "0": map[string]any{ + "enabled": true, + "name": localPart, + "domainId": domainId, + }, + }, + "credentials": map[string]any{ + "0": map[string]any{ + "@type": "Password", + "secret": user.password, + "allowedIps": map[string]any{}, + "expiresAt": nil, + }, + }, + "domainId": domainId, + "encryptionAtRest": map[string]any{"@type": "Disabled"}, + "memberGroupIds": map[string]any{}, + "permissions": map[string]any{"@type": "Inherit"}, + "quotas": map[string]any{ + "maxEmails": 200000, + "maxDiskQuota": 20000000000, + }, + "roles": map[string]any{"@type": "User"}, + }) + require.NoError(t, err) + } + + // fetch all users and double-check whether they are all in there, but also use this + // to detect which primary email address has been assigned to the user by Stalwart, + // and then store it into the user object + { + result, err := postJmap(ctx, h, url, adminUsername, adminPassword, map[string]any{ + "using": []string{"urn:ietf:params:jmap:core", "urn:stalwart:jmap"}, + "methodCalls": []any{ + []any{ + "x:Account/get", + map[string]any{}, + "0", + }, + }, + }) + require.NoError(t, err) + + usersByName := structs.Index(users[:], func(u User) string { return u.name }) + { + mr := result["methodResponses"].([]any) + f := mr[0].([]any) + p := f[1].(map[string]any) + l := p["list"].([]any) + for _, u := range l { + u := u.(map[string]any) + name := u["name"].(string) + email := u["emailAddress"].(string) + if match, ok := usersByName[name]; ok { + require.Equal(t, match.email, email) + aliases := u["aliases"].(map[string]any) + require.Len(t, aliases, 1) + for _, alias := range aliases { + parts := strings.Split(match.alias, "@") + alias := alias.(map[string]any) + require.True(t, alias["enabled"].(bool)) + require.Equal(t, parts[0], alias["name"].(string)) + } + delete(usersByName, name) + } + } + } + require.Empty(t, usersByName) + text := "" + { + if b, err := json.MarshalIndent(result, "", " "); err == nil { + text = string(b) + } + } + t.Logf("Accounts:\n%s", text) } { + + loggerImpl := oclog.NewLogger(oclog.Level("trace")) + logger := &loggerImpl + + auth := &ContextPasswordAuthHttpJmapClientAuthenticator{key: "password"} + + j, _, sessionUrl, err := createJmapClient(container, ctx, auth, logger) + if err != nil { + return nil, err + } + // check whether we can fetch a session for the provisioned users for _, user := range users { - session, err := j.FetchSession(ctx, sessionUrl, user.name, logger) - require.NoError(t, err, "failed to retrieve JMAP session for newly created principal '%s'", user.name) - require.Equal(t, user.name, session.Username) + ctx := context.WithValue(ctx, "password", user.password) + session, err := j.FetchSession(ctx, sessionUrl, user.email, oclog.From(logger.With().Str("username", user.email).Str("password", user.password))) + require.NoError(t, err, "failed to retrieve JMAP session for newly created principal '%s'", user.email) + require.Equal(t, user.email, session.Username) } } } - success = true + loggerImpl := oclog.NewLogger(oclog.Level("trace")) + logger := &loggerImpl + + auth := NewMasterAuthHttpJmapClientAuthenticator(masterUsername, masterPassword) + + j, jmapBaseUrl, sessionUrl, err := createJmapClient(container, ctx, auth, logger) + if err != nil { + return nil, err + } + return &StalwartTest{ t: t, ip: ip, @@ -498,9 +1083,9 @@ func newStalwartTest(t *testing.T, options ...func(map[string]any)) (*StalwartTe var urlHostRegex = regexp.MustCompile(`^(https?://)(.+?)/(.+)$`) -func replaceHost(u string, host string) (string, error) { +func replaceHostProto(u string, proto string, host string) (string, error) { if m := urlHostRegex.FindAllStringSubmatch(u, -1); m != nil { - return fmt.Sprintf("%s%s/%s", m[0][1], host, m[0][3]), nil + return fmt.Sprintf("%s://%s/%s", proto, host, m[0][3]), nil } else { return "", fmt.Errorf("'%v' does not match '%v'", u, urlHostRegex) } @@ -1171,7 +1756,7 @@ func containerTest[OBJ Idable, RESP GetResponse[OBJ], BOXES any, CHANGE Change]( defer s.Close() user := pickUser() - session := s.Session(user.name) + session := s.Session(user.email) ctx := s.Context(session) accountId := acc(session) diff --git a/pkg/jmap/http.go b/pkg/jmap/http.go index ff739c13b2..c9fcef0dcb 100644 --- a/pkg/jmap/http.go +++ b/pkg/jmap/http.go @@ -532,6 +532,7 @@ func (w *HttpWsClientFactory) connect(ctx context.Context, sessionProvider func( h := http.Header{} w.auth(ctx, username, logger, h) + w.logger.Trace().Str("username", log.SafeString(username)).Str("url", log.SafeString(u.String())).Msgf("connecting") c, res, err := w.dialer.DialContext(ctx, u.String(), h) if err != nil { return nil, "", endpoint, jmapError(err, JmapErrorFailedToEstablishWssConnection) diff --git a/pkg/jmap/integration_addressbook_test.go b/pkg/jmap/integration_addressbook_test.go index 97428cc78e..87d6f8c9eb 100644 --- a/pkg/jmap/integration_addressbook_test.go +++ b/pkg/jmap/integration_addressbook_test.go @@ -59,8 +59,8 @@ func TestAddressBooks(t *testing.T) { }, func(orig AddressBook) AddressBookChange { return AddressBookChange{ - Description: ptr(orig.Description + " (changed)"), - IsSubscribed: ptr(!orig.IsSubscribed), + Description: new(orig.Description + " (changed)"), + IsSubscribed: new(!orig.IsSubscribed), } }, func(t *testing.T, orig AddressBook, _ AddressBookChange, changed AddressBook) { @@ -85,7 +85,7 @@ func TestContacts(t *testing.T) { defer s.Close() user := pickUser() - session := s.Session(user.name) + session := s.Session(user.email) ctx := s.Context(session) accountId, addressbookId, expectedContactCardsById, boxes, err := s.fillContacts(t, count, session, ctx, user) @@ -213,8 +213,8 @@ func TestContacts(t *testing.T) { now := time.Now().Truncate(time.Duration(1) * time.Second).UTC() for _, event := range expectedContactCardsById { change := ContactCardChange{ - Language: ptr("xyz"), - Updated: ptr(now), + Language: new("xyz"), + Updated: new(now), } result, err := s.client.UpdateContactCard(accountId, event.Id, change, ctx) require.NoError(err) @@ -359,7 +359,7 @@ func (s *StalwartTest) fillContacts( //NOSONAR user User, ) (AccountId, string, map[string]ContactCard, ContactsBoxes, error) { require := require.New(t) - c, err := NewTestJmapClient(session, user.name, user.password, true, true) + c, err := NewTestJmapClient(session, user.email, user.password, true, true) require.NoError(err) defer c.Close() @@ -401,11 +401,11 @@ func (s *StalwartTest) fillContacts( //NOSONAR card := ContactCardChange{ Type: jscontact.ContactCardType, - Version: ptr(jscontact.JSContactVersion_1_0), + Version: new(jscontact.JSContactVersion_1_0), AddressBookIds: toBoolPtrMap([]string{addressbookId}), ProdId: &productName, Language: &language, - Kind: ptr(jscontact.ContactCardKindIndividual), + Kind: new(jscontact.ContactCardKindIndividual), Name: &nameObj, } diff --git a/pkg/jmap/integration_calendar_test.go b/pkg/jmap/integration_calendar_test.go index 53aaf24976..442165ba47 100644 --- a/pkg/jmap/integration_calendar_test.go +++ b/pkg/jmap/integration_calendar_test.go @@ -49,8 +49,8 @@ func TestCalendars(t *testing.T) { //NOSONAR }, func(orig Calendar) CalendarChange { return CalendarChange{ - Description: ptr(orig.Description + " (changed)"), - IsSubscribed: ptr(!orig.IsSubscribed), + Description: new(orig.Description + " (changed)"), + IsSubscribed: new(!orig.IsSubscribed), } }, func(t *testing.T, orig Calendar, _ CalendarChange, changed Calendar) { @@ -75,7 +75,7 @@ func TestEvents(t *testing.T) { defer s.Close() user := pickUser() - session := s.Session(user.name) + session := s.Session(user.email) ctx := s.Context(session) accountId, calendarId, expectedEventsById, boxes, err := s.fillEvents(t, count, ctx, user) @@ -178,7 +178,7 @@ func TestEvents(t *testing.T) { for _, event := range expectedEventsById { change := CalendarEventChange{ EventChange: jscalendar.EventChange{ - Status: ptr(jscalendar.StatusCancelled), + Status: new(jscalendar.StatusCancelled), ObjectChange: jscalendar.ObjectChange{ Sequence: uintPtr(99), ShowWithoutTime: truep, @@ -382,7 +382,7 @@ func (s *StalwartTest) fillEvents( //NOSONAR user User, ) (AccountId, string, map[string]CalendarEvent, EventsBoxes, error) { require := require.New(t) - c, err := NewTestJmapClient(ctx.Session, user.name, user.password, true, true) + c, err := NewTestJmapClient(ctx.Session, user.email, user.password, true, true) require.NoError(err) defer c.Close() @@ -467,7 +467,7 @@ func (s *StalwartTest) fillEvents( //NOSONAR EventChange: jscalendar.EventChange{ Type: jscalendar.EventType, Start: jscalendar.LocalDateTime(start), - Duration: ptr(jscalendar.Duration(duration)), + Duration: new(jscalendar.Duration(duration)), Status: &status, ObjectChange: jscalendar.ObjectChange{ CommonObjectChange: jscalendar.CommonObjectChange{ diff --git a/pkg/jmap/integration_email_test.go b/pkg/jmap/integration_email_test.go index d7b1e1389f..2c70eb85f4 100644 --- a/pkg/jmap/integration_email_test.go +++ b/pkg/jmap/integration_email_test.go @@ -39,7 +39,7 @@ func TestEmails(t *testing.T) { defer s.Close() user := pickUser() - session := s.Session(user.name) + session := s.Session(user.email) ctx := s.Context(session) accountId := session.PrimaryAccounts.Mail @@ -59,9 +59,11 @@ func TestEmails(t *testing.T) { result, err := s.client.GetIdentities(accountId, []string{}, ctx) require.NoError(err) require.Equal(session.State, result.GetSessionState()) - require.Len(result.Payload.List, 1) - require.Equal(user.email, result.Payload.List[0].Email) - require.Equal(user.description, result.Payload.List[0].Name) + require.Len(result.Payload.List, 2) + emailMatches := structs.Filter(result.Payload.List, func(i Identity) bool { return i.Email == user.email }) + require.Len(emailMatches, 1) + aliasMatches := structs.Filter(result.Payload.List, func(i Identity) bool { return i.Email == user.alias }) + require.Len(aliasMatches, 1) } { @@ -122,7 +124,7 @@ func TestSendingEmails(t *testing.T) { defer s.Close() from := pickUser() - session := s.Session(from.name) + session := s.Session(from.email) ctx := s.Context(session) accountId := session.PrimaryAccounts.Mail @@ -131,7 +133,7 @@ func TestSendingEmails(t *testing.T) { others := structs.Filter(users[:], func(u User) bool { return u.name != from.name }) to = others[rand.Intn(len(others))] } - toSession := s.Session(to.name) + toSession := s.Session(to.email) toAccountId := toSession.PrimaryAccounts.Mail var cc User @@ -139,7 +141,7 @@ func TestSendingEmails(t *testing.T) { others := structs.Filter(users[:], func(u User) bool { return u.name != from.name && u.name != to.name }) cc = others[rand.Intn(len(others))] } - ccSession := s.Session(cc.name) + ccSession := s.Session(cc.email) ccAccountId := ccSession.PrimaryAccounts.Mail var mailboxPerRole map[string]Mailbox @@ -190,8 +192,10 @@ func TestSendingEmails(t *testing.T) { { result, err := s.client.GetIdentities(accountId, []string{}, ctx) require.NoError(err) - require.NotEmpty(result.Payload.List) - identity = result.Payload.List[0] + require.Len(result.Payload.List, 2) + matchesAlias := structs.Filter(result.Payload.List, func(i Identity) bool { return i.Email == from.alias }) + require.Len(matchesAlias, 1) + identity = matchesAlias[0] } create := EmailChange{ @@ -270,7 +274,7 @@ func TestSendingEmails(t *testing.T) { require.Equal(sub.UndoStatus, UndoStatusPending) // this *might* be fragile: if the server is fast enough, would we get "final" here? require.Empty(sub.DsnBlobIds) require.Empty(sub.MdnBlobIds) - require.Equal(from.email, sub.Envelope.MailFrom.Email) + require.Equal(from.alias, sub.Envelope.MailFrom.Email) require.Nil(sub.Envelope.MailFrom.Parameters) require.Len(sub.Envelope.RcptTo, 2) require.Contains(sub.Envelope.RcptTo, Address{Email: to.email}) @@ -325,7 +329,7 @@ func TestSendingEmails(t *testing.T) { require.Equal(1, mailbox.TotalEmails) } } - require.NotEmpty(inboxId, "failed to find the Mailbox with the 'inbox' role for %v", r.user.name) + require.NotEmpty(inboxId, "failed to find the Mailbox with the 'inbox' role for %v", r.user.email) } result, err := s.client.QueryEmails([]AccountId{r.accountId}, EmailFilterCondition{InMailbox: inboxId}, 0, 0, true, 0, rctx) @@ -538,7 +542,7 @@ func (s *StalwartTest) fillEmailsWithImap(folder string, count int, empty bool, } }(c) - if err = c.Login(user.name, user.password).Wait(); err != nil { + if err = c.Login(user.email, user.password).Wait(); err != nil { return nil, 0, err } @@ -586,7 +590,7 @@ func (s *StalwartTest) fillEmailsWithImap(folder string, count int, empty bool, domain := addressParts[0][2] toName := displayName - toAddress := fmt.Sprintf("%s@%s", user.name, domain) + toAddress := fmt.Sprintf("%s@%s", user.email, domain) ccName1 := "Team Lead" ccAddress1 := fmt.Sprintf("lead@%s", domain) ccName2 := "Coworker" diff --git a/pkg/jmap/integration_ws_test.go b/pkg/jmap/integration_ws_test.go index 8bcd0b35e3..6a5dd46854 100644 --- a/pkg/jmap/integration_ws_test.go +++ b/pkg/jmap/integration_ws_test.go @@ -68,7 +68,7 @@ func TestWs(t *testing.T) { defer s.Close() user := pickUser() - session := s.Session(user.name) + session := s.Session(user.email) ctx := s.Context(session) mailAccountId := session.PrimaryAccounts.Mail @@ -77,7 +77,7 @@ func TestWs(t *testing.T) { _, inboxFolder = s.findInbox(t, mailAccountId, ctx) } - l := &testWsPushListener{t: t, username: user.name, logger: s.logger, mailAccountId: mailAccountId} + l := &testWsPushListener{t: t, username: user.email, logger: s.logger, mailAccountId: mailAccountId} s.client.AddWsPushListener(l) require.Equal(uint32(0), l.calls.Load()) diff --git a/pkg/jmap/model_examples.go b/pkg/jmap/model_examples.go index 2585ca15bc..43a42c296b 100644 --- a/pkg/jmap/model_examples.go +++ b/pkg/jmap/model_examples.go @@ -575,7 +575,7 @@ func (e Exemplar) MailboxInbox() (Mailbox, string, string) { Id: e.MailboxInboxId, Name: "Inbox", Role: JmapMailboxRoleInbox, - SortOrder: ptr(0), + SortOrder: new(0), TotalEmails: 1291, UnreadEmails: 82, TotalThreads: 891, @@ -600,7 +600,7 @@ func (e Exemplar) MailboxInboxProjects() (Mailbox, string, string) { Id: e.MailboxProjectId, ParentId: e.MailboxInboxId, Name: "Projects", - SortOrder: ptr(0), + SortOrder: new(0), TotalEmails: 112, UnreadEmails: 3, TotalThreads: 85, @@ -625,7 +625,7 @@ func (e Exemplar) MailboxDrafts() (Mailbox, string, string) { Id: e.MailboxDraftsId, Name: "Drafts", Role: JmapMailboxRoleDrafts, - SortOrder: ptr(0), + SortOrder: new(0), TotalEmails: 12, UnreadEmails: 1, TotalThreads: 12, @@ -650,7 +650,7 @@ func (e Exemplar) MailboxSent() (Mailbox, string, string) { Id: e.MailboxSentId, Name: "Sent Items", Role: JmapMailboxRoleSent, - SortOrder: ptr(0), + SortOrder: new(0), TotalEmails: 1621, UnreadEmails: 0, TotalThreads: 1621, @@ -675,7 +675,7 @@ func (e Exemplar) MailboxJunk() (Mailbox, string, string) { Id: e.MailboxJunkId, Name: "Junk Mail", Role: JmapMailboxRoleJunk, - SortOrder: ptr(0), + SortOrder: new(0), TotalEmails: 251, UnreadEmails: 0, TotalThreads: 251, @@ -700,7 +700,7 @@ func (e Exemplar) MailboxDeleted() (Mailbox, string, string) { Id: e.MailboxDeletedId, Name: "Deleted Items", Role: JmapMailboxRoleTrash, - SortOrder: ptr(0), + SortOrder: new(0), TotalEmails: 99, UnreadEmails: 0, TotalThreads: 91, @@ -918,7 +918,7 @@ func (e Exemplar) AddressBook() AddressBook { func (e Exemplar) AddressBookChange() AddressBookChange { return AddressBookChange{ - Description: ptr("A different name"), + Description: new("A different name"), } } @@ -2325,9 +2325,9 @@ func (e Exemplar) CalendarEventSearchResults() CalendarEventSearchResults { return CalendarEventSearchResults{ Results: []CalendarEvent{ev}, CanCalculateChanges: true, - Position: ptr(uint(3)), - Limit: ptr(uint(10)), - Total: ptr(uint(4)), + Position: new(uint(3)), + Limit: new(uint(10)), + Total: new(uint(4)), } } @@ -2337,8 +2337,8 @@ func (e Exemplar) ContactCardSearchResults() ContactCardSearchResults { return ContactCardSearchResults{ Results: []ContactCard{c1, c2}, CanCalculateChanges: true, - Position: ptr(uint(3)), - Limit: ptr(uint(10)), - Total: ptr(uint(4)), + Position: new(uint(3)), + Limit: new(uint(10)), + Total: new(uint(4)), } } diff --git a/pkg/jmap/tools.go b/pkg/jmap/tools.go index b993ee2a9d..f9f58b6b78 100644 --- a/pkg/jmap/tools.go +++ b/pkg/jmap/tools.go @@ -394,14 +394,10 @@ func mapPairs[K comparable, L, R any](left map[K]L, right map[K]R) map[K]pair[L, } var ( - truep = ptr(true) - falsep = ptr(false) + truep = new(true) + falsep = new(false) ) -func ptr[T any | string | int | uint | bool](t T) *T { - return &t -} - func identity1[T any](t T) T { return t } @@ -410,7 +406,7 @@ func list[T Foo, GETRESP GetResponse[T]](r GETRESP) []T { return r.GetList() } func getid[T Idable](r T) string { return r.GetId() } func uintPtr[T int | uint](i T) *uint { - return ptr(uint(i)) + return new(uint(i)) } func valueIf[T any | uint | int | bool](value *T, condition bool) *T { diff --git a/vendor/github.com/go-crypt/crypt/LICENSE b/vendor/github.com/go-crypt/crypt/LICENSE deleted file mode 100644 index a149832c0d..0000000000 --- a/vendor/github.com/go-crypt/crypt/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2022 github.com/go-crypt/crypt - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/go-crypt/crypt/algorithm/const.go b/vendor/github.com/go-crypt/crypt/algorithm/const.go deleted file mode 100644 index 08c585b4db..0000000000 --- a/vendor/github.com/go-crypt/crypt/algorithm/const.go +++ /dev/null @@ -1,26 +0,0 @@ -package algorithm - -const ( - // DigestSHA1 is te name for SHA1 digests. - DigestSHA1 = "sha1" - - // DigestSHA224 is te name for SHA224 digests. - DigestSHA224 = "sha224" - - // DigestSHA256 is te name for SHA256 digests. - DigestSHA256 = "sha256" - - // DigestSHA384 is te name for SHA384 digests. - DigestSHA384 = "sha384" - - // DigestSHA512 is te name for SHA512 digests. - DigestSHA512 = "sha512" -) - -const ( - // SaltLengthDefault is the default salt size for most implementations. - SaltLengthDefault = 16 - - // KeyLengthDefault is the default key size for most implementations. - KeyLengthDefault = 32 -) diff --git a/vendor/github.com/go-crypt/crypt/algorithm/doc.go b/vendor/github.com/go-crypt/crypt/algorithm/doc.go deleted file mode 100644 index 35a89bc551..0000000000 --- a/vendor/github.com/go-crypt/crypt/algorithm/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package algorithm is a package which contains the individual algorithms and interfaces related to their -// implementation. -package algorithm diff --git a/vendor/github.com/go-crypt/crypt/algorithm/errors.go b/vendor/github.com/go-crypt/crypt/algorithm/errors.go deleted file mode 100644 index 2cec6fc8ae..0000000000 --- a/vendor/github.com/go-crypt/crypt/algorithm/errors.go +++ /dev/null @@ -1,66 +0,0 @@ -package algorithm - -import ( - "errors" -) - -var ( - // ErrEncodedHashInvalidFormat is an error returned when an encoded hash has an invalid format. - ErrEncodedHashInvalidFormat = errors.New("provided encoded hash has an invalid format") - - // ErrEncodedHashInvalidIdentifier is an error returned when an encoded hash has an invalid identifier for the - // given digest. - ErrEncodedHashInvalidIdentifier = errors.New("provided encoded hash has an invalid identifier") - - // ErrEncodedHashInvalidVersion is an error returned when an encoded hash has an unsupported or otherwise invalid - // version. - ErrEncodedHashInvalidVersion = errors.New("provided encoded hash has an invalid version") - - // ErrEncodedHashInvalidOption is an error returned when an encoded hash has an unsupported or otherwise invalid - // option in the option field. - ErrEncodedHashInvalidOption = errors.New("provided encoded hash has an invalid option") - - // ErrEncodedHashInvalidOptionKey is an error returned when an encoded hash has an unknown or otherwise invalid - // option key in the option field. - ErrEncodedHashInvalidOptionKey = errors.New("provided encoded hash has an invalid option key") - - // ErrEncodedHashInvalidOptionValue is an error returned when an encoded hash has an unknown or otherwise invalid - // option value in the option field. - ErrEncodedHashInvalidOptionValue = errors.New("provided encoded hash has an invalid option value") - - // ErrEncodedHashKeyEncoding is an error returned when an encoded hash has a salt with an invalid or unsupported - // encoding. - ErrEncodedHashKeyEncoding = errors.New("provided encoded hash has a key value that can't be decoded") - - // ErrEncodedHashSaltEncoding is an error returned when an encoded hash has a salt with an invalid or unsupported - // encoding. - ErrEncodedHashSaltEncoding = errors.New("provided encoded hash has a salt value that can't be decoded") - - // ErrKeyDerivation is returned when a Key function returns an error. - ErrKeyDerivation = errors.New("failed to derive the key with the provided parameters") - - // ErrSaltEncoding is an error returned when a salt has an invalid or unsupported encoding. - ErrSaltEncoding = errors.New("provided salt has a value that can't be decoded") - - // ErrPasswordInvalid is an error returned when a password has an invalid or unsupported properties. It is NOT - // returned on password mismatches. - ErrPasswordInvalid = errors.New("password is invalid") - - // ErrSaltInvalid is an error returned when a salt has an invalid or unsupported properties. - ErrSaltInvalid = errors.New("salt is invalid") - - // ErrSaltReadRandomBytes is an error returned when generating the random bytes for salt resulted in an error. - ErrSaltReadRandomBytes = errors.New("could not read random bytes for salt") - - // ErrParameterInvalid is an error returned when a parameter has an invalid value. - ErrParameterInvalid = errors.New("parameter is invalid") -) - -// Error format strings. -const ( - ErrFmtInvalidIntParameter = "%w: parameter '%s' must be between %d%s and %d but is set to '%d'" - ErrFmtDigestDecode = "%s decode error: %w" - ErrFmtDigestMatch = "%s match error: %w" - ErrFmtHasherHash = "%s hashing error: %w" - ErrFmtHasherValidation = "%s validation error: %w" -) diff --git a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/const.go b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/const.go deleted file mode 100644 index b7906462b8..0000000000 --- a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/const.go +++ /dev/null @@ -1,46 +0,0 @@ -package shacrypt - -const ( - // EncodingFmt is the encoding format for this algorithm. - EncodingFmt = "$%s$rounds=%d$%s$%s" - - // EncodingFmtRoundsOmitted is the encoding format for this algorithm when the rounds can be omitted. - EncodingFmtRoundsOmitted = "$%s$%s$%s" - - // AlgName is the name for this algorithm. - AlgName = "shacrypt" - - // AlgIdentifierSHA256 is the identifier used in encoded SHA256 variants of this algorithm. - AlgIdentifierSHA256 = "5" - - // AlgIdentifierSHA512 is the identifier used in encoded SHA512 variants of this algorithm. - AlgIdentifierSHA512 = "6" - - // IterationsMin is the minimum number of iterations accepted. - IterationsMin = 1000 - - // IterationsMax is the maximum number of iterations accepted. - IterationsMax = 999999999 - - // IterationsDefaultSHA256 is the default number of iterations for SHA256. - IterationsDefaultSHA256 = 1000000 - - // IterationsDefaultSHA512 is the default number of iterations for SHA512. - IterationsDefaultSHA512 = 500000 - - // IterationsDefaultOmitted is the default number of iterations when the rounds are omitted. - IterationsDefaultOmitted = 5000 - - // SaltLengthMin is the minimum salt length. - SaltLengthMin = 1 - - // SaltLengthMax is the maximum salt length. - SaltLengthMax = 16 - - // SaltCharSet are the valid characters for the salt. - SaltCharSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./" -) - -const ( - variantDefault = VariantSHA512 -) diff --git a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/decoder.go b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/decoder.go deleted file mode 100644 index a0fb81a839..0000000000 --- a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/decoder.go +++ /dev/null @@ -1,136 +0,0 @@ -package shacrypt - -import ( - "fmt" - "strconv" - - "github.com/go-crypt/crypt/algorithm" - "github.com/go-crypt/crypt/internal/encoding" -) - -// RegisterDecoder the decoder with the algorithm.DecoderRegister. -func RegisterDecoder(r algorithm.DecoderRegister) (err error) { - if err = RegisterDecoderSHA256(r); err != nil { - return err - } - - if err = RegisterDecoderSHA512(r); err != nil { - return err - } - - return nil -} - -// RegisterDecoderSHA256 registers specifically the sha256 decoder variant with the algorithm.DecoderRegister. -func RegisterDecoderSHA256(r algorithm.DecoderRegister) (err error) { - if err = r.RegisterDecodeFunc(VariantSHA256.Prefix(), DecodeVariant(VariantSHA256)); err != nil { - return err - } - - return nil -} - -// RegisterDecoderSHA512 registers specifically the sha512 decoder variant with the algorithm.DecoderRegister. -func RegisterDecoderSHA512(r algorithm.DecoderRegister) (err error) { - if err = r.RegisterDecodeFunc(VariantSHA512.Prefix(), DecodeVariant(VariantSHA512)); err != nil { - return err - } - - return nil -} - -// Decode the encoded digest into a algorithm.Digest. -func Decode(encodedDigest string) (digest algorithm.Digest, err error) { - return DecodeVariant(VariantNone)(encodedDigest) -} - -// DecodeVariant the encoded digest into a algorithm.Digest provided it matches the provided Variant. If VariantNone is -// used all variants can be decoded. -func DecodeVariant(v Variant) func(encodedDigest string) (digest algorithm.Digest, err error) { - return func(encodedDigest string) (digest algorithm.Digest, err error) { - var ( - parts []string - variant Variant - ) - - if variant, parts, err = decoderParts(encodedDigest); err != nil { - return nil, fmt.Errorf(algorithm.ErrFmtDigestDecode, AlgName, err) - } - - if v != VariantNone && v != variant { - return nil, fmt.Errorf(algorithm.ErrFmtDigestDecode, AlgName, fmt.Errorf("the '%s' variant cannot be decoded only the '%s' variant can be", variant.String(), v.String())) - } - - if digest, err = decode(variant, parts); err != nil { - return nil, fmt.Errorf(algorithm.ErrFmtDigestDecode, AlgName, err) - } - - return digest, nil - } -} - -func decoderParts(encodedDigest string) (variant Variant, parts []string, err error) { - parts = encoding.Split(encodedDigest, -1) - - if n := len(parts); n != 4 && n != 5 { - return VariantNone, nil, algorithm.ErrEncodedHashInvalidFormat - } - - variant = NewVariant(parts[1]) - - if variant == VariantNone { - return variant, nil, fmt.Errorf("%w: identifier '%s' is not an encoded %s digest", algorithm.ErrEncodedHashInvalidIdentifier, parts[1], AlgName) - } - - return variant, parts[2:], nil -} - -func decode(variant Variant, parts []string) (digest algorithm.Digest, err error) { - decoded := &Digest{ - variant: variant, - } - - var ( - ip, is, ik int - ) - - switch len(parts) { - case 2: - ip, is, ik = -1, 0, 1 - case 3: - ip, is, ik = 0, 1, 2 - } - - if len(parts[ik]) == 0 { - return nil, fmt.Errorf("%w: key has 0 bytes", algorithm.ErrEncodedHashKeyEncoding) - } - - decoded.iterations = IterationsDefaultOmitted - - var params []encoding.Parameter - - if ip >= 0 { - if params, err = encoding.DecodeParameterStr(parts[ip]); err != nil { - return nil, err - } - } - - for _, param := range params { - switch param.Key { - case "rounds": - var rounds uint64 - - if rounds, err = strconv.ParseUint(param.Value, 10, 32); err != nil { - return nil, fmt.Errorf("%w: option '%s' has invalid value '%s': %v", algorithm.ErrEncodedHashInvalidOptionValue, param.Key, param.Value, err) - } - - decoded.iterations = int(rounds) - default: - return nil, fmt.Errorf("%w: option '%s' with value '%s' is unknown", algorithm.ErrEncodedHashInvalidOptionKey, param.Key, param.Value) - } - } - - decoded.salt, decoded.key = []byte(parts[is]), []byte(parts[ik]) - - return decoded, nil -} diff --git a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/digest.go b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/digest.go deleted file mode 100644 index 46a760220b..0000000000 --- a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/digest.go +++ /dev/null @@ -1,83 +0,0 @@ -package shacrypt - -import ( - "crypto/subtle" - "fmt" - "strings" - - xcrypt "github.com/go-crypt/x/crypt" - - "github.com/go-crypt/crypt/algorithm" -) - -// Digest is a digest which handles SHA-crypt hashes like SHA256 or SHA512. -type Digest struct { - variant Variant - - iterations int - salt, key []byte -} - -// Match returns true if the string password matches the current shacrypt.Digest. -func (d *Digest) Match(password string) (match bool) { - return d.MatchBytes([]byte(password)) -} - -// MatchBytes returns true if the []byte passwordBytes matches the current shacrypt.Digest. -func (d *Digest) MatchBytes(passwordBytes []byte) (match bool) { - match, _ = d.MatchBytesAdvanced(passwordBytes) - - return match -} - -// MatchAdvanced is the same as Match except if there is an error it returns that as well. -func (d *Digest) MatchAdvanced(password string) (match bool, err error) { - if match, err = d.MatchBytesAdvanced([]byte(password)); err != nil { - return match, fmt.Errorf(algorithm.ErrFmtDigestMatch, AlgName, err) - } - - return match, nil -} - -// MatchBytesAdvanced is the same as MatchBytes except if there is an error it returns that as well. -func (d *Digest) MatchBytesAdvanced(passwordBytes []byte) (match bool, err error) { - if len(d.key) == 0 { - return false, fmt.Errorf("%w: key has 0 bytes", algorithm.ErrPasswordInvalid) - } - - return subtle.ConstantTimeCompare(d.key, xcrypt.KeySHACrypt(d.variant.HashFunc(), passwordBytes, d.salt, d.iterations)) == 1, nil -} - -// Encode this Digest as a string for storage. -func (d *Digest) Encode() (hash string) { - switch d.iterations { - case IterationsDefaultOmitted: - return strings.ReplaceAll(fmt.Sprintf(EncodingFmtRoundsOmitted, - d.variant.Prefix(), - d.salt, d.key, - ), "\n", "") - default: - return strings.ReplaceAll(fmt.Sprintf(EncodingFmt, - d.variant.Prefix(), d.iterations, - d.salt, d.key, - ), "\n", "") - } -} - -// String returns the storable format of the shacrypt.Digest hash utilizing fmt.Sprintf and shacrypt.EncodingFmt. -func (d *Digest) String() string { - return d.Encode() -} - -func (d *Digest) defaults() { - switch d.variant { - case VariantSHA256, VariantSHA512: - break - default: - d.variant = variantDefault - } - - if d.iterations == 0 { - d.iterations = d.variant.DefaultIterations() - } -} diff --git a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/doc.go b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/doc.go deleted file mode 100644 index 4ac7c8e691..0000000000 --- a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package shacrypt provides helpful abstractions for an implementation of SHA-crypt and implements -// github.com/go-crypt/crypt interfaces. -// -// See https://www.akkadia.org/drepper/SHA-crypt.html for specification details. -// -// This implementation is loaded by crypt.NewDefaultDecoder and crypt.NewDecoderAll. -package shacrypt diff --git a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/hasher.go b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/hasher.go deleted file mode 100644 index 6b8f9d11de..0000000000 --- a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/hasher.go +++ /dev/null @@ -1,156 +0,0 @@ -package shacrypt - -import ( - "fmt" - - xcrypt "github.com/go-crypt/x/crypt" - - "github.com/go-crypt/crypt/algorithm" - "github.com/go-crypt/crypt/internal/random" -) - -// New returns a *Hasher without any settings configured. This d to a SHA512 hash.Hash -// with 1000000 iterations. These settings can be overridden with the methods with the With prefix. -func New(opts ...Opt) (hasher *Hasher, err error) { - hasher = &Hasher{} - - if err = hasher.WithOptions(opts...); err != nil { - return nil, err - } - - if err = hasher.Validate(); err != nil { - return nil, err - } - - return hasher, nil -} - -// Hasher is a algorithm.Hash for SHA-crypt which can be initialized via shacrypt.New using a functional options pattern. -type Hasher struct { - variant Variant - - iterations, bytesSalt int - - d bool -} - -// NewSHA256 returns a *Hasher with the SHA256 hash.Hash which d to 1000000 iterations. These -// settings can be overridden with the methods with the With prefix. -func NewSHA256() (hasher *Hasher, err error) { - return New( - WithVariant(VariantSHA256), - WithIterations(VariantSHA256.DefaultIterations()), - ) -} - -// NewSHA512 returns a *Hasher with the SHA512 hash.Hash which d to 1000000 iterations. These -// settings can be overridden with the methods with the With prefix. -func NewSHA512() (hasher *Hasher, err error) { - return New( - WithVariant(VariantSHA512), - WithIterations(VariantSHA512.DefaultIterations()), - ) -} - -// WithOptions defines the options for this scrypt.Hasher. -func (h *Hasher) WithOptions(opts ...Opt) (err error) { - for _, opt := range opts { - if err = opt(h); err != nil { - return err - } - } - - return nil -} - -// Hash performs the hashing operation and returns either a shacrypt.Digest as a algorithm.Digest or an error. -func (h *Hasher) Hash(password string) (digest algorithm.Digest, err error) { - h.defaults() - - if digest, err = h.hash(password); err != nil { - return nil, fmt.Errorf(algorithm.ErrFmtHasherHash, AlgName, err) - } - - return digest, nil -} - -func (h *Hasher) hash(password string) (digest algorithm.Digest, err error) { - var salt []byte - - if salt, err = random.CharSetBytes(h.bytesSalt, SaltCharSet); err != nil { - return nil, fmt.Errorf("%w: %v", algorithm.ErrSaltReadRandomBytes, err) - } - - return h.hashWithSalt(password, salt) -} - -// HashWithSalt overloads the Hash method allowing the user to provide a salt. It's recommended instead to configure the -// salt size and let this be a random value generated using crypto/rand. -func (h *Hasher) HashWithSalt(password string, salt []byte) (digest algorithm.Digest, err error) { - h.defaults() - - if digest, err = h.hashWithSalt(password, salt); err != nil { - return nil, fmt.Errorf(algorithm.ErrFmtHasherHash, AlgName, err) - } - - return digest, nil -} - -func (h *Hasher) hashWithSalt(password string, salt []byte) (digest algorithm.Digest, err error) { - if s := len(salt); s > SaltLengthMax || s < SaltLengthMin { - return nil, fmt.Errorf("%w: salt bytes must have a length of between %d and %d but has a length of %d", algorithm.ErrSaltInvalid, SaltLengthMin, SaltLengthMax, len(salt)) - } - - d := &Digest{ - variant: h.variant, - iterations: h.iterations, - salt: salt, - } - - d.defaults() - - d.key = xcrypt.KeySHACrypt(d.variant.HashFunc(), []byte(password), d.salt, d.iterations) - - return d, nil -} - -// MustHash overloads the Hash method and panics if the error is not nil. It's recommended if you use this option to -// utilize the Validate method first or handle the panic appropriately. -func (h *Hasher) MustHash(password string) (digest algorithm.Digest) { - var err error - - if digest, err = h.Hash(password); err != nil { - panic(err) - } - - return digest -} - -// Validate checks the settings/parameters for this shacrypt.Hasher and returns an error. -func (h *Hasher) Validate() (err error) { - h.defaults() - - if err = h.validate(); err != nil { - return fmt.Errorf(algorithm.ErrFmtHasherValidation, AlgName, err) - } - - return nil -} - -func (h *Hasher) validate() (err error) { - h.defaults() - - return nil -} - -func (h *Hasher) defaults() { - if h.d { - return - } - - h.d = true - - if h.bytesSalt < SaltLengthMin { - h.bytesSalt = algorithm.SaltLengthDefault - } -} diff --git a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/opts.go b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/opts.go deleted file mode 100644 index 3499882be5..0000000000 --- a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/opts.go +++ /dev/null @@ -1,98 +0,0 @@ -package shacrypt - -import ( - "fmt" - - "github.com/go-crypt/crypt/algorithm" -) - -// Opt describes the functional option pattern for the shacrypt.Hasher. -type Opt func(h *Hasher) (err error) - -// WithVariant configures the shacrypt.Variant of the resulting shacrypt.Digest. -// Default is shacrypt.VariantSHA512. -func WithVariant(variant Variant) Opt { - return func(h *Hasher) (err error) { - switch variant { - case VariantNone: - return nil - case VariantSHA256, VariantSHA512: - h.variant = variant - - return nil - default: - return fmt.Errorf(algorithm.ErrFmtHasherValidation, AlgName, fmt.Errorf("%w: variant '%d' is invalid", algorithm.ErrParameterInvalid, variant)) - } - } -} - -// WithVariantName uses the variant name or identifier to configure the shacrypt.Variant of the resulting shacrypt.Digest. -// Default is shacrypt.VariantSHA512. -func WithVariantName(identifier string) Opt { - return func(h *Hasher) (err error) { - if identifier == "" { - return nil - } - - variant := NewVariant(identifier) - - if variant == VariantNone { - return fmt.Errorf(algorithm.ErrFmtHasherValidation, AlgName, fmt.Errorf("%w: variant identifier '%s' is invalid", algorithm.ErrParameterInvalid, identifier)) - } - - h.variant = variant - - return nil - } -} - -// WithSHA256 adjusts this Hasher to utilize the SHA256 hash.Hash. -func WithSHA256() Opt { - return func(h *Hasher) (err error) { - h.variant = VariantSHA256 - - return nil - } -} - -// WithSHA512 adjusts this Hasher to utilize the SHA512 hash.Hash. -func WithSHA512() Opt { - return func(h *Hasher) (err error) { - h.variant = VariantSHA512 - - return nil - } -} - -// WithIterations sets the iterations parameter of the resulting shacrypt.Digest. -// Minimum 1000, Maximum 999999999. Default is 1000000. -func WithIterations(iterations int) Opt { - return func(h *Hasher) (err error) { - if iterations < IterationsMin || iterations > IterationsMax { - return fmt.Errorf(algorithm.ErrFmtHasherValidation, AlgName, fmt.Errorf(algorithm.ErrFmtInvalidIntParameter, algorithm.ErrParameterInvalid, "iterations", IterationsMin, "", IterationsMax, iterations)) - } - - h.iterations = iterations - - return nil - } -} - -// WithRounds is an alias for shacrypt.WithIterations. -func WithRounds(rounds int) Opt { - return WithIterations(rounds) -} - -// WithSaltLength adjusts the salt size (in bytes) of the resulting shacrypt.Digest. -// Minimum 1, Maximum 16. Default is 16. -func WithSaltLength(bytes int) Opt { - return func(h *Hasher) (err error) { - if bytes < SaltLengthMin || bytes > SaltLengthMax { - return fmt.Errorf(algorithm.ErrFmtHasherValidation, AlgName, fmt.Errorf(algorithm.ErrFmtInvalidIntParameter, algorithm.ErrParameterInvalid, "salt length", SaltLengthMin, "", SaltLengthMax, bytes)) - } - - h.bytesSalt = bytes - - return nil - } -} diff --git a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/variant.go b/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/variant.go deleted file mode 100644 index 681ddc4ecd..0000000000 --- a/vendor/github.com/go-crypt/crypt/algorithm/shacrypt/variant.go +++ /dev/null @@ -1,92 +0,0 @@ -package shacrypt - -import ( - "crypto/sha256" - "crypto/sha512" - - "github.com/go-crypt/crypt/algorithm" -) - -// NewVariant converts an identifier string to a shacrypt.Variant. -func NewVariant(identifier string) Variant { - switch identifier { - case AlgIdentifierSHA256, algorithm.DigestSHA256: - return VariantSHA256 - case AlgIdentifierSHA512, algorithm.DigestSHA512: - return VariantSHA512 - default: - return VariantSHA512 - } -} - -// Variant is a variant of the shacrypt.Digest. -type Variant int - -const ( - // VariantNone is a variant of the shacrypt.Digest which is unknown. - VariantNone Variant = iota - - // VariantSHA256 is a variant of the shacrypt.Digest which uses SHA-256. - VariantSHA256 - - // VariantSHA512 is a variant of the shacrypt.Digest which uses SHA-512. - VariantSHA512 -) - -// String implements the fmt.Stringer returning a string representation of the shacrypt.Variant. -func (v Variant) String() (identifier string) { - switch v { - case VariantSHA256: - return algorithm.DigestSHA256 - case VariantSHA512: - return algorithm.DigestSHA512 - default: - return - } -} - -// Prefix returns the shacrypt.Variant prefix identifier. -func (v Variant) Prefix() (prefix string) { - switch v { - case VariantSHA256: - return AlgIdentifierSHA256 - case VariantSHA512: - return AlgIdentifierSHA512 - default: - return AlgIdentifierSHA512 - } -} - -// Name returns the Variant name. -func (v Variant) Name() (s string) { - switch v { - case VariantSHA256: - return algorithm.DigestSHA256 - case VariantSHA512: - return algorithm.DigestSHA512 - default: - return algorithm.DigestSHA512 - } -} - -// HashFunc returns the internal HMAC HashFunc. -func (v Variant) HashFunc() algorithm.HashFunc { - switch v { - case VariantSHA256: - return sha256.New - case VariantSHA512: - return sha512.New - default: - return sha512.New - } -} - -// DefaultIterations returns the default iterations for the particular variant. -func (v Variant) DefaultIterations() int { - switch v { - case VariantSHA512: - return IterationsDefaultSHA512 - default: - return IterationsDefaultSHA256 - } -} diff --git a/vendor/github.com/go-crypt/crypt/algorithm/types.go b/vendor/github.com/go-crypt/crypt/algorithm/types.go deleted file mode 100644 index 99fcb583ab..0000000000 --- a/vendor/github.com/go-crypt/crypt/algorithm/types.go +++ /dev/null @@ -1,62 +0,0 @@ -package algorithm - -import ( - "fmt" - "hash" -) - -// Hash is an interface which implements password hashing. -type Hash interface { - // Validate checks the hasher configuration to ensure it's valid. This should be used when the Hash is going to be - // reused and you should use it in conjunction with MustHash. - Validate() (err error) - - // Hash performs the hashing operation on a password and resets any relevant parameters such as a manually set salt. - // It then returns a Digest and error. - Hash(password string) (hashed Digest, err error) - - // HashWithSalt is an overload of Digest that also accepts a salt. - HashWithSalt(password string, salt []byte) (hashed Digest, err error) - - // MustHash overloads the Hash method and panics if the error is not nil. It's recommended if you use this method to - // utilize the Validate method first or handle the panic appropriately. - MustHash(password string) (hashed Digest) -} - -// Matcher is an interface used to match passwords. -type Matcher interface { - Match(password string) (match bool) - MatchBytes(passwordBytes []byte) (match bool) - MatchAdvanced(password string) (match bool, err error) - MatchBytesAdvanced(passwordBytes []byte) (match bool, err error) -} - -// Digest represents a hashed password. It's implemented by all hashed password results so that when we pass a -// stored hash into its relevant type we can verify the password against the hash. -type Digest interface { - fmt.Stringer - - Matcher - - Encode() (hash string) -} - -// DecodeFunc describes a function to decode an encoded digest into a algorithm.Digest. -type DecodeFunc func(encodedDigest string) (digest Digest, err error) - -// DecoderRegister describes an implementation that allows registering DecodeFunc's. -type DecoderRegister interface { - RegisterDecodeFunc(prefix string, decoder DecodeFunc) (err error) - RegisterDecodePrefix(prefix, identifier string) (err error) - - Decoder -} - -// Decoder is a representation of a implementation that performs generic decoding. Currently this is just intended for -// use by implementers. -type Decoder interface { - Decode(encodedDigest string) (digest Digest, err error) -} - -// HashFunc is a function which returns a hash.Hash. -type HashFunc func() hash.Hash diff --git a/vendor/github.com/go-crypt/crypt/internal/encoding/base64adapted.go b/vendor/github.com/go-crypt/crypt/internal/encoding/base64adapted.go deleted file mode 100644 index 55b84c1154..0000000000 --- a/vendor/github.com/go-crypt/crypt/internal/encoding/base64adapted.go +++ /dev/null @@ -1,14 +0,0 @@ -package encoding - -import ( - "encoding/base64" -) - -const ( - encodeBase64Adapted = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./" -) - -var ( - // Base64RawAdaptedEncoding is the adapted encoding for crypt purposes without padding. - Base64RawAdaptedEncoding = base64.NewEncoding(encodeBase64Adapted).WithPadding(base64.NoPadding) -) diff --git a/vendor/github.com/go-crypt/crypt/internal/encoding/const.go b/vendor/github.com/go-crypt/crypt/internal/encoding/const.go deleted file mode 100644 index b0a3d56d3e..0000000000 --- a/vendor/github.com/go-crypt/crypt/internal/encoding/const.go +++ /dev/null @@ -1,9 +0,0 @@ -package encoding - -const ( - // Delimiter rune for all encodings. - Delimiter = rune('$') - - // DelimiterStr is the string variation of Delimiter. - DelimiterStr = string(Delimiter) -) diff --git a/vendor/github.com/go-crypt/crypt/internal/encoding/digest.go b/vendor/github.com/go-crypt/crypt/internal/encoding/digest.go deleted file mode 100644 index f854968abe..0000000000 --- a/vendor/github.com/go-crypt/crypt/internal/encoding/digest.go +++ /dev/null @@ -1,10 +0,0 @@ -package encoding - -import ( - "strings" -) - -// Split an encoded digest by the encoding.Delimiter. -func Split(encodedDigest string, n int) (parts []string) { - return strings.SplitN(encodedDigest, DelimiterStr, n) -} diff --git a/vendor/github.com/go-crypt/crypt/internal/encoding/doc.go b/vendor/github.com/go-crypt/crypt/internal/encoding/doc.go deleted file mode 100644 index 30a4f0be5a..0000000000 --- a/vendor/github.com/go-crypt/crypt/internal/encoding/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package encoding is an internal encoding helper package. -package encoding diff --git a/vendor/github.com/go-crypt/crypt/internal/encoding/parameters.go b/vendor/github.com/go-crypt/crypt/internal/encoding/parameters.go deleted file mode 100644 index b7935be9af..0000000000 --- a/vendor/github.com/go-crypt/crypt/internal/encoding/parameters.go +++ /dev/null @@ -1,54 +0,0 @@ -package encoding - -import ( - "fmt" - "strconv" - "strings" -) - -// Parameter is a key value pair. -type Parameter struct { - Key string - Value string -} - -// Int converts the Value to an int using strconv.Atoi. -func (p Parameter) Int() (int, error) { - return strconv.Atoi(p.Value) -} - -const ( - // ParameterDefaultItemSeparator is the default item separator. - ParameterDefaultItemSeparator = "," - - // ParameterDefaultKeyValueSeparator is the default key value separator. - ParameterDefaultKeyValueSeparator = "=" -) - -// DecodeParameterStr is an alias for DecodeParameterStrAdvanced using item separator and key value separator -// of ',' and '=' respectively. -func DecodeParameterStr(input string) (opts []Parameter, err error) { - return DecodeParameterStrAdvanced(input, ParameterDefaultItemSeparator, ParameterDefaultKeyValueSeparator) -} - -// DecodeParameterStrAdvanced decodes parameter strings into a []Parameter where sepItem separates each parameter, and sepKV separates the key and value. -func DecodeParameterStrAdvanced(input string, sepItem, sepKV string) (opts []Parameter, err error) { - if input == "" { - return nil, fmt.Errorf("empty strings can't be decoded to parameters") - } - - o := strings.Split(input, sepItem) - - opts = make([]Parameter, len(o)) - - for i, joined := range o { - kv := strings.SplitN(joined, sepKV, 2) - if len(kv) != 2 { - return nil, fmt.Errorf("parameter pair '%s' is not properly encoded: does not contain kv separator '%s'", joined, sepKV) - } - - opts[i] = Parameter{Key: kv[0], Value: kv[1]} - } - - return opts, nil -} diff --git a/vendor/github.com/go-crypt/crypt/internal/random/bytes.go b/vendor/github.com/go-crypt/crypt/internal/random/bytes.go deleted file mode 100644 index 20bc235354..0000000000 --- a/vendor/github.com/go-crypt/crypt/internal/random/bytes.go +++ /dev/null @@ -1,32 +0,0 @@ -package random - -import ( - "crypto/rand" - "io" -) - -// Bytes returns random arbitrary bytes with a length of n. -func Bytes(n int) (bytes []byte, err error) { - bytes = make([]byte, n) - - if _, err = io.ReadFull(rand.Reader, bytes); err != nil { - return nil, err - } - - return bytes, nil -} - -// CharSetBytes returns random bytes with a length of n from the characters in the charset. -func CharSetBytes(n int, charset string) (bytes []byte, err error) { - bytes = make([]byte, n) - - if _, err = rand.Read(bytes); err != nil { - return nil, err - } - - for i, b := range bytes { - bytes[i] = charset[b%byte(len(charset))] - } - - return bytes, nil -} diff --git a/vendor/github.com/go-crypt/crypt/internal/random/doc.go b/vendor/github.com/go-crypt/crypt/internal/random/doc.go deleted file mode 100644 index 2bf9f24d31..0000000000 --- a/vendor/github.com/go-crypt/crypt/internal/random/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package random is an internal helper package. -package random diff --git a/vendor/github.com/go-crypt/x/LICENSE b/vendor/github.com/go-crypt/x/LICENSE deleted file mode 100644 index 6a66aea5ea..0000000000 --- a/vendor/github.com/go-crypt/x/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2009 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/go-crypt/x/base64/base64.go b/vendor/github.com/go-crypt/x/base64/base64.go deleted file mode 100644 index 2cb382a793..0000000000 --- a/vendor/github.com/go-crypt/x/base64/base64.go +++ /dev/null @@ -1,51 +0,0 @@ -package base64 - -import ( - "encoding/base64" -) - -var AdaptedEncoding = base64.NewEncoding(encodeAdapted) - -// BcryptEncoding is the Bcrypt Base64 Alternative encoding. -var BcryptEncoding = base64.NewEncoding(bcryptB64Alphabet) - -// EncodeCrypt implements the linux crypt lib's B64 encoding. -func EncodeCrypt(src []byte) (dst []byte) { - if len(src) == 0 { - return nil - } - - dst = make([]byte, (len(src)*8+5)/6) - - idst, isrc := 0, 0 - - for isrc < len(src)/3*3 { - v := uint(src[isrc+2])<<16 | uint(src[isrc+1])<<8 | uint(src[isrc]) - dst[idst+0] = cryptB64Alphabet[v&0x3f] - dst[idst+1] = cryptB64Alphabet[v>>6&0x3f] - dst[idst+2] = cryptB64Alphabet[v>>12&0x3f] - dst[idst+3] = cryptB64Alphabet[v>>18] - idst += 4 - isrc += 3 - } - - remainder := len(src) - isrc - - if remainder == 0 { - return dst - } - - v := uint(src[isrc+0]) - if remainder == 2 { - v |= uint(src[isrc+1]) << 8 - } - - dst[idst+0] = cryptB64Alphabet[v&0x3f] - dst[idst+1] = cryptB64Alphabet[v>>6&0x3f] - - if remainder == 2 { - dst[idst+2] = cryptB64Alphabet[v>>12] - } - - return dst -} diff --git a/vendor/github.com/go-crypt/x/base64/const.go b/vendor/github.com/go-crypt/x/base64/const.go deleted file mode 100644 index 2e99236872..0000000000 --- a/vendor/github.com/go-crypt/x/base64/const.go +++ /dev/null @@ -1,7 +0,0 @@ -package base64 - -const ( - cryptB64Alphabet = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - bcryptB64Alphabet = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" - encodeAdapted = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789./" -) diff --git a/vendor/github.com/go-crypt/x/crypt/const.go b/vendor/github.com/go-crypt/x/crypt/const.go deleted file mode 100644 index 09153bfb7f..0000000000 --- a/vendor/github.com/go-crypt/x/crypt/const.go +++ /dev/null @@ -1,223 +0,0 @@ -package crypt - -var permuteTableMD5Crypt = [16]byte{ - 12, 6, 0, - 13, 7, 1, - 14, 8, 2, - 15, 9, 3, - 5, 10, 4, - 11, -} - -var permuteTableSHA1Crypt = [21]byte{ - 2, 1, 0, - 5, 4, 3, - 8, 7, 6, - 11, 10, 9, - 14, 13, 12, - 17, 16, 15, - 0, 19, 18, -} - -var permuteTableSHACryptSHA256 = [32]byte{ - 20, 10, 0, - 11, 1, 21, - 2, 22, 12, - 23, 13, 3, - 14, 4, 24, - 5, 25, 15, - 26, 16, 6, - 17, 7, 27, - 8, 28, 18, - 29, 19, 9, - 30, 31, -} - -var permuteTableSHACryptSHA512 = [64]byte{ - 42, 21, 0, - 1, 43, 22, - 23, 2, 44, - 45, 24, 3, - 4, 46, 25, - 26, 5, 47, - 48, 27, 6, - 7, 49, 28, - 29, 8, 50, - 51, 30, 9, - 10, 52, 31, - 32, 11, 53, - 54, 33, 12, - 13, 55, 34, - 35, 14, 56, - 57, 36, 15, - 16, 58, 37, - 38, 17, 59, - 60, 39, 18, - 19, 61, 40, - 41, 20, 62, - 63, -} - -// The following is the 1517 bytes of Hamlet III.ii which is public domain. This is used by Sun's MD5 Crypt function. -var magicTableMD5CryptSunHamlet = [1517]byte{ - 84, 111, 32, 98, 101, 44, 32, 111, 114, 32, 110, - 111, 116, 32, 116, 111, 32, 98, 101, 44, 45, - 45, 116, 104, 97, 116, 32, 105, 115, 32, 116, - 104, 101, 32, 113, 117, 101, 115, 116, 105, 111, - 110, 58, 45, 45, 10, 87, 104, 101, 116, 104, - 101, 114, 32, 39, 116, 105, 115, 32, 110, 111, - 98, 108, 101, 114, 32, 105, 110, 32, 116, 104, - 101, 32, 109, 105, 110, 100, 32, 116, 111, 32, - 115, 117, 102, 102, 101, 114, 10, 84, 104, 101, - 32, 115, 108, 105, 110, 103, 115, 32, 97, 110, - 100, 32, 97, 114, 114, 111, 119, 115, 32, 111, - 102, 32, 111, 117, 116, 114, 97, 103, 101, 111, - 117, 115, 32, 102, 111, 114, 116, 117, 110, 101, - 10, 79, 114, 32, 116, 111, 32, 116, 97, 107, - 101, 32, 97, 114, 109, 115, 32, 97, 103, 97, - 105, 110, 115, 116, 32, 97, 32, 115, 101, 97, - 32, 111, 102, 32, 116, 114, 111, 117, 98, 108, - 101, 115, 44, 10, 65, 110, 100, 32, 98, 121, - 32, 111, 112, 112, 111, 115, 105, 110, 103, 32, - 101, 110, 100, 32, 116, 104, 101, 109, 63, 45, - 45, 84, 111, 32, 100, 105, 101, 44, 45, 45, - 116, 111, 32, 115, 108, 101, 101, 112, 44, 45, - 45, 10, 78, 111, 32, 109, 111, 114, 101, 59, - 32, 97, 110, 100, 32, 98, 121, 32, 97, 32, - 115, 108, 101, 101, 112, 32, 116, 111, 32, 115, - 97, 121, 32, 119, 101, 32, 101, 110, 100, 10, - 84, 104, 101, 32, 104, 101, 97, 114, 116, 97, - 99, 104, 101, 44, 32, 97, 110, 100, 32, 116, - 104, 101, 32, 116, 104, 111, 117, 115, 97, 110, - 100, 32, 110, 97, 116, 117, 114, 97, 108, 32, - 115, 104, 111, 99, 107, 115, 10, 84, 104, 97, - 116, 32, 102, 108, 101, 115, 104, 32, 105, 115, - 32, 104, 101, 105, 114, 32, 116, 111, 44, 45, - 45, 39, 116, 105, 115, 32, 97, 32, 99, 111, - 110, 115, 117, 109, 109, 97, 116, 105, 111, 110, - 10, 68, 101, 118, 111, 117, 116, 108, 121, 32, - 116, 111, 32, 98, 101, 32, 119, 105, 115, 104, - 39, 100, 46, 32, 84, 111, 32, 100, 105, 101, - 44, 45, 45, 116, 111, 32, 115, 108, 101, 101, - 112, 59, 45, 45, 10, 84, 111, 32, 115, 108, - 101, 101, 112, 33, 32, 112, 101, 114, 99, 104, - 97, 110, 99, 101, 32, 116, 111, 32, 100, 114, - 101, 97, 109, 58, 45, 45, 97, 121, 44, 32, - 116, 104, 101, 114, 101, 39, 115, 32, 116, 104, - 101, 32, 114, 117, 98, 59, 10, 70, 111, 114, - 32, 105, 110, 32, 116, 104, 97, 116, 32, 115, - 108, 101, 101, 112, 32, 111, 102, 32, 100, 101, - 97, 116, 104, 32, 119, 104, 97, 116, 32, 100, - 114, 101, 97, 109, 115, 32, 109, 97, 121, 32, - 99, 111, 109, 101, 44, 10, 87, 104, 101, 110, - 32, 119, 101, 32, 104, 97, 118, 101, 32, 115, - 104, 117, 102, 102, 108, 101, 100, 32, 111, 102, - 102, 32, 116, 104, 105, 115, 32, 109, 111, 114, - 116, 97, 108, 32, 99, 111, 105, 108, 44, 10, - 77, 117, 115, 116, 32, 103, 105, 118, 101, 32, - 117, 115, 32, 112, 97, 117, 115, 101, 58, 32, - 116, 104, 101, 114, 101, 39, 115, 32, 116, 104, - 101, 32, 114, 101, 115, 112, 101, 99, 116, 10, - 84, 104, 97, 116, 32, 109, 97, 107, 101, 115, - 32, 99, 97, 108, 97, 109, 105, 116, 121, 32, - 111, 102, 32, 115, 111, 32, 108, 111, 110, 103, - 32, 108, 105, 102, 101, 59, 10, 70, 111, 114, - 32, 119, 104, 111, 32, 119, 111, 117, 108, 100, - 32, 98, 101, 97, 114, 32, 116, 104, 101, 32, - 119, 104, 105, 112, 115, 32, 97, 110, 100, 32, - 115, 99, 111, 114, 110, 115, 32, 111, 102, 32, - 116, 105, 109, 101, 44, 10, 84, 104, 101, 32, - 111, 112, 112, 114, 101, 115, 115, 111, 114, 39, - 115, 32, 119, 114, 111, 110, 103, 44, 32, 116, - 104, 101, 32, 112, 114, 111, 117, 100, 32, 109, - 97, 110, 39, 115, 32, 99, 111, 110, 116, 117, - 109, 101, 108, 121, 44, 10, 84, 104, 101, 32, - 112, 97, 110, 103, 115, 32, 111, 102, 32, 100, - 101, 115, 112, 105, 115, 39, 100, 32, 108, 111, - 118, 101, 44, 32, 116, 104, 101, 32, 108, 97, - 119, 39, 115, 32, 100, 101, 108, 97, 121, 44, - 10, 84, 104, 101, 32, 105, 110, 115, 111, 108, - 101, 110, 99, 101, 32, 111, 102, 32, 111, 102, - 102, 105, 99, 101, 44, 32, 97, 110, 100, 32, - 116, 104, 101, 32, 115, 112, 117, 114, 110, 115, - 10, 84, 104, 97, 116, 32, 112, 97, 116, 105, - 101, 110, 116, 32, 109, 101, 114, 105, 116, 32, - 111, 102, 32, 116, 104, 101, 32, 117, 110, 119, - 111, 114, 116, 104, 121, 32, 116, 97, 107, 101, - 115, 44, 10, 87, 104, 101, 110, 32, 104, 101, - 32, 104, 105, 109, 115, 101, 108, 102, 32, 109, - 105, 103, 104, 116, 32, 104, 105, 115, 32, 113, - 117, 105, 101, 116, 117, 115, 32, 109, 97, 107, - 101, 10, 87, 105, 116, 104, 32, 97, 32, 98, - 97, 114, 101, 32, 98, 111, 100, 107, 105, 110, - 63, 32, 119, 104, 111, 32, 119, 111, 117, 108, - 100, 32, 116, 104, 101, 115, 101, 32, 102, 97, - 114, 100, 101, 108, 115, 32, 98, 101, 97, 114, - 44, 10, 84, 111, 32, 103, 114, 117, 110, 116, - 32, 97, 110, 100, 32, 115, 119, 101, 97, 116, - 32, 117, 110, 100, 101, 114, 32, 97, 32, 119, - 101, 97, 114, 121, 32, 108, 105, 102, 101, 44, - 10, 66, 117, 116, 32, 116, 104, 97, 116, 32, - 116, 104, 101, 32, 100, 114, 101, 97, 100, 32, - 111, 102, 32, 115, 111, 109, 101, 116, 104, 105, - 110, 103, 32, 97, 102, 116, 101, 114, 32, 100, - 101, 97, 116, 104, 44, 45, 45, 10, 84, 104, - 101, 32, 117, 110, 100, 105, 115, 99, 111, 118, - 101, 114, 39, 100, 32, 99, 111, 117, 110, 116, - 114, 121, 44, 32, 102, 114, 111, 109, 32, 119, - 104, 111, 115, 101, 32, 98, 111, 117, 114, 110, - 10, 78, 111, 32, 116, 114, 97, 118, 101, 108, - 108, 101, 114, 32, 114, 101, 116, 117, 114, 110, - 115, 44, 45, 45, 112, 117, 122, 122, 108, 101, - 115, 32, 116, 104, 101, 32, 119, 105, 108, 108, - 44, 10, 65, 110, 100, 32, 109, 97, 107, 101, - 115, 32, 117, 115, 32, 114, 97, 116, 104, 101, - 114, 32, 98, 101, 97, 114, 32, 116, 104, 111, - 115, 101, 32, 105, 108, 108, 115, 32, 119, 101, - 32, 104, 97, 118, 101, 10, 84, 104, 97, 110, - 32, 102, 108, 121, 32, 116, 111, 32, 111, 116, - 104, 101, 114, 115, 32, 116, 104, 97, 116, 32, - 119, 101, 32, 107, 110, 111, 119, 32, 110, 111, - 116, 32, 111, 102, 63, 10, 84, 104, 117, 115, - 32, 99, 111, 110, 115, 99, 105, 101, 110, 99, - 101, 32, 100, 111, 101, 115, 32, 109, 97, 107, - 101, 32, 99, 111, 119, 97, 114, 100, 115, 32, - 111, 102, 32, 117, 115, 32, 97, 108, 108, 59, - 10, 65, 110, 100, 32, 116, 104, 117, 115, 32, - 116, 104, 101, 32, 110, 97, 116, 105, 118, 101, - 32, 104, 117, 101, 32, 111, 102, 32, 114, 101, - 115, 111, 108, 117, 116, 105, 111, 110, 10, 73, - 115, 32, 115, 105, 99, 107, 108, 105, 101, 100, - 32, 111, 39, 101, 114, 32, 119, 105, 116, 104, - 32, 116, 104, 101, 32, 112, 97, 108, 101, 32, - 99, 97, 115, 116, 32, 111, 102, 32, 116, 104, - 111, 117, 103, 104, 116, 59, 10, 65, 110, 100, - 32, 101, 110, 116, 101, 114, 112, 114, 105, 115, - 101, 115, 32, 111, 102, 32, 103, 114, 101, 97, - 116, 32, 112, 105, 116, 104, 32, 97, 110, 100, - 32, 109, 111, 109, 101, 110, 116, 44, 10, 87, - 105, 116, 104, 32, 116, 104, 105, 115, 32, 114, - 101, 103, 97, 114, 100, 44, 32, 116, 104, 101, - 105, 114, 32, 99, 117, 114, 114, 101, 110, 116, - 115, 32, 116, 117, 114, 110, 32, 97, 119, 114, - 121, 44, 10, 65, 110, 100, 32, 108, 111, 115, - 101, 32, 116, 104, 101, 32, 110, 97, 109, 101, - 32, 111, 102, 32, 97, 99, 116, 105, 111, 110, - 46, 45, 45, 83, 111, 102, 116, 32, 121, 111, - 117, 32, 110, 111, 119, 33, 10, 84, 104, 101, - 32, 102, 97, 105, 114, 32, 79, 112, 104, 101, - 108, 105, 97, 33, 45, 45, 78, 121, 109, 112, - 104, 44, 32, 105, 110, 32, 116, 104, 121, 32, - 111, 114, 105, 115, 111, 110, 115, 10, 66, 101, - 32, 97, 108, 108, 32, 109, 121, 32, 115, 105, - 110, 115, 32, 114, 101, 109, 101, 109, 98, 101, - 114, 39, 100, 46, 10, 0, -} - -var ( - prefixMD5Crypt = []byte("$1$") - prefixSHA1Crypt = []byte("$sha1$") - prefixSunMD5Crypt = []byte("$md5$") - prefixSunMD5CryptRounds = []byte("$md5,rounds=") - sepCrypt = []byte("$") -) diff --git a/vendor/github.com/go-crypt/x/crypt/crypt.go b/vendor/github.com/go-crypt/x/crypt/crypt.go deleted file mode 100644 index 3bd2cdb130..0000000000 --- a/vendor/github.com/go-crypt/x/crypt/crypt.go +++ /dev/null @@ -1,300 +0,0 @@ -package crypt - -import ( - "crypto/hmac" - "crypto/md5" - "crypto/sha1" - "crypto/sha256" - "crypto/sha512" - "hash" - "strconv" -) - -// KeySHACrypt calculates the shacrypt SHA256/SHA512 key given an appropriate hash.Hash, password, salt, and number of rounds. -func KeySHACrypt(hashFunc func() hash.Hash, password, salt []byte, rounds int) []byte { - // Step 1. - digest := hashFunc() - - size := digest.Size() - - switch size { - case sha1.Size: - return KeySHA1Crypt(password, salt, uint32(rounds)) - case sha256.Size, sha512.Size: - break - default: - return nil - } - - length := len(password) - - // Step 2. - digest.Write(password) - - // Step 3. - digest.Write(salt) - - // Step 4. - digestB := hashFunc() - - // Step 5. - digestB.Write(password) - - // Step 6. - digestB.Write(salt) - - // Step 7. - digestB.Write(password) - - // Step 8. - sumB := digestB.Sum(nil) - digestB.Reset() - digestB = nil - - // Step 9 and 10: - digest.Write(repeat(sumB, length)) - - // Step 11. - for i := length; i > 0; i >>= 1 { - if even(i) { - digest.Write(password) - } else { - digest.Write(sumB) - } - } - - clean(sumB) - sumB = nil - - // Step 12. - sumA := digest.Sum(nil) - digest.Reset() - - // Step 13-14. - for i := 0; i < length; i++ { - digest.Write(password) - } - - // Step 15. - sumDP := digest.Sum(nil) - digest.Reset() - - // Step 16. - seqP := repeat(sumDP, length) - sumDP = nil - - // Step 17-18. - for i := 0; i < 16+int(sumA[0]); i++ { - digest.Write(salt) - } - - // Step 19. - sumDS := digest.Sum(nil) - digest.Reset() - - // Step 20. - seqS := repeat(sumDS, len(salt)) - - // Step 21. - for i := 0; i < rounds; i++ { - digest.Reset() - - // Step 21 Sub-Step B and C. - if i&1 != 0 { - // Step 21 Sub-Step B. - digest.Write(seqP) - } else { - // Step 21 Sub-Step C. - digest.Write(sumA) - } - - // Step 21 Sub-Step D. - if i%3 != 0 { - digest.Write(seqS) - } - - // Step 21 Sub-Step E. - if i%7 != 0 { - digest.Write(seqP) - } - - // Step 21 Sub-Step F and G. - if i&1 != 0 { - // Step 21 Sub-Step F. - digest.Write(sumA) - } else { - // Step 21 Sub-Step G. - digest.Write(seqP) - } - - // Sub-Step H. - copy(sumA, digest.Sum(nil)) - } - - digest.Reset() - digest = nil - - seqP, seqS = nil, nil - - switch size { - case sha256.Size: - // Step 22 Sub Step E. - return permute(sumA, permuteTableSHACryptSHA256[:]) - case sha512.Size: - // Step 22 Sub Step E. - return permute(sumA, permuteTableSHACryptSHA512[:]) - } - - return nil -} - -// KeySHA1Crypt calculates the sha1crypt key given a password, salt, and number of rounds. -func KeySHA1Crypt(password, salt []byte, rounds uint32) []byte { - digest := hmac.New(sha1.New, password) - digest.Write(salt) - digest.Write(prefixSHA1Crypt) - digest.Write([]byte(strconv.FormatUint(uint64(rounds), 10))) - - sumA := digest.Sum(nil) - - if rounds == 0 { - return permute(sumA, permuteTableSHA1Crypt[:]) - } - - for rounds--; rounds > 0; rounds-- { - digest.Reset() - - digest.Write(sumA) - - copy(sumA, digest.Sum(nil)) - } - - return permute(sumA, permuteTableSHA1Crypt[:]) -} - -// KeyMD5Crypt calculates the md5crypt key given a password and salt. -func KeyMD5Crypt(password, salt []byte) []byte { - length := len(password) - - digest := md5.New() - - digest.Write(password) - digest.Write(salt) - digest.Write(password) - - sumB := digest.Sum(nil) - - digest.Reset() - - digest.Write(password) - digest.Write(prefixMD5Crypt) - digest.Write(salt) - digest.Write(repeat(sumB, length)) - - clean(sumB) - - for i := length; i > 0; i >>= 1 { - if even(i) { - digest.Write(password[0:1]) - } else { - digest.Write([]byte{0}) - } - } - - sumA := digest.Sum(nil) - - for i := 0; i < 1000; i++ { - digest.Reset() - - if even(i) { - digest.Write(sumA) - } else { - digest.Write(password) - } - - if i%3 != 0 { - digest.Write(salt) - } - - if i%7 != 0 { - digest.Write(password) - } - - if i&1 == 0 { - digest.Write(password) - } else { - digest.Write(sumA) - } - - copy(sumA, digest.Sum(nil)) - } - - return permute(sumA, permuteTableMD5Crypt[:]) -} - -// KeyMD5CryptSun calculates the md5crypt (Sun Version) key given a password, salt, and number rounds. -func KeyMD5CryptSun(password, salt []byte, rounds uint32) []byte { - digest := md5.New() - - digest.Write(password) - - if rounds == 0 { - digest.Write(prefixSunMD5Crypt) - digest.Write(salt) - digest.Write(sepCrypt) - } else { - digest.Write(prefixSunMD5CryptRounds) - digest.Write([]byte(strconv.FormatUint(uint64(rounds), 10))) - digest.Write(sepCrypt) - digest.Write(salt) - digest.Write(sepCrypt) - } - - sumA := digest.Sum(nil) - - iterations := uint32(rounds + 4096) - - bit := func(off uint32) uint32 { - off %= 128 - if (sumA[off/8] & (0x01 << (off % 8))) != 0 { - return 1 - } - - return 0 - } - - var ind7 [md5.Size]byte - - for i := uint32(0); i < iterations; i++ { - digest.Reset() - - digest.Write(sumA) - - for j := 0; j < md5.Size; j++ { - off := (j + 3) % 16 - ind4 := (sumA[j] >> (sumA[off] % 5)) & 0x0F - sh7 := (sumA[off] >> (sumA[j] % 8)) & 0x01 - ind7[j] = (sumA[ind4] >> sh7) & 0x7F - } - - var indA, indB uint32 - - for j := uint(0); j < 8; j++ { - indA |= bit(uint32(ind7[j])) << j - indB |= bit(uint32(ind7[j+8])) << j - } - - indA = (indA >> bit(i)) & 0x7F - indB = (indB >> bit(i+64)) & 0x7F - - if bit(indA)^bit(indB) == 1 { - digest.Write(magicTableMD5CryptSunHamlet[:]) - } - - digest.Write([]byte(strconv.FormatUint(uint64(i), 10))) - - copy(sumA, digest.Sum(nil)) - } - - return permute(sumA, permuteTableMD5Crypt[:]) -} diff --git a/vendor/github.com/go-crypt/x/crypt/util.go b/vendor/github.com/go-crypt/x/crypt/util.go deleted file mode 100644 index c5e6fa4477..0000000000 --- a/vendor/github.com/go-crypt/x/crypt/util.go +++ /dev/null @@ -1,54 +0,0 @@ -package crypt - -import ( - b64 "github.com/go-crypt/x/base64" -) - -func permute(sum, table []byte) []byte { - size := len(table) - - key := make([]byte, size) - - for i := 0; i < size; i++ { - key[i] = sum[table[i]] - } - - return b64.EncodeCrypt(key) -} - -func even(i int) bool { - return i%2 == 0 -} - -var ( - cleanBytes = make([]byte, 64) -) - -func clean(b []byte) { - l := len(b) - - for ; l > 64; l -= 64 { - copy(b[l-64:l], cleanBytes) - } - - if l > 0 { - copy(b[0:l], cleanBytes[0:l]) - } -} - -func repeat(input []byte, length int) []byte { - var ( - seq = make([]byte, length) - unit = len(input) - ) - - j := length / unit * unit - for i := 0; i < j; i += unit { - copy(seq[i:length], input) - } - if j < length { - copy(seq[j:length], input[0:length-j]) - } - - return seq -} diff --git a/vendor/github.com/testcontainers/testcontainers-go/network/network.go b/vendor/github.com/testcontainers/testcontainers-go/network/network.go new file mode 100644 index 0000000000..97e69bb6ba --- /dev/null +++ b/vendor/github.com/testcontainers/testcontainers-go/network/network.go @@ -0,0 +1,196 @@ +package network + +import ( + "context" + "errors" + "fmt" + "maps" + + "github.com/google/uuid" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/client" + + "github.com/testcontainers/testcontainers-go" +) + +// New creates a new network with a random UUID name, calling the already existing GenericNetwork APIs. +// Those existing APIs are deprecated and will be removed in the future, so this function will +// implement the new network APIs when they will be available. +// By default, the network is created with the following options: +// - Driver: bridge +// - Labels: the Testcontainers for Go generic labels, to be managed by Ryuk. Please see the GenericLabels() function +// And those options can be modified by the user, using the CreateModifier function field. +func New(ctx context.Context, opts ...NetworkCustomizer) (*testcontainers.DockerNetwork, error) { + nc := client.NetworkCreateOptions{ + Driver: "bridge", + Labels: testcontainers.GenericLabels(), + } + + for _, opt := range opts { + if err := opt.Customize(&nc); err != nil { + return nil, err + } + } + + //nolint:staticcheck + netReq := testcontainers.NetworkRequest{ + Driver: nc.Driver, + Internal: nc.Internal, + EnableIPv6: nc.EnableIPv6, + Name: uuid.NewString(), + Labels: nc.Labels, + Attachable: nc.Attachable, + IPAM: nc.IPAM, + } + + //nolint:staticcheck + n, err := testcontainers.GenericNetwork(ctx, testcontainers.GenericNetworkRequest{ + NetworkRequest: netReq, + }) + if err != nil { + return nil, err + } + + // Return a DockerNetwork struct instead of the Network interface, + // following the "accept interface, return struct" pattern. + return n.(*testcontainers.DockerNetwork), nil +} + +// NetworkCustomizer is an interface that can be used to configure the network create request. +type NetworkCustomizer interface { + Customize(req *client.NetworkCreateOptions) error +} + +// CustomizeNetworkOption is a type that can be used to configure the network create request. +type CustomizeNetworkOption func(req *client.NetworkCreateOptions) error + +// Customize implements the NetworkCustomizer interface, +// applying the option to the network create request. +func (opt CustomizeNetworkOption) Customize(req *client.NetworkCreateOptions) error { + return opt(req) +} + +// WithAttachable allows to set the network as attachable. +func WithAttachable() CustomizeNetworkOption { + return func(original *client.NetworkCreateOptions) error { + original.Attachable = true + + return nil + } +} + +// WithCheckDuplicate allows to check if a network with the same name already exists. +// +// Deprecated: CheckDuplicate is deprecated since API v1.44, but it defaults to true when sent by the client package to older daemons. +func WithCheckDuplicate() CustomizeNetworkOption { + return func(_ *client.NetworkCreateOptions) error { + return nil + } +} + +// WithDriver allows to override the default network driver, which is "bridge". +func WithDriver(driver string) CustomizeNetworkOption { + return func(original *client.NetworkCreateOptions) error { + original.Driver = driver + + return nil + } +} + +// WithEnableIPv6 allows to set the network as IPv6 enabled. +// Please use this option if and only if IPv6 is enabled on the Docker daemon. +func WithEnableIPv6() CustomizeNetworkOption { + return func(original *client.NetworkCreateOptions) error { + enableIPv6 := true + original.EnableIPv6 = &enableIPv6 + return nil + } +} + +// WithInternal allows to set the network as internal. +func WithInternal() CustomizeNetworkOption { + return func(original *client.NetworkCreateOptions) error { + original.Internal = true + + return nil + } +} + +// WithLabels allows to set the network labels, adding the new ones +// to the default Testcontainers for Go labels. +func WithLabels(labels map[string]string) CustomizeNetworkOption { + return func(original *client.NetworkCreateOptions) error { + maps.Copy(original.Labels, labels) + + return nil + } +} + +// WithIPAM allows to change the default IPAM configuration. +func WithIPAM(ipam *network.IPAM) CustomizeNetworkOption { + return func(original *client.NetworkCreateOptions) error { + original.IPAM = ipam + + return nil + } +} + +// WithNetwork reuses an already existing network, attaching the container to it. +// Finally it sets the network alias on that network to the given alias. +func WithNetwork(aliases []string, nw *testcontainers.DockerNetwork) testcontainers.CustomizeRequestOption { + return WithNetworkName(aliases, nw.Name) +} + +// WithNetworkName attachs a container to an already existing network, by its name. +// If the network is not "bridge", it sets the network alias on that network +// to the given alias, else, it returns an error. This is because network-scoped alias +// is supported only for containers in user defined networks. +func WithNetworkName(aliases []string, networkName string) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + if networkName == "bridge" { + return errors.New("network-scoped aliases are supported only for containers in user defined networks") + } + + // attaching to the network because it was created with success or it already existed. + req.Networks = append(req.Networks, networkName) + + if req.NetworkAliases == nil { + req.NetworkAliases = make(map[string][]string) + } + req.NetworkAliases[networkName] = aliases + + return nil + } +} + +// WithBridgeNetwork attachs a container to the "bridge" network. +// There is no need to set the network alias, as it is not supported for the "bridge" network. +func WithBridgeNetwork() testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + req.Networks = append(req.Networks, "bridge") + return nil + } +} + +// WithNewNetwork creates a new network with random name and customizers, and attaches the container to it. +// Finally it sets the network alias on that network to the given alias. +func WithNewNetwork(ctx context.Context, aliases []string, opts ...NetworkCustomizer) testcontainers.CustomizeRequestOption { + return func(req *testcontainers.GenericContainerRequest) error { + newNetwork, err := New(ctx, opts...) + if err != nil { + return fmt.Errorf("new network: %w", err) + } + + networkName := newNetwork.Name + + // attaching to the network because it was created with success or it already existed. + req.Networks = append(req.Networks, networkName) + + if req.NetworkAliases == nil { + req.NetworkAliases = make(map[string][]string) + } + req.NetworkAliases[networkName] = aliases + + return nil + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 4944bdf929..79f26a56d0 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -487,16 +487,6 @@ github.com/go-chi/chi/v5/middleware # github.com/go-chi/render v1.0.3 ## explicit; go 1.16 github.com/go-chi/render -# github.com/go-crypt/crypt v0.4.5 -## explicit; go 1.23 -github.com/go-crypt/crypt/algorithm -github.com/go-crypt/crypt/algorithm/shacrypt -github.com/go-crypt/crypt/internal/encoding -github.com/go-crypt/crypt/internal/random -# github.com/go-crypt/x v0.4.7 -## explicit; go 1.23.0 -github.com/go-crypt/x/base64 -github.com/go-crypt/x/crypt # github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 ## explicit; go 1.13 github.com/go-git/gcfg @@ -2141,6 +2131,7 @@ github.com/testcontainers/testcontainers-go/internal/config github.com/testcontainers/testcontainers-go/internal/core github.com/testcontainers/testcontainers-go/internal/core/network github.com/testcontainers/testcontainers-go/log +github.com/testcontainers/testcontainers-go/network github.com/testcontainers/testcontainers-go/wait # github.com/testcontainers/testcontainers-go/modules/opensearch v0.42.0 ## explicit; go 1.25.0