From 0262f4621e13a4a7e85bbb5abc21a470d6950111 Mon Sep 17 00:00:00 2001
From: Mechiel Lukkien <mechiel@ueber.net>
Date: Wed, 27 Mar 2024 09:35:16 +0100
Subject: [PATCH] in quickstart, check outgoing smtp connectivity by dialing
 gmail.com mx host

if connection cannot be made, warn about it and point to configuring a
smarthost and the config options.

suggested by arnt & friend
---
 doc.go                                |  6 ++--
 quickstart.go                         | 44 +++++++++++++++++++++++++--
 testdata/integration/moxacmepebble.sh |  2 +-
 testdata/integration/moxmail2.sh      |  2 +-
 4 files changed, 48 insertions(+), 6 deletions(-)

diff --git a/doc.go b/doc.go
index 5e1a66e..dd77620 100644
--- a/doc.go
+++ b/doc.go
@@ -20,7 +20,7 @@ any parameters. Followed by the help and usage information for each command.
 
 	mox [-config config/mox.conf] [-pedantic] ...
 	mox serve
-	mox quickstart [-existing-webserver] [-hostname host] user@domain [user | uid]
+	mox quickstart [-skipdial] [-existing-webserver] [-hostname host] user@domain [user | uid]
 	mox stop
 	mox setaccountpassword account
 	mox setadminpassword
@@ -146,11 +146,13 @@ traffic to your existing backend applications. Look for "WebHandlers:" in the
 output of "mox config describe-domains" and see the output of "mox example
 webhandlers".
 
-	usage: mox quickstart [-existing-webserver] [-hostname host] user@domain [user | uid]
+	usage: mox quickstart [-skipdial] [-existing-webserver] [-hostname host] user@domain [user | uid]
 	  -existing-webserver
 	    	use if a webserver is already running, so mox won't listen on port 80 and 443; you'll have to provide tls certificates/keys, and configure the existing webserver as reverse proxy, forwarding requests to mox.
 	  -hostname string
 	    	hostname mox will run on, by default the hostname of the machine quickstart runs on; if specified, the IPs for the hostname are configured for the public listener
+	  -skipdial
+	    	skip check for outgoing smtp (port 25) connectivity
 
 # mox stop
 
diff --git a/quickstart.go b/quickstart.go
index b2bdddd..c519a24 100644
--- a/quickstart.go
+++ b/quickstart.go
@@ -59,7 +59,7 @@ func pwgen() string {
 }
 
 func cmdQuickstart(c *cmd) {
-	c.params = "[-existing-webserver] [-hostname host] user@domain [user | uid]"
+	c.params = "[-skipdial] [-existing-webserver] [-hostname host] user@domain [user | uid]"
 	c.help = `Quickstart generates configuration files and prints instructions to quickly set up a mox instance.
 
 Quickstart writes configuration files, prints initial admin and account
@@ -95,8 +95,10 @@ webhandlers".
 `
 	var existingWebserver bool
 	var hostname string
+	var skipDial bool
 	c.flag.BoolVar(&existingWebserver, "existing-webserver", false, "use if a webserver is already running, so mox won't listen on port 80 and 443; you'll have to provide tls certificates/keys, and configure the existing webserver as reverse proxy, forwarding requests to mox.")
 	c.flag.StringVar(&hostname, "hostname", "", "hostname mox will run on, by default the hostname of the machine quickstart runs on; if specified, the IPs for the hostname are configured for the public listener")
+	c.flag.BoolVar(&skipDial, "skipdial", false, "skip check for outgoing smtp (port 25) connectivity")
 	args := c.Parse()
 	if len(args) != 1 && len(args) != 2 {
 		c.Usage()
@@ -529,6 +531,44 @@ messages over SMTP.
 		}
 	}
 
+	// Check outgoing SMTP connectivity.
+	if !skipDial {
+		fmt.Printf("Checking if outgoing smtp connections can be made by connecting to gmail.com mx on port 25...")
+		mxctx, mxcancel := context.WithTimeout(context.Background(), 5*time.Second)
+		mx, _, err := resolver.LookupMX(mxctx, "gmail.com.")
+		mxcancel()
+		if err == nil && len(mx) == 0 {
+			err = errors.New("no mx records")
+		}
+		var ok bool
+		if err != nil {
+			fmt.Printf("\n\nERROR: looking up gmail.com mx record: %s\n", err)
+		} else {
+			dialctx, dialcancel := context.WithTimeout(context.Background(), 10*time.Second)
+			d := net.Dialer{}
+			addr := net.JoinHostPort(mx[0].Host, "25")
+			conn, err := d.DialContext(dialctx, "tcp", addr)
+			dialcancel()
+			if err != nil {
+				fmt.Printf("\n\nERROR: connecting to %s: %s\n", addr, err)
+			} else {
+				conn.Close()
+				fmt.Printf(" OK\n")
+				ok = true
+			}
+		}
+		if !ok {
+			fmt.Printf(`
+WARNING: Could not verify outgoing smtp connections can be made, outgoing
+delivery may not be working. Many providers block outgoing smtp connections by
+default, requiring an explicit request or a cooldown period before allowing
+outgoing smtp connections. To send through a smarthost, configure a "Transport"
+in mox.conf and use it in "Routes" in domains.conf. See "mox example transport".
+
+`)
+		}
+	}
+
 	zones := []dns.Domain{
 		{ASCII: "sbl.spamhaus.org"},
 		{ASCII: "bl.spamcop.net"},
@@ -538,7 +578,7 @@ messages over SMTP.
 		var listed bool
 		for _, zone := range zones {
 			for _, ip := range hostIPs {
-				dnsblctx, dnsblcancel := context.WithTimeout(resolveCtx, 5*time.Second)
+				dnsblctx, dnsblcancel := context.WithTimeout(context.Background(), 5*time.Second)
 				status, expl, err := dnsbl.Lookup(dnsblctx, c.log.Logger, resolver, zone, net.ParseIP(ip))
 				dnsblcancel()
 				if status == dnsbl.StatusPass {
diff --git a/testdata/integration/moxacmepebble.sh b/testdata/integration/moxacmepebble.sh
index 9c564f0..e268f73 100755
--- a/testdata/integration/moxacmepebble.sh
+++ b/testdata/integration/moxacmepebble.sh
@@ -7,7 +7,7 @@ apk add unbound curl
 (rm -r /tmp/mox 2>/dev/null || exit 0) # clean slate
 mkdir /tmp/mox
 cd /tmp/mox
-mox quickstart moxtest1@mox1.example "$MOX_UID" > output.txt
+mox quickstart -skipdial moxtest1@mox1.example "$MOX_UID" > output.txt
 
 cp config/mox.conf config/mox.conf.orig
 sed -i -e 's/letsencrypt:/pebble:/g' -e 's/: letsencrypt/: pebble/g' -e 's,DirectoryURL: https://acme-v02.api.letsencrypt.org/directory,DirectoryURL: https://acmepebble.example:14000/dir,' -e 's/SMTP:$/SMTP:\n\t\t\tFirstTimeSenderDelay: 1s/' config/mox.conf
diff --git a/testdata/integration/moxmail2.sh b/testdata/integration/moxmail2.sh
index f8669e1..609d4a1 100755
--- a/testdata/integration/moxmail2.sh
+++ b/testdata/integration/moxmail2.sh
@@ -7,7 +7,7 @@ apk add unbound
 (rm -r /tmp/mox 2>/dev/null || exit 0) # clean slate
 mkdir /tmp/mox
 cd /tmp/mox
-mox quickstart moxtest2@mox2.example "$MOX_UID" > output.txt
+mox quickstart -skipdial moxtest2@mox2.example "$MOX_UID" > output.txt
 
 cp config/mox.conf config/mox.conf.orig
 sed -i -e 's,ACME: .*$,KeyCerts:\n\t\t\t\t-\n\t\t\t\t\tCertFile: /integration/tls/moxmail2.pem\n\t\t\t\t\tKeyFile: /integration/tls/moxmail2-key.pem\n\t\t\t\t-\n\t\t\t\t\tCertFile: /integration/tls/mox2-autoconfig.pem\n\t\t\t\t\tKeyFile: /integration/tls/mox2-autoconfig-key.pem\n\t\t\t\t-\n\t\t\t\t\tCertFile: /integration/tls/mox2-mtasts.pem\n\t\t\t\t\tKeyFile: /integration/tls/mox2-mtasts-key.pem\n,' -e 's/SMTP:$/SMTP:\n\t\t\tFirstTimeSenderDelay: 1s/' config/mox.conf