upgrade deps; rewrite smtp session

This commit is contained in:
Aine
2024-02-19 22:55:14 +02:00
parent 10213cc7d7
commit a01720da00
277 changed files with 106832 additions and 7641 deletions

View File

@@ -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