Merge branch 'main' into feature/107_provide-more-ways-for-middleware-to-interact-with-mail-parts

This commit is contained in:
Winni Neessen 2023-02-03 15:18:43 +01:00
commit f7e1345f3d
Signed by: wneessen
GPG key ID: 385AC9889632126E
12 changed files with 297 additions and 11 deletions

View file

@ -33,7 +33,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
go: [1.16, 1.17, 1.18, 1.19] go: [1.16, 1.17, 1.18, 1.19, '1.20']
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@master uses: actions/checkout@master
@ -42,14 +42,14 @@ jobs:
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- name: Install sendmail - name: Install sendmail
if: matrix.go == 1.19 && matrix.os == 'ubuntu-latest' if: matrix.go == '1.20' && matrix.os == 'ubuntu-latest'
run: | run: |
sudo apt-get -y install sendmail; which sendmail sudo apt-get -y install sendmail; which sendmail
- name: Run Tests - name: Run Tests
run: | run: |
go test -v -race --coverprofile=coverage.coverprofile --covermode=atomic ./... go test -v -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: success() && matrix.go == 1.19 && matrix.os == 'ubuntu-latest' if: success() && matrix.go == '1.20' && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v2 uses: codecov/codecov-action@v2
with: with:
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos

View file

@ -21,7 +21,7 @@ jobs:
steps: steps:
- uses: actions/setup-go@v3 - uses: actions/setup-go@v3
with: with:
go-version: 1.19 go-version: '1.20'
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v3

View file

@ -29,7 +29,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v3 uses: actions/setup-go@v3
with: with:
go-version: 1.19.x go-version: '1.20.x'
- name: Run unit Tests - name: Run unit Tests
run: | run: |

View file

@ -3,7 +3,7 @@
## SPDX-License-Identifier: MIT ## SPDX-License-Identifier: MIT
[run] [run]
go = "1.16" go = "1.20"
tests = true tests = true
[linters] [linters]

View file

@ -61,6 +61,15 @@ standard in a MUA.
We aim for good GoDoc documenation in our library which gives you a full API reference. We also provide a more in-depth documentation website at We aim for good GoDoc documenation in our library which gives you a full API reference. We also provide a more in-depth documentation website at
[go-mail.dev](https://go-mail.dev) [go-mail.dev](https://go-mail.dev)
## Compatibility
Go is growing fast and providing great features with every new release. While we'd love to adopt the latest Go features
into our code, we realize that not everybody using this package can run the latest Go versions. Therefore we try to
implement alternative solutions for Go versions that do not support these features. Yet, the work needed to maintain
the separate versions is not to be underestimated. For that reason, we might retire that code at some point.
We guarantee that go-mail will always support the last four releases of Go. With two Go releases per year, this gives
the user a timeframe of two years to update to the next or even the latest version of Go.
## Support ## Support
We have a support and general discussion channel on Discord. Find us at: [#go-mail](https://discord.gg/dbfQyC4s) We have a support and general discussion channel on Discord. Find us at: [#go-mail](https://discord.gg/dbfQyC4s)

View file

@ -14,6 +14,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/wneessen/go-mail/log"
"github.com/wneessen/go-mail/smtp" "github.com/wneessen/go-mail/smtp"
) )
@ -133,6 +134,9 @@ type Client struct {
// dl enables the debug logging on the SMTP client // dl enables the debug logging on the SMTP client
dl bool 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 // 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 // WithHELO tells the client to use the provided string as HELO/EHLO greeting host
func WithHELO(h string) Option { func WithHELO(h string) Option {
return func(c *Client) error { 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 // SetTLSConfig overrides the current *tls.Config with the given *tls.Config value
func (c *Client) SetTLSConfig(co *tls.Config) error { func (c *Client) SetTLSConfig(co *tls.Config) error {
if co == nil { if co == nil {
@ -481,6 +501,9 @@ func (c *Client) DialWithContext(pc context.Context) error {
if err != nil { if err != nil {
return err return err
} }
if c.l != nil {
c.sc.SetLogger(c.l)
}
if c.dl { if c.dl {
c.sc.SetDebugLog(true) c.sc.SetDebugLog(true)
} }

View file

@ -15,6 +15,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/wneessen/go-mail/log"
"github.com/wneessen/go-mail/smtp" "github.com/wneessen/go-mail/smtp"
) )
@ -106,6 +107,7 @@ func TestNewClientWithOptions(t *testing.T) {
{"WithDSNRcptNotifyType() wrong option", WithDSNRcptNotifyType("FAIL"), true}, {"WithDSNRcptNotifyType() wrong option", WithDSNRcptNotifyType("FAIL"), true},
{"WithoutNoop()", WithoutNoop(), false}, {"WithoutNoop()", WithoutNoop(), false},
{"WithDebugLog()", WithDebugLog(), false}, {"WithDebugLog()", WithDebugLog(), false},
{"WithLogger()", WithLogger(log.New(os.Stderr, log.LevelDebug)), false},
{ {
"WithDSNRcptNotifyType() NEVER combination", "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 // TestClient_DialWithContextInvalidHost tests the DialWithContext method with intentional breaking
// for the Client object // for the Client object
func TestClient_DialWithContextInvalidHost(t *testing.T) { 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...))
}
}

93
log/stdlog_test.go Normal file
View file

@ -0,0 +1,93 @@
// SPDX-FileCopyrightText: Copyright (c) 2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
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" "errors"
"fmt" "fmt"
"io" "io"
"log"
"net" "net"
"net/textproto" "net/textproto"
"os" "os"
"strings" "strings"
"github.com/wneessen/go-mail/log"
) )
// A Client represents a client connection to an SMTP server. // A Client represents a client connection to an SMTP server.
@ -53,7 +54,7 @@ type Client struct {
helloError error // the error from the hello helloError error // the error from the hello
// debug logging // debug logging
debug bool // debug logging is enabled 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 // DSN support
dsnmrtype string // dsnmrtype defines the mail return option in case DSN is enabled 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 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) { func (c *Client) SetDebugLog(v bool) {
c.debug = v c.debug = v
if 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 return
} }
c.logger = nil 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 // SetDSNMailReturnOption sets the DSN mail return option for the Mail method
func (c *Client) SetDSNMailReturnOption(d string) { func (c *Client) SetDSNMailReturnOption(d string) {
c.dsnmrtype = d c.dsnmrtype = d
@ -465,7 +477,7 @@ func (c *Client) debugLog(d logDirection, f string, a ...interface{}) {
p = "C --> S:" p = "C --> S:"
} }
fs := fmt.Sprintf("%s %s", p, f) 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" "io"
"net" "net"
"net/textproto" "net/textproto"
"os"
"runtime" "runtime"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/wneessen/go-mail/log"
) )
type authTest struct { 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 var newClientServer = `220 hello world
250-mx.google.com at your service 250-mx.google.com at your service
250-SIZE 35651584 250-SIZE 35651584