upgrade deps; rewrite smtp session
This commit is contained in:
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