diff --git a/.changeset/fix-forward-threading.md b/.changeset/fix-forward-threading.md new file mode 100644 index 00000000..9df55b1f --- /dev/null +++ b/.changeset/fix-forward-threading.md @@ -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. diff --git a/skills/gws-gmail-forward/SKILL.md b/skills/gws-gmail-forward/SKILL.md index cb99a30e..cc0099a5 100644 --- a/skills/gws-gmail-forward/SKILL.md +++ b/skills/gws-gmail-forward/SKILL.md @@ -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 diff --git a/src/helpers/gmail/forward.rs b/src/helpers/gmail/forward.rs index 79b526f4..6eca2b2b 100644 --- a/src/helpers/gmail/forward.rs +++ b/src/helpers/gmail/forward.rs @@ -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: ")); + assert!(raw.contains("References: ")); 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: "".to_string(), + references: " ".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: ")); + assert!( + raw.contains("References: ") + ); + } + fn make_forward_matches(args: &[&str]) -> ArgMatches { let cmd = Command::new("test") .arg(Arg::new("message-id").long("message-id"))