Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-forward-threading.md
Original file line number Diff line number Diff line change
@@ -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 change: 0 additions & 1 deletion skills/gws-gmail-forward/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
61 changes: 55 additions & 6 deletions src/helpers/gmail/forward.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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"))
Expand Down
Loading