123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132 |
- // Copyright (c) 2020 Shivaram Lingamneni
- // released under the MIT license
-
- package email
-
- import (
- "errors"
- "fmt"
- "net"
- "regexp"
- "strings"
- "time"
-
- "github.com/ergochat/ergo/irc/smtp"
- )
-
- 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
- }
-
- 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 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)
- }
|