From 6633591b510ff65ab5446aa3e6029af1022bfbee Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 3 Feb 2023 10:19:26 +0100 Subject: [PATCH 1/2] Implement Logger interface As stated in https://github.com/wneessen/go-mail/pull/102#issuecomment-1411956040 it would be beneficial if, instead of forcing the Go stdlib logger on the user to provide a simple interface and use that for logging purposes. This PR implements this simple log.Logger interface as well as a standard logger that satisfies this interface. If no custom logger is provided, the Stdlog will be used (which makes use of the Go stdlib again). Accordingly, a `Client.WithLogger` and `Client.SetLogger` have been implemented. Same applies for the smtp counterparts. --- client.go | 23 ++++++++++++ client_test.go | 27 ++++++++++++++ log/log.go | 14 ++++++++ log/stdlog.go | 74 ++++++++++++++++++++++++++++++++++++++ log/stdlog_test.go | 89 ++++++++++++++++++++++++++++++++++++++++++++++ smtp/smtp.go | 22 +++++++++--- smtp/smtp_test.go | 34 ++++++++++++++++++ 7 files changed, 278 insertions(+), 5 deletions(-) create mode 100644 log/log.go create mode 100644 log/stdlog.go create mode 100644 log/stdlog_test.go diff --git a/client.go b/client.go index f0c143a..bc3ac60 100644 --- a/client.go +++ b/client.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "github.com/wneessen/go-mail/log" "github.com/wneessen/go-mail/smtp" ) @@ -133,6 +134,9 @@ type Client struct { // dl enables the debug logging on the SMTP client dl bool + + // l is a logger that implements the log.Logger interface + l log.Logger } // Option returns a function that can be used for grouping Client options @@ -252,6 +256,14 @@ func WithDebugLog() Option { } } +// WithLogger overrides the default log.Logger that is used for debug logging +func WithLogger(l log.Logger) Option { + return func(c *Client) error { + c.l = l + return nil + } +} + // WithHELO tells the client to use the provided string as HELO/EHLO greeting host func WithHELO(h string) Option { return func(c *Client) error { @@ -417,6 +429,14 @@ func (c *Client) SetDebugLog(v bool) { } } +// SetLogger tells the Client which log.Logger to use +func (c *Client) SetLogger(l log.Logger) { + c.l = l + if c.sc != nil { + c.sc.SetLogger(l) + } +} + // SetTLSConfig overrides the current *tls.Config with the given *tls.Config value func (c *Client) SetTLSConfig(co *tls.Config) error { if co == nil { @@ -481,6 +501,9 @@ func (c *Client) DialWithContext(pc context.Context) error { if err != nil { return err } + if c.l != nil { + c.sc.SetLogger(c.l) + } if c.dl { c.sc.SetDebugLog(true) } diff --git a/client_test.go b/client_test.go index 56a3879..b407f55 100644 --- a/client_test.go +++ b/client_test.go @@ -15,6 +15,7 @@ import ( "testing" "time" + "github.com/wneessen/go-mail/log" "github.com/wneessen/go-mail/smtp" ) @@ -106,6 +107,7 @@ func TestNewClientWithOptions(t *testing.T) { {"WithDSNRcptNotifyType() wrong option", WithDSNRcptNotifyType("FAIL"), true}, {"WithoutNoop()", WithoutNoop(), false}, {"WithDebugLog()", WithDebugLog(), false}, + {"WithLogger()", WithLogger(log.New(os.Stderr, log.LevelDebug)), false}, { "WithDSNRcptNotifyType() NEVER combination", @@ -567,6 +569,31 @@ func TestClient_DialWithContext_Debug(t *testing.T) { } } +// TestClient_DialWithContext_Debug_custom tests the DialWithContext method for the Client +// object with debug logging enabled and a custom logger on the SMTP client +func TestClient_DialWithContext_Debug_custom(t *testing.T) { + c, err := getTestClient(true) + if err != nil { + t.Skipf("failed to create test client: %s. Skipping tests", err) + } + ctx := context.Background() + if err := c.DialWithContext(ctx); err != nil { + t.Errorf("failed to dial with context: %s", err) + return + } + if c.co == nil { + t.Errorf("DialWithContext didn't fail but no connection found.") + } + if c.sc == nil { + t.Errorf("DialWithContext didn't fail but no SMTP client found.") + } + c.SetDebugLog(true) + c.SetLogger(log.New(os.Stderr, log.LevelDebug)) + if err := c.Close(); err != nil { + t.Errorf("failed to close connection: %s", err) + } +} + // TestClient_DialWithContextInvalidHost tests the DialWithContext method with intentional breaking // for the Client object func TestClient_DialWithContextInvalidHost(t *testing.T) { diff --git a/log/log.go b/log/log.go new file mode 100644 index 0000000..a40f540 --- /dev/null +++ b/log/log.go @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright (c) 2022-2023 The go-mail Authors +// +// SPDX-License-Identifier: MIT + +// Package log implements a logger interface that can be used within the go-mail package +package log + +// Logger is the log interface for go-mail +type Logger interface { + Errorf(format string, v ...interface{}) + Warnf(format string, v ...interface{}) + Infof(format string, v ...interface{}) + Debugf(format string, v ...interface{}) +} diff --git a/log/stdlog.go b/log/stdlog.go new file mode 100644 index 0000000..bc7bf26 --- /dev/null +++ b/log/stdlog.go @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright (c) 2023 The go-mail Authors +// +// SPDX-License-Identifier: MIT + +package log + +import ( + "fmt" + "io" + "log" +) + +// Level is a type wrapper for an int +type Level int + +// Stdlog is the default logger that satisfies the Logger interface +type Stdlog struct { + l Level + err *log.Logger + warn *log.Logger + info *log.Logger + debug *log.Logger +} + +const ( + // LevelError is the Level for only ERROR log messages + LevelError Level = iota + // LevelWarn is the Level for WARN and higher log messages + LevelWarn + // LevelInfo is the Level for INFO and higher log messages + LevelInfo + // LevelDebug is the Level for DEBUG and higher log messages + LevelDebug +) + +// New returns a new Stdlog type that satisfies the Logger interface +func New(o io.Writer, l Level) *Stdlog { + lf := log.Lmsgprefix | log.LstdFlags + return &Stdlog{ + l: l, + err: log.New(o, "ERROR: ", lf), + warn: log.New(o, " WARN: ", lf), + info: log.New(o, " INFO: ", lf), + debug: log.New(o, "DEBUG: ", lf), + } +} + +// Debugf performs a Printf() on the debug logger +func (l *Stdlog) Debugf(f string, v ...interface{}) { + if l.l >= LevelDebug { + _ = l.debug.Output(2, fmt.Sprintf(f, v...)) + } +} + +// Infof performs a Printf() on the info logger +func (l *Stdlog) Infof(f string, v ...interface{}) { + if l.l >= LevelInfo { + _ = l.info.Output(2, fmt.Sprintf(f, v...)) + } +} + +// Warnf performs a Printf() on the warn logger +func (l *Stdlog) Warnf(f string, v ...interface{}) { + if l.l >= LevelWarn { + _ = l.warn.Output(2, fmt.Sprintf(f, v...)) + } +} + +// Errorf performs a Printf() on the error logger +func (l *Stdlog) Errorf(f string, v ...interface{}) { + if l.l >= LevelError { + _ = l.err.Output(2, fmt.Sprintf(f, v...)) + } +} diff --git a/log/stdlog_test.go b/log/stdlog_test.go new file mode 100644 index 0000000..ec21472 --- /dev/null +++ b/log/stdlog_test.go @@ -0,0 +1,89 @@ +package log + +import ( + "bytes" + "strings" + "testing" +) + +func TestNew(t *testing.T) { + var b bytes.Buffer + l := New(&b, LevelDebug) + if l.l != LevelDebug { + t.Error("Expected level to be LevelDebug, got ", l.l) + } + if l.err == nil || l.warn == nil || l.info == nil || l.debug == nil { + t.Error("Loggers not initialized") + } +} + +func TestDebugf(t *testing.T) { + var b bytes.Buffer + l := New(&b, LevelDebug) + + l.Debugf("test %s", "foo") + expected := "DEBUG: test foo\n" + if !strings.HasSuffix(b.String(), expected) { + t.Errorf("Expected %q, got %q", expected, b.String()) + } + + b.Reset() + l.l = LevelInfo + l.Debugf("test %s", "foo") + if b.String() != "" { + t.Error("Debug message was not expected to be logged") + } +} + +func TestInfof(t *testing.T) { + var b bytes.Buffer + l := New(&b, LevelInfo) + + l.Infof("test %s", "foo") + expected := " INFO: test foo\n" + if !strings.HasSuffix(b.String(), expected) { + t.Errorf("Expected %q, got %q", expected, b.String()) + } + + b.Reset() + l.l = LevelWarn + l.Infof("test %s", "foo") + if b.String() != "" { + t.Error("Info message was not expected to be logged") + } +} + +func TestWarnf(t *testing.T) { + var b bytes.Buffer + l := New(&b, LevelWarn) + + l.Warnf("test %s", "foo") + expected := " WARN: test foo\n" + if !strings.HasSuffix(b.String(), expected) { + t.Errorf("Expected %q, got %q", expected, b.String()) + } + + b.Reset() + l.l = LevelError + l.Warnf("test %s", "foo") + if b.String() != "" { + t.Error("Warn message was not expected to be logged") + } +} + +func TestErrorf(t *testing.T) { + var b bytes.Buffer + l := New(&b, LevelError) + + l.Errorf("test %s", "foo") + expected := "ERROR: test foo\n" + if !strings.HasSuffix(b.String(), expected) { + t.Errorf("Expected %q, got %q", expected, b.String()) + } + b.Reset() + l.l = LevelError - 1 + l.Warnf("test %s", "foo") + if b.String() != "" { + t.Error("Error message was not expected to be logged") + } +} diff --git a/smtp/smtp.go b/smtp/smtp.go index 9b5f9aa..b4d1fdc 100644 --- a/smtp/smtp.go +++ b/smtp/smtp.go @@ -26,11 +26,12 @@ import ( "errors" "fmt" "io" - "log" "net" "net/textproto" "os" "strings" + + "github.com/wneessen/go-mail/log" ) // A Client represents a client connection to an SMTP server. @@ -52,8 +53,8 @@ type Client struct { didHello bool // whether we've said HELO/EHLO helloError error // the error from the hello // debug logging - debug bool // debug logging is enabled - logger *log.Logger // logger will be used for debug logging + debug bool // debug logging is enabled + logger log.Logger // logger will be used for debug logging // DSN support dsnmrtype string // dsnmrtype defines the mail return option in case DSN is enabled dsnrntype string // dsnrntype defines the recipient notify option in case DSN is enabled @@ -441,12 +442,23 @@ func (c *Client) Quit() error { func (c *Client) SetDebugLog(v bool) { c.debug = v if v { - c.logger = log.New(os.Stderr, "[DEBUG] ", log.LstdFlags|log.Lmsgprefix) + if c.logger == nil { + c.logger = log.New(os.Stderr, log.LevelDebug) + } return } c.logger = nil } +// SetLogger overrides the default log.Stdlog for the debug logging with a logger that +// satisfies the log.Logger interface +func (c *Client) SetLogger(l log.Logger) { + if l == nil { + return + } + c.logger = l +} + // SetDSNMailReturnOption sets the DSN mail return option for the Mail method func (c *Client) SetDSNMailReturnOption(d string) { c.dsnmrtype = d @@ -465,7 +477,7 @@ func (c *Client) debugLog(d logDirection, f string, a ...interface{}) { p = "C --> S:" } fs := fmt.Sprintf("%s %s", p, f) - c.logger.Printf(fs, a...) + c.logger.Debugf(fs, a...) } } diff --git a/smtp/smtp_test.go b/smtp/smtp_test.go index 2dbfde8..40b41cb 100644 --- a/smtp/smtp_test.go +++ b/smtp/smtp_test.go @@ -23,10 +23,13 @@ import ( "io" "net" "net/textproto" + "os" "runtime" "strings" "testing" "time" + + "github.com/wneessen/go-mail/log" ) type authTest struct { @@ -661,6 +664,37 @@ func TestClient_SetDebugLog(t *testing.T) { } } +// TestClient_SetLogger tests the Client method with the Client.SetLogger method +// to provide a custom logger +func TestClient_SetLogger(t *testing.T) { + server := strings.Join(strings.Split(newClientServer, "\n"), "\r\n") + + var cmdbuf strings.Builder + bcmdbuf := bufio.NewWriter(&cmdbuf) + out := func() string { + if err := bcmdbuf.Flush(); err != nil { + t.Errorf("failed to flush: %s", err) + } + return cmdbuf.String() + } + var fake faker + fake.ReadWriter = bufio.NewReadWriter(bufio.NewReader(strings.NewReader(server)), bcmdbuf) + c, err := NewClient(fake, "fake.host") + if err != nil { + t.Fatalf("NewClient: %v\n(after %v)", err, out()) + } + defer func() { + _ = c.Close() + }() + c.SetLogger(log.New(os.Stderr, log.LevelDebug)) + if c.logger == nil { + t.Errorf("Expected Logger to be set but received nil") + } + c.logger.Debugf("test") + c.SetLogger(nil) + c.logger.Debugf("test") +} + var newClientServer = `220 hello world 250-mx.google.com at your service 250-SIZE 35651584 From 14be29818d02db256fa0ba917cf6f895daae719d Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Fri, 3 Feb 2023 10:21:27 +0100 Subject: [PATCH 2/2] Add REUSE header --- log/stdlog_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/log/stdlog_test.go b/log/stdlog_test.go index ec21472..efaf00b 100644 --- a/log/stdlog_test.go +++ b/log/stdlog_test.go @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: Copyright (c) 2023 The go-mail Authors +// +// SPDX-License-Identifier: MIT + package log import (