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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. // Copyright (c) 2020 Shivaram Lingamneni
  2. // released under the MIT license
  3. package email
  4. import (
  5. "bufio"
  6. "bytes"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "net"
  11. "os"
  12. "regexp"
  13. "strings"
  14. "time"
  15. "github.com/ergochat/ergo/irc/custime"
  16. "github.com/ergochat/ergo/irc/smtp"
  17. "github.com/ergochat/ergo/irc/utils"
  18. )
  19. var (
  20. ErrBlacklistedAddress = errors.New("Email address is blacklisted")
  21. ErrInvalidAddress = errors.New("Email address is invalid")
  22. ErrNoMXRecord = errors.New("Couldn't resolve MX record")
  23. )
  24. type BlacklistSyntax uint
  25. const (
  26. BlacklistSyntaxGlob BlacklistSyntax = iota
  27. BlacklistSyntaxRegexp
  28. )
  29. func blacklistSyntaxFromString(status string) (BlacklistSyntax, error) {
  30. switch strings.ToLower(status) {
  31. case "glob", "":
  32. return BlacklistSyntaxGlob, nil
  33. case "re", "regex", "regexp":
  34. return BlacklistSyntaxRegexp, nil
  35. default:
  36. return BlacklistSyntaxRegexp, fmt.Errorf("Unknown blacklist syntax type `%s`", status)
  37. }
  38. }
  39. func (bs *BlacklistSyntax) UnmarshalYAML(unmarshal func(interface{}) error) error {
  40. var orig string
  41. var err error
  42. if err = unmarshal(&orig); err != nil {
  43. return err
  44. }
  45. if result, err := blacklistSyntaxFromString(orig); err == nil {
  46. *bs = result
  47. return nil
  48. } else {
  49. return err
  50. }
  51. }
  52. type MTAConfig struct {
  53. Server string
  54. Port int
  55. Username string
  56. Password string
  57. ImplicitTLS bool `yaml:"implicit-tls"`
  58. }
  59. type MailtoConfig struct {
  60. // legacy config format assumed the use of an MTA/smarthost,
  61. // so server, port, etc. appear directly at top level
  62. // XXX: see https://github.com/go-yaml/yaml/issues/63
  63. MTAConfig `yaml:",inline"`
  64. Enabled bool
  65. Sender string
  66. HeloDomain string `yaml:"helo-domain"`
  67. RequireTLS bool `yaml:"require-tls"`
  68. VerifyMessageSubject string `yaml:"verify-message-subject"`
  69. DKIM DKIMConfig
  70. MTAReal MTAConfig `yaml:"mta"`
  71. AddressBlacklist []string `yaml:"address-blacklist"`
  72. AddressBlacklistSyntax BlacklistSyntax `yaml:"address-blacklist-syntax"`
  73. AddressBlacklistFile string `yaml:"address-blacklist-file"`
  74. blacklistRegexes []*regexp.Regexp
  75. Timeout time.Duration
  76. PasswordReset struct {
  77. Enabled bool
  78. Cooldown custime.Duration
  79. Timeout custime.Duration
  80. } `yaml:"password-reset"`
  81. }
  82. func (config *MailtoConfig) compileBlacklistEntry(source string) (re *regexp.Regexp, err error) {
  83. if config.AddressBlacklistSyntax == BlacklistSyntaxGlob {
  84. return utils.CompileGlob(source, false)
  85. } else {
  86. return regexp.Compile(fmt.Sprintf("^%s$", source))
  87. }
  88. }
  89. func (config *MailtoConfig) processBlacklistFile(filename string) (result []*regexp.Regexp, err error) {
  90. f, err := os.Open(filename)
  91. if err != nil {
  92. return
  93. }
  94. defer f.Close()
  95. reader := bufio.NewReader(f)
  96. lineNo := 0
  97. for {
  98. line, err := reader.ReadString('\n')
  99. lineNo++
  100. line = strings.TrimSpace(line)
  101. if line != "" && line[0] != '#' {
  102. if compiled, compileErr := config.compileBlacklistEntry(line); compileErr == nil {
  103. result = append(result, compiled)
  104. } else {
  105. return result, fmt.Errorf("Failed to compile line %d of blacklist-regex-file `%s`: %w", lineNo, line, compileErr)
  106. }
  107. }
  108. switch err {
  109. case io.EOF:
  110. return result, nil
  111. case nil:
  112. continue
  113. default:
  114. return result, err
  115. }
  116. }
  117. }
  118. func (config *MailtoConfig) Postprocess(heloDomain string) (err error) {
  119. if config.Sender == "" {
  120. return errors.New("Invalid mailto sender address")
  121. }
  122. // check for MTA config fields at top level,
  123. // copy to MTAReal if present
  124. if config.Server != "" && config.MTAReal.Server == "" {
  125. config.MTAReal = config.MTAConfig
  126. }
  127. if config.HeloDomain == "" {
  128. config.HeloDomain = heloDomain
  129. }
  130. if config.AddressBlacklistFile != "" {
  131. config.blacklistRegexes, err = config.processBlacklistFile(config.AddressBlacklistFile)
  132. if err != nil {
  133. return err
  134. }
  135. } else if len(config.AddressBlacklist) != 0 {
  136. config.blacklistRegexes = make([]*regexp.Regexp, 0, len(config.AddressBlacklist))
  137. for _, reg := range config.AddressBlacklist {
  138. compiled, err := config.compileBlacklistEntry(reg)
  139. if err != nil {
  140. return err
  141. }
  142. config.blacklistRegexes = append(config.blacklistRegexes, compiled)
  143. }
  144. }
  145. if config.MTAConfig.Server != "" {
  146. // smarthost, nothing more to validate
  147. return nil
  148. }
  149. return config.DKIM.Postprocess()
  150. }
  151. // are we sending email directly, as opposed to deferring to an MTA?
  152. func (config *MailtoConfig) DirectSendingEnabled() bool {
  153. return config.MTAReal.Server == ""
  154. }
  155. // get the preferred MX record hostname, "" on error
  156. func lookupMX(domain string) (server string) {
  157. var minPref uint16
  158. results, err := net.LookupMX(domain)
  159. if err != nil {
  160. return
  161. }
  162. for _, result := range results {
  163. if minPref == 0 || result.Pref < minPref {
  164. server, minPref = result.Host, result.Pref
  165. }
  166. }
  167. return
  168. }
  169. func ComposeMail(config MailtoConfig, recipient, subject string) (message bytes.Buffer) {
  170. fmt.Fprintf(&message, "From: %s\r\n", config.Sender)
  171. fmt.Fprintf(&message, "To: %s\r\n", recipient)
  172. dkimDomain := config.DKIM.Domain
  173. if dkimDomain != "" {
  174. fmt.Fprintf(&message, "Message-ID: <%s@%s>\r\n", utils.GenerateSecretKey(), dkimDomain)
  175. } else {
  176. // #2108: send Message-ID even if dkim is not enabled
  177. fmt.Fprintf(&message, "Message-ID: <%s-%s>\r\n", utils.GenerateSecretKey(), config.Sender)
  178. }
  179. fmt.Fprintf(&message, "Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z))
  180. fmt.Fprintf(&message, "Subject: %s\r\n", subject)
  181. message.WriteString("\r\n") // blank line: end headers, begin message body
  182. return message
  183. }
  184. func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
  185. recipientLower := strings.ToLower(recipient)
  186. for _, reg := range config.blacklistRegexes {
  187. if reg.MatchString(recipientLower) {
  188. return ErrBlacklistedAddress
  189. }
  190. }
  191. if config.DKIM.Domain != "" {
  192. msg, err = DKIMSign(msg, config.DKIM)
  193. if err != nil {
  194. return
  195. }
  196. }
  197. var addr string
  198. var auth smtp.Auth
  199. var implicitTLS bool
  200. if !config.DirectSendingEnabled() {
  201. addr = fmt.Sprintf("%s:%d", config.MTAReal.Server, config.MTAReal.Port)
  202. if config.MTAReal.Username != "" && config.MTAReal.Password != "" {
  203. auth = smtp.PlainAuth("", config.MTAReal.Username, config.MTAReal.Password, config.MTAReal.Server)
  204. }
  205. implicitTLS = config.MTAReal.ImplicitTLS
  206. } else {
  207. idx := strings.IndexByte(recipient, '@')
  208. if idx == -1 {
  209. return ErrInvalidAddress
  210. }
  211. mx := lookupMX(recipient[idx+1:])
  212. if mx == "" {
  213. return ErrNoMXRecord
  214. }
  215. addr = fmt.Sprintf("%s:smtp", mx)
  216. }
  217. return smtp.SendMail(
  218. addr, auth, config.HeloDomain, config.Sender, []string{recipient}, msg,
  219. config.RequireTLS, implicitTLS, config.Timeout,
  220. )
  221. }