diff options
| author | Shulhan <ms@kilabit.info> | 2019-03-26 02:35:56 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2019-03-26 02:35:56 +0700 |
| commit | a4c0f6e3bbf0b3e418abb34acc8811f50b0f0deb (patch) | |
| tree | 5c55ac263b41404b077f98e7152e4fd7a98e72b9 | |
| parent | 4ebea25ba12d695e248519a662f74c73ec7238b0 (diff) | |
| download | pakakeh.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.go | 131 | ||||
| -rw-r--r-- | lib/smtp/client_test.go | 15 | ||||
| -rw-r--r-- | lib/smtp/smtp_test.go | 11 |
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) } |
