-
Notifications
You must be signed in to change notification settings - Fork 0
Add standalone background Gmail reader script #55
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,339 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package main | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "bytes" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "encoding/base64" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "encoding/json" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "io" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "log" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "mime/multipart" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "net/http" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "strconv" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "golang.org/x/oauth2" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "golang.org/x/oauth2/google" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "google.golang.org/api/gmail/v1" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "google.golang.org/api/option" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Retrieve a token, saves the token, then returns the generated client. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func getClient(config *oauth2.Config) *http.Client { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // The file token.json stores the user's access and refresh tokens, and is | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // created automatically when the authorization flow completes for the first | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // time. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tokFile := "token.json" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tok, err := tokenFromFile(tokFile) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tok = getTokenFromWeb(config) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| saveToken(tokFile, tok) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return config.Client(context.Background(), tok) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Request a token from the web, then returns the retrieved token. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func getTokenFromWeb(config *oauth2.Config) *oauth2.Token { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fmt.Printf("Go to the following link in your browser then type the "+ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "authorization code: \n%v\n", authURL) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var authCode string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if _, err := fmt.Scan(&authCode); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Fatalf("Unable to read authorization code: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tok, err := config.Exchange(context.TODO(), authCode) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Fatalf("Unable to retrieve token from web: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return tok | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Retrieves a token from a local file. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func tokenFromFile(file string) (*oauth2.Token, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f, err := os.Open(file) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer f.Close() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tok := &oauth2.Token{} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| err = json.NewDecoder(f).Decode(tok) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return tok, err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Saves a token to a file path. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func saveToken(path string, token *oauth2.Token) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fmt.Printf("Saving credential file to: %s\n", path) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Fatalf("Unable to cache oauth token: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer f.Close() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| json.NewEncoder(f).Encode(token) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Configuration holds the configurable variables | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type Configuration struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PollingInterval time.Duration | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WebhookURL string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SearchQuery string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func loadConfig() Configuration { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cfg := Configuration{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WebhookURL: "http://localhost:8080/webhook", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WebhookURL: "http://localhost:8080/webhook", | |
| WebhookURL: "http://localhost:8080/message", |
Gerifield marked this conversation as resolved.
Show resolved
Hide resolved
Gerifield marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gmail message/attachment bodies are base64url-encoded and commonly omit padding. Using base64.URLEncoding.DecodeString will fail on unpadded data. Prefer base64.RawURLEncoding.DecodeString(...) (or URLEncoding.WithPadding(base64.NoPadding)) for attachObj.Data and part.Body.Data.
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
writer.WriteField(...) return values are ignored. If a write fails, the script will still send the request (possibly missing required fields) with no surfaced error. Capture these errors and return early so the message remains unread and can be retried with a clear log.
| _ = writer.WriteField("Sender", data.Sender) | |
| _ = writer.WriteField("Subject", data.Subject) | |
| _ = writer.WriteField("Date", data.Date) | |
| // Add body if no attachments or to provide context | |
| if data.Body != "" { | |
| _ = writer.WriteField("Body", data.Body) | |
| if err := writer.WriteField("Sender", data.Sender); err != nil { | |
| return fmt.Errorf("failed to write Sender field: %w", err) | |
| } | |
| if err := writer.WriteField("Subject", data.Subject); err != nil { | |
| return fmt.Errorf("failed to write Subject field: %w", err) | |
| } | |
| if err := writer.WriteField("Date", data.Date); err != nil { | |
| return fmt.Errorf("failed to write Date field: %w", err) | |
| } | |
| // Add body if no attachments or to provide context | |
| if data.Body != "" { | |
| if err := writer.WriteField("Body", data.Body); err != nil { | |
| return fmt.Errorf("failed to write Body field: %w", err) | |
| } |
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The repo’s /message endpoint reads the user message from form field message (r.PostFormValue("message")). This client sends Sender/Subject/Date/Body fields but never sends message, so the server will see an empty prompt. Consider populating a message field (e.g., subject + body + metadata) and keeping payload for attachments.
| // Populate unified message field expected by the /message endpoint. | |
| // This combines metadata and body so that r.PostFormValue("message") | |
| // on the server side receives a meaningful prompt. | |
| var messageContentBuilder strings.Builder | |
| if data.Sender != "" { | |
| messageContentBuilder.WriteString("From: ") | |
| messageContentBuilder.WriteString(data.Sender) | |
| messageContentBuilder.WriteString("\n") | |
| } | |
| if data.Subject != "" { | |
| messageContentBuilder.WriteString("Subject: ") | |
| messageContentBuilder.WriteString(data.Subject) | |
| messageContentBuilder.WriteString("\n") | |
| } | |
| if data.Date != "" { | |
| messageContentBuilder.WriteString("Date: ") | |
| messageContentBuilder.WriteString(data.Date) | |
| messageContentBuilder.WriteString("\n") | |
| } | |
| if data.Body != "" { | |
| messageContentBuilder.WriteString("\n") | |
| messageContentBuilder.WriteString(data.Body) | |
| } | |
| messageContent := messageContentBuilder.String() | |
| if messageContent != "" { | |
| _ = writer.WriteField("message", messageContent) | |
| } |
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Attachment filenames come from email content and can contain CR/LF or other special characters. Building Content-Disposition manually with fmt.Sprintf(...) risks header injection / malformed multipart bodies. Prefer multipart.FileContentDisposition(...) (already used in pkg/httpBotter/logic.go) or explicitly reject/strip \r and \n in filenames.
Copilot
AI
Mar 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
multipart.Writer.CreatePart expects a textproto.MIMEHeader, but this code builds a plain map[string][]string. This won’t compile (cannot use h (type map[string][]string) as textproto.MIMEHeader). Use make(textproto.MIMEHeader) (and import net/textproto), or follow the existing pattern used in pkg/httpBotter/logic.go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
json.NewEncoder(f).Encode(token)error is ignored. If the token can’t be written (disk full, permission issues after open, etc.), the script may proceed but re-auth every run. Handle/return the encode error (and consider logging without printing sensitive paths).