Skip to content

Commit 908cf73

Browse files
authored
feat(gmail): auto-populate From display name + fix reply-all to own message (#530)
* feat(gmail): auto-populate From with display name from send-as settings Fetch the user's send-as identities via /users/me/settings/sendAs to set the From header with a display name in all mail helpers, matching Gmail web client behavior. The gmail.modify scope already covers this endpoint. Three resolution cases: - No --from: use the default send-as identity (display name + email) - --from with bare email: enrich from send-as list if the alias exists - --from with display name: use as-is, skip the API call For Workspace accounts where the primary address inherits its display name from the organization directory (sendAs returns empty displayName), falls back to the People API to fetch the profile name. This requires the userinfo.profile scope, which is now included in the identity scopes that auth login always requests. Degrades gracefully with a tip if the scope hasn't been granted yet. Introduces build_api_error, a shared helper that parses Google API JSON error responses (extracting message, reason, and enable URL), matching the executor's handle_error_response pattern. Used by all four Gmail API functions. All error messages printed to stderr are sanitized via sanitize_for_terminal. In +send, auth uses the discovery doc scopes rather than hardcoding gmail.modify, preserving compatibility with narrower send-only OAuth setups. In reply-all, the profile endpoint is always called for self-email dedup since the primary address may differ from the send-as alias. * fix(gmail): handle reply-all to own message correctly When replying-all to a message you sent, the original sender (you) was excluded from To, leaving it empty and producing an error. Gmail web handles this by using the original To recipients as reply targets. Detect self-reply by checking if the original From matches the user's primary email or send-as alias, then swap the candidate logic: - Self-reply: To = original To, CC = original CC - Normal reply: To = Reply-To or From, CC = original To + CC
1 parent 1e90380 commit 908cf73

File tree

7 files changed

+761
-37
lines changed

7 files changed

+761
-37
lines changed

.changeset/gmail-default-sender.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@googleworkspace/cli": minor
3+
---
4+
5+
feat(gmail): auto-populate From header with display name from send-as settings
6+
7+
Fetch the user's send-as identities to set the From header with a display name in all mail helpers (+send, +reply, +reply-all, +forward), matching Gmail web client behavior. Also enriches bare `--from` emails with their configured display name.

.changeset/gmail-self-reply-fix.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@googleworkspace/cli": patch
3+
---
4+
5+
fix(gmail): handle reply-all to own message correctly
6+
7+
Reply-all to a message you sent no longer errors with "No To recipient remains." The original To recipients are now used as reply targets, matching Gmail web client behavior.

src/auth_commands.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,15 @@ async fn handle_login(args: &[String]) -> Result<(), GwsError> {
275275
..Default::default()
276276
};
277277

278-
// Ensure openid + email scopes are always present so we can identify the user
279-
// via the userinfo endpoint after login.
280-
let identity_scopes = ["openid", "https://www.googleapis.com/auth/userinfo.email"];
278+
// Ensure openid + email + profile scopes are always present so we can
279+
// identify the user via the userinfo endpoint after login, and so the
280+
// Gmail helpers can fall back to the People API to populate the From
281+
// display name when the send-as identity lacks one (Workspace accounts).
282+
let identity_scopes = [
283+
"openid",
284+
"https://www.googleapis.com/auth/userinfo.email",
285+
"https://www.googleapis.com/auth/userinfo.profile",
286+
];
281287
for s in &identity_scopes {
282288
if !scopes.iter().any(|existing| existing == s) {
283289
scopes.push(s.to_string());

src/helpers/gmail/forward.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub(super) async fn handle_forward(
1919
doc: &crate::discovery::RestDescription,
2020
matches: &ArgMatches,
2121
) -> Result<(), GwsError> {
22-
let config = parse_forward_args(matches)?;
22+
let mut config = parse_forward_args(matches)?;
2323

2424
let dry_run = matches.get_flag("dry-run");
2525

@@ -34,6 +34,7 @@ pub(super) async fn handle_forward(
3434
.map_err(|e| GwsError::Auth(format!("Gmail auth failed: {e}")))?;
3535
let client = crate::client::build_client()?;
3636
let orig = fetch_message_metadata(&client, &t, &config.message_id).await?;
37+
config.from = resolve_sender(&client, &t, config.from.as_deref()).await?;
3738
(orig, Some(t))
3839
};
3940

0 commit comments

Comments
 (0)