Skip to content

Commit 0498987

Browse files
committed
fix(gmail): use send-as list for display name
1 parent e61769c commit 0498987

3 files changed

Lines changed: 91 additions & 102 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
### Fixed
1111
- Gmail: when `gmail attachment --out` points to a directory (or ends with a trailing slash), combine with `--name` and avoid false cache hits on directories. (#248) — thanks @zerone0x.
1212
- Calendar: fall back to fixed-offset timezones (`Etc/GMT±N`) for recurring events when given RFC3339 offset datetimes; harden Gmail attachment output paths and cache validation; honor proxy defaults for Google API transports. (#228) — thanks @salmonumbrella.
13+
- Gmail: include primary display name in `gmail send` From header when using service account impersonation (domain-wide delegation). (#184) — thanks @salmonumbrella.
1314

1415
## 0.10.0 - 2026-02-14
1516

internal/cmd/gmail_send.go

Lines changed: 58 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -135,35 +135,44 @@ func (c *GmailSendCmd) Run(ctx context.Context, flags *RootFlags) error {
135135
return err
136136
}
137137

138+
sendAsList, sendAsListErr := listSendAs(ctx, svc)
139+
138140
// Determine the From address
139141
fromAddr := account
140142
sendingEmail := account // The email we're sending from (without display name)
141-
if strings.TrimSpace(c.From) != "" {
142-
// Validate that this is a configured send-as alias
143+
if fromEmail := strings.TrimSpace(c.From); fromEmail != "" {
144+
// Validate that this is a configured and verified send-as alias.
143145
var sa *gmail.SendAs
144-
sa, err = svc.Users.Settings.SendAs.Get("me", c.From).Context(ctx).Do()
145-
if err != nil {
146-
return fmt.Errorf("invalid --from address %q: %w", c.From, err)
146+
if sendAsListErr == nil {
147+
sa = findSendAsByEmail(sendAsList, fromEmail)
148+
if sa == nil {
149+
return fmt.Errorf("invalid --from address %q: not found in send-as settings", fromEmail)
150+
}
151+
} else {
152+
// Fallback: preserve legacy behavior if we cannot list settings.
153+
var getErr error
154+
sa, getErr = svc.Users.Settings.SendAs.Get("me", fromEmail).Context(ctx).Do()
155+
if getErr != nil {
156+
return fmt.Errorf("invalid --from address %q: %w", fromEmail, getErr)
157+
}
147158
}
159+
148160
if sa.VerificationStatus != gmailVerificationAccepted {
149-
return fmt.Errorf("--from address %q is not verified (status: %s)", c.From, sa.VerificationStatus)
150-
}
151-
sendingEmail = c.From
152-
fromAddr = c.From
153-
// Include display name if set
154-
displayName := strings.TrimSpace(sa.DisplayName)
155-
if displayName == "" {
156-
if fallback, listErr := sendAsDisplayNameFromList(ctx, svc, c.From); listErr == nil {
157-
displayName = fallback
158-
}
161+
return fmt.Errorf("--from address %q is not verified (status: %s)", fromEmail, sa.VerificationStatus)
159162
}
160-
if displayName != "" {
161-
fromAddr = displayName + " <" + c.From + ">"
163+
164+
sendingEmail = fromEmail
165+
fromAddr = fromEmail
166+
167+
if displayName := strings.TrimSpace(sa.DisplayName); displayName != "" {
168+
fromAddr = displayName + " <" + fromEmail + ">"
162169
}
163170
} else {
164-
// No --from specified: look up the primary account's send-as settings
165-
// to get the display name
166-
displayName := primarySendAsDisplayName(ctx, svc, account)
171+
// No --from specified: best-effort look up the primary account's display name.
172+
displayName := ""
173+
if sendAsListErr == nil {
174+
displayName = primaryDisplayNameFromSendAsList(sendAsList, account)
175+
}
167176
if displayName != "" {
168177
fromAddr = displayName + " <" + account + ">"
169178
}
@@ -251,76 +260,55 @@ func (c *GmailSendCmd) resolveTrackingConfig(account string, toRecipients, ccRec
251260
return trackingCfg, nil
252261
}
253262

254-
func primarySendAsDisplayName(ctx context.Context, svc *gmail.Service, account string) string {
255-
account = strings.TrimSpace(account)
256-
if account == "" || svc == nil {
257-
return ""
258-
}
259-
260-
sa, err := svc.Users.Settings.SendAs.Get("me", account).Context(ctx).Do()
261-
if err == nil {
262-
if displayName := strings.TrimSpace(sa.DisplayName); displayName != "" {
263-
return displayName
264-
}
263+
func listSendAs(ctx context.Context, svc *gmail.Service) ([]*gmail.SendAs, error) {
264+
if svc == nil {
265+
return nil, nil
265266
}
266-
267-
displayName, err := primarySendAsDisplayNameFromList(ctx, svc, account)
267+
resp, err := svc.Users.Settings.SendAs.List("me").Context(ctx).Do()
268268
if err != nil {
269-
return ""
269+
return nil, err
270270
}
271-
return displayName
271+
return resp.SendAs, nil
272272
}
273273

274-
func sendAsDisplayNameFromList(ctx context.Context, svc *gmail.Service, email string) (string, error) {
275-
email = strings.TrimSpace(email)
276-
if email == "" || svc == nil {
277-
return "", nil
278-
}
279-
280-
resp, err := svc.Users.Settings.SendAs.List("me").Context(ctx).Do()
281-
if err != nil {
282-
return "", err
274+
func findSendAsByEmail(sendAs []*gmail.SendAs, email string) *gmail.SendAs {
275+
needle := strings.ToLower(strings.TrimSpace(email))
276+
if needle == "" {
277+
return nil
283278
}
284-
285-
needle := strings.ToLower(email)
286-
for _, sa := range resp.SendAs {
279+
for _, sa := range sendAs {
280+
if sa == nil {
281+
continue
282+
}
287283
if strings.ToLower(strings.TrimSpace(sa.SendAsEmail)) == needle {
288-
return strings.TrimSpace(sa.DisplayName), nil
284+
return sa
289285
}
290286
}
291-
292-
return "", nil
287+
return nil
293288
}
294289

295-
func primarySendAsDisplayNameFromList(ctx context.Context, svc *gmail.Service, account string) (string, error) {
290+
func primaryDisplayNameFromSendAsList(sendAs []*gmail.SendAs, account string) string {
296291
account = strings.TrimSpace(account)
297-
if account == "" || svc == nil {
298-
return "", nil
292+
if account == "" {
293+
return ""
299294
}
300295

301-
resp, err := svc.Users.Settings.SendAs.List("me").Context(ctx).Do()
302-
if err != nil {
303-
return "", err
296+
if sa := findSendAsByEmail(sendAs, account); sa != nil {
297+
if displayName := strings.TrimSpace(sa.DisplayName); displayName != "" {
298+
return displayName
299+
}
304300
}
305301

306-
needle := strings.ToLower(account)
307-
var primary *gmail.SendAs
308-
for _, sa := range resp.SendAs {
309-
if sa.IsPrimary {
310-
primary = sa
302+
for _, sa := range sendAs {
303+
if sa == nil || !sa.IsPrimary {
304+
continue
311305
}
312-
if strings.ToLower(strings.TrimSpace(sa.SendAsEmail)) == needle {
313-
if displayName := strings.TrimSpace(sa.DisplayName); displayName != "" {
314-
return displayName, nil
315-
}
306+
if displayName := strings.TrimSpace(sa.DisplayName); displayName != "" {
307+
return displayName
316308
}
317309
}
318310

319-
if primary != nil {
320-
return strings.TrimSpace(primary.DisplayName), nil
321-
}
322-
323-
return "", nil
311+
return ""
324312
}
325313

326314
func buildSendBatches(toRecipients, ccRecipients, bccRecipients []string, track, trackSplit bool) []sendBatch {

internal/cmd/gmail_send_test.go

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -261,12 +261,16 @@ func TestGmailSendCmd_RunJSON_WithFrom(t *testing.T) {
261261
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
262262
path := strings.TrimPrefix(r.URL.Path, "/gmail/v1")
263263
switch {
264-
case r.Method == http.MethodGet && path == "/users/me/settings/sendAs/alias@example.com":
264+
case r.Method == http.MethodGet && path == "/users/me/settings/sendAs":
265265
w.Header().Set("Content-Type", "application/json")
266266
_ = json.NewEncoder(w).Encode(map[string]any{
267-
"sendAsEmail": "alias@example.com",
268-
"displayName": "Alias",
269-
"verificationStatus": "accepted",
267+
"sendAs": []map[string]any{
268+
{
269+
"sendAsEmail": "alias@example.com",
270+
"displayName": "Alias",
271+
"verificationStatus": "accepted",
272+
},
273+
},
270274
})
271275
return
272276
case r.Method == http.MethodPost && path == "/users/me/messages/send":
@@ -323,17 +327,8 @@ func TestGmailSendCmd_RunJSON_WithFromDisplayNameFallbackToList(t *testing.T) {
323327
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
324328
path := strings.TrimPrefix(r.URL.Path, "/gmail/v1")
325329
switch {
326-
case r.Method == http.MethodGet && path == "/users/me/settings/sendAs/alias@example.com":
327-
// Return send-as settings with empty display name but valid verification.
328-
w.Header().Set("Content-Type", "application/json")
329-
_ = json.NewEncoder(w).Encode(map[string]any{
330-
"sendAsEmail": "alias@example.com",
331-
"displayName": "",
332-
"verificationStatus": "accepted",
333-
})
334-
return
335330
case r.Method == http.MethodGet && path == "/users/me/settings/sendAs":
336-
// Fallback list endpoint returns the alias with a populated display name.
331+
// List endpoint provides verification + display name (works for service-account impersonation too).
337332
w.Header().Set("Content-Type", "application/json")
338333
_ = json.NewEncoder(w).Encode(map[string]any{
339334
"sendAs": []map[string]any{
@@ -377,7 +372,7 @@ func TestGmailSendCmd_RunJSON_WithFromDisplayNameFallbackToList(t *testing.T) {
377372

378373
cmd := &GmailSendCmd{
379374
To: "a@example.com",
380-
From: "alias@example.com",
375+
From: " alias@example.com ",
381376
Subject: "Hello",
382377
Body: "Body",
383378
}
@@ -399,13 +394,18 @@ func TestGmailSendCmd_RunJSON_PrimaryAccountDisplayName(t *testing.T) {
399394
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
400395
path := strings.TrimPrefix(r.URL.Path, "/gmail/v1")
401396
switch {
402-
case r.Method == http.MethodGet && path == "/users/me/settings/sendAs/a@b.com":
403-
// Return send-as settings with display name for primary account
397+
case r.Method == http.MethodGet && path == "/users/me/settings/sendAs":
398+
// List endpoint returns the primary entry with display name.
404399
w.Header().Set("Content-Type", "application/json")
405400
_ = json.NewEncoder(w).Encode(map[string]any{
406-
"sendAsEmail": "a@b.com",
407-
"displayName": "Primary User",
408-
"verificationStatus": "accepted",
401+
"sendAs": []map[string]any{
402+
{
403+
"sendAsEmail": "a@b.com",
404+
"displayName": "Primary User",
405+
"verificationStatus": "accepted",
406+
"isPrimary": true,
407+
},
408+
},
409409
})
410410
return
411411
case r.Method == http.MethodPost && path == "/users/me/messages/send":
@@ -463,21 +463,17 @@ func TestGmailSendCmd_RunJSON_PrimaryAccountDisplayNameFallbackToList(t *testing
463463
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
464464
path := strings.TrimPrefix(r.URL.Path, "/gmail/v1")
465465
switch {
466-
case r.Method == http.MethodGet && path == "/users/me/settings/sendAs/a@b.com":
467-
// Simulate missing display name in get response.
468-
w.Header().Set("Content-Type", "application/json")
469-
_ = json.NewEncoder(w).Encode(map[string]any{
470-
"sendAsEmail": "a@b.com",
471-
"displayName": "",
472-
"verificationStatus": "accepted",
473-
})
474-
return
475466
case r.Method == http.MethodGet && path == "/users/me/settings/sendAs":
476467
w.Header().Set("Content-Type", "application/json")
477468
_ = json.NewEncoder(w).Encode(map[string]any{
478469
"sendAs": []map[string]any{
479470
{
480471
"sendAsEmail": "a@b.com",
472+
"displayName": "",
473+
"verificationStatus": "accepted",
474+
},
475+
{
476+
"sendAsEmail": "primary@example.com",
481477
"displayName": "Primary User",
482478
"verificationStatus": "accepted",
483479
"isPrimary": true,
@@ -661,11 +657,15 @@ func TestGmailSendCmd_Run_FromUnverified(t *testing.T) {
661657

662658
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
663659
path := strings.TrimPrefix(r.URL.Path, "/gmail/v1")
664-
if r.Method == http.MethodGet && path == "/users/me/settings/sendAs/alias@example.com" {
660+
if r.Method == http.MethodGet && path == "/users/me/settings/sendAs" {
665661
w.Header().Set("Content-Type", "application/json")
666662
_ = json.NewEncoder(w).Encode(map[string]any{
667-
"sendAsEmail": "alias@example.com",
668-
"verificationStatus": "pending",
663+
"sendAs": []map[string]any{
664+
{
665+
"sendAsEmail": "alias@example.com",
666+
"verificationStatus": "pending",
667+
},
668+
},
669669
})
670670
return
671671
}

0 commit comments

Comments
 (0)