add SMTP support
it will be used in future update to add email sending capabilities
This commit is contained in:
parent
0661876e99
commit
da0ccc6426
11 changed files with 302 additions and 12 deletions
|
@ -71,6 +71,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
rootCmd.CompletionOptions.DisableDefaultCmd = true
|
||||||
rootCmd.Flags().BoolP("version", "v", false, "")
|
rootCmd.Flags().BoolP("version", "v", false, "")
|
||||||
rootCmd.Version = version.GetAsString()
|
rootCmd.Version = version.GetAsString()
|
||||||
rootCmd.SetVersionTemplate(`{{printf "SFTPGo "}}{{printf "%s" .Version}}
|
rootCmd.SetVersionTemplate(`{{printf "SFTPGo "}}{{printf "%s" .Version}}
|
||||||
|
|
54
cmd/smtptest.go
Normal file
54
cmd/smtptest.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/drakkan/sftpgo/v2/config"
|
||||||
|
"github.com/drakkan/sftpgo/v2/logger"
|
||||||
|
"github.com/drakkan/sftpgo/v2/smtp"
|
||||||
|
"github.com/drakkan/sftpgo/v2/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
smtpTestRecipient string
|
||||||
|
smtpTestCmd = &cobra.Command{
|
||||||
|
Use: "smtptest",
|
||||||
|
Short: "Test the SMTP configuration",
|
||||||
|
Long: `SFTPGo will try to send a test email to the specified recipient.
|
||||||
|
If the SMTP configuration is correct you should receive this email.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
logger.DisableLogger()
|
||||||
|
logger.EnableConsoleLogger(zerolog.DebugLevel)
|
||||||
|
configDir = util.CleanDirInput(configDir)
|
||||||
|
err := config.LoadConfig(configDir, configFile)
|
||||||
|
if err != nil {
|
||||||
|
logger.WarnToConsole("Unable to initialize data provider, config load error: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
smtpConfig := config.GetSMTPConfig()
|
||||||
|
err = smtpConfig.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorToConsole("unable to initialize SMTP configuration: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
err = smtp.SendEmail(smtpTestRecipient, "SFTPGo - Testing Email Settings", "It appears your SFTPGo email is setup correctly!",
|
||||||
|
smtp.EmailContentTypeTextPlain)
|
||||||
|
if err != nil {
|
||||||
|
logger.WarnToConsole("Error sending email: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
logger.InfoToConsole("No errors were reported while sending an email. Please check your inbox to make sure.")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
addConfigFlags(smtpTestCmd)
|
||||||
|
smtpTestCmd.Flags().StringVar(&smtpTestRecipient, "recipient", "", `email address to send the test e-mail to`)
|
||||||
|
smtpTestCmd.MarkFlagRequired("recipient") //nolint:errcheck
|
||||||
|
|
||||||
|
rootCmd.AddCommand(smtpTestCmd)
|
||||||
|
}
|
|
@ -86,6 +86,12 @@ Command-line flags should be specified in the Subsystem declaration.
|
||||||
logger.Error(logSender, connectionID, "unable to initialize plugin system: %v", err)
|
logger.Error(logSender, connectionID, "unable to initialize plugin system: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
smtpConfig := config.GetSMTPConfig()
|
||||||
|
err = smtpConfig.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(logSender, connectionID, "unable to initialize SMTP configuration: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
dataProviderConf := config.GetProviderConf()
|
dataProviderConf := config.GetProviderConf()
|
||||||
if dataProviderConf.Driver == dataprovider.SQLiteDataProviderName || dataProviderConf.Driver == dataprovider.BoltDataProviderName {
|
if dataProviderConf.Driver == dataprovider.SQLiteDataProviderName || dataProviderConf.Driver == dataprovider.BoltDataProviderName {
|
||||||
logger.Debug(logSender, connectionID, "data provider %#v not supported in subsystem mode, using %#v provider",
|
logger.Debug(logSender, connectionID, "data provider %#v not supported in subsystem mode, using %#v provider",
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"github.com/drakkan/sftpgo/v2/mfa"
|
"github.com/drakkan/sftpgo/v2/mfa"
|
||||||
"github.com/drakkan/sftpgo/v2/sdk/plugin"
|
"github.com/drakkan/sftpgo/v2/sdk/plugin"
|
||||||
"github.com/drakkan/sftpgo/v2/sftpd"
|
"github.com/drakkan/sftpgo/v2/sftpd"
|
||||||
|
"github.com/drakkan/sftpgo/v2/smtp"
|
||||||
"github.com/drakkan/sftpgo/v2/telemetry"
|
"github.com/drakkan/sftpgo/v2/telemetry"
|
||||||
"github.com/drakkan/sftpgo/v2/util"
|
"github.com/drakkan/sftpgo/v2/util"
|
||||||
"github.com/drakkan/sftpgo/v2/version"
|
"github.com/drakkan/sftpgo/v2/version"
|
||||||
|
@ -107,6 +108,7 @@ type globalConfig struct {
|
||||||
MFAConfig mfa.Config `json:"mfa" mapstructure:"mfa"`
|
MFAConfig mfa.Config `json:"mfa" mapstructure:"mfa"`
|
||||||
TelemetryConfig telemetry.Conf `json:"telemetry" mapstructure:"telemetry"`
|
TelemetryConfig telemetry.Conf `json:"telemetry" mapstructure:"telemetry"`
|
||||||
PluginsConfig []plugin.Config `json:"plugins" mapstructure:"plugins"`
|
PluginsConfig []plugin.Config `json:"plugins" mapstructure:"plugins"`
|
||||||
|
SMTPConfig smtp.Config `json:"smtp" mapstructure:"smtp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -305,6 +307,16 @@ func Init() {
|
||||||
TLSCipherSuites: nil,
|
TLSCipherSuites: nil,
|
||||||
},
|
},
|
||||||
PluginsConfig: nil,
|
PluginsConfig: nil,
|
||||||
|
SMTPConfig: smtp.Config{
|
||||||
|
Host: "",
|
||||||
|
Port: 25,
|
||||||
|
From: "",
|
||||||
|
User: "",
|
||||||
|
Password: "",
|
||||||
|
AuthType: 0,
|
||||||
|
Encryption: 0,
|
||||||
|
Domain: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
viper.SetEnvPrefix(configEnvPrefix)
|
viper.SetEnvPrefix(configEnvPrefix)
|
||||||
|
@ -411,6 +423,11 @@ func GetMFAConfig() mfa.Config {
|
||||||
return globalConf.MFAConfig
|
return globalConf.MFAConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSMTPConfig returns the SMTP configuration
|
||||||
|
func GetSMTPConfig() smtp.Config {
|
||||||
|
return globalConf.SMTPConfig
|
||||||
|
}
|
||||||
|
|
||||||
// HasServicesToStart returns true if the config defines at least a service to start.
|
// HasServicesToStart returns true if the config defines at least a service to start.
|
||||||
// Supported services are SFTP, FTP and WebDAV
|
// Supported services are SFTP, FTP and WebDAV
|
||||||
func HasServicesToStart() bool {
|
func HasServicesToStart() bool {
|
||||||
|
@ -426,19 +443,24 @@ func HasServicesToStart() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getRedactedPassword() string {
|
||||||
|
return "[redacted]"
|
||||||
|
}
|
||||||
|
|
||||||
func getRedactedGlobalConf() globalConfig {
|
func getRedactedGlobalConf() globalConfig {
|
||||||
conf := globalConf
|
conf := globalConf
|
||||||
conf.Common.Actions.Hook = util.GetRedactedURL(conf.Common.Actions.Hook)
|
conf.Common.Actions.Hook = util.GetRedactedURL(conf.Common.Actions.Hook)
|
||||||
conf.Common.StartupHook = util.GetRedactedURL(conf.Common.StartupHook)
|
conf.Common.StartupHook = util.GetRedactedURL(conf.Common.StartupHook)
|
||||||
conf.Common.PostConnectHook = util.GetRedactedURL(conf.Common.PostConnectHook)
|
conf.Common.PostConnectHook = util.GetRedactedURL(conf.Common.PostConnectHook)
|
||||||
conf.SFTPD.KeyboardInteractiveHook = util.GetRedactedURL(conf.SFTPD.KeyboardInteractiveHook)
|
conf.SFTPD.KeyboardInteractiveHook = util.GetRedactedURL(conf.SFTPD.KeyboardInteractiveHook)
|
||||||
conf.HTTPDConfig.SigningPassphrase = "[redacted]"
|
conf.HTTPDConfig.SigningPassphrase = getRedactedPassword()
|
||||||
conf.ProviderConf.Password = "[redacted]"
|
conf.ProviderConf.Password = getRedactedPassword()
|
||||||
conf.ProviderConf.Actions.Hook = util.GetRedactedURL(conf.ProviderConf.Actions.Hook)
|
conf.ProviderConf.Actions.Hook = util.GetRedactedURL(conf.ProviderConf.Actions.Hook)
|
||||||
conf.ProviderConf.ExternalAuthHook = util.GetRedactedURL(conf.ProviderConf.ExternalAuthHook)
|
conf.ProviderConf.ExternalAuthHook = util.GetRedactedURL(conf.ProviderConf.ExternalAuthHook)
|
||||||
conf.ProviderConf.PreLoginHook = util.GetRedactedURL(conf.ProviderConf.PreLoginHook)
|
conf.ProviderConf.PreLoginHook = util.GetRedactedURL(conf.ProviderConf.PreLoginHook)
|
||||||
conf.ProviderConf.PostLoginHook = util.GetRedactedURL(conf.ProviderConf.PostLoginHook)
|
conf.ProviderConf.PostLoginHook = util.GetRedactedURL(conf.ProviderConf.PostLoginHook)
|
||||||
conf.ProviderConf.CheckPasswordHook = util.GetRedactedURL(conf.ProviderConf.CheckPasswordHook)
|
conf.ProviderConf.CheckPasswordHook = util.GetRedactedURL(conf.ProviderConf.CheckPasswordHook)
|
||||||
|
conf.SMTPConfig.Password = getRedactedPassword()
|
||||||
return conf
|
return conf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1146,6 +1168,14 @@ func setViperDefaults() {
|
||||||
viper.SetDefault("telemetry.certificate_file", globalConf.TelemetryConfig.CertificateFile)
|
viper.SetDefault("telemetry.certificate_file", globalConf.TelemetryConfig.CertificateFile)
|
||||||
viper.SetDefault("telemetry.certificate_key_file", globalConf.TelemetryConfig.CertificateKeyFile)
|
viper.SetDefault("telemetry.certificate_key_file", globalConf.TelemetryConfig.CertificateKeyFile)
|
||||||
viper.SetDefault("telemetry.tls_cipher_suites", globalConf.TelemetryConfig.TLSCipherSuites)
|
viper.SetDefault("telemetry.tls_cipher_suites", globalConf.TelemetryConfig.TLSCipherSuites)
|
||||||
|
viper.SetDefault("smtp.host", globalConf.SMTPConfig.Host)
|
||||||
|
viper.SetDefault("smtp.port", globalConf.SMTPConfig.Port)
|
||||||
|
viper.SetDefault("smtp.from", globalConf.SMTPConfig.From)
|
||||||
|
viper.SetDefault("smtp.user", globalConf.SMTPConfig.User)
|
||||||
|
viper.SetDefault("smtp.password", globalConf.SMTPConfig.Password)
|
||||||
|
viper.SetDefault("smtp.auth_type", globalConf.SMTPConfig.AuthType)
|
||||||
|
viper.SetDefault("smtp.encryption", globalConf.SMTPConfig.Encryption)
|
||||||
|
viper.SetDefault("smtp.domain", globalConf.SMTPConfig.Domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookupBoolFromEnv(envName string) (bool, bool) {
|
func lookupBoolFromEnv(envName string) (bool, bool) {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"github.com/drakkan/sftpgo/v2/kms"
|
"github.com/drakkan/sftpgo/v2/kms"
|
||||||
"github.com/drakkan/sftpgo/v2/mfa"
|
"github.com/drakkan/sftpgo/v2/mfa"
|
||||||
"github.com/drakkan/sftpgo/v2/sftpd"
|
"github.com/drakkan/sftpgo/v2/sftpd"
|
||||||
|
"github.com/drakkan/sftpgo/v2/smtp"
|
||||||
"github.com/drakkan/sftpgo/v2/util"
|
"github.com/drakkan/sftpgo/v2/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -42,6 +43,7 @@ func TestLoadConfigTest(t *testing.T) {
|
||||||
assert.NotEqual(t, dataprovider.Config{}, config.GetProviderConf())
|
assert.NotEqual(t, dataprovider.Config{}, config.GetProviderConf())
|
||||||
assert.NotEqual(t, sftpd.Configuration{}, config.GetSFTPDConfig())
|
assert.NotEqual(t, sftpd.Configuration{}, config.GetSFTPDConfig())
|
||||||
assert.NotEqual(t, httpclient.Config{}, config.GetHTTPConfig())
|
assert.NotEqual(t, httpclient.Config{}, config.GetHTTPConfig())
|
||||||
|
assert.NotEqual(t, smtp.Config{}, config.GetSMTPConfig())
|
||||||
confName := tempConfigName + ".json"
|
confName := tempConfigName + ".json"
|
||||||
configFilePath := filepath.Join(configDir, confName)
|
configFilePath := filepath.Join(configDir, confName)
|
||||||
err = config.LoadConfig(configDir, confName)
|
err = config.LoadConfig(configDir, confName)
|
||||||
|
@ -340,6 +342,24 @@ func TestSSHCommandsFromEnv(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSMTPFromEnv(t *testing.T) {
|
||||||
|
reset()
|
||||||
|
|
||||||
|
os.Setenv("SFTPGO_SMTP__HOST", "smtp.example.com")
|
||||||
|
os.Setenv("SFTPGO_SMTP__PORT", "587")
|
||||||
|
t.Cleanup(func() {
|
||||||
|
os.Unsetenv("SFTPGO_SMTP__HOST")
|
||||||
|
os.Unsetenv("SFTPGO_SMTP__PORT")
|
||||||
|
})
|
||||||
|
|
||||||
|
configDir := ".."
|
||||||
|
err := config.LoadConfig(configDir, "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
smtpConfig := config.GetSMTPConfig()
|
||||||
|
assert.Equal(t, "smtp.example.com", smtpConfig.Host)
|
||||||
|
assert.Equal(t, 587, smtpConfig.Port)
|
||||||
|
}
|
||||||
|
|
||||||
func TestMFAFromEnv(t *testing.T) {
|
func TestMFAFromEnv(t *testing.T) {
|
||||||
reset()
|
reset()
|
||||||
|
|
||||||
|
|
|
@ -9,11 +9,14 @@ Usage:
|
||||||
sftpgo [command]
|
sftpgo [command]
|
||||||
|
|
||||||
Available Commands:
|
Available Commands:
|
||||||
gen A collection of useful generators
|
gen A collection of useful generators
|
||||||
help Help about any command
|
help Help about any command
|
||||||
initprovider Initializes and/or updates the configured data provider
|
initprovider Initializes and/or updates the configured data provider
|
||||||
portable Serve a single directory
|
portable Serve a single directory
|
||||||
serve Start the SFTP Server
|
revertprovider Revert the configured data provider to a previous version
|
||||||
|
serve Start the SFTPGo service
|
||||||
|
smtptest Test the SMTP configuration
|
||||||
|
startsubsys Use SFTPGo as SFTP file transfer subsystem
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
-h, --help help for sftpgo
|
-h, --help help for sftpgo
|
||||||
|
@ -257,6 +260,15 @@ The configuration file contains the following sections:
|
||||||
- `name`, string. Unique configuration name. This name should not be changed if there are users or admins using the configuration. The name is not exposed to the authentication apps. Default: `Default`.
|
- `name`, string. Unique configuration name. This name should not be changed if there are users or admins using the configuration. The name is not exposed to the authentication apps. Default: `Default`.
|
||||||
- `issuer`, string. Name of the issuing Organization/Company. Default: `SFTPGo`.
|
- `issuer`, string. Name of the issuing Organization/Company. Default: `SFTPGo`.
|
||||||
- `algo`, string. Algorithm to use for HMAC. The supported algorithms are: `sha1`, `sha256`, `sha512`. Currently Google Authenticator app on iPhone seems to only support `sha1`, please check the compatibility with your target apps/device before setting a different algorithm. You can also define multiple configurations, for example one that uses `sha256` or `sha512` and another one that uses `sha1` and instruct your users to use the appropriate configuration for their devices/apps. The algorithm should not be changed if there are users or admins using the configuration. Default: `sha1`.
|
- `algo`, string. Algorithm to use for HMAC. The supported algorithms are: `sha1`, `sha256`, `sha512`. Currently Google Authenticator app on iPhone seems to only support `sha1`, please check the compatibility with your target apps/device before setting a different algorithm. You can also define multiple configurations, for example one that uses `sha256` or `sha512` and another one that uses `sha1` and instruct your users to use the appropriate configuration for their devices/apps. The algorithm should not be changed if there are users or admins using the configuration. Default: `sha1`.
|
||||||
|
- **smtp**, SMTP configuration enables SFTPGo email sending capabilities
|
||||||
|
- `host`, string. Location of SMTP email server. Leavy empty to disable email sending capabilities. Default: empty.
|
||||||
|
- `port`, integer. Port of SMTP email server.
|
||||||
|
- `from`, string. From address, for example `SFTPGo <sftpgo@example.com>`. Default: empty
|
||||||
|
- `user`, string. SMTP username. Default: empty
|
||||||
|
- `password`, string. SMTP password. Leaving both username and password empty the SMTP authentication will be disabled. Default: empty
|
||||||
|
- `auth_type`, integer. 0 means `Plain`, 1 means `Login`, 2 means `CRAM-MD5`. Default: `0`.
|
||||||
|
- `encryption`, integer. 0 means no encryption, 1 means `TLS`, 2 means `STARTTLS`. Default: `0`.
|
||||||
|
- `domain`, string. Domain to use for `HELO` command, if empty `localhost` will be used. Default: empty.
|
||||||
- **plugins**, list of external plugins. Each plugin is configured using a struct with the following fields:
|
- **plugins**, list of external plugins. Each plugin is configured using a struct with the following fields:
|
||||||
- `type`, string. Defines the plugin type. Supported types: `notifier`, `kms`, `auth`.
|
- `type`, string. Defines the plugin type. Supported types: `notifier`, `kms`, `auth`.
|
||||||
- `notifier_options`, struct. Defines the options for notifier plugins.
|
- `notifier_options`, struct. Defines the options for notifier plugins.
|
||||||
|
|
5
go.mod
5
go.mod
|
@ -29,7 +29,7 @@ require (
|
||||||
github.com/klauspost/compress v1.13.6
|
github.com/klauspost/compress v1.13.6
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
||||||
github.com/lestrrat-go/jwx v1.2.6
|
github.com/lestrrat-go/jwx v1.2.7
|
||||||
github.com/lib/pq v1.10.3
|
github.com/lib/pq v1.10.3
|
||||||
github.com/lithammer/shortuuid/v3 v3.0.7
|
github.com/lithammer/shortuuid/v3 v3.0.7
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
|
@ -43,7 +43,7 @@ require (
|
||||||
github.com/pkg/sftp v1.13.3
|
github.com/pkg/sftp v1.13.3
|
||||||
github.com/pquerna/otp v1.3.0
|
github.com/pquerna/otp v1.3.0
|
||||||
github.com/prometheus/client_golang v1.11.0
|
github.com/prometheus/client_golang v1.11.0
|
||||||
github.com/prometheus/common v0.30.0 // indirect
|
github.com/prometheus/common v0.31.0 // indirect
|
||||||
github.com/rs/cors v1.8.0
|
github.com/rs/cors v1.8.0
|
||||||
github.com/rs/xid v1.3.0
|
github.com/rs/xid v1.3.0
|
||||||
github.com/rs/zerolog v1.25.0
|
github.com/rs/zerolog v1.25.0
|
||||||
|
@ -55,6 +55,7 @@ require (
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
github.com/studio-b12/gowebdav v0.0.0-20210917133250-a3a86976a1df
|
github.com/studio-b12/gowebdav v0.0.0-20210917133250-a3a86976a1df
|
||||||
github.com/wagslane/go-password-validator v0.3.0
|
github.com/wagslane/go-password-validator v0.3.0
|
||||||
|
github.com/xhit/go-simple-mail/v2 v2.10.0
|
||||||
github.com/yl2chen/cidranger v1.0.2
|
github.com/yl2chen/cidranger v1.0.2
|
||||||
go.etcd.io/bbolt v1.3.6
|
go.etcd.io/bbolt v1.3.6
|
||||||
go.uber.org/automaxprocs v1.4.0
|
go.uber.org/automaxprocs v1.4.0
|
||||||
|
|
10
go.sum
10
go.sum
|
@ -544,12 +544,14 @@ github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBB
|
||||||
github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4=
|
github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4=
|
||||||
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
|
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
|
||||||
github.com/lestrrat-go/codegen v1.0.1/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM=
|
github.com/lestrrat-go/codegen v1.0.1/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM=
|
||||||
|
github.com/lestrrat-go/codegen v1.0.2/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM=
|
||||||
github.com/lestrrat-go/httpcc v1.0.0 h1:FszVC6cKfDvBKcJv646+lkh4GydQg2Z29scgUfkOpYc=
|
github.com/lestrrat-go/httpcc v1.0.0 h1:FszVC6cKfDvBKcJv646+lkh4GydQg2Z29scgUfkOpYc=
|
||||||
github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
|
github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
|
||||||
github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A=
|
github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A=
|
||||||
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
|
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
|
||||||
github.com/lestrrat-go/jwx v1.2.6 h1:XAgfuHaOB7fDZ/6WhVgl8K89af768dU+3Nx4DlTbLIk=
|
|
||||||
github.com/lestrrat-go/jwx v1.2.6/go.mod h1:tJuGuAI3LC71IicTx82Mz1n3w9woAs2bYJZpkjJQ5aU=
|
github.com/lestrrat-go/jwx v1.2.6/go.mod h1:tJuGuAI3LC71IicTx82Mz1n3w9woAs2bYJZpkjJQ5aU=
|
||||||
|
github.com/lestrrat-go/jwx v1.2.7 h1:wO7fEc3PW56wpQBMU5CyRkrk4DVsXxCoJg7oIm5HHE4=
|
||||||
|
github.com/lestrrat-go/jwx v1.2.7/go.mod h1:bw24IXWbavc0R2RsOtpXL7RtMyP589yZ1+L7kd09ZGA=
|
||||||
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
|
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
|
||||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
@ -679,8 +681,8 @@ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T
|
||||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||||
github.com/prometheus/common v0.30.0 h1:JEkYlQnpzrzQFxi6gnukFPdQ+ac82oRhzMcIduJu/Ug=
|
github.com/prometheus/common v0.31.0 h1:FTJdLTjtrh4dXlCjpzdZJXMnejSTL5F/nVQm5sNwD34=
|
||||||
github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
github.com/prometheus/common v0.31.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||||
|
@ -762,6 +764,8 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM
|
||||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
|
github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
|
||||||
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
|
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
|
||||||
|
github.com/xhit/go-simple-mail/v2 v2.10.0 h1:nib6RaJ4qVh5HD9UE9QJqnUZyWp3upv+Z6CFxaMj0V8=
|
||||||
|
github.com/xhit/go-simple-mail/v2 v2.10.0/go.mod h1:kA1XbQfCI4JxQ9ccSN6VFyIEkkugOm7YiPkA5hKiQn4=
|
||||||
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
|
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
|
||||||
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
|
|
@ -110,6 +110,13 @@ func (s *Service) Start() error {
|
||||||
logger.ErrorToConsole("unable to initialize plugin system: %v", err)
|
logger.ErrorToConsole("unable to initialize plugin system: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
smtpConfig := config.GetSMTPConfig()
|
||||||
|
err = smtpConfig.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(logSender, "", "unable to initialize SMTP configuration: %v", err)
|
||||||
|
logger.ErrorToConsole("unable to initialize SMTP configuration: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
providerConf := config.GetProviderConf()
|
providerConf := config.GetProviderConf()
|
||||||
|
|
||||||
|
|
10
sftpgo.json
10
sftpgo.json
|
@ -254,5 +254,15 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"smtp": {
|
||||||
|
"host": "",
|
||||||
|
"port": 25,
|
||||||
|
"from": "",
|
||||||
|
"user": "",
|
||||||
|
"password": "",
|
||||||
|
"auth_type": 0,
|
||||||
|
"encryption": 0,
|
||||||
|
"domain": ""
|
||||||
|
},
|
||||||
"plugins": []
|
"plugins": []
|
||||||
}
|
}
|
145
smtp/smtp.go
Normal file
145
smtp/smtp.go
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
// Package smtp provides supports for sending emails
|
||||||
|
package smtp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
mail "github.com/xhit/go-simple-mail/v2"
|
||||||
|
|
||||||
|
"github.com/drakkan/sftpgo/v2/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
logSender = "smtp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EmailContentType defines the support content types for email body
|
||||||
|
type EmailContentType int
|
||||||
|
|
||||||
|
// Supporte email body content type
|
||||||
|
const (
|
||||||
|
EmailContentTypeTextPlain EmailContentType = iota
|
||||||
|
EmailContentTypeTextHTML
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
smtpServer *mail.SMTPServer
|
||||||
|
from string
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config defines the SMTP configuration to use to send emails
|
||||||
|
type Config struct {
|
||||||
|
// Location of SMTP email server. Leavy empty to disable email sending capabilities
|
||||||
|
Host string `json:"host" mapstructure:"host"`
|
||||||
|
// Port of SMTP email server
|
||||||
|
Port int `json:"port" mapstructure:"port"`
|
||||||
|
// From address, for example "SFTPGo <sftpgo@example.com>"
|
||||||
|
From string `json:"from" mapstructure:"from"`
|
||||||
|
// SMTP username
|
||||||
|
User string `json:"user" mapstructure:"user"`
|
||||||
|
// SMTP password. Leaving both username and password empty the SMTP authentication
|
||||||
|
// will be disabled
|
||||||
|
Password string `json:"password" mapstructure:"password"`
|
||||||
|
// 0 Plain
|
||||||
|
// 1 Login
|
||||||
|
// 2 CRAM-MD5
|
||||||
|
AuthType int `json:"auth_type" mapstructure:"auth_type"`
|
||||||
|
// 0 no encryption
|
||||||
|
// 1 TLS
|
||||||
|
// 2 start TLS
|
||||||
|
Encryption int `json:"encryption" mapstructure:"encryption"`
|
||||||
|
// Domain to use for HELO command, if empty localhost will be used
|
||||||
|
Domain string `json:"domain" mapstructure:"domain"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize initialized and validates the SMTP configuration
|
||||||
|
func (c *Config) Initialize() error {
|
||||||
|
smtpServer = nil
|
||||||
|
if c.Host == "" {
|
||||||
|
logger.Debug(logSender, "", "configuration disabled, email capabilities will not be available")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if c.Port <= 0 || c.Port > 65535 {
|
||||||
|
return fmt.Errorf("smtp: invalid port %v", c.Port)
|
||||||
|
}
|
||||||
|
if c.AuthType < 0 || c.AuthType > 2 {
|
||||||
|
return fmt.Errorf("smtp: invalid auth type %v", c.AuthType)
|
||||||
|
}
|
||||||
|
if c.Encryption < 0 || c.Encryption > 2 {
|
||||||
|
return fmt.Errorf("smtp: invalid encryption %v", c.Encryption)
|
||||||
|
}
|
||||||
|
from = c.From
|
||||||
|
smtpServer = mail.NewSMTPClient()
|
||||||
|
smtpServer.Host = c.Host
|
||||||
|
smtpServer.Port = c.Port
|
||||||
|
smtpServer.Username = c.User
|
||||||
|
smtpServer.Password = c.Password
|
||||||
|
smtpServer.Authentication = c.getAuthType()
|
||||||
|
smtpServer.Encryption = c.getEncryption()
|
||||||
|
smtpServer.KeepAlive = false
|
||||||
|
smtpServer.ConnectTimeout = 10 * time.Second
|
||||||
|
smtpServer.SendTimeout = 30 * time.Second
|
||||||
|
if c.Domain != "" {
|
||||||
|
smtpServer.Helo = c.Domain
|
||||||
|
}
|
||||||
|
logger.Debug(logSender, "", "configuration successfully initialized, host: %#v, port: %v, username: %#v, auth: %v, encryption: %v, helo: %#v",
|
||||||
|
smtpServer.Host, smtpServer.Port, smtpServer.Username, smtpServer.Authentication, smtpServer.Encryption, smtpServer.Helo)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) getEncryption() mail.Encryption {
|
||||||
|
switch c.Encryption {
|
||||||
|
case 1:
|
||||||
|
return mail.EncryptionSSLTLS
|
||||||
|
case 2:
|
||||||
|
return mail.EncryptionSTARTTLS
|
||||||
|
default:
|
||||||
|
return mail.EncryptionNone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) getAuthType() mail.AuthType {
|
||||||
|
if c.User == "" && c.Password == "" {
|
||||||
|
return mail.AuthNone
|
||||||
|
}
|
||||||
|
switch c.AuthType {
|
||||||
|
case 1:
|
||||||
|
return mail.AuthLogin
|
||||||
|
case 2:
|
||||||
|
return mail.AuthCRAMMD5
|
||||||
|
default:
|
||||||
|
return mail.AuthPlain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendEmail tries to send an email using the specified parameters.
|
||||||
|
// If the contentType is 0 text/plain is assumed, otherwise text/html
|
||||||
|
func SendEmail(to, subject, body string, contentType EmailContentType) error {
|
||||||
|
if smtpServer == nil {
|
||||||
|
return errors.New("smtp: not configured")
|
||||||
|
}
|
||||||
|
smtpClient, err := smtpServer.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("smtp: unable to connect: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
email := mail.NewMSG()
|
||||||
|
if from != "" {
|
||||||
|
email.SetFrom(from)
|
||||||
|
}
|
||||||
|
email.AddTo(to).SetSubject(subject)
|
||||||
|
switch contentType {
|
||||||
|
case EmailContentTypeTextPlain:
|
||||||
|
email.SetBody(mail.TextPlain, body)
|
||||||
|
case EmailContentTypeTextHTML:
|
||||||
|
email.SetBody(mail.TextHTML, body)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("smtp: unsupported body content type %v", contentType)
|
||||||
|
}
|
||||||
|
if email.Error != nil {
|
||||||
|
return fmt.Errorf("smtp: email error: %w", email.Error)
|
||||||
|
}
|
||||||
|
return email.Send(smtpClient)
|
||||||
|
}
|
Loading…
Reference in a new issue