diff --git a/bot/email.go b/bot/email.go index 0f46527..a2c521d 100644 --- a/bot/email.go +++ b/bot/email.go @@ -39,7 +39,7 @@ func (b *Bot) SetSendmail(sendmail func(string, string, string) error) { func (b *Bot) Sendmail(eventID id.EventID, from, to, data string) (bool, error) { err := b.sendmail(from, to, data) if err != nil { - if strings.HasPrefix(err.Error(), "45") { + if strings.HasPrefix(err.Error(), "4") { b.log.Debug("email %s (from=%s to=%s) was added to the queue: %v", eventID, from, to, err) return true, b.enqueueEmail(eventID.String(), from, to, data) } diff --git a/smtp/server.go b/smtp/server.go index 51fd685..89c332f 100644 --- a/smtp/server.go +++ b/smtp/server.go @@ -2,7 +2,6 @@ package smtp import ( "context" - "errors" "io" "strings" @@ -14,6 +13,13 @@ import ( "gitlab.com/etke.cc/postmoogle/utils" ) +// ErrBanned returned to banned hosts +var ErrBanned = &smtp.SMTPError{ + Code: 554, + EnhancedCode: smtp.EnhancedCode{5, 5, 4}, + Message: "please, don't bother me anymore, kupo.", +} + type mailServer struct { bot matrixbot log *logger.Logger @@ -24,17 +30,19 @@ type mailServer struct { func (m *mailServer) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) { m.log.Debug("Login state=%+v username=%+v", state, username) if m.bot.IsBanned(state.RemoteAddr) { - return nil, errors.New("please, don't bother me anymore") + return nil, ErrBanned } if !utils.AddressValid(username) { + m.log.Debug("address %s is invalid", username) m.bot.Ban(state.RemoteAddr) - return nil, errors.New("please, provide an email address") + return nil, ErrBanned } if !m.bot.AllowAuth(username, password) { + m.log.Debug("username=%s or password= is invalid", username) m.bot.Ban(state.RemoteAddr) - return nil, errors.New("email or password is invalid") + return nil, ErrBanned } return &outgoingSession{ @@ -51,7 +59,7 @@ func (m *mailServer) Login(state *smtp.ConnectionState, username, password strin func (m *mailServer) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) { m.log.Debug("AnonymousLogin state=%+v", state) if m.bot.IsBanned(state.RemoteAddr) { - return nil, errors.New("please, don't bother me anymore") + return nil, ErrBanned } return &incomingSession{ diff --git a/smtp/session.go b/smtp/session.go index 3d656d4..7151989 100644 --- a/smtp/session.go +++ b/smtp/session.go @@ -35,8 +35,9 @@ type incomingSession struct { func (s *incomingSession) Mail(from string, opts smtp.MailOptions) error { sentry.GetHubFromContext(s.ctx).Scope().SetTag("from", from) if !utils.AddressValid(from) { + s.log.Debug("address %s is invalid", from) s.ban(s.addr) - return errors.New("please, provide email address") + return ErrBanned } s.from = from s.log.Debug("mail from %s, options: %+v", from, opts) @@ -56,20 +57,20 @@ func (s *incomingSession) Rcpt(to string) error { if !domainok { s.log.Debug("wrong domain of %s", to) s.ban(s.addr) - return smtp.ErrAuthRequired + return ErrBanned } roomID, ok := s.getRoomID(utils.Mailbox(to)) if !ok { s.log.Debug("mapping for %s not found", to) s.ban(s.addr) - return smtp.ErrAuthRequired + return ErrBanned } validations := s.getFilters(roomID) if !validateEmail(s.from, s.to, s.log, validations) { s.ban(s.addr) - return smtp.ErrAuthRequired + return ErrBanned } s.log.Debug("mail to %s", to) diff --git a/utils/email.go b/utils/email.go index 9b3a590..f90206f 100644 --- a/utils/email.go +++ b/utils/email.go @@ -10,7 +10,7 @@ import ( "time" "github.com/emersion/go-msgauth/dkim" - "gitlab.com/etke.cc/go/secgen" + "github.com/jhillyerd/enmime" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" "maunium.net/go/mautrix/id" @@ -143,75 +143,33 @@ func (e *Email) Content(threadID id.EventID, options *ContentOptions) *event.Con // Compose converts the email object to a string (to be used for delivery via SMTP) and possibly DKIM-signs it func (e *Email) Compose(privkey string) string { + mail := enmime.Builder(). + From("", e.From). + To("", e.To). + Header("Message-Id", e.MessageID). + Subject(e.Subject). + Text([]byte(e.Text)). + HTML([]byte(e.HTML)) + if e.InReplyTo != "" { + mail = mail.Header("In-Reply-To", e.InReplyTo) + } + if e.References != "" { + mail = mail.Header("References", e.References) + } + + root, err := mail.Build() + if err != nil { + log.Error("cannot compose email: %v", err) + return "" + } var data strings.Builder + err = root.Encode(&data) + if err != nil { + log.Error("cannot encode email: %v", err) + return "" + } domain := strings.SplitN(e.From, "@", 2)[1] - boundaryName := "postmoogle" + secgen.Password(32) - boundary := "--" + boundaryName - - data.WriteString("MIME-Version: 1.0") - data.WriteString("\r\n") - - data.WriteString("Content-Type: multipart/alternative; boundary=") - data.WriteString(boundaryName) - data.WriteString("\r\n") - - data.WriteString("Content-Transfer-Encoding: 8BIT") - data.WriteString("\r\n") - - data.WriteString("From: ") - data.WriteString(e.From) - data.WriteString("\r\n") - - data.WriteString("To: ") - data.WriteString(e.To) - data.WriteString("\r\n") - - data.WriteString("Message-Id: ") - data.WriteString(e.MessageID) - data.WriteString("\r\n") - - data.WriteString("Date: ") - data.WriteString(e.Date) - data.WriteString("\r\n") - - if e.InReplyTo != "" { - data.WriteString("In-Reply-To: ") - data.WriteString(e.InReplyTo) - data.WriteString("\r\n") - } - - if e.References != "" { - data.WriteString("References: ") - data.WriteString(e.References) - data.WriteString("\r\n") - } - - data.WriteString("Subject: ") - data.WriteString(e.Subject) - data.WriteString("\r\n") - data.WriteString("\r\n") - - data.WriteString(boundary) - data.WriteString("\r\n") - data.WriteString("Content-Type: text/html; charset=\"UTF-8\"") - data.WriteString("\r\n") - data.WriteString(e.HTML) - data.WriteString("\r\n") - data.WriteString("\r\n") - - data.WriteString(boundary) - data.WriteString("\r\n") - data.WriteString("Content-Type: text/plain; charset=\"UTF-8\"") - data.WriteString("\r\n") - data.WriteString(e.Text) - data.WriteString("\r\n") - data.WriteString("\r\n") - - data.WriteString(boundary) - data.WriteString("--") - data.WriteString("\r\n") - return e.sign(domain, privkey, data) }