mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
feat: smtp mail provider implementation
This commit is contained in:
parent
ddc29f0414
commit
4706809170
@ -180,6 +180,10 @@ You can specify configuration options either via a config file (default: `config
|
|||||||
| `db.charset` | `WAKAPI_DB_CHARSET` | `utf8mb4` | Database connection charset (for MySQL only) |
|
| `db.charset` | `WAKAPI_DB_CHARSET` | `utf8mb4` | Database connection charset (for MySQL only) |
|
||||||
| `db.max_conn` | `WAKAPI_DB_MAX_CONNECTIONS` | `2` | Maximum number of database connections |
|
| `db.max_conn` | `WAKAPI_DB_MAX_CONNECTIONS` | `2` | Maximum number of database connections |
|
||||||
| `db.ssl` | `WAKAPI_DB_SSL` | `false` | Whether to use TLS encryption for database connection (Postgres and CockroachDB only) |
|
| `db.ssl` | `WAKAPI_DB_SSL` | `false` | Whether to use TLS encryption for database connection (Postgres and CockroachDB only) |
|
||||||
|
| `mail.enabled` | `WAKAPI_MAIL_ENABLED` | `true` | Whether to allow Wakapi to send e-mail (e.g. for password resets) |
|
||||||
|
| `mail.provider` | `WAKAPI_MAIL_PROVIDER` | `smtp` | Implementation to use for sending mails (one of [`smtp`, `mailwhale`]) |
|
||||||
|
| `mail.smtp.*` | `WAKAPI_MAIL_SMTP_*` | `-` | Various options to configure SMTP. See [default config](config.default.yaml) for details |
|
||||||
|
| `mail.mailwhale.*` | `WAKAPI_MAIL_MAILWHALE_*` | `-` | Various options to configure [MailWhale](https://mailwhale.dev) sending service. See [default config](config.default.yaml) for details |
|
||||||
| `sentry.dsn` | `WAKAPI_SENTRY_DSN` | – | DSN for to integrate [Sentry](https://sentry.io) for error logging and tracing (leave empty to disable) |
|
| `sentry.dsn` | `WAKAPI_SENTRY_DSN` | – | DSN for to integrate [Sentry](https://sentry.io) for error logging and tracing (leave empty to disable) |
|
||||||
| `sentry.enable_tracing` | `WAKAPI_SENTRY_TRACING` | `false` | Whether to enable Sentry request tracing |
|
| `sentry.enable_tracing` | `WAKAPI_SENTRY_TRACING` | `false` | Whether to enable Sentry request tracing |
|
||||||
| `sentry.sample_rate` | `WAKAPI_SENTRY_SAMPLE_RATE` | `0.75` | Probability of tracing a request in Sentry |
|
| `sentry.sample_rate` | `WAKAPI_SENTRY_SAMPLE_RATE` | `0.75` | Probability of tracing a request in Sentry |
|
||||||
|
@ -39,4 +39,19 @@ sentry:
|
|||||||
dsn: # leave blank to disable sentry integration
|
dsn: # leave blank to disable sentry integration
|
||||||
enable_tracing: true # whether to use performance monitoring
|
enable_tracing: true # whether to use performance monitoring
|
||||||
sample_rate: 0.75 # probability of tracing a request
|
sample_rate: 0.75 # probability of tracing a request
|
||||||
sample_rate_heartbeats: 0.1 # probability of tracing a heartbeat request
|
sample_rate_heartbeats: 0.1 # probability of tracing a heartbeat request
|
||||||
|
|
||||||
|
mail:
|
||||||
|
enabled: true # whether to enable mails (used for password resets, reports, etc.)
|
||||||
|
provider: smtp # method for sending mails, currently one of ['smtp', 'mailwhale']
|
||||||
|
smtp: # smtp settings when sending mails via smtp
|
||||||
|
host:
|
||||||
|
port:
|
||||||
|
username:
|
||||||
|
password:
|
||||||
|
tls:
|
||||||
|
sender: Wakapi <noreply@wakapi.dev>
|
||||||
|
mailwhale: # mailwhale.dev settings when using mailwhale as sending service
|
||||||
|
url:
|
||||||
|
client_id:
|
||||||
|
client_secret:
|
||||||
|
@ -50,10 +50,12 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
MailProviderSmtp = "smtp"
|
||||||
MailProviderMailWhale = "mailwhale"
|
MailProviderMailWhale = "mailwhale"
|
||||||
)
|
)
|
||||||
|
|
||||||
var emailProviders = []string{
|
var emailProviders = []string{
|
||||||
|
MailProviderSmtp,
|
||||||
MailProviderMailWhale,
|
MailProviderMailWhale,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,7 +99,7 @@ type serverConfig struct {
|
|||||||
ListenIpV4 string `yaml:"listen_ipv4" default:"127.0.0.1" env:"WAKAPI_LISTEN_IPV4"`
|
ListenIpV4 string `yaml:"listen_ipv4" default:"127.0.0.1" env:"WAKAPI_LISTEN_IPV4"`
|
||||||
ListenIpV6 string `yaml:"listen_ipv6" default:"::1" env:"WAKAPI_LISTEN_IPV6"`
|
ListenIpV6 string `yaml:"listen_ipv6" default:"::1" env:"WAKAPI_LISTEN_IPV6"`
|
||||||
BasePath string `yaml:"base_path" default:"/" env:"WAKAPI_BASE_PATH"`
|
BasePath string `yaml:"base_path" default:"/" env:"WAKAPI_BASE_PATH"`
|
||||||
PublicUrl string `yaml:"public_url" default:"https://wakapi.dev/" env:"WAKAPI_PUBLIC_URL"`
|
PublicUrl string `yaml:"public_url" default:"http://localhost:3000" env:"WAKAPI_PUBLIC_URL"`
|
||||||
TlsCertPath string `yaml:"tls_cert_path" default:"" env:"WAKAPI_TLS_CERT_PATH"`
|
TlsCertPath string `yaml:"tls_cert_path" default:"" env:"WAKAPI_TLS_CERT_PATH"`
|
||||||
TlsKeyPath string `yaml:"tls_key_path" default:"" env:"WAKAPI_TLS_KEY_PATH"`
|
TlsKeyPath string `yaml:"tls_key_path" default:"" env:"WAKAPI_TLS_KEY_PATH"`
|
||||||
}
|
}
|
||||||
@ -110,8 +112,10 @@ type sentryConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type mailConfig struct {
|
type mailConfig struct {
|
||||||
Provider string `env:"WAKAPI_MAIL_PROVIDER"`
|
Enabled bool `env:"WAKAPI_MAIL_ENABLED" default:"true"`
|
||||||
|
Provider string `env:"WAKAPI_MAIL_PROVIDER" default:"smtp"`
|
||||||
MailWhale *MailwhaleMailConfig `yaml:"mailwhale"`
|
MailWhale *MailwhaleMailConfig `yaml:"mailwhale"`
|
||||||
|
Smtp *SMTPMailConfig `yaml:"smtp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MailwhaleMailConfig struct {
|
type MailwhaleMailConfig struct {
|
||||||
@ -120,6 +124,15 @@ type MailwhaleMailConfig struct {
|
|||||||
ClientSecret string `yaml:"client_secret" env:"WAKAPI_MAIL_MAILWHALE_CLIENT_SECRET"`
|
ClientSecret string `yaml:"client_secret" env:"WAKAPI_MAIL_MAILWHALE_CLIENT_SECRET"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SMTPMailConfig struct {
|
||||||
|
Host string `env:"WAKAPI_MAIL_SMTP_HOST"`
|
||||||
|
Port uint `env:"WAKAPI_MAIL_SMTP_PORT"`
|
||||||
|
Username string `env:"WAKAPI_MAIL_SMTP_USER"`
|
||||||
|
Password string `env:"WAKAPI_MAIL_SMTP_PASS"`
|
||||||
|
TLS bool `env:"WAKAPI_MAIL_SMTP_TLS"`
|
||||||
|
Sender string `env:"WAKAPI_MAIL_SMTP_SENDER"`
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Env string `default:"dev" env:"ENVIRONMENT"`
|
Env string `default:"dev" env:"ENVIRONMENT"`
|
||||||
Version string `yaml:"-"`
|
Version string `yaml:"-"`
|
||||||
@ -263,6 +276,10 @@ func (c *serverConfig) GetPublicUrl() string {
|
|||||||
return strings.TrimSuffix(c.PublicUrl, "/")
|
return strings.TrimSuffix(c.PublicUrl, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *SMTPMailConfig) ConnStr() string {
|
||||||
|
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
||||||
|
}
|
||||||
|
|
||||||
func IsDev(env string) bool {
|
func IsDev(env string) bool {
|
||||||
return env == "dev" || env == "development"
|
return env == "dev" || env == "development"
|
||||||
}
|
}
|
||||||
|
2
go.mod
2
go.mod
@ -4,6 +4,8 @@ go 1.13
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
|
||||||
|
github.com/emersion/go-smtp v0.15.0
|
||||||
github.com/emvi/logbuch v1.1.1
|
github.com/emvi/logbuch v1.1.1
|
||||||
github.com/getsentry/sentry-go v0.10.0
|
github.com/getsentry/sentry-go v0.10.0
|
||||||
github.com/go-co-op/gocron v0.3.3
|
github.com/go-co-op/gocron v0.3.3
|
||||||
|
7
go.sum
7
go.sum
@ -77,6 +77,10 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1
|
|||||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||||
|
github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8=
|
||||||
|
github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||||
github.com/emvi/logbuch v1.1.1 h1:poBGNbHy/nB95oNoqLKAaJoBrcKxTO0W9DhMijKEkkU=
|
github.com/emvi/logbuch v1.1.1 h1:poBGNbHy/nB95oNoqLKAaJoBrcKxTO0W9DhMijKEkkU=
|
||||||
github.com/emvi/logbuch v1.1.1/go.mod h1:J2Wgbr3BuSc1JO+D2MBVh6q3WPVSK5GzktwWz8pvkKw=
|
github.com/emvi/logbuch v1.1.1/go.mod h1:J2Wgbr3BuSc1JO+D2MBVh6q3WPVSK5GzktwWz8pvkKw=
|
||||||
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
|
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
|
||||||
@ -99,6 +103,7 @@ github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/
|
|||||||
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
|
||||||
github.com/go-co-op/gocron v0.3.3 h1:QnarcMZWWKrEP25uCbtDiLsnnGw+PhCjL3wNITdWJOs=
|
github.com/go-co-op/gocron v0.3.3 h1:QnarcMZWWKrEP25uCbtDiLsnnGw+PhCjL3wNITdWJOs=
|
||||||
github.com/go-co-op/gocron v0.3.3/go.mod h1:Y9PWlYqDChf2Nbgg7kfS+ZsXHDTZbMZYPEQ0MILqH+M=
|
github.com/go-co-op/gocron v0.3.3/go.mod h1:Y9PWlYqDChf2Nbgg7kfS+ZsXHDTZbMZYPEQ0MILqH+M=
|
||||||
|
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
@ -166,6 +171,7 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
|
|||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
@ -408,6 +414,7 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
|
|||||||
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
||||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
|
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
45
models/mail.go
Normal file
45
models/mail.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Mail struct {
|
||||||
|
From MailAddress
|
||||||
|
To MailAddresses
|
||||||
|
Subject string
|
||||||
|
Body string
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mail) WithText(text string) *Mail {
|
||||||
|
m.Body = text
|
||||||
|
m.Type = "text/plain; charset=UTF-8"
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mail) WithHTML(html string) *Mail {
|
||||||
|
m.Body = html
|
||||||
|
m.Type = "text/html; charset=UTF-8"
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mail) String() string {
|
||||||
|
return fmt.Sprintf("To: %s\r\n"+
|
||||||
|
"From: %s\r\n"+
|
||||||
|
"Subject: %s\r\n"+
|
||||||
|
"Content-Type: %s\r\n"+
|
||||||
|
"\r\n"+
|
||||||
|
"%s\r\n",
|
||||||
|
strings.Join(m.To.RawStrings(), ", "),
|
||||||
|
m.From.String(),
|
||||||
|
m.Subject,
|
||||||
|
m.Type,
|
||||||
|
m.Body,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mail) Reader() *strings.Reader {
|
||||||
|
return strings.NewReader(m.String())
|
||||||
|
}
|
57
models/mail_address.go
Normal file
57
models/mail_address.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
const (
|
||||||
|
MailPattern = "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+"
|
||||||
|
EmailAddrPattern = ".*\\s<(" + MailPattern + ")>|(" + MailPattern + ")"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mailRegex *regexp.Regexp
|
||||||
|
emailAddrRegex *regexp.Regexp
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
mailRegex = regexp.MustCompile(MailPattern)
|
||||||
|
emailAddrRegex = regexp.MustCompile(EmailAddrPattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MailAddress string
|
||||||
|
|
||||||
|
type MailAddresses []MailAddress
|
||||||
|
|
||||||
|
func (m MailAddress) String() string {
|
||||||
|
return string(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MailAddress) Raw() string {
|
||||||
|
match := emailAddrRegex.FindStringSubmatch(string(m))
|
||||||
|
if len(match) == 3 {
|
||||||
|
if match[2] != "" {
|
||||||
|
return match[2]
|
||||||
|
}
|
||||||
|
return match[1]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MailAddress) Valid() bool {
|
||||||
|
return emailAddrRegex.Match([]byte(m))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MailAddresses) Strings() []string {
|
||||||
|
out := make([]string, len(m))
|
||||||
|
for i, s := range m {
|
||||||
|
out[i] = s.String()
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MailAddresses) RawStrings() []string {
|
||||||
|
out := make([]string, len(m))
|
||||||
|
for i, s := range m {
|
||||||
|
out[i] = s.Raw()
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
@ -2,14 +2,6 @@ package models
|
|||||||
|
|
||||||
import "regexp"
|
import "regexp"
|
||||||
|
|
||||||
const (
|
|
||||||
MailPattern = "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
mailRegex *regexp.Regexp
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
mailRegex = regexp.MustCompile(MailPattern)
|
mailRegex = regexp.MustCompile(MailPattern)
|
||||||
}
|
}
|
||||||
@ -17,7 +9,7 @@ func init() {
|
|||||||
type User struct {
|
type User struct {
|
||||||
ID string `json:"id" gorm:"primary_key"`
|
ID string `json:"id" gorm:"primary_key"`
|
||||||
ApiKey string `json:"api_key" gorm:"unique"`
|
ApiKey string `json:"api_key" gorm:"unique"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email" gorm:"uniqueIndex:idx_user_email"`
|
||||||
Password string `json:"-"`
|
Password string `json:"-"`
|
||||||
CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
||||||
LastLoggedInAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
LastLoggedInAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"`
|
||||||
|
@ -141,6 +141,7 @@ func (r *UserRepository) Update(user *models.User) (*models.User, error) {
|
|||||||
"share_machines": user.ShareMachines,
|
"share_machines": user.ShareMachines,
|
||||||
"wakatime_api_key": user.WakatimeApiKey,
|
"wakatime_api_key": user.WakatimeApiKey,
|
||||||
"has_data": user.HasData,
|
"has_data": user.HasData,
|
||||||
|
"reset_token": user.ResetToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
result := r.db.Model(user).Updates(updateMap)
|
result := r.db.Model(user).Updates(updateMap)
|
||||||
|
@ -234,6 +234,7 @@ func (h *LoginHandler) PostSetPassword(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
user.Password = setRequest.Password
|
user.Password = setRequest.Password
|
||||||
|
user.ResetToken = ""
|
||||||
if hash, err := utils.HashBcrypt(user.Password, h.config.Security.PasswordSalt); err != nil {
|
if hash, err := utils.HashBcrypt(user.Password, h.config.Security.PasswordSalt); err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to set new password"))
|
templates[conf.SetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("failed to set new password"))
|
||||||
@ -256,6 +257,12 @@ func (h *LoginHandler) PostResetPassword(w http.ResponseWriter, r *http.Request)
|
|||||||
loadTemplates()
|
loadTemplates()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !h.config.Mail.Enabled {
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
templates[conf.ResetPasswordTemplate].Execute(w, h.buildViewModel(r).WithError("mailing is disabled on this server"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var resetRequest models.ResetPasswordRequest
|
var resetRequest models.ResetPasswordRequest
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
@ -277,7 +284,7 @@ func (h *LoginHandler) PostResetPassword(w http.ResponseWriter, r *http.Request)
|
|||||||
go func(user *models.User) {
|
go func(user *models.User) {
|
||||||
link := fmt.Sprintf("%s/set-password?token=%s", h.config.Server.GetPublicUrl(), user.ResetToken)
|
link := fmt.Sprintf("%s/set-password?token=%s", h.config.Server.GetPublicUrl(), user.ResetToken)
|
||||||
if err := h.mailSrvc.SendPasswordResetMail(user, link); err != nil {
|
if err := h.mailSrvc.SendPasswordResetMail(user, link); err != nil {
|
||||||
logbuch.Error("%v", err)
|
logbuch.Error("failed to send password reset mail to %s – %v", user.ID, err)
|
||||||
} else {
|
} else {
|
||||||
logbuch.Info("sent password reset mail to %s", user.ID)
|
logbuch.Info("sent password reset mail to %s", user.ID)
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,61 @@
|
|||||||
package mail
|
package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/emvi/logbuch"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"github.com/markbates/pkger"
|
||||||
conf "github.com/muety/wakapi/config"
|
conf "github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/models"
|
|
||||||
"github.com/muety/wakapi/services"
|
"github.com/muety/wakapi/services"
|
||||||
|
"io/ioutil"
|
||||||
|
"text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tplPath = "/views/mail"
|
||||||
|
tplNamePasswordReset = "reset_password"
|
||||||
|
subjectPasswordReset = "Wakapi – Password Reset"
|
||||||
|
)
|
||||||
|
|
||||||
|
type passwordResetLinkTplData struct {
|
||||||
|
ResetLink string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Factory
|
||||||
func NewMailService() services.IMailService {
|
func NewMailService() services.IMailService {
|
||||||
config := conf.Get()
|
config := conf.Get()
|
||||||
if config.Mail.Provider == conf.MailProviderMailWhale {
|
if config.Mail.Enabled {
|
||||||
return NewMailWhaleService(config.Mail.MailWhale)
|
if config.Mail.Provider == conf.MailProviderMailWhale {
|
||||||
|
return NewMailWhaleService(config.Mail.MailWhale)
|
||||||
|
} else if config.Mail.Provider == conf.MailProviderSmtp {
|
||||||
|
return NewSMTPMailService(config.Mail.Smtp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &NoopMailService{}
|
return &NoopMailService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type NoopMailService struct{}
|
func getPasswordResetTemplate(data passwordResetLinkTplData) (*bytes.Buffer, error) {
|
||||||
|
tpl, err := loadTemplate(tplNamePasswordReset)
|
||||||
func (n NoopMailService) SendPasswordResetMail(recipient *models.User, resetLink string) error {
|
if err != nil {
|
||||||
logbuch.Info("noop mail service doing nothing instead of sending password reset mail to %s", recipient.ID)
|
return nil, err
|
||||||
return nil
|
}
|
||||||
|
var rendered bytes.Buffer
|
||||||
|
if err := tpl.Execute(&rendered, data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &rendered, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadTemplate(tplName string) (*template.Template, error) {
|
||||||
|
tplFile, err := pkger.Open(fmt.Sprintf("%s/%s.tpl.html", tplPath, tplName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer tplFile.Close()
|
||||||
|
|
||||||
|
tplData, err := ioutil.ReadAll(tplFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return template.New(tplName).Parse(string(tplData))
|
||||||
}
|
}
|
||||||
|
@ -5,21 +5,13 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/markbates/pkger"
|
|
||||||
conf "github.com/muety/wakapi/config"
|
conf "github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"text/template"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
type MailWhaleMailService struct {
|
||||||
tplPath = "/views/mail"
|
|
||||||
tplNamePasswordReset = "reset_password"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MailWhaleService struct {
|
|
||||||
config *conf.MailwhaleMailConfig
|
config *conf.MailwhaleMailConfig
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
@ -33,8 +25,8 @@ type MailWhaleSendRequest struct {
|
|||||||
TemplateVars map[string]string `json:"template_vars"`
|
TemplateVars map[string]string `json:"template_vars"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMailWhaleService(config *conf.MailwhaleMailConfig) *MailWhaleService {
|
func NewMailWhaleService(config *conf.MailwhaleMailConfig) *MailWhaleMailService {
|
||||||
return &MailWhaleService{
|
return &MailWhaleMailService{
|
||||||
config: config,
|
config: config,
|
||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Timeout: 10 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
@ -42,25 +34,15 @@ func NewMailWhaleService(config *conf.MailwhaleMailConfig) *MailWhaleService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MailWhaleService) SendPasswordResetMail(recipient *models.User, resetLink string) error {
|
func (s *MailWhaleMailService) SendPasswordResetMail(recipient *models.User, resetLink string) error {
|
||||||
tpl, err := m.loadTemplate(tplNamePasswordReset)
|
template, err := getPasswordResetTemplate(passwordResetLinkTplData{ResetLink: resetLink})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return s.send(recipient.Email, subjectPasswordReset, template.String(), true)
|
||||||
type data struct {
|
|
||||||
ResetLink string
|
|
||||||
}
|
|
||||||
|
|
||||||
var rendered bytes.Buffer
|
|
||||||
if err := tpl.Execute(&rendered, data{ResetLink: resetLink}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.send(recipient.Email, "Wakapi – Password Reset", rendered.String(), true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MailWhaleService) send(to, subject, body string, isHtml bool) error {
|
func (s *MailWhaleMailService) send(to, subject, body string, isHtml bool) error {
|
||||||
if to == "" {
|
if to == "" {
|
||||||
return errors.New("no recipient mail address set, cannot send password reset link")
|
return errors.New("no recipient mail address set, cannot send password reset link")
|
||||||
}
|
}
|
||||||
@ -76,36 +58,21 @@ func (m *MailWhaleService) send(to, subject, body string, isHtml bool) error {
|
|||||||
}
|
}
|
||||||
payload, _ := json.Marshal(sendRequest)
|
payload, _ := json.Marshal(sendRequest)
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/mail", m.config.Url), bytes.NewBuffer(payload))
|
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/mail", s.config.Url), bytes.NewBuffer(payload))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.SetBasicAuth(m.config.ClientId, m.config.ClientSecret)
|
req.SetBasicAuth(s.config.ClientId, s.config.ClientSecret)
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
res, err := m.httpClient.Do(req)
|
res, err := s.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if res.StatusCode >= 400 {
|
if res.StatusCode >= 400 {
|
||||||
return errors.New(fmt.Sprintf("failed to send password reset mail to %v, got status %d from mailwhale", to, res.StatusCode))
|
return errors.New(fmt.Sprintf("got status %d from mailwhale", res.StatusCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MailWhaleService) loadTemplate(tplName string) (*template.Template, error) {
|
|
||||||
tplFile, err := pkger.Open(fmt.Sprintf("%s/%s.tpl.html", tplPath, tplName))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer tplFile.Close()
|
|
||||||
|
|
||||||
tplData, err := ioutil.ReadAll(tplFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return template.New(tplName).Parse(string(tplData))
|
|
||||||
}
|
|
||||||
|
13
services/mail/noop.go
Normal file
13
services/mail/noop.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/emvi/logbuch"
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NoopMailService struct{}
|
||||||
|
|
||||||
|
func (n *NoopMailService) SendPasswordResetMail(recipient *models.User, resetLink string) error {
|
||||||
|
logbuch.Info("noop mail service doing nothing instead of sending password reset mail to %s", recipient.ID)
|
||||||
|
return nil
|
||||||
|
}
|
100
services/mail/smtp.go
Normal file
100
services/mail/smtp.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/emersion/go-sasl"
|
||||||
|
"github.com/emersion/go-smtp"
|
||||||
|
conf "github.com/muety/wakapi/config"
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SMTPMailService struct {
|
||||||
|
config *conf.SMTPMailConfig
|
||||||
|
auth sasl.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSMTPMailService(config *conf.SMTPMailConfig) *SMTPMailService {
|
||||||
|
return &SMTPMailService{
|
||||||
|
config: config,
|
||||||
|
auth: sasl.NewPlainClient(
|
||||||
|
"",
|
||||||
|
config.Username,
|
||||||
|
config.Password,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SMTPMailService) SendPasswordResetMail(recipient *models.User, resetLink string) error {
|
||||||
|
template, err := getPasswordResetTemplate(passwordResetLinkTplData{ResetLink: resetLink})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mail := &models.Mail{
|
||||||
|
From: models.MailAddress(s.config.Sender),
|
||||||
|
To: models.MailAddresses([]models.MailAddress{models.MailAddress(recipient.Email)}),
|
||||||
|
Subject: subjectPasswordReset,
|
||||||
|
}
|
||||||
|
mail.WithHTML(template.String())
|
||||||
|
|
||||||
|
return s.send(
|
||||||
|
s.config.ConnStr(),
|
||||||
|
s.config.TLS,
|
||||||
|
s.auth,
|
||||||
|
mail.From.Raw(),
|
||||||
|
mail.To.RawStrings(),
|
||||||
|
mail.Reader(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SMTPMailService) send(addr string, tls bool, a sasl.Client, from string, to []string, r io.Reader) error {
|
||||||
|
dial := smtp.Dial
|
||||||
|
if tls {
|
||||||
|
dial = func(addr string) (*smtp.Client, error) {
|
||||||
|
return smtp.DialTLS(addr, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := dial(addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
if ok, _ := c.Extension("STARTTLS"); ok {
|
||||||
|
if err = c.StartTLS(nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a != nil {
|
||||||
|
if ok, _ := c.Extension("AUTH"); !ok {
|
||||||
|
return errors.New("smtp: server doesn't support AUTH")
|
||||||
|
}
|
||||||
|
if err = c.Auth(a); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = c.Mail(from, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, addr := range to {
|
||||||
|
if err = c.Rcpt(addr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w, err := c.Data()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = io.Copy(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = w.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Quit()
|
||||||
|
}
|
1
services/mail/utils.go
Normal file
1
services/mail/utils.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package mail
|
Loading…
x
Reference in New Issue
Block a user