smtp.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. // Package smtp provides supports for sending emails
  2. package smtp
  3. import (
  4. "bytes"
  5. "errors"
  6. "fmt"
  7. "html/template"
  8. "path/filepath"
  9. "time"
  10. mail "github.com/xhit/go-simple-mail/v2"
  11. "github.com/drakkan/sftpgo/v2/logger"
  12. "github.com/drakkan/sftpgo/v2/util"
  13. )
  14. const (
  15. logSender = "smtp"
  16. )
  17. // EmailContentType defines the support content types for email body
  18. type EmailContentType int
  19. // Supporte email body content type
  20. const (
  21. EmailContentTypeTextPlain EmailContentType = iota
  22. EmailContentTypeTextHTML
  23. )
  24. const (
  25. templateEmailDir = "email"
  26. templateRetentionCheckResult = "retention-check-report.html"
  27. templatePasswordReset = "reset-password.html"
  28. )
  29. var (
  30. smtpServer *mail.SMTPServer
  31. from string
  32. emailTemplates = make(map[string]*template.Template)
  33. )
  34. // IsEnabled returns true if an SMTP server is configured
  35. func IsEnabled() bool {
  36. return smtpServer != nil
  37. }
  38. // Config defines the SMTP configuration to use to send emails
  39. type Config struct {
  40. // Location of SMTP email server. Leavy empty to disable email sending capabilities
  41. Host string `json:"host" mapstructure:"host"`
  42. // Port of SMTP email server
  43. Port int `json:"port" mapstructure:"port"`
  44. // From address, for example "SFTPGo <[email protected]>".
  45. // Many SMTP servers reject emails without a `From` header so, if not set,
  46. // SFTPGo will try to use the username as fallback, this may or may not be appropriate
  47. From string `json:"from" mapstructure:"from"`
  48. // SMTP username
  49. User string `json:"user" mapstructure:"user"`
  50. // SMTP password. Leaving both username and password empty the SMTP authentication
  51. // will be disabled
  52. Password string `json:"password" mapstructure:"password"`
  53. // 0 Plain
  54. // 1 Login
  55. // 2 CRAM-MD5
  56. AuthType int `json:"auth_type" mapstructure:"auth_type"`
  57. // 0 no encryption
  58. // 1 TLS
  59. // 2 start TLS
  60. Encryption int `json:"encryption" mapstructure:"encryption"`
  61. // Domain to use for HELO command, if empty localhost will be used
  62. Domain string `json:"domain" mapstructure:"domain"`
  63. // Path to the email templates. This can be an absolute path or a path relative to the config dir.
  64. // Templates are searched within a subdirectory named "email" in the specified path
  65. TemplatesPath string `json:"templates_path" mapstructure:"templates_path"`
  66. }
  67. // Initialize initialized and validates the SMTP configuration
  68. func (c *Config) Initialize(configDir string) error {
  69. smtpServer = nil
  70. if c.Host == "" {
  71. logger.Debug(logSender, "", "configuration disabled, email capabilities will not be available")
  72. return nil
  73. }
  74. if c.Port <= 0 || c.Port > 65535 {
  75. return fmt.Errorf("smtp: invalid port %v", c.Port)
  76. }
  77. if c.AuthType < 0 || c.AuthType > 2 {
  78. return fmt.Errorf("smtp: invalid auth type %v", c.AuthType)
  79. }
  80. if c.Encryption < 0 || c.Encryption > 2 {
  81. return fmt.Errorf("smtp: invalid encryption %v", c.Encryption)
  82. }
  83. templatesPath := c.TemplatesPath
  84. if templatesPath == "" || !util.IsFileInputValid(templatesPath) {
  85. return fmt.Errorf("smtp: invalid templates path %#v", templatesPath)
  86. }
  87. if !filepath.IsAbs(templatesPath) {
  88. templatesPath = filepath.Join(configDir, templatesPath)
  89. }
  90. loadTemplates(filepath.Join(templatesPath, templateEmailDir))
  91. from = c.From
  92. smtpServer = mail.NewSMTPClient()
  93. smtpServer.Host = c.Host
  94. smtpServer.Port = c.Port
  95. smtpServer.Username = c.User
  96. smtpServer.Password = c.Password
  97. smtpServer.Authentication = c.getAuthType()
  98. smtpServer.Encryption = c.getEncryption()
  99. smtpServer.KeepAlive = false
  100. smtpServer.ConnectTimeout = 10 * time.Second
  101. smtpServer.SendTimeout = 30 * time.Second
  102. if c.Domain != "" {
  103. smtpServer.Helo = c.Domain
  104. }
  105. logger.Debug(logSender, "", "configuration successfully initialized, host: %#v, port: %v, username: %#v, auth: %v, encryption: %v, helo: %#v",
  106. smtpServer.Host, smtpServer.Port, smtpServer.Username, smtpServer.Authentication, smtpServer.Encryption, smtpServer.Helo)
  107. return nil
  108. }
  109. func (c *Config) getEncryption() mail.Encryption {
  110. switch c.Encryption {
  111. case 1:
  112. return mail.EncryptionSSLTLS
  113. case 2:
  114. return mail.EncryptionSTARTTLS
  115. default:
  116. return mail.EncryptionNone
  117. }
  118. }
  119. func (c *Config) getAuthType() mail.AuthType {
  120. if c.User == "" && c.Password == "" {
  121. return mail.AuthNone
  122. }
  123. switch c.AuthType {
  124. case 1:
  125. return mail.AuthLogin
  126. case 2:
  127. return mail.AuthCRAMMD5
  128. default:
  129. return mail.AuthPlain
  130. }
  131. }
  132. func loadTemplates(templatesPath string) {
  133. logger.Debug(logSender, "", "loading templates from %#v", templatesPath)
  134. retentionCheckPath := filepath.Join(templatesPath, templateRetentionCheckResult)
  135. retentionTmpl := util.LoadTemplate(nil, retentionCheckPath)
  136. passwordResetPath := filepath.Join(templatesPath, templatePasswordReset)
  137. pwdResetTmpl := util.LoadTemplate(nil, passwordResetPath)
  138. emailTemplates[templateRetentionCheckResult] = retentionTmpl
  139. emailTemplates[templatePasswordReset] = pwdResetTmpl
  140. }
  141. // RenderRetentionReportTemplate executes the retention report template
  142. func RenderRetentionReportTemplate(buf *bytes.Buffer, data interface{}) error {
  143. if smtpServer == nil {
  144. return errors.New("smtp: not configured")
  145. }
  146. return emailTemplates[templateRetentionCheckResult].Execute(buf, data)
  147. }
  148. func RenderPasswordResetTemplate(buf *bytes.Buffer, data interface{}) 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, subject, body string, contentType EmailContentType) 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. if from != "" {
  165. email.SetFrom(from)
  166. } else {
  167. email.SetFrom(smtpServer.Username)
  168. }
  169. email.AddTo(to).SetSubject(subject)
  170. switch contentType {
  171. case EmailContentTypeTextPlain:
  172. email.SetBody(mail.TextPlain, body)
  173. case EmailContentTypeTextHTML:
  174. email.SetBody(mail.TextHTML, body)
  175. default:
  176. return fmt.Errorf("smtp: unsupported body content type %v", contentType)
  177. }
  178. if email.Error != nil {
  179. return fmt.Errorf("smtp: email error: %w", email.Error)
  180. }
  181. return email.Send(smtpClient)
  182. }