summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2022-02-18 15:44:47 +0700
committerShulhan <ms@kilabit.info>2022-02-18 15:45:59 +0700
commitd539f4db0c075b4f4087757ba5d96a678dc33962 (patch)
tree5a4867e2d7ea06696def5a39e6b46d854ccfa641
parent7743eb30823bdee702fa1dbcedd2e5e621a543e3 (diff)
downloadpakakeh.go-d539f4db0c075b4f4087757ba5d96a678dc33962.tar.xz
lib/smtp: refactoring NewClient to use struct instead of parameters
Previously, to create new client one must pass three parameters to NewClient function: localName, remoteURL, and insecure. If we want to add another parameters in the future, it will cause the function signature changes. This changes simplify creating NewClient by passing single struct with new parameters: AuthUser, AuthPass, and AuthMechanism. If both AuthUser and AuthPass is not empty, the NewClient will authenticate the connection, minimize number of step on the caller.
-rw-r--r--cmd/smtpcli/client.go13
-rw-r--r--lib/smtp/client.go86
-rw-r--r--lib/smtp/client_options.go47
-rw-r--r--lib/smtp/client_test.go15
-rw-r--r--lib/smtp/smtp_test.go17
5 files changed, 124 insertions, 54 deletions
diff --git a/cmd/smtpcli/client.go b/cmd/smtpcli/client.go
index d3cb12bc..3281362e 100644
--- a/cmd/smtpcli/client.go
+++ b/cmd/smtpcli/client.go
@@ -10,6 +10,7 @@ import (
"io/ioutil"
"log"
"os"
+ "strings"
"github.com/shuLhan/share/lib/smtp"
)
@@ -28,6 +29,12 @@ type client struct {
}
func newClient(remoteURL string) (cli *client, err error) {
+ var (
+ clientOpts = smtp.ClientOptions{
+ ServerUrl: remoteURL,
+ }
+ )
+
cli = &client{
remoteURL: remoteURL,
input: make([]byte, 0, 128),
@@ -36,7 +43,7 @@ func newClient(remoteURL string) (cli *client, err error) {
return
}
- cli.con, err = smtp.NewClient("", remoteURL, false)
+ cli.con, err = smtp.NewClient(clientOpts)
if err != nil {
return nil, err
}
@@ -45,7 +52,7 @@ func newClient(remoteURL string) (cli *client, err error) {
}
func (cli *client) handleInput() (isQuit bool) {
- input := bytes.ToLower(bytes.TrimSpace(cli.input))
+ input := bytes.TrimSpace(cli.input)
var (
res *smtp.Response
@@ -53,7 +60,7 @@ func (cli *client) handleInput() (isQuit bool) {
)
ins := bytes.Split(input, []byte{' '})
- cmd := string(ins[0])
+ cmd := strings.ToLower(string(ins[0]))
switch cmd {
case "":
res, err = cli.con.SendCommand(noop)
diff --git a/lib/smtp/client.go b/lib/smtp/client.go
index 041d8f15..1068fb5e 100644
--- a/lib/smtp/client.go
+++ b/lib/smtp/client.go
@@ -24,18 +24,19 @@ import (
// Client for SMTP.
//
type Client struct {
+ opts ClientOptions
+
// ServerInfo contains the server information, from the response of
// EHLO command.
ServerInfo *ServerInfo
conn net.Conn
- raddr *net.TCPAddr
+ raddr net.TCPAddr
serverName string
data []byte
buf bytes.Buffer
- insecure bool
isTLS bool
isStartTLS bool
}
@@ -43,51 +44,36 @@ type Client struct {
//
// NewClient create and initialize connection to remote SMTP server.
//
-// The localName define the client domain address, used when issuing EHLO
-// command to server. If its empty, it will set to current operating system's
-// hostname.
-//
-// The remoteURL use the following format,
-//
-// remoteURL = [ scheme "://" ](domain | IP-address [":" port])
-// scheme = "smtp" / "smtps" / "smtp+starttls"
-//
-// 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).
-// If scheme is "smtp+starttls" and no port is given, client will connect to
-// remote address at port 587.
+// When connected, the client send implicit EHLO command issued to server
+// immediately.
+// If scheme is "smtp+starttls", the connection automatically upgraded to
+// TLS after EHLO command success.
//
-// The "insecure" parameter, if set to true, will disable verifying
-// remote certificate when connecting with TLS or STARTTLS.
-//
-// On success, it will return connected client, with implicit EHLO command
-// issued to server immediately. If scheme is "smtp+starttls", the connection
-// also automatically upgraded to TLS after EHLO command success.
+// If both AuthUser and AuthPass in the ClientOptions is not empty, the client
+// will try to authenticate to remote server.
//
// On fail, it will return nil client with an error.
//
-func NewClient(localName, remoteURL string, insecure bool) (cl *Client, err error) {
+func NewClient(opts ClientOptions) (cl *Client, err error) {
var (
- logp = "NewClient"
- rurl *url.URL
- port uint16
- scheme string
+ logp = "NewClient"
+
+ res *Response
+ rurl *url.URL
+ port uint16
)
- rurl, err = url.Parse(remoteURL)
+ rurl, err = url.Parse(opts.ServerUrl)
if err != nil {
- return nil, fmt.Errorf("smtp: %s: %w", logp, err)
+ return nil, fmt.Errorf("%s: %w", logp, err)
}
cl = &Client{
- raddr: &net.TCPAddr{},
- insecure: insecure,
+ opts: opts,
}
- scheme = strings.ToLower(rurl.Scheme)
- switch scheme {
+ rurl.Scheme = strings.ToLower(rurl.Scheme)
+ switch rurl.Scheme {
case "smtp":
port = 25
case "smtps":
@@ -97,18 +83,17 @@ func NewClient(localName, remoteURL string, insecure bool) (cl *Client, err erro
port = 587
cl.isStartTLS = true
default:
- return nil, fmt.Errorf("smtp: %s: invalid scheme %q", logp, scheme)
+ return nil, fmt.Errorf("%s: invalid server URL scheme", logp)
}
cl.serverName, cl.raddr.IP, port = libnet.ParseIPPort(rurl.Host, port)
if cl.raddr.IP == nil {
cl.raddr.IP, err = lookup(cl.serverName)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("%s: %w", logp, err)
}
if cl.raddr.IP == nil {
- err = fmt.Errorf("smtp: %s: '%s' does not have MX record or IP address", logp, cl.serverName)
- return nil, err
+ return nil, fmt.Errorf("%s: %q does not have MX record or IP address", logp, cl.serverName)
}
}
@@ -116,16 +101,29 @@ func NewClient(localName, remoteURL string, insecure bool) (cl *Client, err erro
cl.raddr.Port = int(port)
if debug.Value >= 3 {
- fmt.Printf("smtp: %s: remote address '%v'\n", logp, cl.raddr)
+ fmt.Printf("%s: remote address is %v\n", logp, cl.raddr)
}
- _, err = cl.connect(localName)
+ _, err = cl.connect(opts.LocalName)
if err != nil {
return nil, fmt.Errorf("%s: %w", logp, err)
}
if debug.Value >= 3 {
- fmt.Printf("smtp: %s: ServerInfo: %+v\n", logp, cl.ServerInfo)
+ fmt.Printf("%s: ServerInfo: %+v\n", logp, cl.ServerInfo)
+ }
+
+ if len(opts.AuthUser) == 0 || len(opts.AuthPass) == 0 {
+ // Do not authenticate this connection, yet.
+ return cl, nil
+ }
+
+ res, err = cl.Authenticate(cl.opts.AuthMechanism, cl.opts.AuthUser, cl.opts.AuthPass)
+ if err != nil {
+ return nil, fmt.Errorf("%s: %w", logp, err)
+ }
+ if res.Code != StatusAuthenticated {
+ return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message)
}
return cl, nil
@@ -161,7 +159,7 @@ func (cl *Client) Authenticate(mech SaslMechanism, username, password string) (
func (cl *Client) connect(localName string) (res *Response, err error) {
logp := "connect"
- cl.conn, err = net.DialTCP("tcp", nil, cl.raddr)
+ cl.conn, err = net.DialTCP("tcp", nil, &cl.raddr)
if err != nil {
return nil, err
}
@@ -169,7 +167,7 @@ func (cl *Client) connect(localName string) (res *Response, err error) {
if cl.isTLS {
tlsConfig := &tls.Config{
ServerName: cl.serverName,
- InsecureSkipVerify: cl.insecure,
+ InsecureSkipVerify: cl.opts.Insecure,
}
cl.conn = tls.Client(cl.conn, tlsConfig)
@@ -514,7 +512,7 @@ func (cl *Client) StartTLS() (res *Response, err error) {
tlsConfig := &tls.Config{
ServerName: cl.serverName,
- InsecureSkipVerify: cl.insecure,
+ InsecureSkipVerify: cl.opts.Insecure,
}
cl.conn = tls.Client(cl.conn, tlsConfig)
diff --git a/lib/smtp/client_options.go b/lib/smtp/client_options.go
new file mode 100644
index 00000000..fbdf2dd3
--- /dev/null
+++ b/lib/smtp/client_options.go
@@ -0,0 +1,47 @@
+// Copyright 2022, Shulhan <ms@kilabit.info>. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package smtp
+
+//
+// ClientOptions contains all options to create new client.
+//
+type ClientOptions struct {
+ // LocalName define the client domain address, used when issuing EHLO
+ // command to server.
+ // If its empty, it will set to current operating system's
+ // hostname.
+ // The LocalName only has effect when client is connecting from
+ // server-to-server.
+ LocalName string
+
+ // ServerUrl use the following format,
+ //
+ // ServerUrl = [ scheme "://" ](domain | IP-address)[":" port]
+ // scheme = "smtp" / "smtps" / "smtp+starttls"
+ //
+ // 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).
+ // If scheme is "smtp+starttls" and no port is given, client will
+ // connect to remote address at port 587.
+ ServerUrl string
+
+ // The user name to authenticate to remote server.
+ //
+ // AuthUser and AuthPass enable automatic authentication when creating
+ // new Client, as long as one is not empty.
+ AuthUser string
+
+ // The user password to authenticate to remote server.
+ AuthPass string
+
+ // The SASL mechanism used for authentication.
+ AuthMechanism SaslMechanism
+
+ // Insecure if set to true it will disable verifying remote certificate when
+ // connecting with TLS or STARTTLS.
+ Insecure bool
+}
diff --git a/lib/smtp/client_test.go b/lib/smtp/client_test.go
index 7f304e21..06678346 100644
--- a/lib/smtp/client_test.go
+++ b/lib/smtp/client_test.go
@@ -112,12 +112,23 @@ func TestAuth(t *testing.T) {
}
func TestAuth2(t *testing.T) {
- cl, err := NewClient("", testSMTPSAddress, true)
+ var (
+ opts = ClientOptions{
+ ServerUrl: testSMTPSAddress,
+ Insecure: true,
+ }
+
+ cl *Client
+ err error
+ cmd string
+ )
+
+ cl, err = NewClient(opts)
if err != nil {
t.Fatal(err)
}
- cmd := "AUTH PLAIN\r\n"
+ cmd = "AUTH PLAIN\r\n"
res, err := cl.SendCommand([]byte(cmd))
if err != nil {
t.Fatal(err)
diff --git a/lib/smtp/smtp_test.go b/lib/smtp/smtp_test.go
index 64090bb4..4ade34ae 100644
--- a/lib/smtp/smtp_test.go
+++ b/lib/smtp/smtp_test.go
@@ -85,19 +85,26 @@ func testRunServer() {
}
func TestMain(m *testing.M) {
- var err error
+ var (
+ opts = ClientOptions{
+ ServerUrl: testSMTPSAddress,
+ Insecure: true,
+ }
+
+ err error
+ s int
+ )
testRunServer()
time.Sleep(100 * time.Millisecond)
- testClient, err = NewClient("", testSMTPSAddress, true)
+ testClient, err = NewClient(opts)
if err != nil {
- log.Fatal("NewClient: " + err.Error())
+ log.Fatal(err.Error())
}
- s := m.Run()
-
+ s = m.Run()
os.Exit(s)
}