upgrade deps; rewrite smtp session
This commit is contained in:
@@ -93,7 +93,7 @@ func NewManager(cfg *Config) *Manager {
|
||||
s.ErrorLog = loggerWrapper{func(s string, i ...any) { cfg.Logger.Error().Msgf(s, i...) }}
|
||||
s.ReadTimeout = 10 * time.Second
|
||||
s.WriteTimeout = 10 * time.Second
|
||||
s.MaxMessageBytes = cfg.MaxSize * 1024 * 1024
|
||||
s.MaxMessageBytes = int64(cfg.MaxSize * 1024 * 1024)
|
||||
s.AllowInsecureAuth = !cfg.TLSRequired
|
||||
s.EnableREQUIRETLS = cfg.TLSRequired
|
||||
s.EnableSMTPUTF8 = true
|
||||
|
||||
@@ -43,60 +43,16 @@ type mailServer struct {
|
||||
sender MailSender
|
||||
}
|
||||
|
||||
// Login used for outgoing mail submissions only (when you use postmoogle as smtp server in your scripts)
|
||||
func (m *mailServer) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
|
||||
m.log.Debug().Str("username", username).Any("state", state).Msg("Login")
|
||||
ctx := context.Background()
|
||||
if m.bot.IsBanned(ctx, state.RemoteAddr) {
|
||||
return nil, ErrBanned
|
||||
}
|
||||
|
||||
if !email.AddressValid(username) {
|
||||
m.log.Debug().Str("address", username).Msg("address is invalid")
|
||||
m.bot.BanAuth(ctx, state.RemoteAddr)
|
||||
return nil, ErrBanned
|
||||
}
|
||||
|
||||
roomID, allow := m.bot.AllowAuth(ctx, username, password)
|
||||
if !allow {
|
||||
m.log.Debug().Str("username", username).Msg("username or password is invalid")
|
||||
m.bot.BanAuth(ctx, state.RemoteAddr)
|
||||
return nil, ErrBanned
|
||||
}
|
||||
|
||||
return &outgoingSession{
|
||||
ctx: sentry.SetHubOnContext(context.Background(), sentry.CurrentHub().Clone()),
|
||||
sendmail: m.sender.Send,
|
||||
privkey: m.bot.GetDKIMprivkey(ctx),
|
||||
from: username,
|
||||
log: m.log,
|
||||
domains: m.domains,
|
||||
getRoomID: m.bot.GetMapping,
|
||||
fromRoom: roomID,
|
||||
tos: []string{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AnonymousLogin used for incoming mail submissions only
|
||||
func (m *mailServer) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
|
||||
m.log.Debug().Any("state", state).Msg("AnonymousLogin")
|
||||
ctx := context.Background()
|
||||
if m.bot.IsBanned(ctx, state.RemoteAddr) {
|
||||
return nil, ErrBanned
|
||||
}
|
||||
|
||||
return &incomingSession{
|
||||
ctx: sentry.SetHubOnContext(context.Background(), sentry.CurrentHub().Clone()),
|
||||
getRoomID: m.bot.GetMapping,
|
||||
getFilters: m.bot.GetIFOptions,
|
||||
receiveEmail: m.ReceiveEmail,
|
||||
ban: m.bot.BanAuto,
|
||||
greylisted: m.bot.IsGreylisted,
|
||||
trusted: m.bot.IsTrusted,
|
||||
log: m.log,
|
||||
domains: m.domains,
|
||||
addr: state.RemoteAddr,
|
||||
tos: []string{},
|
||||
func (m *mailServer) NewSession(con *smtp.Conn) (smtp.Session, error) {
|
||||
ctx := sentry.SetHubOnContext(context.Background(), sentry.CurrentHub().Clone())
|
||||
return &session{
|
||||
log: m.log,
|
||||
bot: m.bot,
|
||||
domains: m.domains,
|
||||
sendmail: m.sender.Send,
|
||||
conn: con,
|
||||
ctx: ctx,
|
||||
privkey: m.bot.GetDKIMprivkey(ctx),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
253
smtp/session.go
253
smtp/session.go
@@ -10,18 +10,22 @@ import (
|
||||
|
||||
"github.com/emersion/go-msgauth/dkim"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/jhillyerd/enmime"
|
||||
"github.com/rs/zerolog"
|
||||
"gitlab.com/etke.cc/go/validator"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"gitlab.com/etke.cc/postmoogle/email"
|
||||
"gitlab.com/etke.cc/postmoogle/utils"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
// GraylistCode SMTP code
|
||||
const GraylistCode = 451
|
||||
const (
|
||||
// GraylistCode SMTP code
|
||||
GraylistCode = 451
|
||||
// Incoming is the direction of the email
|
||||
Incoming = "incoming"
|
||||
// Outgoing is the direction of the email
|
||||
Outoing = "outgoing"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidEmail for invalid emails :)
|
||||
@@ -30,89 +34,106 @@ var (
|
||||
GraylistEnhancedCode = smtp.EnhancedCode{4, 5, 1}
|
||||
)
|
||||
|
||||
// incomingSession represents an SMTP-submission session receiving emails from remote servers
|
||||
type incomingSession struct {
|
||||
log *zerolog.Logger
|
||||
getRoomID func(context.Context, string) (id.RoomID, bool)
|
||||
getFilters func(context.Context, id.RoomID) email.IncomingFilteringOptions
|
||||
receiveEmail func(context.Context, *email.Email) error
|
||||
greylisted func(context.Context, net.Addr) bool
|
||||
trusted func(net.Addr) bool
|
||||
ban func(context.Context, net.Addr)
|
||||
domains []string
|
||||
roomID id.RoomID
|
||||
type session struct {
|
||||
log *zerolog.Logger
|
||||
bot matrixbot
|
||||
ctx context.Context //nolint:containedctx // that's session
|
||||
conn *smtp.Conn
|
||||
domains []string
|
||||
sendmail func(string, string, string) error
|
||||
|
||||
ctx context.Context //nolint:containedctx // that's session
|
||||
addr net.Addr
|
||||
tos []string
|
||||
from string
|
||||
dir string
|
||||
tos []string
|
||||
from string
|
||||
roomID id.RoomID
|
||||
privkey string
|
||||
fromRoom id.RoomID
|
||||
}
|
||||
|
||||
func (s *incomingSession) Mail(from string, opts smtp.MailOptions) error {
|
||||
sentry.GetHubFromContext(s.ctx).Scope().SetTag("from", from)
|
||||
if !email.AddressValid(from) {
|
||||
s.log.Debug().Str("from", from).Msg("address is invalid")
|
||||
s.ban(s.ctx, s.addr)
|
||||
func (s *session) AuthPlain(username, password string) error {
|
||||
addr := s.conn.Conn().RemoteAddr()
|
||||
if s.bot.IsBanned(s.ctx, addr) {
|
||||
return ErrBanned
|
||||
}
|
||||
s.from = email.Address(from)
|
||||
s.log.Debug().Str("from", from).Any("options", opts).Msg("incoming mail")
|
||||
if !email.AddressValid(username) {
|
||||
s.log.Debug().Str("address", username).Msg("address is invalid")
|
||||
s.bot.BanAuth(s.ctx, addr)
|
||||
return ErrBanned
|
||||
}
|
||||
roomID, allow := s.bot.AllowAuth(s.ctx, username, password)
|
||||
if !allow {
|
||||
s.log.Debug().Str("username", username).Msg("username or password is invalid")
|
||||
s.bot.BanAuth(s.ctx, addr)
|
||||
return ErrBanned
|
||||
}
|
||||
|
||||
s.dir = Outoing
|
||||
s.from = username
|
||||
s.fromRoom = roomID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *incomingSession) Rcpt(to string) error {
|
||||
sentry.GetHubFromContext(s.ctx).Scope().SetTag("to", to)
|
||||
func (s *session) Mail(from string, _ *smtp.MailOptions) error {
|
||||
if s.dir == Outoing {
|
||||
if err := s.validateOutgoingMail(from); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if !email.AddressValid(from) {
|
||||
s.log.Debug().Str("from", from).Msg("address is invalid")
|
||||
s.bot.BanAuto(s.ctx, s.conn.Conn().RemoteAddr())
|
||||
return ErrBanned
|
||||
}
|
||||
s.from = email.Address(from)
|
||||
s.log.Debug().Str("from", from).Msg("incoming mail")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *session) Rcpt(to string, _ *smtp.RcptOptions) error {
|
||||
s.tos = append(s.tos, to)
|
||||
hostname := utils.Hostname(to)
|
||||
var domainok bool
|
||||
for _, domain := range s.domains {
|
||||
if hostname == domain {
|
||||
domainok = true
|
||||
break
|
||||
s.log.Debug().Str("to", to).Msg("mail")
|
||||
if s.dir != Outoing {
|
||||
if err := s.validateIncomingRcpt(to); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !domainok {
|
||||
s.log.Debug().Str("to", to).Msg("wrong domain")
|
||||
return ErrNoUser
|
||||
}
|
||||
|
||||
var ok bool
|
||||
s.roomID, ok = s.getRoomID(s.ctx, utils.Mailbox(to))
|
||||
if !ok {
|
||||
s.log.Debug().Str("to", to).Msg("mapping not found")
|
||||
return ErrNoUser
|
||||
}
|
||||
|
||||
s.log.Debug().Str("to", to).Msg("mail")
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAddr gets real address of incoming email serder,
|
||||
// including special case of trusted proxy
|
||||
func (s *incomingSession) getAddr(envelope *enmime.Envelope) net.Addr {
|
||||
if !s.trusted(s.addr) {
|
||||
return s.addr
|
||||
func (s *session) Data(r io.Reader) error {
|
||||
if s.dir == Outoing {
|
||||
return s.outgoingData(r)
|
||||
}
|
||||
|
||||
addrHeader := envelope.GetHeader("X-Real-Addr")
|
||||
if addrHeader == "" {
|
||||
return s.addr
|
||||
}
|
||||
|
||||
host, portString, _ := net.SplitHostPort(addrHeader) //nolint:errcheck // it is real addr
|
||||
if host == "" {
|
||||
return s.addr
|
||||
}
|
||||
|
||||
var port int
|
||||
port, _ = strconv.Atoi(portString) //nolint:errcheck // it's a real addr
|
||||
|
||||
realAddr := &net.TCPAddr{IP: net.ParseIP(host), Port: port}
|
||||
s.log.Info().Str("addr", realAddr.String()).Msg("real address")
|
||||
return realAddr
|
||||
return s.incomingData(r)
|
||||
}
|
||||
|
||||
func (s *incomingSession) Data(r io.Reader) error {
|
||||
func (s *session) Reset() {}
|
||||
|
||||
func (s *session) Logout() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *session) outgoingData(r io.Reader) error {
|
||||
parser := enmime.NewParser()
|
||||
envelope, err := parser.ReadEnvelope(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
eml := email.FromEnvelope(s.tos[0], envelope)
|
||||
for _, to := range s.tos {
|
||||
eml.RcptTo = to
|
||||
err := s.sendmail(eml.From, to, eml.Compose(s.privkey))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *session) incomingData(r io.Reader) error {
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
s.log.Error().Err(err).Msg("cannot read DATA")
|
||||
@@ -126,12 +147,12 @@ func (s *incomingSession) Data(r io.Reader) error {
|
||||
}
|
||||
addr := s.getAddr(envelope)
|
||||
reader.Seek(0, io.SeekStart) //nolint:errcheck // becase we're sure that's ok
|
||||
validations := s.getFilters(s.ctx, s.roomID)
|
||||
validations := s.bot.GetIFOptions(s.ctx, s.roomID)
|
||||
if !validateIncoming(s.from, s.tos[0], addr, s.log, validations) {
|
||||
s.ban(s.ctx, addr)
|
||||
s.bot.BanAuth(s.ctx, addr)
|
||||
return ErrBanned
|
||||
}
|
||||
if s.greylisted(s.ctx, addr) {
|
||||
if s.bot.IsGreylisted(s.ctx, addr) {
|
||||
return &smtp.SMTPError{
|
||||
Code: GraylistCode,
|
||||
EnhancedCode: GraylistEnhancedCode,
|
||||
@@ -155,7 +176,7 @@ func (s *incomingSession) Data(r io.Reader) error {
|
||||
eml := email.FromEnvelope(s.tos[0], envelope)
|
||||
for _, to := range s.tos {
|
||||
eml.RcptTo = to
|
||||
err := s.receiveEmail(s.ctx, eml)
|
||||
err := s.bot.IncomingEmail(s.ctx, eml)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -163,25 +184,8 @@ func (s *incomingSession) Data(r io.Reader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *incomingSession) Reset() {}
|
||||
func (s *incomingSession) Logout() error { return nil }
|
||||
|
||||
// outgoingSession represents an SMTP-submission session sending emails from external scripts, using postmoogle as SMTP server
|
||||
type outgoingSession struct {
|
||||
log *zerolog.Logger
|
||||
sendmail func(string, string, string) error
|
||||
privkey string
|
||||
domains []string
|
||||
getRoomID func(context.Context, string) (id.RoomID, bool)
|
||||
|
||||
ctx context.Context //nolint:containedctx // that's session
|
||||
tos []string
|
||||
from string
|
||||
fromRoom id.RoomID
|
||||
}
|
||||
|
||||
func (s *outgoingSession) Mail(from string, _ smtp.MailOptions) error {
|
||||
sentry.GetHubFromContext(s.ctx).Scope().SetTag("from", from)
|
||||
// validateOutgoingMail checks if the sender is allowed to send mail
|
||||
func (s *session) validateOutgoingMail(from string) error {
|
||||
if !email.AddressValid(from) {
|
||||
return ErrInvalidEmail
|
||||
}
|
||||
@@ -198,7 +202,7 @@ func (s *outgoingSession) Mail(from string, _ smtp.MailOptions) error {
|
||||
return ErrNoUser
|
||||
}
|
||||
|
||||
roomID, ok := s.getRoomID(s.ctx, utils.Mailbox(from))
|
||||
roomID, ok := s.bot.GetMapping(s.ctx, utils.Mailbox(from))
|
||||
if !ok {
|
||||
s.log.Debug().Str("from", from).Msg("mapping not found")
|
||||
return ErrNoUser
|
||||
@@ -210,33 +214,56 @@ func (s *outgoingSession) Mail(from string, _ smtp.MailOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *outgoingSession) Rcpt(to string) error {
|
||||
sentry.GetHubFromContext(s.ctx).Scope().SetTag("to", to)
|
||||
s.tos = append(s.tos, to)
|
||||
// validateIncomingRcpt checks if the recipient is allowed to receive mail
|
||||
func (s *session) validateIncomingRcpt(to string) error {
|
||||
hostname := utils.Hostname(to)
|
||||
var domainok bool
|
||||
for _, domain := range s.domains {
|
||||
if hostname == domain {
|
||||
domainok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !domainok {
|
||||
s.log.Debug().Str("to", to).Msg("wrong domain")
|
||||
return ErrNoUser
|
||||
}
|
||||
|
||||
var ok bool
|
||||
s.roomID, ok = s.bot.GetMapping(s.ctx, utils.Mailbox(to))
|
||||
if !ok {
|
||||
s.log.Debug().Str("to", to).Msg("mapping not found")
|
||||
return ErrNoUser
|
||||
}
|
||||
|
||||
s.log.Debug().Str("to", to).Msg("mail")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *outgoingSession) Data(r io.Reader) error {
|
||||
parser := enmime.NewParser()
|
||||
envelope, err := parser.ReadEnvelope(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
eml := email.FromEnvelope(s.tos[0], envelope)
|
||||
for _, to := range s.tos {
|
||||
eml.RcptTo = to
|
||||
err := s.sendmail(eml.From, to, eml.Compose(s.privkey))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// getAddr gets real address of incoming email serder,
|
||||
// including special case of trusted proxy
|
||||
func (s *session) getAddr(envelope *enmime.Envelope) net.Addr {
|
||||
remoteAddr := s.conn.Conn().RemoteAddr()
|
||||
if !s.bot.IsTrusted(remoteAddr) {
|
||||
return remoteAddr
|
||||
}
|
||||
|
||||
return nil
|
||||
addrHeader := envelope.GetHeader("X-Real-Addr")
|
||||
if addrHeader == "" {
|
||||
return remoteAddr
|
||||
}
|
||||
|
||||
host, portString, _ := net.SplitHostPort(addrHeader) //nolint:errcheck // it is real addr
|
||||
if host == "" {
|
||||
return remoteAddr
|
||||
}
|
||||
|
||||
var port int
|
||||
port, _ = strconv.Atoi(portString) //nolint:errcheck // it's a real addr
|
||||
realAddr := &net.TCPAddr{IP: net.ParseIP(host), Port: port}
|
||||
s.log.Info().Str("addr", realAddr.String()).Msg("real address")
|
||||
return realAddr
|
||||
}
|
||||
func (s *outgoingSession) Reset() {}
|
||||
func (s *outgoingSession) Logout() error { return nil }
|
||||
|
||||
func validateIncoming(from, to string, senderAddr net.Addr, log *zerolog.Logger, options email.IncomingFilteringOptions) bool {
|
||||
var sender net.IP
|
||||
|
||||
Reference in New Issue
Block a user