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.
This commit is contained in:
Winni Neessen 2023-02-03 10:19:26 +01:00
parent 5ceede61b6
commit 6633591b51
Signed by: wneessen
GPG key ID: 5F3AF39B820C119D
7 changed files with 278 additions and 5 deletions

View file

@ -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)
}

View file

@ -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) {

14
log/log.go Normal file
View file

@ -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{})
}

74
log/stdlog.go Normal file
View file

@ -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...))
}
}

89
log/stdlog_test.go Normal file
View file

@ -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")
}
}

View file

@ -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.
@ -53,7 +54,7 @@ type Client struct {
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
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...)
}
}

View file

@ -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