feat(gmail): take the star off a mail once it has become a task
A starred Gmail message became a task and kept its star, so a starred inbox only grew — the IMAP side has always unflagged what it imports, and Gmail could not, because the scope was gmail.readonly. This gives Gmail the same one write: after the task is made, the STARRED label comes off the message, in one batchModify for the whole sweep. Nothing else is touched — not the body, not other labels, and a label trigger's label stays where it is, since that label is the person's own. The scope goes from gmail.readonly to gmail.modify, which is the narrowest scope Google offers that can remove a label. An existing connection keeps working on its old read-only grant — the sync still reads, the unstar is a best-effort 403 that is logged, not fatal — and starts clearing stars once the account is reconnected and the new grant is in place. It self-heals: a mail that could not be unstarred stays starred, stays in the next query, and is tried again. The consent screen now asks for more than it used to; the settings copy, the docs and the scope table all say modify rather than read-only in the same change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7770ba9844
commit
84ca506961
@@ -215,6 +215,40 @@ func Get(ctx context.Context, client *http.Client, accessToken, url string, out
|
||||
return json.NewDecoder(resp.Body).Decode(out)
|
||||
}
|
||||
|
||||
// Post sends a JSON body to a Google API and, unlike Get, does not insist on a
|
||||
// body coming back. A write call — modifying labels on a message — answers with
|
||||
// 204 No Content when it worked, so anything 2xx is success and only the body is
|
||||
// decoded, and only when the caller asked for one.
|
||||
func Post(ctx context.Context, client *http.Client, accessToken, url string, body, out any) error {
|
||||
payload, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(string(payload)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
return ErrUnauthorized
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("google: %s", resp.Status)
|
||||
}
|
||||
if out == nil || resp.StatusCode == http.StatusNoContent {
|
||||
return nil
|
||||
}
|
||||
return json.NewDecoder(resp.Body).Decode(out)
|
||||
}
|
||||
|
||||
func randomString(n int) (string, error) {
|
||||
b := make([]byte, n)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user