Skip to content

Commit

Permalink
Implement Logger interface
Browse files Browse the repository at this point in the history
As stated in #102 (comment) 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.
  • Loading branch information
wneessen committed Feb 3, 2023
1 parent 5ceede6 commit 6633591
Show file tree
Hide file tree
Showing 7 changed files with 278 additions and 5 deletions.
23 changes: 23 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"strings"
"time"

"github.com/wneessen/go-mail/log"
"github.com/wneessen/go-mail/smtp"
)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand Down
27 changes: 27 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"testing"
"time"

"github.com/wneessen/go-mail/log"
"github.com/wneessen/go-mail/smtp"
)

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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) {
Expand Down
14 changes: 14 additions & 0 deletions log/log.go
Original file line number Diff line number Diff line change
@@ -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 changes: 74 additions & 0 deletions log/stdlog.go
Original file line number Diff line number Diff line change
@@ -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 changes: 89 additions & 0 deletions log/stdlog_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
22 changes: 17 additions & 5 deletions smtp/smtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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...)
}
}

Expand Down
34 changes: 34 additions & 0 deletions smtp/smtp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ import (
"io"
"net"
"net/textproto"
"os"
"runtime"
"strings"
"testing"
"time"

"github.com/wneessen/go-mail/log"
)

type authTest struct {
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 6633591

Please sign in to comment.