177 lines
3.9 KiB
Go
177 lines
3.9 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/emersion/go-message/mail"
|
|
"github.com/emersion/go-milter"
|
|
"github.com/emersion/go-smtp"
|
|
"go.uber.org/zap"
|
|
"golang.org/x/time/rate"
|
|
)
|
|
|
|
const ConnectionTimeout = 3 * time.Minute
|
|
|
|
// ErrRateLimit indicates that the message is rejected due to a rate limit.
|
|
var ErrRateLimit = &smtp.SMTPError{
|
|
Code: 450,
|
|
EnhancedCode: [3]int{4, 2, 0},
|
|
Message: "too many requests",
|
|
}
|
|
|
|
// ErrBadGateway indicates a downstream system failure.
|
|
var ErrBadGateway = &smtp.SMTPError{
|
|
Code: 451,
|
|
EnhancedCode: [3]int{4, 4, 3},
|
|
Message: "bad gateway",
|
|
}
|
|
|
|
// Backend provides a backend implementation of an SMTP server.
|
|
type Backend struct {
|
|
AllowedDomains []string
|
|
|
|
Log *zap.Logger
|
|
|
|
RootContext context.Context
|
|
|
|
RSpam *milter.Client
|
|
|
|
OverallLimiter *rate.Limiter
|
|
}
|
|
|
|
// NewSession implements smtp.Backend
|
|
func (b *Backend) NewSession(conn *smtp.Conn) (smtp.Session, error) {
|
|
ctx, cancel := context.WithTimeout(b.RootContext, ConnectionTimeout)
|
|
defer cancel()
|
|
|
|
if err := b.OverallLimiter.Wait(ctx); err != nil {
|
|
return nil, ErrRateLimit
|
|
}
|
|
|
|
rs, err := b.RSpam.Session()
|
|
if err != nil {
|
|
return nil, ErrBadGateway
|
|
}
|
|
|
|
return &Session{
|
|
b: b,
|
|
conn: conn,
|
|
rspam: rs,
|
|
}, nil
|
|
}
|
|
|
|
// Session provides an implementation of an SMTP session.
|
|
type Session struct {
|
|
b *Backend
|
|
conn *smtp.Conn
|
|
rspam *milter.ClientSession
|
|
}
|
|
|
|
// Discard currently processed message.
|
|
func (session *Session) Reset() {
|
|
panic("not implemented") // TODO: Implement
|
|
}
|
|
|
|
// Free all resources associated with session.
|
|
func (session *Session) Logout() error {
|
|
panic("not implemented") // TODO: Implement
|
|
}
|
|
|
|
// Authenticate the user using SASL PLAIN.
|
|
func (session *Session) AuthPlain(username string, password string) error {
|
|
return smtp.ErrAuthUnsupported
|
|
}
|
|
|
|
// Set return path for currently processed message.
|
|
func (session *Session) Mail(from string, opts *smtp.MailOptions) error {
|
|
action, err := session.rspam.Mail(from, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("filter failure: %w", err)
|
|
}
|
|
|
|
return parseAction(action)
|
|
}
|
|
|
|
// Add recipient for currently processed message.
|
|
func (session *Session) Rcpt(to string) error {
|
|
if err := session.validateDomain(to); err != nil {
|
|
return &smtp.SMTPError{
|
|
Code: 550,
|
|
EnhancedCode: [3]int{5, 1, 2},
|
|
Message: err.Error(),
|
|
}
|
|
}
|
|
|
|
action, err := session.rspam.Rcpt(to, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("filter failure: %w", err)
|
|
}
|
|
|
|
return parseAction(action)
|
|
}
|
|
|
|
// Set currently processed message contents and send it.
|
|
//
|
|
// r must be consumed before Data returns.
|
|
func (session *Session) Data(r io.Reader) error {
|
|
// TODO: handle the modifyAction below, whatever it is.
|
|
_, action, err := session.rspam.BodyReadFrom(r)
|
|
if err != nil {
|
|
return fmt.Errorf("filter failure: %w", err)
|
|
}
|
|
|
|
return parseAction(action)
|
|
}
|
|
|
|
func (session *Session) validateDomain(to string) error {
|
|
toAddr, err := mail.ParseAddress(to)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse address: %w", err)
|
|
}
|
|
|
|
pieces := strings.Split(toAddr.Address, "@")
|
|
if len(pieces) != 2 {
|
|
return fmt.Errorf("invalid address")
|
|
}
|
|
|
|
for _, d := range session.b.AllowedDomains {
|
|
if strings.Contains(pieces[2], d) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("invalid domain")
|
|
}
|
|
|
|
func parseAction(action *milter.Action) error {
|
|
switch action.Code {
|
|
case milter.ActAccept:
|
|
return nil
|
|
case milter.ActContinue:
|
|
return nil
|
|
case milter.ActDiscard:
|
|
return milterReject(action)
|
|
case milter.ActReject:
|
|
return milterReject(action)
|
|
case milter.ActTempFail:
|
|
return milterReject(action)
|
|
case milter.ActReplyCode:
|
|
return milterReject(action)
|
|
case milter.ActSkip:
|
|
return milterReject(action)
|
|
}
|
|
|
|
return fmt.Errorf("unhandled action code %q", action.Code)
|
|
}
|
|
|
|
func milterReject(action *milter.Action) error {
|
|
return &smtp.SMTPError{
|
|
Code: action.SMTPCode,
|
|
EnhancedCode: [3]int{4, 2, 0},
|
|
Message: action.SMTPText,
|
|
}
|
|
}
|