Initial commit
This commit is contained in:
commit
86b92eec9d
33 changed files with 5407 additions and 0 deletions
563
checker/collect.go
Normal file
563
checker/collect.go
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
const defaultEHLOName = "mx-checker.happydomain.org"
|
||||
const smtpPort = 25
|
||||
|
||||
// mxServiceBody mirrors the shape of svcs.MXs in happyDomain. We decode
|
||||
// it by hand (rather than importing the happyDomain server) to keep the
|
||||
// build surface small (checker-srv follows the same pattern).
|
||||
type mxServiceBody struct {
|
||||
MXs []struct {
|
||||
Hdr struct {
|
||||
Name string `json:"Name"`
|
||||
} `json:"Hdr"`
|
||||
Preference uint16 `json:"Preference"`
|
||||
Mx string `json:"Mx"`
|
||||
} `json:"mx"`
|
||||
}
|
||||
|
||||
func (p *smtpProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (any, error) {
|
||||
domain, _ := sdk.GetOption[string](opts, "domain")
|
||||
domain = strings.TrimSuffix(strings.TrimSpace(domain), ".")
|
||||
if domain == "" {
|
||||
return nil, fmt.Errorf("domain is required")
|
||||
}
|
||||
if !isValidHostname(domain) {
|
||||
return nil, fmt.Errorf("invalid domain %q", domain)
|
||||
}
|
||||
|
||||
helo, _ := sdk.GetOption[string](opts, "helo_name")
|
||||
helo = strings.TrimSpace(helo)
|
||||
if helo == "" {
|
||||
helo = defaultEHLOName
|
||||
}
|
||||
if !isValidHostname(helo) {
|
||||
return nil, fmt.Errorf("invalid helo_name %q", helo)
|
||||
}
|
||||
|
||||
timeoutSecs := sdk.GetFloatOption(opts, "timeout", 12)
|
||||
if timeoutSecs < 1 {
|
||||
timeoutSecs = 12
|
||||
}
|
||||
perEndpoint := time.Duration(timeoutSecs * float64(time.Second))
|
||||
|
||||
testNull := sdk.GetBoolOption(opts, "test_null_sender", true)
|
||||
testPostmaster := sdk.GetBoolOption(opts, "test_postmaster", true)
|
||||
testOpenRelay := sdk.GetBoolOption(opts, "test_open_relay", true)
|
||||
probeRcpt, _ := sdk.GetOption[string](opts, "test_probe_address")
|
||||
probeRcpt = strings.TrimSpace(probeRcpt)
|
||||
if probeRcpt == "" || !isValidMailbox(probeRcpt) {
|
||||
probeRcpt = "postmaster@example.com"
|
||||
}
|
||||
// Never use a recipient inside the domain under test; that would turn
|
||||
// an accept into a false-positive open relay.
|
||||
if addrDomain, _, ok := splitMail(probeRcpt); ok && strings.EqualFold(addrDomain, domain) {
|
||||
probeRcpt = "postmaster@example.com"
|
||||
}
|
||||
|
||||
data := &SMTPData{
|
||||
Domain: domain,
|
||||
RunAt: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
resolver := net.DefaultResolver
|
||||
lookupCtx, cancel := context.WithTimeout(ctx, perEndpoint)
|
||||
defer cancel()
|
||||
|
||||
// Prefer the service body when supplied (authoritative, already
|
||||
// parsed from the zone); fall back to a live MX lookup.
|
||||
var mxTargets []mxTargetRaw
|
||||
if body, ok := sdk.GetOption[json.RawMessage](opts, "service"); ok && len(body) > 0 {
|
||||
mxTargets = parseServiceBody(body)
|
||||
}
|
||||
if len(mxTargets) == 0 {
|
||||
var err error
|
||||
mxTargets, err = lookupMX(lookupCtx, resolver, domain)
|
||||
if err != nil {
|
||||
data.MX.Error = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
// RFC 7505 null MX sentinel.
|
||||
if len(mxTargets) == 1 && (mxTargets[0].Target == "" || mxTargets[0].Target == ".") && mxTargets[0].Preference == 0 {
|
||||
data.MX.NullMX = true
|
||||
return data, nil
|
||||
}
|
||||
|
||||
if len(mxTargets) == 0 && data.MX.Error == "" {
|
||||
// Implicit MX (RFC 5321 § 5.1): fall back to the bare domain.
|
||||
data.MX.ImplicitMX = true
|
||||
mxTargets = []mxTargetRaw{{Preference: 0, Target: domain}}
|
||||
}
|
||||
|
||||
for _, t := range mxTargets {
|
||||
rec := MXRecord{
|
||||
Preference: t.Preference,
|
||||
Target: strings.TrimSuffix(t.Target, "."),
|
||||
}
|
||||
if rec.Target == "" {
|
||||
continue
|
||||
}
|
||||
if ip := net.ParseIP(rec.Target); ip != nil {
|
||||
rec.IsIPLiteral = true
|
||||
}
|
||||
// Detect CNAME (RFC 5321 § 5.1 forbids MX → CNAME).
|
||||
if !rec.IsIPLiteral {
|
||||
if cname, err := resolver.LookupCNAME(lookupCtx, rec.Target); err == nil {
|
||||
canon := strings.TrimSuffix(cname, ".")
|
||||
if canon != "" && !strings.EqualFold(canon, rec.Target) {
|
||||
rec.IsCNAME = true
|
||||
rec.CNAMEChain = []string{rec.Target, canon}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if rec.IsIPLiteral {
|
||||
if ip := net.ParseIP(rec.Target); ip != nil {
|
||||
if v4 := ip.To4(); v4 != nil {
|
||||
rec.IPv4 = append(rec.IPv4, v4.String())
|
||||
} else {
|
||||
rec.IPv6 = append(rec.IPv6, ip.String())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ips, err := resolver.LookupIPAddr(lookupCtx, rec.Target)
|
||||
if err != nil {
|
||||
rec.ResolveError = err.Error()
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if v4 := ip.IP.To4(); v4 != nil {
|
||||
rec.IPv4 = append(rec.IPv4, v4.String())
|
||||
} else {
|
||||
rec.IPv6 = append(rec.IPv6, ip.IP.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
data.MX.Records = append(data.MX.Records, rec)
|
||||
}
|
||||
|
||||
// Probe every (target, ip) pair.
|
||||
for _, rec := range data.MX.Records {
|
||||
for _, ip := range rec.IPv4 {
|
||||
ep := probeEndpoint(ctx, probeInputs{
|
||||
target: rec.Target,
|
||||
ip: ip,
|
||||
isV6: false,
|
||||
domain: domain,
|
||||
heloName: helo,
|
||||
timeout: perEndpoint,
|
||||
testNull: testNull,
|
||||
testPostmaster: testPostmaster,
|
||||
testOpenRelay: testOpenRelay,
|
||||
openRelayRcpt: probeRcpt,
|
||||
})
|
||||
data.Endpoints = append(data.Endpoints, ep)
|
||||
}
|
||||
for _, ip := range rec.IPv6 {
|
||||
ep := probeEndpoint(ctx, probeInputs{
|
||||
target: rec.Target,
|
||||
ip: ip,
|
||||
isV6: true,
|
||||
domain: domain,
|
||||
heloName: helo,
|
||||
timeout: perEndpoint,
|
||||
testNull: testNull,
|
||||
testPostmaster: testPostmaster,
|
||||
testOpenRelay: testOpenRelay,
|
||||
openRelayRcpt: probeRcpt,
|
||||
})
|
||||
data.Endpoints = append(data.Endpoints, ep)
|
||||
}
|
||||
}
|
||||
|
||||
computeCoverage(data)
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
type mxTargetRaw struct {
|
||||
Preference uint16
|
||||
Target string
|
||||
}
|
||||
|
||||
// parseServiceBody extracts the MX list from a happyDomain svcs.MXs
|
||||
// payload. Returns nil when the payload doesn't look like one; we fall
|
||||
// back to a live DNS lookup in that case.
|
||||
func parseServiceBody(raw json.RawMessage) []mxTargetRaw {
|
||||
// happyDomain wraps the body in ServiceMessage{Type, Service:<body>}.
|
||||
// We accept either the full ServiceMessage or the body directly.
|
||||
var envelope struct {
|
||||
Type string `json:"_svctype"`
|
||||
Service json.RawMessage `json:"Service"`
|
||||
}
|
||||
var body json.RawMessage
|
||||
if err := json.Unmarshal(raw, &envelope); err == nil && len(envelope.Service) > 0 {
|
||||
body = envelope.Service
|
||||
} else {
|
||||
body = raw
|
||||
}
|
||||
var parsed mxServiceBody
|
||||
if err := json.Unmarshal(body, &parsed); err != nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]mxTargetRaw, 0, len(parsed.MXs))
|
||||
for _, m := range parsed.MXs {
|
||||
out = append(out, mxTargetRaw{Preference: m.Preference, Target: m.Mx})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// lookupMX runs a DNS MX query and returns the records, or nil when
|
||||
// NXDOMAIN / no records (so the caller can trigger the implicit-MX path).
|
||||
func lookupMX(ctx context.Context, r *net.Resolver, domain string) ([]mxTargetRaw, error) {
|
||||
records, err := r.LookupMX(ctx, dns.Fqdn(domain))
|
||||
if err != nil {
|
||||
var dnsErr *net.DNSError
|
||||
if errors.As(err, &dnsErr) && dnsErr.IsNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
// net.LookupMX returns an error on the RFC 7505 null-MX sentinel
|
||||
// because "." fails host validation. Surface it as a synthetic
|
||||
// record so the caller can detect the null-MX case.
|
||||
if strings.Contains(err.Error(), "cannot unmarshal DNS message") {
|
||||
return []mxTargetRaw{{Preference: 0, Target: "."}}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
out := make([]mxTargetRaw, 0, len(records))
|
||||
for _, m := range records {
|
||||
out = append(out, mxTargetRaw{Preference: m.Pref, Target: strings.TrimSuffix(m.Host, ".")})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type probeInputs struct {
|
||||
target, ip, domain, heloName string
|
||||
isV6 bool
|
||||
timeout time.Duration
|
||||
testNull, testPostmaster bool
|
||||
testOpenRelay bool
|
||||
openRelayRcpt string
|
||||
}
|
||||
|
||||
func probeEndpoint(ctx context.Context, in probeInputs) EndpointProbe {
|
||||
start := time.Now()
|
||||
ep := EndpointProbe{
|
||||
Target: in.target,
|
||||
Port: smtpPort,
|
||||
IP: in.ip,
|
||||
IsIPv6: in.isV6,
|
||||
Address: net.JoinHostPort(in.ip, strconv.Itoa(smtpPort)),
|
||||
}
|
||||
defer func() { ep.ElapsedMS = time.Since(start).Milliseconds() }()
|
||||
|
||||
// Reverse DNS: orthogonal to the SMTP connection, so we run it even
|
||||
// if the connection later fails.
|
||||
ptrCtx, ptrCancel := context.WithTimeout(ctx, in.timeout)
|
||||
names, err := net.DefaultResolver.LookupAddr(ptrCtx, in.ip)
|
||||
ptrCancel()
|
||||
switch {
|
||||
case err != nil:
|
||||
ep.PTRError = err.Error()
|
||||
case len(names) == 0:
|
||||
ep.PTRError = "no PTR records"
|
||||
default:
|
||||
ep.PTR = strings.TrimSuffix(names[0], ".")
|
||||
// FCrDNS: PTR's forward lookup must include our IP.
|
||||
fwdCtx, fwdCancel := context.WithTimeout(ctx, in.timeout)
|
||||
ips, ferr := net.DefaultResolver.LookupIPAddr(fwdCtx, ep.PTR)
|
||||
fwdCancel()
|
||||
if ferr == nil {
|
||||
for _, a := range ips {
|
||||
if a.IP.String() == in.ip || a.IP.Equal(net.ParseIP(in.ip)) {
|
||||
ep.FCrDNSPass = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialCtx, cancel := context.WithTimeout(ctx, in.timeout)
|
||||
defer cancel()
|
||||
|
||||
dialer := &net.Dialer{}
|
||||
conn, err := dialer.DialContext(dialCtx, "tcp", ep.Address)
|
||||
if err != nil {
|
||||
ep.Error = "tcp: " + err.Error()
|
||||
return ep
|
||||
}
|
||||
ep.TCPConnected = true
|
||||
_ = conn.SetDeadline(time.Now().Add(in.timeout))
|
||||
|
||||
sc := newSMTPConn(conn, in.timeout)
|
||||
// One defer covers both the plaintext and post-STARTTLS cases: after
|
||||
// swap() the smtpConn owns the tls.Conn whose Close propagates to the
|
||||
// underlying TCP fd, so a separate `defer conn.Close()` would only
|
||||
// double-close the same descriptor.
|
||||
defer sc.close()
|
||||
|
||||
// Read the banner (220).
|
||||
code, text, _, err := sc.readResponse()
|
||||
if err != nil {
|
||||
ep.Error = "banner: " + err.Error()
|
||||
return ep
|
||||
}
|
||||
ep.BannerReceived = true
|
||||
ep.BannerCode = code
|
||||
ep.BannerLine = strings.TrimSpace(strings.ReplaceAll(text, "\n", " | "))
|
||||
ep.BannerHostname = parseBanner(text)
|
||||
if code != 220 {
|
||||
ep.Error = fmt.Sprintf("banner: unexpected code %d", code)
|
||||
return ep
|
||||
}
|
||||
|
||||
// EHLO (fall back to HELO on 5xx).
|
||||
_, text, lines, err := sc.cmd("EHLO " + in.heloName)
|
||||
if err != nil {
|
||||
ep.Error = "ehlo: " + err.Error()
|
||||
return ep
|
||||
}
|
||||
if lines[0][0] == '5' {
|
||||
// Try HELO.
|
||||
_, _, heloLines, herr := sc.cmd("HELO " + in.heloName)
|
||||
if herr != nil || len(heloLines) == 0 || heloLines[0][0] != '2' {
|
||||
ep.Error = "ehlo/helo both rejected"
|
||||
return ep
|
||||
}
|
||||
ep.EHLOReceived = true
|
||||
ep.EHLOFallbackHELO = true
|
||||
ep.EHLOHostname = strings.TrimSpace(strings.SplitN(text, " ", 2)[0])
|
||||
return ep
|
||||
}
|
||||
ep.EHLOReceived = true
|
||||
greeting, exts := parseEHLO(lines)
|
||||
ep.EHLOHostname = greeting
|
||||
ep.Extensions = exts
|
||||
idx := buildExtensions(exts)
|
||||
|
||||
ep.STARTTLSOffered = idx.has("STARTTLS")
|
||||
ep.HasPipelining = idx.has("PIPELINING")
|
||||
ep.Has8BITMIME = idx.has("8BITMIME")
|
||||
ep.HasSMTPUTF8 = idx.has("SMTPUTF8")
|
||||
ep.HasCHUNKING = idx.has("CHUNKING")
|
||||
ep.HasDSN = idx.has("DSN")
|
||||
ep.HasENHANCEDCODE = idx.has("ENHANCEDSTATUSCODES")
|
||||
ep.SizeLimit = idx.parseSize()
|
||||
ep.AUTHPreTLS = idx.parseAuth()
|
||||
|
||||
// STARTTLS.
|
||||
if ep.STARTTLSOffered {
|
||||
code, _, _, terr := sc.cmd("STARTTLS")
|
||||
if terr == nil && code == 220 {
|
||||
tlsConn := tls.Client(conn, tlsProbeConfig(in.target))
|
||||
_ = tlsConn.SetDeadline(time.Now().Add(in.timeout))
|
||||
if herr := tlsConn.Handshake(); herr != nil {
|
||||
ep.Error = "tls-handshake: " + herr.Error()
|
||||
return ep
|
||||
}
|
||||
ep.STARTTLSUpgraded = true
|
||||
state := tlsConn.ConnectionState()
|
||||
ep.TLSVersion = tls.VersionName(state.Version)
|
||||
ep.TLSCipher = tls.CipherSuiteName(state.CipherSuite)
|
||||
sc.swap(tlsConn)
|
||||
// Re-EHLO over TLS (mandatory per RFC 3207).
|
||||
_, _, lines2, eerr := sc.cmd("EHLO " + in.heloName)
|
||||
if eerr == nil && len(lines2) > 0 && lines2[0][0] == '2' {
|
||||
_, exts2 := parseEHLO(lines2)
|
||||
ep.PostTLSExtensions = exts2
|
||||
idx2 := buildExtensions(exts2)
|
||||
ep.AUTHPostTLS = idx2.parseAuth()
|
||||
// Union the feature flags: some servers only advertise
|
||||
// 8BITMIME, PIPELINING, etc. after STARTTLS.
|
||||
if !ep.HasPipelining {
|
||||
ep.HasPipelining = idx2.has("PIPELINING")
|
||||
}
|
||||
if !ep.Has8BITMIME {
|
||||
ep.Has8BITMIME = idx2.has("8BITMIME")
|
||||
}
|
||||
if !ep.HasSMTPUTF8 {
|
||||
ep.HasSMTPUTF8 = idx2.has("SMTPUTF8")
|
||||
}
|
||||
if !ep.HasCHUNKING {
|
||||
ep.HasCHUNKING = idx2.has("CHUNKING")
|
||||
}
|
||||
if !ep.HasDSN {
|
||||
ep.HasDSN = idx2.has("DSN")
|
||||
}
|
||||
if !ep.HasENHANCEDCODE {
|
||||
ep.HasENHANCEDCODE = idx2.has("ENHANCEDSTATUSCODES")
|
||||
}
|
||||
if ep.SizeLimit == 0 {
|
||||
ep.SizeLimit = idx2.parseSize()
|
||||
}
|
||||
}
|
||||
} else if terr != nil {
|
||||
ep.Error = "starttls: " + terr.Error()
|
||||
return ep
|
||||
} else {
|
||||
ep.Error = fmt.Sprintf("starttls: unexpected code %d", code)
|
||||
// Don't bail; still run transactional probes over plaintext
|
||||
// so the operator sees what the server does without TLS.
|
||||
}
|
||||
}
|
||||
|
||||
// RCPT-level probes. Each runs in its own MAIL/RSET pair so an earlier
|
||||
// reject does not mask later ones.
|
||||
runRCPT := func(from, to string) (int, string) {
|
||||
code, text, _, err := sc.cmd("MAIL FROM:<" + from + ">")
|
||||
if err != nil {
|
||||
return -1, err.Error()
|
||||
}
|
||||
if code != 250 {
|
||||
defer sc.cmd("RSET")
|
||||
return code, strings.TrimSpace(text)
|
||||
}
|
||||
code, text, _, err = sc.cmd("RCPT TO:<" + to + ">")
|
||||
sc.cmd("RSET")
|
||||
if err != nil {
|
||||
return -1, err.Error()
|
||||
}
|
||||
return code, strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
if in.testNull {
|
||||
c, t := runRCPT("", "postmaster@"+in.domain)
|
||||
ok := c >= 200 && c < 300
|
||||
ep.NullSenderAccepted = &ok
|
||||
ep.NullSenderResponse = fmt.Sprintf("%d %s", c, t)
|
||||
}
|
||||
if in.testPostmaster {
|
||||
from := "checker@" + in.heloName
|
||||
c, t := runRCPT(from, "postmaster@"+in.domain)
|
||||
ok := c >= 200 && c < 300
|
||||
ep.PostmasterAccepted = &ok
|
||||
ep.PostmasterResponse = fmt.Sprintf("%d %s", c, t)
|
||||
}
|
||||
if in.testOpenRelay && in.openRelayRcpt != "" {
|
||||
from := "checker@" + in.heloName
|
||||
c, t := runRCPT(from, in.openRelayRcpt)
|
||||
ok := c >= 200 && c < 300
|
||||
ep.OpenRelay = &ok
|
||||
ep.OpenRelayResponse = fmt.Sprintf("%d %s", c, t)
|
||||
ep.OpenRelayRecipient = in.openRelayRcpt
|
||||
}
|
||||
|
||||
return ep
|
||||
}
|
||||
|
||||
func splitMail(addr string) (domain, local string, ok bool) {
|
||||
at := strings.LastIndex(addr, "@")
|
||||
if at <= 0 || at == len(addr)-1 {
|
||||
return "", "", false
|
||||
}
|
||||
return addr[at+1:], addr[:at], true
|
||||
}
|
||||
|
||||
// isValidHostname rejects anything that could smuggle SMTP commands
|
||||
// (CR, LF, spaces, angle brackets) or is otherwise not a plausible
|
||||
// hostname. We use it on every user-supplied value that ends up
|
||||
// concatenated into an SMTP command line.
|
||||
func isValidHostname(s string) bool {
|
||||
if s == "" || len(s) > 253 {
|
||||
return false
|
||||
}
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z',
|
||||
r >= 'A' && r <= 'Z',
|
||||
r >= '0' && r <= '9',
|
||||
r == '.', r == '-':
|
||||
continue
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isValidMailbox accepts a conservative subset of RFC 5321 addr-spec:
|
||||
// printable ASCII local-part with no SMTP metacharacters, followed by
|
||||
// "@" and a valid hostname. Quoted local-parts are not allowed.
|
||||
func isValidMailbox(s string) bool {
|
||||
at := strings.LastIndex(s, "@")
|
||||
if at <= 0 || at == len(s)-1 {
|
||||
return false
|
||||
}
|
||||
local := s[:at]
|
||||
if len(local) > 64 {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(local); i++ {
|
||||
c := local[i]
|
||||
if c <= 0x20 || c >= 0x7f {
|
||||
return false
|
||||
}
|
||||
switch c {
|
||||
case '<', '>', '(', ')', '[', ']', ',', ';', ':', '"', '\\', '@':
|
||||
return false
|
||||
}
|
||||
}
|
||||
return isValidHostname(s[at+1:])
|
||||
}
|
||||
|
||||
func computeCoverage(data *SMTPData) {
|
||||
if len(data.Endpoints) == 0 {
|
||||
return
|
||||
}
|
||||
allSTARTTLS := true
|
||||
allAcceptMail := true
|
||||
for _, ep := range data.Endpoints {
|
||||
if ep.TCPConnected {
|
||||
data.Coverage.AnyReachable = true
|
||||
if ep.IsIPv6 {
|
||||
data.Coverage.HasIPv6 = true
|
||||
} else {
|
||||
data.Coverage.HasIPv4 = true
|
||||
}
|
||||
}
|
||||
if ep.BannerReceived {
|
||||
data.Coverage.AnyBanner = true
|
||||
}
|
||||
if ep.EHLOReceived {
|
||||
data.Coverage.AnyEHLO = true
|
||||
}
|
||||
if ep.STARTTLSUpgraded {
|
||||
data.Coverage.AnySTARTTLS = true
|
||||
} else {
|
||||
allSTARTTLS = false
|
||||
}
|
||||
// An endpoint "accepts mail" when the null-sender probe, if run,
|
||||
// was accepted and the postmaster probe, if run, was accepted.
|
||||
acc := true
|
||||
if ep.NullSenderAccepted != nil && !*ep.NullSenderAccepted {
|
||||
acc = false
|
||||
}
|
||||
if ep.PostmasterAccepted != nil && !*ep.PostmasterAccepted {
|
||||
acc = false
|
||||
}
|
||||
if !ep.EHLOReceived {
|
||||
acc = false
|
||||
}
|
||||
if !acc {
|
||||
allAcceptMail = false
|
||||
}
|
||||
}
|
||||
data.Coverage.AllSTARTTLS = allSTARTTLS
|
||||
data.Coverage.AllAcceptMail = allAcceptMail
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue