mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-05 08:02:14 +00:00
129 lines
3.5 KiB
Go
129 lines
3.5 KiB
Go
package email
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/textproto"
|
|
"os"
|
|
"strings"
|
|
"syscall"
|
|
"unicode"
|
|
|
|
"github.com/emersion/go-imap/v2"
|
|
"github.com/warmbly/warmbly/internal/models"
|
|
)
|
|
|
|
// ProbeResult is the outcome of one credential probe. Reason is one of the
|
|
// models.MailProbe* values; Detail is the server's reply or the dial error.
|
|
type ProbeResult struct {
|
|
OK bool
|
|
Reason string
|
|
Detail string
|
|
// Port and Security are set when the server was reached on a port other
|
|
// than the one asked for.
|
|
Port int
|
|
Security string
|
|
}
|
|
|
|
// Leg converts the result to the wire shape the worker publishes.
|
|
func (r ProbeResult) Leg() models.EmailValidationLeg {
|
|
return models.EmailValidationLeg{OK: r.OK, Reason: r.Reason, Detail: r.Detail, Port: r.Port, Security: r.Security}
|
|
}
|
|
|
|
func probeOK() ProbeResult { return ProbeResult{OK: true} }
|
|
|
|
// probeFail classifies err. A deadline wins over the given reason, because a
|
|
// server that never answered has not refused anything; a reply that did
|
|
// arrive keeps its reason even if the deadline passed while reading it.
|
|
func probeFail(ctx context.Context, reason string, err error) ProbeResult {
|
|
if !isServerReply(err) && timedOut(ctx, err) {
|
|
reason = models.MailProbeTimeout
|
|
}
|
|
detail := probeDetail(err)
|
|
var opErr *net.OpError
|
|
if errors.As(err, &opErr) {
|
|
detail = netDetail(err)
|
|
}
|
|
return ProbeResult{Reason: reason, Detail: detail}
|
|
}
|
|
|
|
// isServerReply reports whether err is the server's own tagged answer.
|
|
func isServerReply(err error) bool {
|
|
var te *textproto.Error
|
|
var ie *imap.Error
|
|
return errors.As(err, &te) || errors.As(err, &ie)
|
|
}
|
|
|
|
// netDetail describes a socket failure in closed words. A net.OpError names
|
|
// both ends of the socket when the worker binds a local address, and the
|
|
// worker's address is not the customer's to see.
|
|
func netDetail(err error) string {
|
|
var dnsErr *net.DNSError
|
|
if errors.As(err, &dnsErr) {
|
|
if dnsErr.IsNotFound {
|
|
return "the host name does not exist"
|
|
}
|
|
return "the host name could not be resolved"
|
|
}
|
|
switch {
|
|
case errors.Is(err, syscall.ECONNREFUSED):
|
|
return "the port refused the connection"
|
|
case errors.Is(err, syscall.EHOSTUNREACH), errors.Is(err, syscall.ENETUNREACH):
|
|
return "the host is not reachable from the worker's network"
|
|
case errors.Is(err, syscall.ECONNRESET), errors.Is(err, syscall.EPIPE), errors.Is(err, io.EOF), errors.Is(err, io.ErrUnexpectedEOF):
|
|
return "the server closed the connection"
|
|
}
|
|
var ne net.Error
|
|
if errors.As(err, &ne) && ne.Timeout() {
|
|
return "the connection timed out"
|
|
}
|
|
return "the connection failed"
|
|
}
|
|
|
|
func probeFailText(reason, detail string) ProbeResult {
|
|
return ProbeResult{Reason: reason, Detail: detail}
|
|
}
|
|
|
|
// timedOut reports whether err, or the context it ran under, is a deadline.
|
|
func timedOut(ctx context.Context, err error) bool {
|
|
if ctx.Err() != nil {
|
|
return true
|
|
}
|
|
if err == nil {
|
|
return false
|
|
}
|
|
var ne net.Error
|
|
if errors.As(err, &ne) && ne.Timeout() {
|
|
return true
|
|
}
|
|
return errors.Is(err, context.DeadlineExceeded) || errors.Is(err, os.ErrDeadlineExceeded)
|
|
}
|
|
|
|
// probeDetail flattens an error into one printable line, capped, so a server
|
|
// reply can be shown at the form without dragging a protocol dump along.
|
|
func probeDetail(err error) string {
|
|
if err == nil {
|
|
return ""
|
|
}
|
|
const maxDetail = 240
|
|
var b strings.Builder
|
|
space := false
|
|
for _, r := range err.Error() {
|
|
if unicode.IsSpace(r) || !unicode.IsPrint(r) {
|
|
space = true
|
|
continue
|
|
}
|
|
if space && b.Len() > 0 {
|
|
b.WriteByte(' ')
|
|
}
|
|
space = false
|
|
b.WriteRune(r)
|
|
if b.Len() >= maxDetail {
|
|
break
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|