Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(smtp): add support for oauth2 and gmail generators #44

Merged
merged 5 commits into from
Nov 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ require (
github.com/mitchellh/mapstructure v1.2.2 // indirect
github.com/onsi/ginkgo v1.8.0
github.com/onsi/gomega v1.5.0
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
github.com/pelletier/go-toml v1.7.0 // indirect
github.com/sirupsen/logrus v1.2.0
github.com/spf13/afero v1.2.2 // indirect
Expand Down
13 changes: 13 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
Expand Down Expand Up @@ -168,17 +170,26 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down Expand Up @@ -208,6 +219,8 @@ golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
Expand Down
2 changes: 2 additions & 0 deletions pkg/generators/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package generators
import (
"fmt"
"github.com/containrrr/shoutrrr/pkg/generators/basic"
"github.com/containrrr/shoutrrr/pkg/generators/xouath2"
t "github.com/containrrr/shoutrrr/pkg/types"
"strings"
)

var generatorMap = map[string]func() t.Generator{
"basic": func() t.Generator { return &basic.Generator{} },
"oauth2": func() t.Generator { return &xouath2.Generator{} },
}

func NewGenerator(identifier string) (t.Generator, error) {
Expand Down
189 changes: 189 additions & 0 deletions pkg/generators/xouath2/xoauth2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package xouath2

import (
"encoding/json"
"fmt"
"github.com/containrrr/shoutrrr/pkg/services/smtp"
"github.com/containrrr/shoutrrr/pkg/types"
"golang.org/x/net/context"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"io/ioutil"
"strings"
)

type Generator struct{}

func (g *Generator) Generate(_ types.Service, props map[string]string, args []string) (types.ServiceConfig, error) {

if provider, found := props["provider"]; found {
if provider == "gmail" {
return oauth2GeneratorGmail(args[0])
}
}

if len(args) > 0 {
return oauth2GeneratorFile(args[0])
} else {
return oauth2Generator()
}

}

func oauth2GeneratorFile(file string) (*smtp.Config, error) {
jsonData, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}

var p struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
RedirectURL string `json:"redirect_url"`
AuthURL string `json:"auth_url"`
TokenURL string `json:"token_url"`
Hostname string `json:"smtp_hostname"`
Scopes []string `json:"scopes"`
}

if err := json.Unmarshal(jsonData, &p); err != nil {
return nil, err
}

conf := oauth2.Config{
ClientID: p.ClientID,
ClientSecret: p.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: p.AuthURL,
TokenURL: p.TokenURL,
AuthStyle: oauth2.AuthStyleAutoDetect,
},
RedirectURL: p.RedirectURL,
Scopes: p.Scopes,
}

return generateOauth2Config(&conf, p.Hostname)
}

func oauth2Generator() (*smtp.Config, error) {

var clientId string
fmt.Print("ClientID: ")
_, err := fmt.Scanln(&clientId)
if err != nil {
return nil, err
}

var clientSecret string
fmt.Print("ClientSecret: ")
_, err = fmt.Scanln(&clientSecret)
if err != nil {
return nil, err
}

var authUrl string
fmt.Print("AuthURL: ")
_, err = fmt.Scanln(&authUrl)
if err != nil {
return nil, err
}

var tokenUrl string
fmt.Print("TokenURL: ")
_, err = fmt.Scanln(&tokenUrl)
if err != nil {
return nil, err
}

var redirectUrl string
fmt.Print("RedirectURL: ")
_, err = fmt.Scanln(&redirectUrl)
if err != nil {
return nil, err
}

var scopes string
fmt.Print("Scopes: ")
_, err = fmt.Scanln(&scopes)
if err != nil {
return nil, err
}

var hostname string
fmt.Print("SMTP Hostname: ")
_, err = fmt.Scanln(&hostname)
if err != nil {
return nil, err
}

conf := oauth2.Config{
ClientID: clientId,
ClientSecret: clientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: authUrl,
TokenURL: tokenUrl,
AuthStyle: oauth2.AuthStyleAutoDetect,
},
RedirectURL: redirectUrl,
Scopes: strings.Split(scopes, ","),
}

return generateOauth2Config(&conf, hostname)
}

func oauth2GeneratorGmail(credFile string) (*smtp.Config, error) {
data, err := ioutil.ReadFile(credFile)
if err != nil {
return nil, err
}

conf, err := google.ConfigFromJSON(data, "https://mail.google.com/")
if err != nil {
return nil, err
}

return generateOauth2Config(conf, "smtp.gmail.com")

}

func generateOauth2Config(conf *oauth2.Config, host string) (*smtp.Config, error) {

fmt.Printf("Visit the following URL to authenticate:\n%s\n\n", conf.AuthCodeURL(""))

var verCode string
fmt.Print("Enter verification code: ")
_, err := fmt.Scanln(&verCode)
if err != nil {
return nil, err
}

ctx := context.Background()

token, err := conf.Exchange(ctx, verCode)
if err != nil {
return nil, err
}

var sender string
fmt.Print("Enter sender e-mail: ")
_, err = fmt.Scanln(&sender)
if err != nil {
return nil, err
}

svcConf := &smtp.Config{
Host: host,
Port: 25,
Username: sender,
Password: token.AccessToken,
FromAddress: sender,
FromName: "Shoutrrr",
ToAddresses: []string{sender},
Auth: smtp.OAuth2,
UseStartTLS: true,
UseHTML: true,
}

return svcConf, nil
}

2 changes: 2 additions & 0 deletions pkg/services/smtp/smtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ func (service *Service) getAuth() (smtp.Auth, failure) {
return smtp.PlainAuth("", config.Username, config.Password, config.Host), nil
case authTypes.CRAMMD5:
return smtp.CRAMMD5Auth(config.Username, config.Password), nil
case authTypes.OAuth2:
return OAuth2Auth(config.Username, config.Password), nil
default:
return nil, fail(FailAuthType, nil, config.Auth.String())
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/services/smtp/smtp_authtype.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type authTypeVals struct {
Plain authType
CRAMMD5 authType
Unknown authType
OAuth2 authType
Enum types.EnumFormatter
}

Expand All @@ -20,12 +21,14 @@ var authTypes = &authTypeVals{
Plain: 1,
CRAMMD5: 2,
Unknown: 3,
OAuth2: 4,
Enum: format.CreateEnumFormatter(
[]string{
"None",
"Plain",
"CRAMMD5",
"Unknown",
"OAuth2",
}),
}

Expand All @@ -36,3 +39,5 @@ func (at authType) String() string {
func parseAuth(s string) authType {
return authType(authTypes.Enum.Parse(s))
}

var OAuth2 = authTypes.OAuth2
28 changes: 28 additions & 0 deletions pkg/services/smtp/smtp_oauth2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package smtp

import (
"net/smtp"
)

type oauth2Auth struct {
username, accessToken string
}

// OAuth2Auth returns an Auth that implements the SASL XOAUTH2 authentication
// as per https://developers.google.com/gmail/imap/xoauth2-protocol
func OAuth2Auth(username, accessToken string) smtp.Auth {
return &oauth2Auth{username, accessToken}
}

func (a *oauth2Auth) Start(_ *smtp.ServerInfo) (string, []byte, error) {

resp := []byte("user=" + a.username + "\x01auth=Bearer " + a.accessToken + "\x01\x01")

return "XOAUTH2", resp, nil
}

func (a *oauth2Auth) Next(_ []byte, _ bool) ([]byte, error) {
return nil, nil
}