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.1KB

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