Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package maddy
import (
"errors"
"fmt"
"net"
"os"
"path/filepath"

Expand Down Expand Up @@ -53,13 +54,33 @@ func logOutput(_ *config.Map, node config.Node) (interface{}, error) {

func LogOutputOption(args []string) (log.Output, error) {
outs := make([]log.Output, 0, len(args))
for i, arg := range args {
for i := 0; i < len(args); i++ {
arg := args[i]
switch arg {
case "stderr":
outs = append(outs, log.WriterOutput(os.Stderr, false))
case "stderr_ts":
outs = append(outs, log.WriterOutput(os.Stderr, true))
case "syslog":
case "syslog", "syslog+tcp", "syslog+udp":
network := "udp"
if arg == "syslog+tcp" {
network = "tcp"
}
// Optional next arg: remote address in host:port form.
if i+1 < len(args) {
if _, _, err := net.SplitHostPort(args[i+1]); err == nil {
i++
syslogOut, err := log.RemoteSyslogOutput(network, args[i])
if err != nil {
return nil, fmt.Errorf("failed to connect to remote syslog at %s: %v", args[i], err)
}
outs = append(outs, syslogOut)
break
}
}
if arg != "syslog" {
return nil, fmt.Errorf("'%s' requires a remote address (host:port)", arg)
}
syslogOut, err := log.SyslogOutput()
if err != nil {
return nil, fmt.Errorf("failed to connect to syslog daemon: %v", err)
Expand Down
31 changes: 31 additions & 0 deletions framework/log/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ type Logger struct {
// Additional fields that will be added
// to the Msg output.
Fields map[string]interface{}

// LogFields are rendered as [key=value ...] before the message text so
// that individual mail flows can be identified and filtered with grep.
// Use With() to populate this.
LogFields []string
}

func (l *Logger) Zap() *zap.Logger {
Expand Down Expand Up @@ -171,6 +176,17 @@ func fieldsToMap(fields []interface{}, out map[string]interface{}) {
func (l *Logger) formatMsg(msg string, fields map[string]interface{}) string {
formatted := strings.Builder{}

if len(l.LogFields) > 0 {
formatted.WriteRune('[')
for i, f := range l.LogFields {
if i > 0 {
formatted.WriteRune(' ')
}
formatted.WriteString(f)
}
formatted.WriteString("] ")
}

formatted.WriteString(msg)
formatted.WriteRune('\t')

Expand Down Expand Up @@ -252,6 +268,21 @@ func (l *Logger) Sublogger(name string) *Logger {
}
}

// With returns a copy of the logger with additional key=value pairs prepended
// to every log message as [key=value ...]. Multiple With calls accumulate.
// Use this to attach identifiers like domain or message-ID so log lines can
// be filtered with grep.
func (l *Logger) With(kvpairs ...interface{}) *Logger {
inherited := make([]string, len(l.LogFields), len(l.LogFields)+len(kvpairs)/2)
copy(inherited, l.LogFields)
for i := 0; i+1 < len(kvpairs); i += 2 {
inherited = append(inherited, fmt.Sprintf("%v=%v", kvpairs[i], kvpairs[i+1]))
}
newL := *l
newL.LogFields = inherited
return &newL
}

// DefaultLogger is the global Logger object that is used by
// package-level logging functions.
//
Expand Down
8 changes: 8 additions & 0 deletions framework/log/syslog.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,11 @@ func SyslogOutput() (Output, error) {
w, err := syslog.New(syslog.LOG_MAIL|syslog.LOG_INFO, "maddy")
return syslogOut{w}, err
}

// RemoteSyslogOutput returns a log.Output implementation that sends
// messages to a remote syslog daemon at addr (host:port) using network
// ("udp" or "tcp").
func RemoteSyslogOutput(network, addr string) (Output, error) {
w, err := syslog.Dial(network, addr, syslog.LOG_MAIL|syslog.LOG_INFO, "maddy")
return syslogOut{w}, err
}
4 changes: 4 additions & 0 deletions framework/log/syslog_stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ import (
func SyslogOutput() (Output, error) {
return nil, errors.New("log: syslog output is not supported on windows")
}

func RemoteSyslogOutput(network, addr string) (Output, error) {
return nil, errors.New("log: syslog output is not supported on windows")
}
14 changes: 13 additions & 1 deletion internal/endpoint/smtp/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ type Session struct {
delivery module.Delivery
deliveryErr error

log *log.Logger
log *log.Logger
connLog *log.Logger // connection-scoped logger; log is temporarily replaced per message
}

func (s *Session) AuthMechanisms() []string {
Expand Down Expand Up @@ -149,6 +150,9 @@ func (s *Session) abort(ctx context.Context) {
func (s *Session) cleanSession() {
s.releaseLimits()

if s.connLog != nil {
s.log = s.connLog
}
s.mailFrom = ""
s.opts = smtp.MailOptions{}
s.msgMeta = nil
Expand Down Expand Up @@ -271,6 +275,14 @@ func (s *Session) startDelivery(ctx context.Context, from string, opts smtp.Mail
s.mailFrom = cleanFrom
s.delivery = delivery

if s.connLog != nil {
if domain != "" {
s.log = s.connLog.With("msg_id", msgMeta.ID, "from_domain", domain)
} else {
s.log = s.connLog.With("msg_id", msgMeta.ID)
}
}

return msgMeta.ID, nil
}

Expand Down
1 change: 1 addition & 0 deletions internal/endpoint/smtp/smtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ func (endp *Endpoint) newSession(conn *smtp.Conn) *Session {
s := &Session{
endp: endp,
log: endp.log,
connLog: endp.log,
sessionCtx: context.Background(),
}

Expand Down
17 changes: 10 additions & 7 deletions internal/target/delivery.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
package target

import (
"strings"

"github.com/foxcpp/maddy/framework/log"
"github.com/foxcpp/maddy/framework/module"
)

// DeliveryLogger returns a logger annotated with msg_id and from_domain so
// every log line can be filtered by message ID or sender domain.
func DeliveryLogger(parent *log.Logger, msgMeta *module.MsgMetadata) *log.Logger {
l := parent.Sublogger("")
fields := make(map[string]interface{}, len(l.Fields)+1)
for k, v := range l.Fields {
fields[k] = v
kvpairs := []interface{}{"msg_id", msgMeta.ID}
if msgMeta.OriginalFrom != "" {
if _, domain, ok := strings.Cut(msgMeta.OriginalFrom, "@"); ok && domain != "" {
kvpairs = append(kvpairs, "from_domain", domain)
}
}
fields["msg_id"] = msgMeta.ID
l.Fields = fields
return l
return parent.Sublogger("").With(kvpairs...)
}