feat: add cmd/smtp-test for debugging smtp auth and delivery

This commit is contained in:
AJ ONeal 2026-01-20 19:09:17 -07:00
parent 939c733ace
commit d3b59aebff
No known key found for this signature in database
3 changed files with 167 additions and 0 deletions

7
cmd/smtp-test/go.mod Normal file
View File

@ -0,0 +1,7 @@
module github.com/therootcompany/golib/smtp-test
go 1.25.4
require golang.org/x/term v0.39.0
require golang.org/x/sys v0.40.0 // indirect

4
cmd/smtp-test/go.sum Normal file
View File

@ -0,0 +1,4 @@
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=

156
cmd/smtp-test/main.go Normal file
View File

@ -0,0 +1,156 @@
package main
import (
"flag"
"fmt"
"net/smtp"
"os"
"strings"
"golang.org/x/term"
)
const (
name = "smtp-test"
licenseYear = "2026"
licenseOwner = "AJ ONeal"
licenseType = "CC0-1.0"
)
// set by GoReleaser via ldflags
var (
version = "0.0.0-dev"
commit = "0000000"
date = "0001-01-01T00:00:00Z"
)
func printVersion() {
if len(commit) > 7 {
commit = commit[:7]
}
fmt.Fprintf(os.Stderr, "%s v%s %s (%s)\n", name, version, commit, date)
fmt.Fprintf(os.Stderr, "Copyright (C) %s %s\n", licenseYear, licenseOwner)
fmt.Fprintf(os.Stderr, "Licensed under the %s license\n", licenseType)
}
type CLIConfig struct {
showVersion bool
user string
from string
to string
host string // e.g. smtp.mailgun.org:587 or smtp.gmail.com:587
subject string
body string
}
func main() {
cfg := CLIConfig{
subject: "smtp-test - connectivity check",
body: "This is a test message from smtp-test.\nIf you received this, SMTP auth + send worked.",
}
mainFlags := flag.NewFlagSet("", flag.ContinueOnError)
mainFlags.BoolVar(&cfg.showVersion, "version", false, "Print version and exit")
mainFlags.StringVar(&cfg.user, "user", os.Getenv("SMTP_USER"), "Auth email e.g. 'you@gmail.com' or set SMTP_USER")
mainFlags.StringVar(&cfg.from, "from", os.Getenv("SMTP_FROM"), "Sender email, e.g. 'you@gmail.com' or set SMTP_FROM")
mainFlags.StringVar(&cfg.to, "to", os.Getenv("SMTP_TO"), "Recipient email, e.g. 'test@yourdomain.com' or set SMTP_TO")
mainFlags.StringVar(&cfg.host, "host", os.Getenv("SMTP_HOST"), "SMTP server + port, e.g. 'smtp.gmail.com:587' or set SMTP_HOST")
mainFlags.StringVar(&cfg.subject, "subject", cfg.subject, "Subject line (default: connectivity check)")
mainFlags.StringVar(&cfg.body, "body", cfg.body, "Plain text body (default: test message)")
mainFlags.Usage = func() {
printVersion()
out := mainFlags.Output()
fmt.Fprintf(out, "\n")
fmt.Fprintf(out, "USAGE\n")
fmt.Fprintf(out, " smtp-test [options]\n")
fmt.Fprintf(out, " (or provide most values via environment variables)\n\n")
mainFlags.PrintDefaults()
fmt.Fprintf(out, "\nExamples:\n")
fmt.Fprintf(out, " SMTP_HOST=smtp.mailgun.org:587 SMTP_FROM=you@mg.domain SMTP_TO=you@gmail.com smtp-test\n")
fmt.Fprintf(out, " smtp-test -host smtp.gmail.com:587 -from you@gmail.com -to debug@yourself.com\n")
}
if len(os.Args) > 1 {
switch os.Args[1] {
case "-V", "version", "-version", "--version":
printVersion()
return
case "help", "-help", "--help":
mainFlags.SetOutput(os.Stdout)
mainFlags.Usage()
return
}
}
if err := mainFlags.Parse(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
mainFlags.SetOutput(os.Stderr)
mainFlags.Usage()
os.Exit(1)
}
if cfg.showVersion {
printVersion()
return
}
// Required fields
if cfg.from == "" || cfg.to == "" || cfg.host == "" {
fmt.Fprintln(os.Stderr, "Missing required parameters: --from, --to, --host (or matching env vars)")
mainFlags.Usage()
os.Exit(1)
}
user := cfg.user // usually same as from for plain auth
pass, hasPass := os.LookupEnv("SMTP_PASS") // SMTP_PASS to be consistent style with your SMB_PASSWORD
if !hasPass {
fmt.Fprintf(os.Stderr, "SMTP_PASS is not set → ")
fmt.Print("Password: ")
password, err := term.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
fmt.Fprintf(os.Stderr, "failed to read password: %v\n", err)
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "\n")
pass = strings.TrimRight(string(password), "\r\n \t")
}
fmt.Printf("Trying to send from %s → %s via %s ...\n", cfg.from, cfg.to, cfg.host)
trySMTP(cfg.host, cfg.from, user, pass, cfg.to, cfg.subject, cfg.body)
fmt.Println("")
}
func trySMTP(addr, from, username, password, to, subject, body string) {
// Most modern SMTP servers expect 587 + STARTTLS (not native 465 SSL)
// net/smtp.SendMail automatically attempts STARTTLS when available.
auth := smtp.PlainAuth("", username, password, strings.Split(addr, ":")[0])
// Build minimal RFC-compliant message
msg := (fmt.Appendf(
[]byte{},
"To: %s\r\n"+
"From: %s\r\n"+
"Subject: %s\r\n"+
"\r\n"+
"%s\r\n",
to, from, subject, body,
))
err := smtp.SendMail(addr, auth, from, []string{to}, msg)
if err != nil {
fmt.Fprintf(os.Stderr, "SMTP error: %v\n", err)
// Common helpful hints
if strings.Contains(err.Error(), "535") || strings.Contains(err.Error(), "authentication") {
fmt.Fprintln(os.Stderr, "→ Check username/password (Gmail may need app password)")
}
if strings.Contains(err.Error(), "STARTTLS") || strings.Contains(err.Error(), "TLS") {
fmt.Fprintln(os.Stderr, "→ Server may require TLS — try port 587 instead of 465, or vice versa")
}
return
}
fmt.Printf("Success! Email sent:\n%s\n", string(msg))
}