smtp.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. // Copyright (C) 2019-2022 Nicola Murino
  2. //
  3. // This program is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU Affero General Public License as published
  5. // by the Free Software Foundation, version 3.
  6. //
  7. // This program is distributed in the hope that it will be useful,
  8. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. // GNU Affero General Public License for more details.
  11. //
  12. // You should have received a copy of the GNU Affero General Public License
  13. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. // Package smtp provides supports for sending emails
  15. package smtp
  16. import (
  17. "bytes"
  18. "errors"
  19. "fmt"
  20. "html/template"
  21. "path/filepath"
  22. "time"
  23. mail "github.com/xhit/go-simple-mail/v2"
  24. "github.com/drakkan/sftpgo/v2/internal/logger"
  25. "github.com/drakkan/sftpgo/v2/internal/util"
  26. )
  27. const (
  28. logSender = "smtp"
  29. )
  30. // EmailContentType defines the support content types for email body
  31. type EmailContentType int
  32. // Supported email body content type
  33. const (
  34. EmailContentTypeTextPlain EmailContentType = iota
  35. EmailContentTypeTextHTML
  36. )
  37. const (
  38. templateEmailDir = "email"
  39. templatePasswordReset = "reset-password.html"
  40. )
  41. var (
  42. smtpServer *mail.SMTPServer
  43. from string
  44. emailTemplates = make(map[string]*template.Template)
  45. )
  46. // IsEnabled returns true if an SMTP server is configured
  47. func IsEnabled() bool {
  48. return smtpServer != nil
  49. }
  50. // Config defines the SMTP configuration to use to send emails
  51. type Config struct {
  52. // Location of SMTP email server. Leavy empty to disable email sending capabilities
  53. Host string `json:"host" mapstructure:"host"`
  54. // Port of SMTP email server
  55. Port int `json:"port" mapstructure:"port"`
  56. // From address, for example "SFTPGo <[email protected]>".
  57. // Many SMTP servers reject emails without a `From` header so, if not set,
  58. // SFTPGo will try to use the username as fallback, this may or may not be appropriate
  59. From string `json:"from" mapstructure:"from"`
  60. // SMTP username
  61. User string `json:"user" mapstructure:"user"`
  62. // SMTP password. Leaving both username and password empty the SMTP authentication
  63. // will be disabled
  64. Password string `json:"password" mapstructure:"password"`
  65. // 0 Plain
  66. // 1 Login
  67. // 2 CRAM-MD5
  68. AuthType int `json:"auth_type" mapstructure:"auth_type"`
  69. // 0 no encryption
  70. // 1 TLS
  71. // 2 start TLS
  72. Encryption int `json:"encryption" mapstructure:"encryption"`
  73. // Domain to use for HELO command, if empty localhost will be used
  74. Domain string `json:"domain" mapstructure:"domain"`
  75. // Path to the email templates. This can be an absolute path or a path relative to the config dir.
  76. // Templates are searched within a subdirectory named "email" in the specified path
  77. TemplatesPath string `json:"templates_path" mapstructure:"templates_path"`
  78. }
  79. // Initialize initialized and validates the SMTP configuration
  80. func (c *Config) Initialize(configDir string) error {
  81. smtpServer = nil
  82. if c.Host == "" {
  83. logger.Debug(logSender, "", "configuration disabled, email capabilities will not be available")
  84. return nil
  85. }
  86. if c.Port <= 0 || c.Port > 65535 {
  87. return fmt.Errorf("smtp: invalid port %v", c.Port)
  88. }
  89. if c.AuthType < 0 || c.AuthType > 2 {
  90. return fmt.Errorf("smtp: invalid auth type %v", c.AuthType)
  91. }
  92. if c.Encryption < 0 || c.Encryption > 2 {
  93. return fmt.Errorf("smtp: invalid encryption %v", c.Encryption)
  94. }
  95. templatesPath := util.FindSharedDataPath(c.TemplatesPath, configDir)
  96. if templatesPath == "" {
  97. return fmt.Errorf("smtp: invalid templates path %#v", templatesPath)
  98. }
  99. loadTemplates(filepath.Join(templatesPath, templateEmailDir))
  100. from = c.From
  101. smtpServer = mail.NewSMTPClient()
  102. smtpServer.Host = c.Host
  103. smtpServer.Port = c.Port
  104. smtpServer.Username = c.User
  105. smtpServer.Password = c.Password
  106. smtpServer.Authentication = c.getAuthType()
  107. smtpServer.Encryption = c.getEncryption()
  108. smtpServer.KeepAlive = false
  109. smtpServer.ConnectTimeout = 10 * time.Second
  110. smtpServer.SendTimeout = 120 * time.Second
  111. if c.Domain != "" {
  112. smtpServer.Helo = c.Domain
  113. }
  114. logger.Debug(logSender, "", "configuration successfully initialized, host: %#v, port: %v, username: %#v, auth: %v, encryption: %v, helo: %#v",
  115. smtpServer.Host, smtpServer.Port, smtpServer.Username, smtpServer.Authentication, smtpServer.Encryption, smtpServer.Helo)
  116. return nil
  117. }
  118. func (c *Config) getEncryption() mail.Encryption {
  119. switch c.Encryption {
  120. case 1:
  121. return mail.EncryptionSSLTLS
  122. case 2:
  123. return mail.EncryptionSTARTTLS
  124. default:
  125. return mail.EncryptionNone
  126. }
  127. }
  128. func (c *Config) getAuthType() mail.AuthType {
  129. if c.User == "" && c.Password == "" {
  130. return mail.AuthNone
  131. }
  132. switch c.AuthType {
  133. case 1:
  134. return mail.AuthLogin
  135. case 2:
  136. return mail.AuthCRAMMD5
  137. default:
  138. return mail.AuthPlain
  139. }
  140. }
  141. func loadTemplates(templatesPath string) {
  142. logger.Debug(logSender, "", "loading templates from %#v", templatesPath)
  143. passwordResetPath := filepath.Join(templatesPath, templatePasswordReset)
  144. pwdResetTmpl := util.LoadTemplate(nil, passwordResetPath)
  145. emailTemplates[templatePasswordReset] = pwdResetTmpl
  146. }
  147. // RenderPasswordResetTemplate executes the password reset template
  148. func RenderPasswordResetTemplate(buf *bytes.Buffer, data any) error {
  149. if smtpServer == nil {
  150. return errors.New("smtp: not configured")
  151. }
  152. return emailTemplates[templatePasswordReset].Execute(buf, data)
  153. }
  154. // SendEmail tries to send an email using the specified parameters.
  155. func SendEmail(to []string, subject, body string, contentType EmailContentType, attachments ...mail.File) error {
  156. if smtpServer == nil {
  157. return errors.New("smtp: not configured")
  158. }
  159. smtpClient, err := smtpServer.Connect()
  160. if err != nil {
  161. return fmt.Errorf("smtp: unable to connect: %w", err)
  162. }
  163. email := mail.NewMSG()
  164. email.AllowDuplicateAddress = true
  165. if from != "" {
  166. email.SetFrom(from)
  167. } else {
  168. email.SetFrom(smtpServer.Username)
  169. }
  170. email.AddTo(to...).SetSubject(subject)
  171. switch contentType {
  172. case EmailContentTypeTextPlain:
  173. email.SetBody(mail.TextPlain, body)
  174. case EmailContentTypeTextHTML:
  175. email.SetBody(mail.TextHTML, body)
  176. default:
  177. return fmt.Errorf("smtp: unsupported body content type %v", contentType)
  178. }
  179. for _, attachment := range attachments {
  180. email.Attach(&attachment)
  181. }
  182. if email.Error != nil {
  183. return fmt.Errorf("smtp: email error: %w", email.Error)
  184. }
  185. return email.Send(smtpClient)
  186. }