oauth.go

 1package config
 2
 3import (
 4	_ "embed"
 5	"fmt"
 6	"os"
 7	"os/exec"
 8	"path/filepath"
 9	"strings"
10)
11
12//go:embed oauth_script.py
13var embeddedOAuthScript []byte
14
15// IsOAuth2 returns true if the account uses OAuth2 authentication.
16func (a *Account) IsOAuth2() bool {
17	return a.AuthMethod == "oauth2"
18}
19
20// OAuthScriptPath returns the path to the OAuth2 Python helper script.
21// The script is embedded in the binary and extracted to ~/.config/matcha/oauth/
22// on first use.
23func OAuthScriptPath() (string, error) {
24	dir, err := configDir()
25	if err != nil {
26		return "", err
27	}
28
29	scriptDir := filepath.Join(dir, "oauth")
30	scriptPath := filepath.Join(scriptDir, "oauth.py")
31
32	// Always overwrite with the embedded version to stay in sync with the binary
33	if err := os.MkdirAll(scriptDir, 0700); err != nil {
34		return "", fmt.Errorf("could not create oauth directory: %w", err)
35	}
36	if err := os.WriteFile(scriptPath, embeddedOAuthScript, 0700); err != nil {
37		return "", fmt.Errorf("could not extract oauth script: %w", err)
38	}
39
40	return scriptPath, nil
41}
42
43// GetOAuth2Token retrieves a fresh OAuth2 access token for the account by
44// invoking the Python helper script. The script handles token refresh
45// automatically.
46func GetOAuth2Token(email string) (string, error) {
47	script, err := OAuthScriptPath()
48	if err != nil {
49		return "", err
50	}
51
52	cmd := exec.Command("python3", script, "token", email) //nolint:noctx
53	cmd.Stderr = os.Stderr
54	out, err := cmd.Output()
55	if err != nil {
56		return "", fmt.Errorf("oauth2 token retrieval failed: %w", err)
57	}
58
59	token := strings.TrimSpace(string(out))
60	if token == "" {
61		return "", fmt.Errorf("oauth2: empty access token returned")
62	}
63
64	return token, nil
65}
66
67// RunOAuth2Flow launches the OAuth2 authorization flow by invoking the Python
68// helper script. It opens the user's browser for authorization.
69// provider should be "gmail" or "outlook". If empty, the script auto-detects from the email.
70// clientID and clientSecret are optional — if empty, the script uses stored credentials.
71func RunOAuth2Flow(email, provider, clientID, clientSecret string) error {
72	script, err := OAuthScriptPath()
73	if err != nil {
74		return err
75	}
76
77	args := []string{script, "auth", email}
78	if provider != "" {
79		args = append(args, "--provider", provider)
80	}
81	if clientID != "" && clientSecret != "" {
82		args = append(args, "--client-id", clientID, "--client-secret", clientSecret)
83	}
84
85	cmd := exec.Command("python3", args...) //nolint:noctx
86	cmd.Stdout = os.Stdout
87	cmd.Stderr = os.Stderr
88	return cmd.Run()
89}