You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

email.go 3.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. // Copyright (c) 2020 Shivaram Lingamneni
  2. // released under the MIT license
  3. package email
  4. import (
  5. "errors"
  6. "fmt"
  7. "net"
  8. "regexp"
  9. "strings"
  10. "github.com/ergochat/ergo/irc/smtp"
  11. )
  12. var (
  13. ErrBlacklistedAddress = errors.New("Email address is blacklisted")
  14. ErrInvalidAddress = errors.New("Email address is blacklisted")
  15. ErrNoMXRecord = errors.New("Couldn't resolve MX record")
  16. )
  17. type MTAConfig struct {
  18. Server string
  19. Port int
  20. Username string
  21. Password string
  22. }
  23. type MailtoConfig struct {
  24. // legacy config format assumed the use of an MTA/smarthost,
  25. // so server, port, etc. appear directly at top level
  26. // XXX: see https://github.com/go-yaml/yaml/issues/63
  27. MTAConfig `yaml:",inline"`
  28. Enabled bool
  29. Sender string
  30. HeloDomain string `yaml:"helo-domain"`
  31. RequireTLS bool `yaml:"require-tls"`
  32. VerifyMessageSubject string `yaml:"verify-message-subject"`
  33. DKIM DKIMConfig
  34. MTAReal MTAConfig `yaml:"mta"`
  35. BlacklistRegexes []string `yaml:"blacklist-regexes"`
  36. blacklistRegexes []*regexp.Regexp
  37. }
  38. func (config *MailtoConfig) Postprocess(heloDomain string) (err error) {
  39. if config.Sender == "" {
  40. return errors.New("Invalid mailto sender address")
  41. }
  42. // check for MTA config fields at top level,
  43. // copy to MTAReal if present
  44. if config.Server != "" && config.MTAReal.Server == "" {
  45. config.MTAReal = config.MTAConfig
  46. }
  47. if config.HeloDomain == "" {
  48. config.HeloDomain = heloDomain
  49. }
  50. for _, reg := range config.BlacklistRegexes {
  51. compiled, err := regexp.Compile(fmt.Sprintf("^%s$", reg))
  52. if err != nil {
  53. return err
  54. }
  55. config.blacklistRegexes = append(config.blacklistRegexes, compiled)
  56. }
  57. if config.MTAConfig.Server != "" {
  58. // smarthost, nothing more to validate
  59. return nil
  60. }
  61. return config.DKIM.Postprocess()
  62. }
  63. // get the preferred MX record hostname, "" on error
  64. func lookupMX(domain string) (server string) {
  65. var minPref uint16
  66. results, err := net.LookupMX(domain)
  67. if err != nil {
  68. return
  69. }
  70. for _, result := range results {
  71. if minPref == 0 || result.Pref < minPref {
  72. server, minPref = result.Host, result.Pref
  73. }
  74. }
  75. return
  76. }
  77. func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
  78. for _, reg := range config.blacklistRegexes {
  79. if reg.MatchString(recipient) {
  80. return ErrBlacklistedAddress
  81. }
  82. }
  83. if config.DKIM.Domain != "" {
  84. msg, err = DKIMSign(msg, config.DKIM)
  85. if err != nil {
  86. return
  87. }
  88. }
  89. var addr string
  90. var auth smtp.Auth
  91. if config.MTAReal.Server != "" {
  92. addr = fmt.Sprintf("%s:%d", config.MTAReal.Server, config.MTAReal.Port)
  93. if config.MTAReal.Username != "" && config.MTAReal.Password != "" {
  94. auth = smtp.PlainAuth("", config.MTAReal.Username, config.MTAReal.Password, config.MTAReal.Server)
  95. }
  96. } else {
  97. idx := strings.IndexByte(recipient, '@')
  98. if idx == -1 {
  99. return ErrInvalidAddress
  100. }
  101. mx := lookupMX(recipient[idx+1:])
  102. if mx == "" {
  103. return ErrNoMXRecord
  104. }
  105. addr = fmt.Sprintf("%s:smtp", mx)
  106. }
  107. return smtp.SendMail(addr, auth, config.HeloDomain, config.Sender, []string{recipient}, msg, config.RequireTLS)
  108. }