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 4.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. // Copyright (c) 2020 Shivaram Lingamneni
  2. // released under the MIT license
  3. package email
  4. import (
  5. "bytes"
  6. "errors"
  7. "fmt"
  8. "net"
  9. "regexp"
  10. "strings"
  11. "time"
  12. "github.com/ergochat/ergo/irc/custime"
  13. "github.com/ergochat/ergo/irc/smtp"
  14. "github.com/ergochat/ergo/irc/utils"
  15. )
  16. var (
  17. ErrBlacklistedAddress = errors.New("Email address is blacklisted")
  18. ErrInvalidAddress = errors.New("Email address is invalid")
  19. ErrNoMXRecord = errors.New("Couldn't resolve MX record")
  20. )
  21. type MTAConfig struct {
  22. Server string
  23. Port int
  24. Username string
  25. Password string
  26. ImplicitTLS bool `yaml:"implicit-tls"`
  27. }
  28. type MailtoConfig struct {
  29. // legacy config format assumed the use of an MTA/smarthost,
  30. // so server, port, etc. appear directly at top level
  31. // XXX: see https://github.com/go-yaml/yaml/issues/63
  32. MTAConfig `yaml:",inline"`
  33. Enabled bool
  34. Sender string
  35. HeloDomain string `yaml:"helo-domain"`
  36. RequireTLS bool `yaml:"require-tls"`
  37. VerifyMessageSubject string `yaml:"verify-message-subject"`
  38. DKIM DKIMConfig
  39. MTAReal MTAConfig `yaml:"mta"`
  40. BlacklistRegexes []string `yaml:"blacklist-regexes"`
  41. blacklistRegexes []*regexp.Regexp
  42. Timeout time.Duration
  43. PasswordReset struct {
  44. Enabled bool
  45. Cooldown custime.Duration
  46. Timeout custime.Duration
  47. } `yaml:"password-reset"`
  48. }
  49. func (config *MailtoConfig) Postprocess(heloDomain string) (err error) {
  50. if config.Sender == "" {
  51. return errors.New("Invalid mailto sender address")
  52. }
  53. // check for MTA config fields at top level,
  54. // copy to MTAReal if present
  55. if config.Server != "" && config.MTAReal.Server == "" {
  56. config.MTAReal = config.MTAConfig
  57. }
  58. if config.HeloDomain == "" {
  59. config.HeloDomain = heloDomain
  60. }
  61. for _, reg := range config.BlacklistRegexes {
  62. compiled, err := regexp.Compile(fmt.Sprintf("^%s$", reg))
  63. if err != nil {
  64. return err
  65. }
  66. config.blacklistRegexes = append(config.blacklistRegexes, compiled)
  67. }
  68. if config.MTAConfig.Server != "" {
  69. // smarthost, nothing more to validate
  70. return nil
  71. }
  72. return config.DKIM.Postprocess()
  73. }
  74. // are we sending email directly, as opposed to deferring to an MTA?
  75. func (config *MailtoConfig) DirectSendingEnabled() bool {
  76. return config.MTAReal.Server == ""
  77. }
  78. // get the preferred MX record hostname, "" on error
  79. func lookupMX(domain string) (server string) {
  80. var minPref uint16
  81. results, err := net.LookupMX(domain)
  82. if err != nil {
  83. return
  84. }
  85. for _, result := range results {
  86. if minPref == 0 || result.Pref < minPref {
  87. server, minPref = result.Host, result.Pref
  88. }
  89. }
  90. return
  91. }
  92. func ComposeMail(config MailtoConfig, recipient, subject string) (message bytes.Buffer) {
  93. fmt.Fprintf(&message, "From: %s\r\n", config.Sender)
  94. fmt.Fprintf(&message, "To: %s\r\n", recipient)
  95. dkimDomain := config.DKIM.Domain
  96. if dkimDomain != "" {
  97. fmt.Fprintf(&message, "Message-ID: <%s@%s>\r\n", utils.GenerateSecretKey(), dkimDomain)
  98. }
  99. fmt.Fprintf(&message, "Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z))
  100. fmt.Fprintf(&message, "Subject: %s\r\n", subject)
  101. message.WriteString("\r\n") // blank line: end headers, begin message body
  102. return message
  103. }
  104. func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
  105. for _, reg := range config.blacklistRegexes {
  106. if reg.MatchString(recipient) {
  107. return ErrBlacklistedAddress
  108. }
  109. }
  110. if config.DKIM.Domain != "" {
  111. msg, err = DKIMSign(msg, config.DKIM)
  112. if err != nil {
  113. return
  114. }
  115. }
  116. var addr string
  117. var auth smtp.Auth
  118. var implicitTLS bool
  119. if !config.DirectSendingEnabled() {
  120. addr = fmt.Sprintf("%s:%d", config.MTAReal.Server, config.MTAReal.Port)
  121. if config.MTAReal.Username != "" && config.MTAReal.Password != "" {
  122. auth = smtp.PlainAuth("", config.MTAReal.Username, config.MTAReal.Password, config.MTAReal.Server)
  123. }
  124. implicitTLS = config.MTAReal.ImplicitTLS
  125. } else {
  126. idx := strings.IndexByte(recipient, '@')
  127. if idx == -1 {
  128. return ErrInvalidAddress
  129. }
  130. mx := lookupMX(recipient[idx+1:])
  131. if mx == "" {
  132. return ErrNoMXRecord
  133. }
  134. addr = fmt.Sprintf("%s:smtp", mx)
  135. }
  136. return smtp.SendMail(
  137. addr, auth, config.HeloDomain, config.Sender, []string{recipient}, msg,
  138. config.RequireTLS, implicitTLS, config.Timeout,
  139. )
  140. }