smtp.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  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. templatePasswordExpiration = "password-expiration.html"
  41. )
  42. var (
  43. smtpServer *mail.SMTPServer
  44. from string
  45. emailTemplates = make(map[string]*template.Template)
  46. )
  47. // IsEnabled returns true if an SMTP server is configured
  48. func IsEnabled() bool {
  49. return smtpServer != nil
  50. }
  51. // Config defines the SMTP configuration to use to send emails
  52. type Config struct {
  53. // Location of SMTP email server. Leavy empty to disable email sending capabilities
  54. Host string `json:"host" mapstructure:"host"`
  55. // Port of SMTP email server
  56. Port int `json:"port" mapstructure:"port"`
  57. // From address, for example "SFTPGo <[email protected]>".
  58. // Many SMTP servers reject emails without a `From` header so, if not set,
  59. // SFTPGo will try to use the username as fallback, this may or may not be appropriate
  60. From string `json:"from" mapstructure:"from"`
  61. // SMTP username
  62. User string `json:"user" mapstructure:"user"`
  63. // SMTP password. Leaving both username and password empty the SMTP authentication
  64. // will be disabled
  65. Password string `json:"password" mapstructure:"password"`
  66. // 0 Plain
  67. // 1 Login
  68. // 2 CRAM-MD5
  69. AuthType int `json:"auth_type" mapstructure:"auth_type"`
  70. // 0 no encryption
  71. // 1 TLS
  72. // 2 start TLS
  73. Encryption int `json:"encryption" mapstructure:"encryption"`
  74. // Domain to use for HELO command, if empty localhost will be used
  75. Domain string `json:"domain" mapstructure:"domain"`
  76. // Path to the email templates. This can be an absolute path or a path relative to the config dir.
  77. // Templates are searched within a subdirectory named "email" in the specified path
  78. TemplatesPath string `json:"templates_path" mapstructure:"templates_path"`
  79. }
  80. // Initialize initialized and validates the SMTP configuration
  81. func (c *Config) Initialize(configDir string) error {
  82. smtpServer = nil
  83. if c.Host == "" {
  84. logger.Debug(logSender, "", "configuration disabled, email capabilities will not be available")
  85. return nil
  86. }
  87. if c.Port <= 0 || c.Port > 65535 {
  88. return fmt.Errorf("smtp: invalid port %v", c.Port)
  89. }
  90. if c.AuthType < 0 || c.AuthType > 2 {
  91. return fmt.Errorf("smtp: invalid auth type %v", c.AuthType)
  92. }
  93. if c.Encryption < 0 || c.Encryption > 2 {
  94. return fmt.Errorf("smtp: invalid encryption %v", c.Encryption)
  95. }
  96. templatesPath := util.FindSharedDataPath(c.TemplatesPath, configDir)
  97. if templatesPath == "" {
  98. return fmt.Errorf("smtp: invalid templates path %#v", templatesPath)
  99. }
  100. loadTemplates(filepath.Join(templatesPath, templateEmailDir))
  101. from = c.From
  102. smtpServer = mail.NewSMTPClient()
  103. smtpServer.Host = c.Host
  104. smtpServer.Port = c.Port
  105. smtpServer.Username = c.User
  106. smtpServer.Password = c.Password
  107. smtpServer.Authentication = c.getAuthType()
  108. smtpServer.Encryption = c.getEncryption()
  109. smtpServer.KeepAlive = false
  110. smtpServer.ConnectTimeout = 10 * time.Second
  111. smtpServer.SendTimeout = 120 * time.Second
  112. if c.Domain != "" {
  113. smtpServer.Helo = c.Domain
  114. }
  115. logger.Debug(logSender, "", "configuration successfully initialized, host: %#v, port: %v, username: %#v, auth: %v, encryption: %v, helo: %#v",
  116. smtpServer.Host, smtpServer.Port, smtpServer.Username, smtpServer.Authentication, smtpServer.Encryption, smtpServer.Helo)
  117. return nil
  118. }
  119. func (c *Config) getEncryption() mail.Encryption {
  120. switch c.Encryption {
  121. case 1:
  122. return mail.EncryptionSSLTLS
  123. case 2:
  124. return mail.EncryptionSTARTTLS
  125. default:
  126. return mail.EncryptionNone
  127. }
  128. }
  129. func (c *Config) getAuthType() mail.AuthType {
  130. if c.User == "" && c.Password == "" {
  131. return mail.AuthNone
  132. }
  133. switch c.AuthType {
  134. case 1:
  135. return mail.AuthLogin
  136. case 2:
  137. return mail.AuthCRAMMD5
  138. default:
  139. return mail.AuthPlain
  140. }
  141. }
  142. func loadTemplates(templatesPath string) {
  143. logger.Debug(logSender, "", "loading templates from %#v", templatesPath)
  144. passwordResetPath := filepath.Join(templatesPath, templatePasswordReset)
  145. pwdResetTmpl := util.LoadTemplate(nil, passwordResetPath)
  146. passwordExpirationPath := filepath.Join(templatesPath, templatePasswordExpiration)
  147. pwdExpirationTmpl := util.LoadTemplate(nil, passwordExpirationPath)
  148. emailTemplates[templatePasswordReset] = pwdResetTmpl
  149. emailTemplates[templatePasswordExpiration] = pwdExpirationTmpl
  150. }
  151. // RenderPasswordResetTemplate executes the password reset template
  152. func RenderPasswordResetTemplate(buf *bytes.Buffer, data any) error {
  153. if smtpServer == nil {
  154. return errors.New("smtp: not configured")
  155. }
  156. return emailTemplates[templatePasswordReset].Execute(buf, data)
  157. }
  158. // RenderPasswordExpirationTemplate executes the password expiration template
  159. func RenderPasswordExpirationTemplate(buf *bytes.Buffer, data any) error {
  160. if smtpServer == nil {
  161. return errors.New("smtp: not configured")
  162. }
  163. return emailTemplates[templatePasswordExpiration].Execute(buf, data)
  164. }
  165. // SendEmail tries to send an email using the specified parameters.
  166. func SendEmail(to []string, subject, body string, contentType EmailContentType, attachments ...mail.File) error {
  167. if smtpServer == nil {
  168. return errors.New("smtp: not configured")
  169. }
  170. if len(to) == 0 {
  171. return errors.New("smtp: cannot send an email without recipients")
  172. }
  173. smtpClient, err := smtpServer.Connect()
  174. if err != nil {
  175. return fmt.Errorf("smtp: unable to connect: %w", err)
  176. }
  177. email := mail.NewMSG()
  178. email.AllowDuplicateAddress = true
  179. if from != "" {
  180. email.SetFrom(from)
  181. } else {
  182. email.SetFrom(smtpServer.Username)
  183. }
  184. email.AddTo(to...).SetSubject(subject)
  185. switch contentType {
  186. case EmailContentTypeTextPlain:
  187. email.SetBody(mail.TextPlain, body)
  188. case EmailContentTypeTextHTML:
  189. email.SetBody(mail.TextHTML, body)
  190. default:
  191. return fmt.Errorf("smtp: unsupported body content type %v", contentType)
  192. }
  193. for _, attachment := range attachments {
  194. email.Attach(&attachment)
  195. }
  196. if email.Error != nil {
  197. return fmt.Errorf("smtp: email error: %w", email.Error)
  198. }
  199. return email.Send(smtpClient)
  200. }