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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  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. Protocol string `yaml:"protocol"`
  69. LocalAddress string `yaml:"local-address"`
  70. localAddress net.Addr
  71. VerifyMessageSubject string `yaml:"verify-message-subject"`
  72. DKIM DKIMConfig
  73. MTAReal MTAConfig `yaml:"mta"`
  74. AddressBlacklist []string `yaml:"address-blacklist"`
  75. AddressBlacklistSyntax BlacklistSyntax `yaml:"address-blacklist-syntax"`
  76. AddressBlacklistFile string `yaml:"address-blacklist-file"`
  77. blacklistRegexes []*regexp.Regexp
  78. Timeout time.Duration
  79. PasswordReset struct {
  80. Enabled bool
  81. Cooldown custime.Duration
  82. Timeout custime.Duration
  83. } `yaml:"password-reset"`
  84. }
  85. func (config *MailtoConfig) compileBlacklistEntry(source string) (re *regexp.Regexp, err error) {
  86. if config.AddressBlacklistSyntax == BlacklistSyntaxGlob {
  87. return utils.CompileGlob(source, false)
  88. } else {
  89. return regexp.Compile(fmt.Sprintf("^%s$", source))
  90. }
  91. }
  92. func (config *MailtoConfig) processBlacklistFile(filename string) (result []*regexp.Regexp, err error) {
  93. f, err := os.Open(filename)
  94. if err != nil {
  95. return
  96. }
  97. defer f.Close()
  98. reader := bufio.NewReader(f)
  99. lineNo := 0
  100. for {
  101. line, err := reader.ReadString('\n')
  102. lineNo++
  103. line = strings.TrimSpace(line)
  104. if line != "" && line[0] != '#' {
  105. if compiled, compileErr := config.compileBlacklistEntry(line); compileErr == nil {
  106. result = append(result, compiled)
  107. } else {
  108. return result, fmt.Errorf("Failed to compile line %d of blacklist-regex-file `%s`: %w", lineNo, line, compileErr)
  109. }
  110. }
  111. switch err {
  112. case io.EOF:
  113. return result, nil
  114. case nil:
  115. continue
  116. default:
  117. return result, err
  118. }
  119. }
  120. }
  121. func (config *MailtoConfig) Postprocess(heloDomain string) (err error) {
  122. if config.Sender == "" {
  123. return errors.New("Invalid mailto sender address")
  124. }
  125. // check for MTA config fields at top level,
  126. // copy to MTAReal if present
  127. if config.Server != "" && config.MTAReal.Server == "" {
  128. config.MTAReal = config.MTAConfig
  129. }
  130. if config.HeloDomain == "" {
  131. config.HeloDomain = heloDomain
  132. }
  133. if config.AddressBlacklistFile != "" {
  134. config.blacklistRegexes, err = config.processBlacklistFile(config.AddressBlacklistFile)
  135. if err != nil {
  136. return err
  137. }
  138. } else if len(config.AddressBlacklist) != 0 {
  139. config.blacklistRegexes = make([]*regexp.Regexp, 0, len(config.AddressBlacklist))
  140. for _, reg := range config.AddressBlacklist {
  141. compiled, err := config.compileBlacklistEntry(reg)
  142. if err != nil {
  143. return err
  144. }
  145. config.blacklistRegexes = append(config.blacklistRegexes, compiled)
  146. }
  147. }
  148. config.Protocol = strings.ToLower(config.Protocol)
  149. if config.Protocol == "" {
  150. config.Protocol = "tcp"
  151. }
  152. if !(config.Protocol == "tcp" || config.Protocol == "tcp4" || config.Protocol == "tcp6") {
  153. return fmt.Errorf("Invalid protocol for email sending: `%s`", config.Protocol)
  154. }
  155. if config.LocalAddress != "" {
  156. ipAddr := net.ParseIP(config.LocalAddress)
  157. if ipAddr == nil {
  158. return fmt.Errorf("Could not parse local-address for email sending: `%s`", config.LocalAddress)
  159. }
  160. config.localAddress = &net.TCPAddr{
  161. IP: ipAddr,
  162. Port: 0,
  163. }
  164. }
  165. if config.MTAConfig.Server != "" {
  166. // smarthost, nothing more to validate
  167. return nil
  168. }
  169. return config.DKIM.Postprocess()
  170. }
  171. // are we sending email directly, as opposed to deferring to an MTA?
  172. func (config *MailtoConfig) DirectSendingEnabled() bool {
  173. return config.MTAReal.Server == ""
  174. }
  175. // get the preferred MX record hostname, "" on error
  176. func lookupMX(domain string) (server string) {
  177. var minPref uint16
  178. results, err := net.LookupMX(domain)
  179. if err != nil {
  180. return
  181. }
  182. for _, result := range results {
  183. if minPref == 0 || result.Pref < minPref {
  184. server, minPref = result.Host, result.Pref
  185. }
  186. }
  187. return
  188. }
  189. func ComposeMail(config MailtoConfig, recipient, subject string) (message bytes.Buffer) {
  190. fmt.Fprintf(&message, "From: %s\r\n", config.Sender)
  191. fmt.Fprintf(&message, "To: %s\r\n", recipient)
  192. dkimDomain := config.DKIM.Domain
  193. if dkimDomain != "" {
  194. fmt.Fprintf(&message, "Message-ID: <%s@%s>\r\n", utils.GenerateSecretKey(), dkimDomain)
  195. } else {
  196. // #2108: send Message-ID even if dkim is not enabled
  197. fmt.Fprintf(&message, "Message-ID: <%s-%s>\r\n", utils.GenerateSecretKey(), config.Sender)
  198. }
  199. fmt.Fprintf(&message, "Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z))
  200. fmt.Fprintf(&message, "Subject: %s\r\n", subject)
  201. message.WriteString("\r\n") // blank line: end headers, begin message body
  202. return message
  203. }
  204. func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
  205. recipientLower := strings.ToLower(recipient)
  206. for _, reg := range config.blacklistRegexes {
  207. if reg.MatchString(recipientLower) {
  208. return ErrBlacklistedAddress
  209. }
  210. }
  211. if config.DKIM.Domain != "" {
  212. msg, err = DKIMSign(msg, config.DKIM)
  213. if err != nil {
  214. return
  215. }
  216. }
  217. var addr string
  218. var auth smtp.Auth
  219. var implicitTLS bool
  220. if !config.DirectSendingEnabled() {
  221. addr = fmt.Sprintf("%s:%d", config.MTAReal.Server, config.MTAReal.Port)
  222. if config.MTAReal.Username != "" && config.MTAReal.Password != "" {
  223. auth = smtp.PlainAuth("", config.MTAReal.Username, config.MTAReal.Password, config.MTAReal.Server)
  224. }
  225. implicitTLS = config.MTAReal.ImplicitTLS
  226. } else {
  227. idx := strings.IndexByte(recipient, '@')
  228. if idx == -1 {
  229. return ErrInvalidAddress
  230. }
  231. mx := lookupMX(recipient[idx+1:])
  232. if mx == "" {
  233. return ErrNoMXRecord
  234. }
  235. addr = fmt.Sprintf("%s:smtp", mx)
  236. }
  237. return smtp.SendMail(
  238. addr, auth, config.HeloDomain, config.Sender, []string{recipient}, msg,
  239. config.RequireTLS, implicitTLS, config.Protocol, config.localAddress, config.Timeout,
  240. )
  241. }