mirror of
https://github.com/wneessen/go-mail.git
synced 2024-12-22 18:50:37 +01:00
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:
parent
5ceede61b6
commit
6633591b51
7 changed files with 278 additions and 5 deletions
23
client.go
23
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)
|
||||
}
|
||||
|
|
|
@ -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
14
log/log.go
Normal 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
74
log/stdlog.go
Normal 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
89
log/stdlog_test.go
Normal 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")
|
||||
}
|
||||
}
|
22
smtp/smtp.go
22
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...)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue