smtp.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  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 := util.FindSharedDataPath(c.TemplatesPath, configDir)
  84. if templatesPath == "" {
  85. return fmt.Errorf("smtp: invalid templates path %#v", templatesPath)
  86. }
  87. loadTemplates(filepath.Join(templatesPath, templateEmailDir))
  88. from = c.From
  89. smtpServer = mail.NewSMTPClient()
  90. smtpServer.Host = c.Host
  91. smtpServer.Port = c.Port
  92. smtpServer.Username = c.User
  93. smtpServer.Password = c.Password
  94. smtpServer.Authentication = c.getAuthType()
  95. smtpServer.Encryption = c.getEncryption()
  96. smtpServer.KeepAlive = false
  97. smtpServer.ConnectTimeout = 10 * time.Second
  98. smtpServer.SendTimeout = 30 * time.Second
  99. if c.Domain != "" {
  100. smtpServer.Helo = c.Domain
  101. }
  102. logger.Debug(logSender, "", "configuration successfully initialized, host: %#v, port: %v, username: %#v, auth: %v, encryption: %v, helo: %#v",
  103. smtpServer.Host, smtpServer.Port, smtpServer.Username, smtpServer.Authentication, smtpServer.Encryption, smtpServer.Helo)
  104. return nil
  105. }
  106. func (c *Config) getEncryption() mail.Encryption {
  107. switch c.Encryption {
  108. case 1:
  109. return mail.EncryptionSSLTLS
  110. case 2:
  111. return mail.EncryptionSTARTTLS
  112. default:
  113. return mail.EncryptionNone
  114. }
  115. }
  116. func (c *Config) getAuthType() mail.AuthType {
  117. if c.User == "" && c.Password == "" {
  118. return mail.AuthNone
  119. }
  120. switch c.AuthType {
  121. case 1:
  122. return mail.AuthLogin
  123. case 2:
  124. return mail.AuthCRAMMD5
  125. default:
  126. return mail.AuthPlain
  127. }
  128. }
  129. func loadTemplates(templatesPath string) {
  130. logger.Debug(logSender, "", "loading templates from %#v", templatesPath)
  131. retentionCheckPath := filepath.Join(templatesPath, templateRetentionCheckResult)
  132. retentionTmpl := util.LoadTemplate(nil, retentionCheckPath)
  133. passwordResetPath := filepath.Join(templatesPath, templatePasswordReset)
  134. pwdResetTmpl := util.LoadTemplate(nil, passwordResetPath)
  135. emailTemplates[templateRetentionCheckResult] = retentionTmpl
  136. emailTemplates[templatePasswordReset] = pwdResetTmpl
  137. }
  138. // RenderRetentionReportTemplate executes the retention report template
  139. func RenderRetentionReportTemplate(buf *bytes.Buffer, data any) error {
  140. if smtpServer == nil {
  141. return errors.New("smtp: not configured")
  142. }
  143. return emailTemplates[templateRetentionCheckResult].Execute(buf, data)
  144. }
  145. // RenderPasswordResetTemplate executes the password reset template
  146. func RenderPasswordResetTemplate(buf *bytes.Buffer, data any) error {
  147. if smtpServer == nil {
  148. return errors.New("smtp: not configured")
  149. }
  150. return emailTemplates[templatePasswordReset].Execute(buf, data)
  151. }
  152. // SendEmail tries to send an email using the specified parameters.
  153. func SendEmail(to, subject, body string, contentType EmailContentType) error {
  154. if smtpServer == nil {
  155. return errors.New("smtp: not configured")
  156. }
  157. smtpClient, err := smtpServer.Connect()
  158. if err != nil {
  159. return fmt.Errorf("smtp: unable to connect: %w", err)
  160. }
  161. email := mail.NewMSG()
  162. if from != "" {
  163. email.SetFrom(from)
  164. } else {
  165. email.SetFrom(smtpServer.Username)
  166. }
  167. email.AddTo(to).SetSubject(subject)
  168. switch contentType {
  169. case EmailContentTypeTextPlain:
  170. email.SetBody(mail.TextPlain, body)
  171. case EmailContentTypeTextHTML:
  172. email.SetBody(mail.TextHTML, body)
  173. default:
  174. return fmt.Errorf("smtp: unsupported body content type %v", contentType)
  175. }
  176. if email.Error != nil {
  177. return fmt.Errorf("smtp: email error: %w", email.Error)
  178. }
  179. return email.Send(smtpClient)
  180. }