aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2019-03-26 02:35:56 +0700
committerShulhan <ms@kilabit.info>2019-03-26 02:35:56 +0700
commita4c0f6e3bbf0b3e418abb34acc8811f50b0f0deb (patch)
tree5c55ac263b41404b077f98e7152e4fd7a98e72b9
parent4ebea25ba12d695e248519a662f74c73ec7238b0 (diff)
downloadpakakeh.go-a4c0f6e3bbf0b3e418abb34acc8811f50b0f0deb.tar.xz
smtp: change the NewClient remote address to URL based format
Previously, the remote address parameter on NewClient use the format of "(hostname|ip-address)[:port]" to connect. This format does not have any flags to indicate whether server support TLS or not. We can check the port number and assume that port number 465 or 587 to support TLS, but we can't do TLS on non well known port. This commit change the remote address parameter to use URL based format, remoteURL = [ scheme "://" ](domain | IP-address [":" port]) scheme = "smtp" / "smtps" If scheme is "smtp", client will assume that no STARTTLS command will be issued after connection has been established; otherwise if scheme is "smtps", client will send STARTTLS command on any or missing port given in URL.
-rw-r--r--lib/smtp/client.go131
-rw-r--r--lib/smtp/client_test.go15
-rw-r--r--lib/smtp/smtp_test.go11
3 files changed, 117 insertions, 40 deletions
diff --git a/lib/smtp/client.go b/lib/smtp/client.go
index da360ab3..344c8a43 100644
--- a/lib/smtp/client.go
+++ b/lib/smtp/client.go
@@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"net"
+ "net/url"
"os"
"strings"
@@ -26,41 +27,83 @@ type Client struct {
// EHLO command.
ServerInfo *ServerInfo
- data []byte
- buf bytes.Buffer
- raddr *net.TCPAddr
- conn net.Conn
+ data []byte
+ buf bytes.Buffer
+
+ serverName string
+ raddr *net.TCPAddr
+ conn net.Conn
+ insecure bool
+ isTLS bool
}
//
-// NewClient create and initialize TCP address to remote SMTP server. The
-// returned client is not connected to the server yet.
+// NewClient create and initialize connection to remote SMTP server.
+// The remoteURL use the following format,
//
-func NewClient(raddr string) (cl *Client, err error) {
- cl = &Client{
- data: make([]byte, 4096),
- }
+// remoteURL = [ scheme "://" ](domain | IP-address [":" port])
+// scheme = "smtp" / "smtps"
+//
+// If scheme is "smtp" and no port is given, client will connect to remote
+// address at port "25".
+// If scheme is "smtps" and no port is given, client will connect to remote
+// address at port "465" (implicit TLS).
+//
+// The second parameter "insecure", if set to true, will disable verifying
+// remote certificate when connecting with TLS.
+//
+// Note that, the returned client is not connected to the remote yet.
+//
+func NewClient(remoteURL string, insecure bool) (cl *Client, err error) {
+ var (
+ rurl *url.URL
+ ip net.IP
+ hostname string
+ port uint16
+ isTLS bool
+ )
- ip, port, err := libnet.ParseIPPort(raddr, 25)
+ rurl, err = url.Parse(remoteURL)
if err != nil {
- ip, err = lookup(raddr)
+ return nil, fmt.Errorf("smtp: NewClient: " + err.Error())
+ }
+
+ if strings.ToLower(rurl.Scheme) == "smtps" {
+ port = 465
+ isTLS = true
+ } else {
+ port = 25
+ }
+
+ hostname, ip, port = libnet.ParseIPPort(rurl.Host, port)
+ if ip == nil {
+ ip, err = lookup(hostname)
if err != nil {
return nil, err
}
if ip == nil {
- err = fmt.Errorf("client.NewClient: '%s' does not have MX record or IP address", raddr)
+ err = fmt.Errorf("smtp: NewClient: '%s' does not have MX record or IP address", cl.serverName)
return nil, err
}
-
- port = 25
+ }
+ if len(hostname) == 0 {
+ insecure = true
}
- cl.raddr = &net.TCPAddr{
- IP: ip,
- Port: int(port),
+ cl = &Client{
+ data: make([]byte, 4096),
+ serverName: hostname,
+ raddr: &net.TCPAddr{
+ IP: ip,
+ Port: int(port),
+ },
+ insecure: insecure,
+ isTLS: isTLS,
}
- fmt.Printf("NewClient: %v\n", cl.raddr)
+ if debug.Value >= 3 {
+ fmt.Printf("smtp: NewClient: %v\n", cl.raddr)
+ }
return cl, nil
}
@@ -88,18 +131,28 @@ func (cl *Client) Authenticate(mech Mechanism, username, password string) (
//
// Connect open a connection to server and return server greeting.
+// If remoteURL scheme is "smtps", the client will issue STARTTLS command
+// immediately after connect.
//
-func (cl *Client) Connect(insecure bool) (res *Response, err error) {
- tlsConfig := &tls.Config{
- InsecureSkipVerify: insecure, // nolint: gosec
+func (cl *Client) Connect() (res *Response, err error) {
+ cl.conn, err = net.DialTCP("tcp", nil, cl.raddr)
+ if err != nil {
+ return nil, err
}
- cl.conn, err = tls.Dial("tcp", cl.raddr.String(), tlsConfig)
+ res, err = cl.recv()
if err != nil {
- return nil, err
+ return res, err
+ }
+ if res.Code != StatusReady {
+ return res, fmt.Errorf("server return %d, want 220", res.Code)
}
- return cl.recv()
+ if !cl.isTLS {
+ return res, nil
+ }
+
+ return cl.startTLS()
}
//
@@ -257,6 +310,10 @@ func (cl *Client) MailTx(mail *MailTx) (res *Response, err error) {
// SendCommand send any custom command to server.
//
func (cl *Client) SendCommand(cmd []byte) (res *Response, err error) {
+ if debug.Value >= 3 {
+ fmt.Printf("smtp: Client.SendCommand: %s", cmd)
+ }
+
_, err = cl.conn.Write(cmd)
if err != nil {
return nil, err
@@ -344,7 +401,7 @@ func (cl *Client) recv() (res *Response, err error) {
}
if debug.Value > 0 {
- fmt.Printf("Client.recv: %s\n", cl.buf.Bytes())
+ fmt.Printf("Client.recv: %s", cl.buf.Bytes())
}
res, err = NewResponse(cl.buf.Bytes())
@@ -354,3 +411,25 @@ func (cl *Client) recv() (res *Response, err error) {
return res, nil
}
+
+func (cl *Client) startTLS() (res *Response, err error) {
+ req := []byte("STARTTLS\r\n")
+ res, err = cl.SendCommand(req)
+ if err != nil {
+ return nil, err
+ }
+
+ if res.Code != StatusReady {
+ return nil, fmt.Errorf("smtp: STARTTLS response %d, want 220",
+ res.Code)
+ }
+
+ tlsConfig := &tls.Config{
+ ServerName: cl.serverName,
+ InsecureSkipVerify: cl.insecure, // nolint: gosec
+ }
+
+ cl.conn = tls.Client(cl.conn, tlsConfig)
+
+ return res, nil
+}
diff --git a/lib/smtp/client_test.go b/lib/smtp/client_test.go
index 0bf15ddc..84d81a99 100644
--- a/lib/smtp/client_test.go
+++ b/lib/smtp/client_test.go
@@ -8,7 +8,6 @@ import (
"encoding/base64"
"net"
"testing"
- "time"
"github.com/shuLhan/share/lib/test"
)
@@ -20,18 +19,18 @@ func TestNewClient(t *testing.T) {
expErr string
}{{
desc: "With invalid IP",
- raddr: "!",
+ raddr: "smtp://1234",
expErr: "lookup !: no such host",
}, {
desc: "With no MX",
- raddr: "example.com",
+ raddr: "smtp://example.com",
expErr: "",
}}
for _, c := range cases {
t.Log(c.desc)
- _, err := NewClient(c.raddr)
+ _, err := NewClient(c.raddr, true)
if err != nil {
test.Assert(t, "error", c.expErr, err.Error(), true)
}
@@ -39,14 +38,12 @@ func TestNewClient(t *testing.T) {
}
func TestConnect(t *testing.T) {
- time.Sleep(1 * time.Second)
-
expRes := &Response{
Code: 220,
Message: testServer.Env.PrimaryDomain.Name,
}
- res, err := testClient.Connect(true)
+ res, err := testClient.Connect()
if err != nil {
t.Fatal(err)
}
@@ -149,12 +146,12 @@ func TestAuth(t *testing.T) {
}
func TestAuth2(t *testing.T) {
- cl, err := NewClient(testTLSAddress)
+ cl, err := NewClient(testClientSMTPAddress, true)
if err != nil {
t.Fatal(err)
}
- _, err = cl.Connect(true)
+ _, err = cl.Connect()
if err != nil {
t.Fatal(err)
}
diff --git a/lib/smtp/smtp_test.go b/lib/smtp/smtp_test.go
index 55b9e42d..23fe3ec5 100644
--- a/lib/smtp/smtp_test.go
+++ b/lib/smtp/smtp_test.go
@@ -13,10 +13,11 @@ import (
)
const (
- testAddress = "127.0.0.1:2525"
- testDomain = "mail.kilabit.local"
- testPassword = "secret"
- testTLSAddress = "127.0.0.1:2533"
+ testAddress = "127.0.0.1:2525"
+ testDomain = "mail.kilabit.local"
+ testPassword = "secret"
+ testTLSAddress = "127.0.0.1:2533"
+ testClientSMTPAddress = "smtp://127.0.0.1:2525"
)
var (
@@ -68,7 +69,7 @@ func TestMain(m *testing.M) {
}
}()
- testClient, err = NewClient(testTLSAddress)
+ testClient, err = NewClient(testClientSMTPAddress, true)
if err != nil {
log.Fatal(err)
}