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}