fix(gmail): bring +forward behavior in line with Gmail web UI (#353)

- Pass original threadId to keep forward in sender's thread
- Add In-Reply-To and References headers for RFC 5322 compliance
- Add blank line between forwarded message metadata and body
- Remove spurious closing delimiter from forwarded message block
- Update SKILL.md to remove outdated threading tip
- Add test for References chain construction

Closes #88
This commit is contained in:
Malo Bourgon
2026-03-09 16:47:35 -07:00
committed by GitHub
parent 06aa698e23
commit 5e7d1200df
3 changed files with 60 additions and 7 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---
Bring `+forward` behavior in line with Gmail's web UI: keep the forward in the sender's original thread, add a blank line between the forwarded message metadata and body, and remove the spurious closing delimiter.
-1
View File
@@ -44,7 +44,6 @@ gws gmail +forward --message-id 18f1a2b3c4d --to dave@example.com --cc eve@examp
## Tips
- Includes the original message with sender, date, subject, and recipients.
- Sends the forward as a new message rather than forcing it into the original thread.
## See Also
+55 -6
View File
@@ -46,7 +46,14 @@ pub(super) async fn handle_forward(
&original,
);
super::send_raw_email(doc, matches, &raw, None, token.as_deref()).await
super::send_raw_email(
doc,
matches,
&raw,
Some(&original.thread_id),
token.as_deref(),
)
.await
}
pub(super) struct ForwardConfig {
@@ -73,9 +80,16 @@ fn create_forward_raw_message(
body: Option<&str>,
original: &OriginalMessage,
) -> String {
let references = if original.references.is_empty() {
original.message_id_header.clone()
} else {
format!("{} {}", original.references, original.message_id_header)
};
let mut headers = format!(
"To: {}\r\nSubject: {}\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8",
to, subject
"To: {}\r\nSubject: {}\r\nIn-Reply-To: {}\r\nReferences: {}\r\n\
MIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8",
to, subject, original.message_id_header, references
);
if let Some(from) = from {
@@ -101,8 +115,8 @@ fn format_forwarded_message(original: &OriginalMessage) -> String {
Date: {}\r\n\
Subject: {}\r\n\
To: {}\r\n\
{}{}\r\n\
----------",
{}\r\n\
{}",
original.from,
original.date,
original.subject,
@@ -171,9 +185,14 @@ mod tests {
assert!(raw.contains("To: dave@example.com"));
assert!(raw.contains("Subject: Fwd: Hello"));
assert!(raw.contains("In-Reply-To: <abc@example.com>"));
assert!(raw.contains("References: <abc@example.com>"));
assert!(raw.contains("---------- Forwarded message ---------"));
assert!(raw.contains("From: alice@example.com"));
assert!(raw.contains("Original content"));
// Blank line separates metadata block from body
assert!(raw.contains("To: bob@example.com\r\n\r\nOriginal content"));
// No closing ---------- delimiter
assert!(!raw.ends_with("----------"));
}
#[test]
@@ -205,6 +224,36 @@ mod tests {
assert!(raw.contains("Cc: carol@example.com"));
}
#[test]
fn test_create_forward_raw_message_references_chain() {
let original = super::super::OriginalMessage {
thread_id: "t1".to_string(),
message_id_header: "<msg-2@example.com>".to_string(),
references: "<msg-0@example.com> <msg-1@example.com>".to_string(),
from: "alice@example.com".to_string(),
reply_to: "".to_string(),
to: "bob@example.com".to_string(),
cc: "".to_string(),
subject: "Hello".to_string(),
date: "Mon, 1 Jan 2026 00:00:00 +0000".to_string(),
body_text: "Original content".to_string(),
};
let raw = create_forward_raw_message(
"dave@example.com",
None,
None,
"Fwd: Hello",
None,
&original,
);
assert!(raw.contains("In-Reply-To: <msg-2@example.com>"));
assert!(
raw.contains("References: <msg-0@example.com> <msg-1@example.com> <msg-2@example.com>")
);
}
fn make_forward_matches(args: &[&str]) -> ArgMatches {
let cmd = Command::new("test")
.arg(Arg::new("message-id").long("message-id"))