Compare commits

...

2 Commits

Author SHA1 Message Date
bel
1b74eb0d16 oauth can refresh 2026-02-23 23:58:23 -07:00
bel
5a4ce70451 can oauth 2026-02-23 22:59:40 -07:00
6 changed files with 144 additions and 5 deletions

View File

@@ -1,6 +1,6 @@
module gitea.inhome.blapointe.com/contact/cmd/recv module gitea.inhome.blapointe.com/contact/cmd/recv
go 1.20 go 1.24.0
replace gitea.inhome.blapointe.com/local-sandbox/contact => ../../ replace gitea.inhome.blapointe.com/local-sandbox/contact => ../../
@@ -10,9 +10,12 @@ require (
) )
require ( require (
cloud.google.com/go/compute/metadata v0.3.0 // indirect
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e // indirect github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e // indirect
github.com/emersion/go-imap v1.2.0 // indirect github.com/emersion/go-imap v1.2.0 // indirect
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/google/uuid v1.6.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
) )

View File

@@ -1,3 +1,5 @@
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
gitea.inhome.blapointe.com/local/args v0.0.0-20231109145953-eb2e1c1b8d56 h1:zTGGZ77KLFagqUvDSgTOnm0qF+iSLwQWiEtGjb2jjlY= gitea.inhome.blapointe.com/local/args v0.0.0-20231109145953-eb2e1c1b8d56 h1:zTGGZ77KLFagqUvDSgTOnm0qF+iSLwQWiEtGjb2jjlY=
gitea.inhome.blapointe.com/local/args v0.0.0-20231109145953-eb2e1c1b8d56/go.mod h1:SqCOE3bE3wvrztVIQGHuyxHKfDjRKU9EWhBdkmkiwyc= gitea.inhome.blapointe.com/local/args v0.0.0-20231109145953-eb2e1c1b8d56/go.mod h1:SqCOE3bE3wvrztVIQGHuyxHKfDjRKU9EWhBdkmkiwyc=
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e h1:mQTN05gz0rDZSABqKMzAPMb5ATWcvvdMljRzEh0LjBo= github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e h1:mQTN05gz0rDZSABqKMzAPMb5ATWcvvdMljRzEh0LjBo=
@@ -7,7 +9,13 @@ github.com/emersion/go-imap v1.2.0/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5N
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= 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-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=

View File

@@ -24,6 +24,7 @@ func main() {
as.Append(args.STRING, "p", "password", emailer.Password) as.Append(args.STRING, "p", "password", emailer.Password)
as.Append(args.INT, "b", "dont read more than this many characters", 4096) as.Append(args.INT, "b", "dont read more than this many characters", 4096)
as.Append(args.BOOL, "json", "output as json", false) as.Append(args.BOOL, "json", "output as json", false)
as.Append(args.STRING, "oauth", "", emailer.OAuth)
if err := as.Parse(); err != nil { if err := as.Parse(); err != nil {
panic(err) panic(err)
} }
@@ -33,6 +34,7 @@ func main() {
emailer.Limit = as.Get("n").GetInt() emailer.Limit = as.Get("n").GetInt()
emailer.From = as.GetString("u") emailer.From = as.GetString("u")
emailer.Password = as.GetString("p") emailer.Password = as.GetString("p")
emailer.OAuth = as.GetString("oauth")
var msgs <-chan *mail.Message var msgs <-chan *mail.Message
var err error var err error

121
email.go
View File

@@ -1,17 +1,30 @@
package contact package contact
import ( import (
"bytes"
"context"
"crypto/sha256"
"crypto/tls" "crypto/tls"
"encoding/base64"
"encoding/json"
"fmt" "fmt"
"io"
"log" "log"
"net/http"
"net/mail" "net/mail"
"net/smtp" "net/smtp"
"net/url"
"os" "os"
"strings" "strings"
"time"
"github.com/bytbox/go-pop3" "github.com/bytbox/go-pop3"
"github.com/emersion/go-imap" "github.com/emersion/go-imap"
"github.com/emersion/go-imap/client" "github.com/emersion/go-imap/client"
"github.com/emersion/go-sasl"
"github.com/google/uuid"
"golang.org/x/oauth2/authhandler"
"golang.org/x/oauth2/google"
) )
type Emailer struct { type Emailer struct {
@@ -21,6 +34,7 @@ type Emailer struct {
IMAP string IMAP string
Password string Password string
Limit int Limit int
OAuth string
} }
func NewEmailer() *Emailer { func NewEmailer() *Emailer {
@@ -34,6 +48,7 @@ func NewEmailer() *Emailer {
From: envOr("FROM", "breellocaldev@gmail.com"), From: envOr("FROM", "breellocaldev@gmail.com"),
SMTP: envOr("SMTP", "smtp.gmail.com:465"), SMTP: envOr("SMTP", "smtp.gmail.com:465"),
Password: envOr("PASSWORD", "lhnjijrvqaesiufp"), Password: envOr("PASSWORD", "lhnjijrvqaesiufp"),
OAuth: envOr("OAUTH", ""),
} }
} }
@@ -42,12 +57,114 @@ func (e *Emailer) Read() (chan *mail.Message, error) {
} }
func (e *Emailer) ReadIMAP() (chan *mail.Message, error) { func (e *Emailer) ReadIMAP() (chan *mail.Message, error) {
if e.OAuth != "" {
return e.oauthThenReadIMAP()
}
return e.readIMAP(e.From, e.Password)
}
func (e *Emailer) oauthThenReadIMAP() (chan *mail.Message, error) {
configJSON, err := e.configJSON()
if err != nil {
return nil, err
}
if token, _ := os.ReadFile(e.OAuth + ".token"); len(token) == 0 {
state := uuid.NewString()
verifier := uuid.NewString()
s256 := sha256.Sum256([]byte(verifier))
challenge := base64.RawURLEncoding.EncodeToString(s256[:])
pkceParams := authhandler.PKCEParams{Challenge: challenge, ChallengeMethod: "S256", Verifier: verifier}
config, err := google.ConfigFromJSON([]byte(configJSON), "https://mail.google.com/")
if err != nil {
return nil, err
}
credentialsParams := google.CredentialsParams{Scopes: config.Scopes, State: state, AuthHandler: func(authCodeURL string) (string, string, error) {
fmt.Println(authCodeURL + "&access_type=offline&prompt=consent")
var callback string
_, err := fmt.Scan(&callback)
u, _ := url.Parse(callback)
fmt.Println()
return u.Query().Get("code"), state, err
}, PKCE: &pkceParams}
credentials, err := google.CredentialsFromJSONWithParams(context.Background(), []byte(configJSON), credentialsParams)
if err != nil {
return nil, err
}
token, err := credentials.TokenSource.Token()
if err != nil {
return nil, err
}
tokenFileJSON, _ := json.Marshal(token)
os.WriteFile(e.OAuth+".token", tokenFileJSON, os.ModePerm)
log.Printf("%s", tokenFileJSON)
}
var tokenStruct struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
Expiry time.Time `json:"expiry"`
ExpiresIn int `json:"expires_in"`
}
token, _ := os.ReadFile(e.OAuth + ".token")
json.Unmarshal(token, &tokenStruct)
if time.Until(tokenStruct.Expiry) < time.Minute*5 {
var appStruct struct {
Installed struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
GrantType string `json:"grant_type"`
RefreshToken string `json:"refresh_token"`
} `json:"installed"`
}
json.Unmarshal([]byte(configJSON), &appStruct)
appStruct.Installed.GrantType = "refresh_token"
appStruct.Installed.RefreshToken = tokenStruct.RefreshToken
b, _ := json.Marshal(appStruct.Installed)
resp, err := http.Post(`https://oauth2.googleapis.com/token`, `application/json`, bytes.NewReader(b))
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, _ = io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("(%d) %s", resp.StatusCode, b)
}
tokenStruct.Expiry = time.Time{}
json.Unmarshal(b, &tokenStruct)
tokenStruct.Expiry = time.Now().Add(time.Duration(tokenStruct.ExpiresIn) * time.Second)
tokenStruct.RefreshToken = appStruct.Installed.RefreshToken
b, _ = json.Marshal(tokenStruct)
os.WriteFile(e.OAuth+".token", b, os.ModePerm)
}
log.Println("OAUTH", e.From, time.Until(tokenStruct.Expiry), tokenStruct.AccessToken)
return e.readIMAP(e.From, tokenStruct.AccessToken)
}
func (e *Emailer) configJSON() (string, error) {
b, err := os.ReadFile(e.OAuth)
return string(b), err
}
func (e *Emailer) readIMAP(authU, authP string) (chan *mail.Message, error) {
c, err := client.DialTLS(e.IMAP, nil) c, err := client.DialTLS(e.IMAP, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := c.Login(e.From, e.Password); err != nil { if e.OAuth == "" {
return nil, err if err := c.Login(authU, authP); err != nil {
return nil, err
}
} else {
if err := c.Authenticate(sasl.NewOAuthBearerClient(&sasl.OAuthBearerOptions{
Username: authU,
Token: authP,
Host: "",
Port: 0,
})); err != nil {
return nil, err
}
} }
mbox, err := c.Select("INBOX", true) //readonly mbox, err := c.Select("INBOX", true) //readonly
if err != nil { if err != nil {

5
go.mod
View File

@@ -1,13 +1,16 @@
module gitea.inhome.blapointe.com/local-sandbox/contact module gitea.inhome.blapointe.com/local-sandbox/contact
go 1.17 go 1.24.0
require ( require (
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e
github.com/emersion/go-imap v1.2.0 github.com/emersion/go-imap v1.2.0
github.com/google/uuid v1.6.0
golang.org/x/oauth2 v0.35.0
) )
require ( require (
cloud.google.com/go/compute/metadata v0.3.0 // indirect
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
) )

6
go.sum
View File

@@ -1,3 +1,5 @@
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e h1:mQTN05gz0rDZSABqKMzAPMb5ATWcvvdMljRzEh0LjBo= github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e h1:mQTN05gz0rDZSABqKMzAPMb5ATWcvvdMljRzEh0LjBo=
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e/go.mod h1:alXX+s7a4cKaIprgjeEboqi4Tm7XR/HXEwUTxUV/ywU= github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e/go.mod h1:alXX+s7a4cKaIprgjeEboqi4Tm7XR/HXEwUTxUV/ywU=
github.com/emersion/go-imap v1.2.0 h1:lyUQ3+EVM21/qbWE/4Ya5UG9r5+usDxlg4yfp3TgHFA= github.com/emersion/go-imap v1.2.0 h1:lyUQ3+EVM21/qbWE/4Ya5UG9r5+usDxlg4yfp3TgHFA=
@@ -6,6 +8,10 @@ github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwd
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= 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-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=