fix(gmail): use people profile for primary sender name
This commit is contained in:
parent
e97cad62f6
commit
3eb8330159
@ -17,6 +17,7 @@
|
||||
- Gmail: build outbound `Date` headers with the configured timezone so replies do not inherit a wrong host-local offset. (#514, #472) — thanks @dinakars777.
|
||||
- Gmail: preserve renewed watch expiration fields when a long-running `gmail watch serve` process records push delivery state after `gmail watch renew` runs separately. (#526)
|
||||
- Gmail: auto-fill draft reply subjects from the original message when `gmail drafts create --reply-to-message-id` omits `--subject`. (#488) — thanks @jbowerbir.
|
||||
- Gmail: fall back to the People profile name for primary-account `From` headers when Gmail send-as settings omit a display name. (#431) — thanks @moeedahmed.
|
||||
- Gmail: reuse the shared paginated list runner for thread and message search so `--all`, `--page`, text, and JSON output stay consistent.
|
||||
- Drive: print large upload progress to stderr while keeping JSON output parseable. (#529)
|
||||
- Drive: include `hasThumbnail` and `thumbnailLink` in `drive ls`, `drive search`, and `drive get` JSON responses. (#486) — thanks @gtapps.
|
||||
@ -25,6 +26,7 @@
|
||||
- Drive: include `driveId` in `drive ls`, `drive search`, and `drive get` field masks so Shared Drive files can be identified in JSON output. (#524) — thanks @LeanSheng.
|
||||
- Gmail: expose reply threading headers in default `gmail get --format metadata` output and fail explicit reply targets that cannot provide a `Message-ID`. (#528, #512) — thanks @solomonneas.
|
||||
- Docs: include available tab names when `docs cat --tab` / structure lookup cannot find the requested tab. (#532) — thanks @johnbenjaminlewis.
|
||||
- Docs: size Markdown images consistently for `docs write --replace --markdown` by reusing the Docs image insertion path after Drive conversion. (#518) — thanks @vinothd-oai.
|
||||
|
||||
## 0.13.0 - 2026-04-20
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"google.golang.org/api/gmail/v1"
|
||||
"google.golang.org/api/people/v1"
|
||||
|
||||
"github.com/steipete/gogcli/internal/config"
|
||||
"github.com/steipete/gogcli/internal/outfmt"
|
||||
@ -99,11 +100,40 @@ func resolveComposeFrom(ctx context.Context, svc *gmail.Service, account, from s
|
||||
if sendAsListErr == nil {
|
||||
if displayName := primaryDisplayNameFromSendAsList(sendAsList, account); displayName != "" {
|
||||
result.header = displayName + " <" + account + ">"
|
||||
} else if displayName := primaryDisplayNameFromPeople(ctx, account); displayName != "" {
|
||||
result.header = displayName + " <" + account + ">"
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func primaryDisplayNameFromPeople(ctx context.Context, account string) string {
|
||||
svc, err := newPeopleContactsService(ctx, account)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
person, err := svc.People.Get(peopleMeResource).PersonFields("names").Context(ctx).Do()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return primaryDisplayNameFromPerson(person)
|
||||
}
|
||||
|
||||
func primaryDisplayNameFromPerson(person *people.Person) string {
|
||||
if person == nil {
|
||||
return ""
|
||||
}
|
||||
for _, name := range person.Names {
|
||||
if name == nil {
|
||||
continue
|
||||
}
|
||||
if displayName := strings.TrimSpace(name.DisplayName); displayName != "" {
|
||||
return displayName
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func prepareComposeReply(ctx context.Context, svc *gmail.Service, replyToMessageID, threadID string, quote bool, plainBody, htmlBody string) (*replyInfo, string, string, error) {
|
||||
info, err := fetchReplyInfo(ctx, svc, replyToMessageID, threadID, quote)
|
||||
if err != nil {
|
||||
|
||||
@ -14,6 +14,7 @@ import (
|
||||
|
||||
"google.golang.org/api/gmail/v1"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/api/people/v1"
|
||||
|
||||
"github.com/steipete/gogcli/internal/outfmt"
|
||||
"github.com/steipete/gogcli/internal/ui"
|
||||
@ -704,6 +705,185 @@ func TestGmailSendCmd_RunJSON_PrimaryAccountDisplayNameFallbackToList(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
func TestGmailSendCmd_RunJSON_PrimaryAccountDisplayNameFallbackToPeople(t *testing.T) {
|
||||
origGmail := newGmailService
|
||||
origPeople := newPeopleContactsService
|
||||
t.Cleanup(func() {
|
||||
newGmailService = origGmail
|
||||
newPeopleContactsService = origPeople
|
||||
})
|
||||
|
||||
var rawSent string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/gmail/v1")
|
||||
switch {
|
||||
case r.Method == http.MethodGet && path == "/users/me/settings/sendAs":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"sendAs": []map[string]any{
|
||||
{
|
||||
"sendAsEmail": "a@b.com",
|
||||
"verificationStatus": "accepted",
|
||||
"isPrimary": true,
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/people/me"):
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"resourceName": "people/me",
|
||||
"names": []map[string]any{{"displayName": "People User"}},
|
||||
})
|
||||
return
|
||||
case r.Method == http.MethodPost && path == "/users/me/messages/send":
|
||||
var msg gmail.Message
|
||||
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
|
||||
t.Fatalf("decode sent message: %v", err)
|
||||
}
|
||||
raw, err := base64.RawURLEncoding.DecodeString(msg.Raw)
|
||||
if err != nil {
|
||||
t.Fatalf("decode raw message: %v", err)
|
||||
}
|
||||
rawSent = string(raw)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "m3c",
|
||||
"threadId": "t3c",
|
||||
})
|
||||
return
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
gmailSvc, err := gmail.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithHTTPClient(srv.Client()),
|
||||
option.WithEndpoint(srv.URL+"/"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewGmailService: %v", err)
|
||||
}
|
||||
peopleSvc, err := people.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithHTTPClient(srv.Client()),
|
||||
option.WithEndpoint(srv.URL+"/"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewPeopleService: %v", err)
|
||||
}
|
||||
newGmailService = func(context.Context, string) (*gmail.Service, error) { return gmailSvc, nil }
|
||||
newPeopleContactsService = func(context.Context, string) (*people.Service, error) { return peopleSvc, nil }
|
||||
|
||||
u, err := ui.New(ui.Options{Stdout: os.Stdout, Stderr: os.Stderr, Color: "never"})
|
||||
if err != nil {
|
||||
t.Fatalf("ui.New: %v", err)
|
||||
}
|
||||
ctx := outfmt.WithMode(ui.WithUI(context.Background(), u), outfmt.Mode{JSON: true})
|
||||
|
||||
cmd := &GmailSendCmd{
|
||||
To: "recipient@example.com",
|
||||
Subject: "Hello",
|
||||
Body: "Body",
|
||||
}
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
if err := cmd.Run(ctx, &RootFlags{Account: "a@b.com"}); err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
})
|
||||
if !strings.Contains(out, "\"from\"") || !strings.Contains(out, "People User <a@b.com>") {
|
||||
t.Fatalf("expected from with People display name, got: %q", out)
|
||||
}
|
||||
if !strings.Contains(rawSent, `From: "People User" <a@b.com>`) {
|
||||
t.Fatalf("expected raw From header to use People display name, got: %q", rawSent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGmailSendCmd_RunJSON_PrimaryAccountPeopleFallbackFailureIgnored(t *testing.T) {
|
||||
origGmail := newGmailService
|
||||
origPeople := newPeopleContactsService
|
||||
t.Cleanup(func() {
|
||||
newGmailService = origGmail
|
||||
newPeopleContactsService = origPeople
|
||||
})
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/gmail/v1")
|
||||
switch {
|
||||
case r.Method == http.MethodGet && path == "/users/me/settings/sendAs":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"sendAs": []map[string]any{
|
||||
{
|
||||
"sendAsEmail": "a@b.com",
|
||||
"verificationStatus": "accepted",
|
||||
"isPrimary": true,
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/people/me"):
|
||||
http.Error(w, "People API unavailable", http.StatusForbidden)
|
||||
return
|
||||
case r.Method == http.MethodPost && path == "/users/me/messages/send":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "m3d",
|
||||
"threadId": "t3d",
|
||||
})
|
||||
return
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
gmailSvc, err := gmail.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithHTTPClient(srv.Client()),
|
||||
option.WithEndpoint(srv.URL+"/"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewGmailService: %v", err)
|
||||
}
|
||||
peopleSvc, err := people.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithHTTPClient(srv.Client()),
|
||||
option.WithEndpoint(srv.URL+"/"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewPeopleService: %v", err)
|
||||
}
|
||||
newGmailService = func(context.Context, string) (*gmail.Service, error) { return gmailSvc, nil }
|
||||
newPeopleContactsService = func(context.Context, string) (*people.Service, error) { return peopleSvc, nil }
|
||||
|
||||
u, err := ui.New(ui.Options{Stdout: os.Stdout, Stderr: os.Stderr, Color: "never"})
|
||||
if err != nil {
|
||||
t.Fatalf("ui.New: %v", err)
|
||||
}
|
||||
ctx := outfmt.WithMode(ui.WithUI(context.Background(), u), outfmt.Mode{JSON: true})
|
||||
|
||||
cmd := &GmailSendCmd{
|
||||
To: "recipient@example.com",
|
||||
Subject: "Hello",
|
||||
Body: "Body",
|
||||
}
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
if err := cmd.Run(ctx, &RootFlags{Account: "a@b.com"}); err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
})
|
||||
if !strings.Contains(out, "\"from\": \"a@b.com\"") {
|
||||
t.Fatalf("expected bare from when People fallback fails, got: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGmailSendCmd_RunJSON_PrimaryAccountNoDisplayName(t *testing.T) {
|
||||
origNew := newGmailService
|
||||
t.Cleanup(func() { newGmailService = origNew })
|
||||
|
||||
Loading…
Reference in New Issue
Block a user