Skip to content

Commit

Permalink
Breaking Change: functional options, New interfacenames
Browse files Browse the repository at this point in the history
  • Loading branch information
mschneider82 committed Feb 6, 2020
1 parent 1247c53 commit 63a5cca
Show file tree
Hide file tree
Showing 12 changed files with 380 additions and 127 deletions.
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# milter

This is a fork of github.com/phalaaxx/milter, added the following pull requests:
This is a heavily fork of github.com/phalaaxx/milter, added the following pull requests:

* https://github.com/phalaaxx/milter/pull/16
* https://github.com/phalaaxx/milter/pull/14
Expand All @@ -21,4 +21,13 @@ and also _test cases using my [milterclient](https://github.com/mschneider82/mil
* Changed: EnvFrom and RcptTo addresses are now always converted to lowercase
* Added all Protocol Options and Actions from libmilter (session.go)
* Added SymListFactory to Set the list of macros that the milter wants to receive from the MTA for a protocol stage
* Refactored to common consts names for Milter Commands
* Refactored to common consts names for Milter Commands

### Breaking Changes for v2:

* Breaking API Changes.
* Changed to functional options
* New Interface names
* Added RequestMacros (SetSymlist) to MilterFactory
* See example_test.go and server_test.go howto use this Library.
* Added DefaultSession as basic implementation (can be used or not.)
42 changes: 38 additions & 4 deletions consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,42 @@ const (
SMFIM_EOH /* 6 = end of header */
)

// Macro http://www.postfix.org/MILTER_README.html#macros
type Macro string

const (
MACRO_QUEUEID Macro = "i" // DATA, EOH, EOM - Queue ID, also Postfix queue file name
MACRO_MYHOSTNAME Macro = "j" // Always - Value of myhostname
MACRO_VALIDCLIENTNAME Macro = "_" // Always - The validated client name and address
MACRO_AUTH_SASL_LOGINNAME Macro = "{auth_authen}" // MAIL, DATA, EOH, EOM - SASL login name
MACRO_AUTH_SASL_SENDER Macro = "{auth_author}" // MAIL, DATA, EOH, EOM - SASL sender
MACRO_AUTH_SASL_LOGINMETHOD Macro = "{auth_type}" // MAIL, DATA, EOH, EOM - SASL login method
MACRO_REMOTECLIENTIP Macro = "{client_addr}" // Always - Remote client IP address
MACRO_CLIENT_CONNECTIONS Macro = "{client_connections}" // CONNECT - Connection concurrency for this client (zero if the client is excluded from all smtpd_client_* limits).
MACRO_CLIENT_NAME Macro = "{client_name}" // Always - Remote client hostname address → name lookup or name → address verification fails: "unknown"
MACRO_CLIENT_TCPPORT Macro = "{client_port}" // Always (Postfix ≥2.5) - Remote client TCP port
MACRO_CLIENT_PTR Macro = "{client_ptr}" // CONNECT, HELO, MAIL, DATA - Client name from address → name lookup address → name lookup fails: "unknown"
MACRO_CLIENT_TLS_CERT_ISSUER Macro = "{cert_issuer}" // HELO, MAIL, DATA, EOH, EOM - TLS client certificate issuer
MACRO_CLIENT_TLS_CERT_SUBJECT Macro = "{cert_subject}" // HELO, MAIL, DATA, EOH, EOM - TLS client certificate subject
MACRO_CLIENT_TLS_CIPHER_BITS Macro = "{cipher_bits}" // HELO, MAIL, DATA, EOH, EOM - TLS session key size
MACRO_CLIENT_TLS_CIPHER Macro = "{cipher}" // HELO, MAIL, DATA, EOH, EOM - TLS cipher
MACRO_DAEMON_ADDR Macro = "{daemon_addr}" // Always (Postfix ≥3.2) - Local server IP address
MACRO_DAEMON_NAME Macro = "{daemon_name}" // Always - value of milter_macro_daemon_name
MACRO_DAEMON_PORT Macro = "{daemon_port}" // Always (Postfix ≥3.2) -Local server TCP port
MACRO_MAIL_ADDR Macro = "{mail_addr}" // MAIL - Sender address
MACRO_MAIL_HOST Macro = "{mail_host}" // MAIL (Postfix ≥ 2.6, only with smtpd_milters) - Sender next-hop destination
MACRO_MAIL_MAILER Macro = "{mail_mailer}" // MAIL (Postfix ≥ 2.6, only with smtpd_milters) - Sender mail delivery transport
MACRO_RCPT_ADDR Macro = "{rcpt_addr}" // RCPT - Recipient address with rejected recipient: descriptive text
MACRO_RCPT_HOST Macro = "{rcpt_host}" // RCPT (Postfix ≥ 2.6, only with smtpd_milters) - Recipient next-hop destination with rejected recipient: enhanced status code
MACRO_RCPT_MAILER Macro = "{rcpt_mailer}" // RCPT (Postfix ≥ 2.6, only with smtpd_milters) - Recipient mail delivery transport With Protocol Stage: rejected recipient: "error"
MACRO_TLS_VERSION Macro = "{tls_version}" // HELO, MAIL, DATA, EOH, EOM - TLS protocol version
MACRO_V Macro = "v" // Always - value of milter_macro_v (default: $mail_name $mail_version)
)

// OptAction sets which actions the milter wants to perform.
// Multiple options can be set using a bitmask.
type OptAction uint32

// OptProtocol masks out unwanted parts of the SMTP transaction.
// Multiple options can be set using a bitmask.
type OptProtocol uint32

const (
// set which actions the milter wants to perform
OptNone OptAction = 0x00 /* SMFIF_NONE no flags */
Expand All @@ -86,7 +114,13 @@ const (
OptSetSymList OptAction = 0x100 /* SMFIF_SETSYMLIST filter can send set of symbols (macros) that it wants */
// OptAllActions SMFI_CURR_ACTS Set of all actions in the current milter version */
OptAllActions OptAction = OptAddHeader | OptChangeBody | OptAddRcpt | OptRemoveRcpt | OptChangeHeader | OptQuarantine | OptChangeFrom | OptAddRcptPartial | OptSetSymList
)

// OptProtocol masks out unwanted parts of the SMTP transaction.
// Multiple options can be set using a bitmask.
type OptProtocol uint32

const (
// mask out unwanted parts of the SMTP transaction
OptNoConnect OptProtocol = 0x01 /* SMFIP_NOCONNECT MTA should not send connect info */
OptNoHelo OptProtocol = 0x02 /* SMFIP_NOHELO MTA should not send HELO info */
Expand Down
94 changes: 94 additions & 0 deletions defaultsession.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package milter

import (
"bytes"
"fmt"
"net"
"net/textproto"
)

// A DefaultSession can be used as a basic implementation for SessionHandler Interface
// It has already a From and Rcpts[] field, mostly used to embett in your own
// Session struct.
type DefaultSession struct {
SID string // Session ID
MID string // Mail ID
From string
ClientName string
HeloName string
ClientIP net.IP
Rcpts []string
MessageHeaders textproto.MIMEHeader
Message *bytes.Buffer
}

// https://github.com/mschneider82/milter/blob/master/interface.go
func (e *DefaultSession) Init(sid, mid string) {
e.SID, e.MID = sid, mid
e.Message = new(bytes.Buffer)
return
}

func (e *DefaultSession) Disconnect() {
return
}

func (e *DefaultSession) Connect(name, family string, port uint16, ip net.IP, m *Modifier) (Response, error) {
e.ClientName = name
e.ClientIP = ip
return RespContinue, nil
}

func (e *DefaultSession) Helo(name string, m *Modifier) (Response, error) {
e.HeloName = name
return RespContinue, nil
}

func (e *DefaultSession) MailFrom(from string, m *Modifier) (Response, error) {
e.From = from
return RespContinue, nil
}

func (e *DefaultSession) RcptTo(rcptTo string, m *Modifier) (Response, error) {
e.Rcpts = append(e.Rcpts, rcptTo)
return RespContinue, nil
}

/* handle headers one by one */
func (e *DefaultSession) Header(name, value string, m *Modifier) (Response, error) {
headerLine := fmt.Sprintf("%s: %s\r\n", name, value)
if _, err := e.Message.WriteString(headerLine); err != nil {
return nil, err
}
return RespContinue, nil
}

// emptyLine is needed between Headers and Body
const emptyLine = "\r\n"

/* at end of headers initialize message buffer and add headers to it */
func (e *DefaultSession) Headers(headers textproto.MIMEHeader, m *Modifier) (Response, error) {
// return accept if not a multipart message
e.MessageHeaders = headers

if _, err := e.Message.WriteString(emptyLine); err != nil {
return nil, err
}
// continue with milter processing
return RespContinue, nil
}

// accept body chunk
func (e *DefaultSession) BodyChunk(chunk []byte, m *Modifier) (Response, error) {
// save chunk to buffer
if _, err := e.Message.Write(chunk); err != nil {
return nil, err
}
return RespContinue, nil
}

/* Body is called when email message body has been sent */
func (e *DefaultSession) Body(m *Modifier) (Response, error) {
// accept message by default
return RespAccept, nil
}
1 change: 1 addition & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ import (
var (
ErrCloseSession = errors.New("Stop current milter processing")
ErrMacroNoData = errors.New("Macro definition with no data")
ErrNoListenAddr = errors.New("no listen addr specified")
)
48 changes: 48 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package milter_test

import (
"fmt"
"io/ioutil"
"log"

"github.com/mschneider82/milter"
)

// A Session embetted the SessionHandler Interface
type Session struct {
milter.DefaultSession
}

func (s *Session) Body(m *milter.Modifier) (milter.Response, error) {
b, _ := ioutil.ReadAll(s.Message)
log.Printf("Mail's first 100 chars: %s", string(b[0:100]))
return milter.RespAccept, nil
}

func ExampleRun() {
panichandler := func(e error) {
fmt.Printf("Panic happend: %s\n", e.Error())
}

setsymlist := make(milter.RequestMacros)
setsymlist[milter.SMFIM_CONNECT] = []milter.Macro{milter.MACRO_DAEMON_NAME, milter.Macro("{client_addr}")}

milterfactory := func() (milter.SessionHandler, milter.OptAction, milter.OptProtocol, milter.RequestMacros) {
return &Session{},
milter.OptAllActions, // BitMask for wanted Actions
0, // BitMask for unwanted SMTP Parts; 0 = nothing to opt out
setsymlist // optional: can be nil
}

m := milter.New(milterfactory,
milter.WithTCPListener("127.0.0.1:12349"),
milter.WithLogger(milter.StdOutLogger),
milter.WithPanicHandler(panichandler),
)
err := m.Run()
if err != nil {
log.Fatalf("Error: %s", err.Error())
}

defer m.Close()
}
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ github.com/mschneider82/milter v1.1.2/go.mod h1:tO5zYqjX2wmwfver/a2lKFVXrNGauvUY
github.com/mschneider82/milterclient v0.0.0-20180417152208-081e1cb2de4b/go.mod h1:aT17FNxMNvVNjk2Kvpz6Dzpbjw2s5GTPiTOjTj7uNWI=
github.com/mschneider82/milterclient v0.0.0-20191210092858-6c7cf5b778f0 h1:YNVPhcCZzpyeC+bEMcjUYPO0x2FcBisUAZ64AuKOFVw=
github.com/mschneider82/milterclient v0.0.0-20191210092858-6c7cf5b778f0/go.mod h1:lMwEdshxgvrMcN+Ok0mTvY6MAdZSR8CYydBQnvbSLTI=
github.com/phalaaxx/milter v0.0.0-20190809081256-3fbd661ff459 h1:5kK/Cr8N1Ir1QEmdqjZyBDAIQQCMBBNsRwdJR7jVD80=
github.com/phalaaxx/milter v0.0.0-20190809081256-3fbd661ff459/go.mod h1:CCDGSeT1mS/L54GyiT/VNpBYpx84PUrPU+DazSVGpOk=
4 changes: 2 additions & 2 deletions interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"net/textproto"
)

// Milter is an interface for milter callback handlers
type Milter interface {
// SessionHandler is an interface for milter callback handlers
type SessionHandler interface {
// Init is called on begin of a new Mail, before Connect() and before MailFrom()
// Can be used to Reset session state
// On MailFrom mailID is available
Expand Down
22 changes: 20 additions & 2 deletions logger.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
package milter

// Logger is a interface to inject a custom logger
type Logger interface {
import "log"

// CustomLogger is a interface to inject a custom logger
type CustomLogger interface {
Printf(format string, v ...interface{})
}

type nopLogger struct{}

func (n nopLogger) Printf(format string, v ...interface{}) {}

// NopLogger can be used to discard all logs caused by milter library
var NopLogger = CustomLogger(nopLogger{})

// StdOutLogger is the default logger used if no Logger was supplied
var StdOutLogger = CustomLogger(stdoutLogger{})

type stdoutLogger struct{}

func (s stdoutLogger) Printf(format string, v ...interface{}) {
log.Printf(format, v...)
}
73 changes: 73 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package milter

import (
"log"
"net"
)

// An Option configures a Server using the functional options paradigm
// popularized by Rob Pike.
type Option interface {
apply(*Server)
}

// An ListenerOption configures a Server using the functional options paradigm
// popularized by Rob Pike.
type ListenerOption interface {
lapply(*Server)
}

type optionFunc func(*Server)

func (f optionFunc) apply(Server *Server) { f(Server) }

type loptionFunc func(*Server)

func (f loptionFunc) lapply(Server *Server) { f(Server) }

// WithLogger adds an Logger
func WithLogger(l CustomLogger) Option {
return optionFunc(func(server *Server) {
server.logger = l
})
}

// WithListener adds an Listener
func WithListener(listener net.Listener) ListenerOption {
return loptionFunc(func(server *Server) {
server.listener = listener
})
}

// WithTCPListener e.g. "127.0.0.1:12349"
func WithTCPListener(address string) ListenerOption {
protocol := "tcp"
// bind to listening address
socket, err := net.Listen(protocol, address)
if err != nil {
log.Fatal(err)
}

return WithListener(socket)
}

// WithUnixSocket e.g. "/var/spool/postfix/var/run/milter/milter.sock"
// make sure that the file does not exist!
func WithUnixSocket(file string) ListenerOption {
protocol := "unix"
// bind to listening address
socket, err := net.Listen(protocol, file)
if err != nil {
log.Fatal(err)
}

return WithListener(socket)
}

// WithPanicHandler Adds the error panic handler
// Multiple panic handlers are supported
func WithPanicHandler(handler func(error)) Option {
return optionFunc(func(server *Server) {
server.errHandlers = append(server.errHandlers, handler)
})
}
Loading

0 comments on commit 63a5cca

Please sign in to comment.