-
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
Changes from all commits
54e6960
230a03b
2bc354a
efd78fc
61f7195
16d15cd
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 |
|---|---|---|
|
|
@@ -28,3 +28,6 @@ history-gemini/* | |
| !history-gemini/.gitkeep | ||
| bot-context/* | ||
| !bot-context/.gitkeep | ||
|
|
||
| credentials.json | ||
| token.json | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,366 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package main | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "bytes" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "encoding/base64" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "encoding/json" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "errors" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "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 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Start a local web server to catch the callback | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| codeCh := make(chan string) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mux := http.NewServeMux() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| srv := &http.Server{Addr: "localhost:9099", Handler: mux} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| code := r.URL.Query().Get("code") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if code != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fmt.Fprintf(w, "Authorization successful! You can close this window and return to the terminal.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| codeCh <- code | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fmt.Fprintf(w, "Error: No code found in request.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| go func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Fatalf("HTTP server ListenAndServe: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Ensure the redirect URL is set to our local server | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| config.RedirectURL = "http://localhost:9099/" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fmt.Printf("Go to the following link in your browser to authorize:\n%v\n\nWaiting for authorization...\n", authURL) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Wait for the code | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authCode := <-codeCh | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Shutdown the server gracefully | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := srv.Shutdown(context.Background()); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Printf("HTTP server Shutdown: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tok, err := config.Exchange(context.Background(), 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", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SearchQuery: "(label:Assistant OR to:gergo254+assistant@gmail.com) is:unread", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PollingInterval: 60 * time.Second, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if val := os.Getenv("WEBHOOK_URL"); val != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cfg.WebhookURL = val | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if val := os.Getenv("SEARCH_QUERY"); val != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cfg.SearchQuery = val | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if val := os.Getenv("POLLING_INTERVAL"); val != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if i, err := strconv.Atoi(val); err == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cfg.PollingInterval = time.Duration(i) * time.Second | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Printf("Invalid POLLING_INTERVAL: %v, using default 60s", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return cfg | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type emailData struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ID string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Sender string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Subject string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Date string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Body string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Attachments []attachment | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type attachment struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Filename string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Data []byte | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MimeType string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func main() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cfg := loadConfig() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Printf("Starting Gmail Reader Script with config: %+v\n", cfg) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ctx := context.Background() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| b, err := os.ReadFile("credentials.json") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Fatalf("Unable to read client secret file: %v. Please make sure credentials.json is present in the current directory.", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If modifying these scopes, delete your previously saved token.json. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| config, err := google.ConfigFromJSON(b, gmail.MailGoogleComScope) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Fatalf("Unable to parse client secret file to config: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client := getClient(config) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| srv, err := gmail.NewService(ctx, option.WithHTTPClient(client)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Fatalf("Unable to retrieve Gmail client: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Start polling loop | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ticker := time.NewTicker(cfg.PollingInterval) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer ticker.Stop() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Run once immediately | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pollEmails(srv, cfg) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for range ticker.C { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pollEmails(srv, cfg) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func pollEmails(srv *gmail.Service, cfg Configuration) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Printf("Polling emails with query: %s\n", cfg.SearchQuery) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user := "me" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| r, err := srv.Users.Messages.List(user).Q(cfg.SearchQuery).MaxResults(5).Do() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Printf("Unable to retrieve messages: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(r.Messages) == 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Println("No matching messages found.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, m := range r.Messages { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| processMessage(srv, user, m.Id, cfg.WebhookURL) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func processMessage(srv *gmail.Service, user string, msgId string, webhookURL string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| msg, err := srv.Users.Messages.Get(user, msgId).Format("full").Do() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Printf("Unable to get message %s: %v", msgId, err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data := extractEmailData(srv, user, msg) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Send to webhook | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| err = forwardToWebhook(data, webhookURL) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Printf("Failed to forward message %s: %v", msgId, err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Leave as unread so it gets picked up again | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Mark as read | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Printf("Message %s forwarded successfully, marking as read.", msgId) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mods := &gmail.ModifyMessageRequest{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RemoveLabelIds: []string{"UNREAD"}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _, err = srv.Users.Messages.Modify(user, msgId, mods).Do() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Printf("Unable to modify message %s: %v", msgId, err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func extractEmailData(srv *gmail.Service, user string, msg *gmail.Message) emailData { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data := emailData{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ID: msg.Id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Extract headers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, header := range msg.Payload.Headers { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch strings.ToLower(header.Name) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "from": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data.Sender = header.Value | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "subject": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data.Subject = header.Value | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "date": | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data.Date = header.Value | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Extract body and attachments | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extractParts(srv, user, msg.Id, msg.Payload, &data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func extractParts(srv *gmail.Service, user string, msgId string, part *gmail.MessagePart, data *emailData) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if part.Filename != "" && part.Body != nil && part.Body.AttachmentId != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // It's an attachment | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| attachObj, err := srv.Users.Messages.Attachments.Get(user, msgId, part.Body.AttachmentId).Do() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Printf("Unable to get attachment %s for message %s: %v", part.Filename, msgId, err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| decoded, err := base64.URLEncoding.DecodeString(attachObj.Data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Printf("Unable to decode attachment %s: %v", part.Filename, err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data.Attachments = append(data.Attachments, attachment{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Filename: part.Filename, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Data: decoded, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MimeType: part.MimeType, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if part.MimeType == "text/plain" && part.Body != nil && part.Body.Data != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // It's the body | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| decoded, err := base64.URLEncoding.DecodeString(part.Body.Data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data.Body += string(decoded) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if part.MimeType == "text/html" && data.Body == "" && part.Body != nil && part.Body.Data != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Fallback to HTML body if plain text is not found yet | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| decoded, err := base64.URLEncoding.DecodeString(part.Body.Data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+267
to
+287
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data.Body += string(decoded) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Recursively check parts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, p := range part.Parts { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extractParts(srv, user, msgId, p, data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func forwardToWebhook(data emailData, webhookURL string) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body := &bytes.Buffer{} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| writer := multipart.NewWriter(body) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Add metadata fields | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ = 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) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+303
to
+309
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ = 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.
Uh oh!
There was an error while loading. Please reload this page.