mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-24 12:59:33 +01:00
8b2f6de3e0
* Add email-based password reset Fixes #734 * rename SETPASS to RESETPASS * review fixes * abuse mitigations * SENDPASS and RESETPASS should both touch the client login throttle * Produce a logline and a sno on SENDPASS (since it actually sends an email) * don't re-retrieve the settings value * add email confirmation for NS SET EMAIL * smtp: if require-tls is disabled, don't validate server cert * review fixes * remove cooldown for NS SET EMAIL If you accidentally set the wrong address, the cooldown would prevent you from fixing your mistake. Since we touch the registration throttle anyway, this shouldn't present more of an abuse concern than registration itself.
154 lines
4.1 KiB
Go
154 lines
4.1 KiB
Go
// Copyright (c) 2020 Shivaram Lingamneni
|
|
// released under the MIT license
|
|
|
|
package email
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/ergochat/ergo/irc/custime"
|
|
"github.com/ergochat/ergo/irc/smtp"
|
|
"github.com/ergochat/ergo/irc/utils"
|
|
)
|
|
|
|
var (
|
|
ErrBlacklistedAddress = errors.New("Email address is blacklisted")
|
|
ErrInvalidAddress = errors.New("Email address is invalid")
|
|
ErrNoMXRecord = errors.New("Couldn't resolve MX record")
|
|
)
|
|
|
|
type MTAConfig struct {
|
|
Server string
|
|
Port int
|
|
Username string
|
|
Password string
|
|
}
|
|
|
|
type MailtoConfig struct {
|
|
// legacy config format assumed the use of an MTA/smarthost,
|
|
// so server, port, etc. appear directly at top level
|
|
// XXX: see https://github.com/go-yaml/yaml/issues/63
|
|
MTAConfig `yaml:",inline"`
|
|
Enabled bool
|
|
Sender string
|
|
HeloDomain string `yaml:"helo-domain"`
|
|
RequireTLS bool `yaml:"require-tls"`
|
|
VerifyMessageSubject string `yaml:"verify-message-subject"`
|
|
DKIM DKIMConfig
|
|
MTAReal MTAConfig `yaml:"mta"`
|
|
BlacklistRegexes []string `yaml:"blacklist-regexes"`
|
|
blacklistRegexes []*regexp.Regexp
|
|
Timeout time.Duration
|
|
PasswordReset struct {
|
|
Enabled bool
|
|
Cooldown custime.Duration
|
|
Timeout custime.Duration
|
|
} `yaml:"password-reset"`
|
|
}
|
|
|
|
func (config *MailtoConfig) Postprocess(heloDomain string) (err error) {
|
|
if config.Sender == "" {
|
|
return errors.New("Invalid mailto sender address")
|
|
}
|
|
|
|
// check for MTA config fields at top level,
|
|
// copy to MTAReal if present
|
|
if config.Server != "" && config.MTAReal.Server == "" {
|
|
config.MTAReal = config.MTAConfig
|
|
}
|
|
|
|
if config.HeloDomain == "" {
|
|
config.HeloDomain = heloDomain
|
|
}
|
|
|
|
for _, reg := range config.BlacklistRegexes {
|
|
compiled, err := regexp.Compile(fmt.Sprintf("^%s$", reg))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
config.blacklistRegexes = append(config.blacklistRegexes, compiled)
|
|
}
|
|
|
|
if config.MTAConfig.Server != "" {
|
|
// smarthost, nothing more to validate
|
|
return nil
|
|
}
|
|
|
|
return config.DKIM.Postprocess()
|
|
}
|
|
|
|
// are we sending email directly, as opposed to deferring to an MTA?
|
|
func (config *MailtoConfig) DirectSendingEnabled() bool {
|
|
return config.MTAReal.Server == ""
|
|
}
|
|
|
|
// get the preferred MX record hostname, "" on error
|
|
func lookupMX(domain string) (server string) {
|
|
var minPref uint16
|
|
results, err := net.LookupMX(domain)
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, result := range results {
|
|
if minPref == 0 || result.Pref < minPref {
|
|
server, minPref = result.Host, result.Pref
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func ComposeMail(config MailtoConfig, recipient, subject string) (message bytes.Buffer) {
|
|
fmt.Fprintf(&message, "From: %s\r\n", config.Sender)
|
|
fmt.Fprintf(&message, "To: %s\r\n", recipient)
|
|
dkimDomain := config.DKIM.Domain
|
|
if dkimDomain != "" {
|
|
fmt.Fprintf(&message, "Message-ID: <%s@%s>\r\n", utils.GenerateSecretKey(), dkimDomain)
|
|
}
|
|
fmt.Fprintf(&message, "Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z))
|
|
fmt.Fprintf(&message, "Subject: %s\r\n", subject)
|
|
message.WriteString("\r\n") // blank line: end headers, begin message body
|
|
return message
|
|
}
|
|
|
|
func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
|
|
for _, reg := range config.blacklistRegexes {
|
|
if reg.MatchString(recipient) {
|
|
return ErrBlacklistedAddress
|
|
}
|
|
}
|
|
|
|
if config.DKIM.Domain != "" {
|
|
msg, err = DKIMSign(msg, config.DKIM)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
var addr string
|
|
var auth smtp.Auth
|
|
if !config.DirectSendingEnabled() {
|
|
addr = fmt.Sprintf("%s:%d", config.MTAReal.Server, config.MTAReal.Port)
|
|
if config.MTAReal.Username != "" && config.MTAReal.Password != "" {
|
|
auth = smtp.PlainAuth("", config.MTAReal.Username, config.MTAReal.Password, config.MTAReal.Server)
|
|
}
|
|
} else {
|
|
idx := strings.IndexByte(recipient, '@')
|
|
if idx == -1 {
|
|
return ErrInvalidAddress
|
|
}
|
|
mx := lookupMX(recipient[idx+1:])
|
|
if mx == "" {
|
|
return ErrNoMXRecord
|
|
}
|
|
addr = fmt.Sprintf("%s:smtp", mx)
|
|
}
|
|
|
|
return smtp.SendMail(addr, auth, config.HeloDomain, config.Sender, []string{recipient}, msg, config.RequireTLS, config.Timeout)
|
|
}
|