123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- // Copyright (c) 2020 Shivaram Lingamneni
- // released under the MIT license
-
- package email
-
- import (
- "bufio"
- "bytes"
- "errors"
- "fmt"
- "io"
- "net"
- "os"
- "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 BlacklistSyntax uint
-
- const (
- BlacklistSyntaxGlob BlacklistSyntax = iota
- BlacklistSyntaxRegexp
- )
-
- func blacklistSyntaxFromString(status string) (BlacklistSyntax, error) {
- switch strings.ToLower(status) {
- case "glob", "":
- return BlacklistSyntaxGlob, nil
- case "re", "regex", "regexp":
- return BlacklistSyntaxRegexp, nil
- default:
- return BlacklistSyntaxRegexp, fmt.Errorf("Unknown blacklist syntax type `%s`", status)
- }
- }
-
- func (bs *BlacklistSyntax) UnmarshalYAML(unmarshal func(interface{}) error) error {
- var orig string
- var err error
- if err = unmarshal(&orig); err != nil {
- return err
- }
- if result, err := blacklistSyntaxFromString(orig); err == nil {
- *bs = result
- return nil
- } else {
- return err
- }
- }
-
- type MTAConfig struct {
- Server string
- Port int
- Username string
- Password string
- ImplicitTLS bool `yaml:"implicit-tls"`
- }
-
- 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"`
- Protocol string `yaml:"protocol"`
- LocalAddress string `yaml:"local-address"`
- localAddress net.Addr
- VerifyMessageSubject string `yaml:"verify-message-subject"`
- DKIM DKIMConfig
- MTAReal MTAConfig `yaml:"mta"`
- AddressBlacklist []string `yaml:"address-blacklist"`
- AddressBlacklistSyntax BlacklistSyntax `yaml:"address-blacklist-syntax"`
- AddressBlacklistFile string `yaml:"address-blacklist-file"`
- blacklistRegexes []*regexp.Regexp
- Timeout time.Duration
- PasswordReset struct {
- Enabled bool
- Cooldown custime.Duration
- Timeout custime.Duration
- } `yaml:"password-reset"`
- }
-
- func (config *MailtoConfig) compileBlacklistEntry(source string) (re *regexp.Regexp, err error) {
- if config.AddressBlacklistSyntax == BlacklistSyntaxGlob {
- return utils.CompileGlob(source, false)
- } else {
- return regexp.Compile(fmt.Sprintf("^%s$", source))
- }
- }
-
- func (config *MailtoConfig) processBlacklistFile(filename string) (result []*regexp.Regexp, err error) {
- f, err := os.Open(filename)
- if err != nil {
- return
- }
- defer f.Close()
- reader := bufio.NewReader(f)
- lineNo := 0
- for {
- line, err := reader.ReadString('\n')
- lineNo++
- line = strings.TrimSpace(line)
- if line != "" && line[0] != '#' {
- if compiled, compileErr := config.compileBlacklistEntry(line); compileErr == nil {
- result = append(result, compiled)
- } else {
- return result, fmt.Errorf("Failed to compile line %d of blacklist-regex-file `%s`: %w", lineNo, line, compileErr)
- }
- }
- switch err {
- case io.EOF:
- return result, nil
- case nil:
- continue
- default:
- return result, err
- }
- }
- }
-
- 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
- }
-
- if config.AddressBlacklistFile != "" {
- config.blacklistRegexes, err = config.processBlacklistFile(config.AddressBlacklistFile)
- if err != nil {
- return err
- }
- } else if len(config.AddressBlacklist) != 0 {
- config.blacklistRegexes = make([]*regexp.Regexp, 0, len(config.AddressBlacklist))
- for _, reg := range config.AddressBlacklist {
- compiled, err := config.compileBlacklistEntry(reg)
- if err != nil {
- return err
- }
- config.blacklistRegexes = append(config.blacklistRegexes, compiled)
- }
- }
-
- config.Protocol = strings.ToLower(config.Protocol)
- if config.Protocol == "" {
- config.Protocol = "tcp"
- }
- if !(config.Protocol == "tcp" || config.Protocol == "tcp4" || config.Protocol == "tcp6") {
- return fmt.Errorf("Invalid protocol for email sending: `%s`", config.Protocol)
- }
-
- if config.LocalAddress != "" {
- ipAddr := net.ParseIP(config.LocalAddress)
- if ipAddr == nil {
- return fmt.Errorf("Could not parse local-address for email sending: `%s`", config.LocalAddress)
- }
- config.localAddress = &net.TCPAddr{
- IP: ipAddr,
- Port: 0,
- }
- }
-
- 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)
- } else {
- // #2108: send Message-ID even if dkim is not enabled
- fmt.Fprintf(&message, "Message-ID: <%s-%s>\r\n", utils.GenerateSecretKey(), config.Sender)
- }
- 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) {
- recipientLower := strings.ToLower(recipient)
- for _, reg := range config.blacklistRegexes {
- if reg.MatchString(recipientLower) {
- return ErrBlacklistedAddress
- }
- }
-
- if config.DKIM.Domain != "" {
- msg, err = DKIMSign(msg, config.DKIM)
- if err != nil {
- return
- }
- }
-
- var addr string
- var auth smtp.Auth
- var implicitTLS bool
- 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)
- }
- implicitTLS = config.MTAReal.ImplicitTLS
- } 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, implicitTLS, config.Protocol, config.localAddress, config.Timeout,
- )
- }
|