FYI
' --html ``` ## Tips - Includes the original message with sender, date, subject, and recipients. +- With `--html`, the forwarded block uses Gmail's `gmail_quote` CSS classes and preserves the original message's HTML formatting. Use HTML fragment tags (``, ``, ``, etc.) — no ``/`` wrapper needed.
+- With `--html`, inline images embedded in the forwarded message (`cid:` references) will appear broken. Externally hosted images are unaffected.
## See Also
diff --git a/skills/gws-gmail-reply-all/SKILL.md b/skills/gws-gmail-reply-all/SKILL.md
index d7e7c9cd..11b4d0f0 100644
--- a/skills/gws-gmail-reply-all/SKILL.md
+++ b/skills/gws-gmail-reply-all/SKILL.md
@@ -24,16 +24,17 @@ gws gmail +reply-all --message-id `, ``, ``, etc.) — no ``/`` wrapper needed.
+- With `--html`, inline images embedded in the quoted message (`cid:` references) will appear broken. Externally hosted images are unaffected.
## See Also
diff --git a/skills/gws-gmail-reply/SKILL.md b/skills/gws-gmail-reply/SKILL.md
index b14ad354..06edc485 100644
--- a/skills/gws-gmail-reply/SKILL.md
+++ b/skills/gws-gmail-reply/SKILL.md
@@ -24,15 +24,16 @@ gws gmail +reply --message-id `, ``, ``, etc.) — no ``/`` wrapper needed.
+- With `--html`, inline images embedded in the quoted message (`cid:` references) will appear broken. Externally hosted images are unaffected.
+- `--to` adds extra recipients to the To field.
+- For reply-all, use `+reply-all` instead.
## See Also
diff --git a/skills/gws-gmail-send/SKILL.md b/skills/gws-gmail-send/SKILL.md
index b1b270ee..8df1de3a 100644
--- a/skills/gws-gmail-send/SKILL.md
+++ b/skills/gws-gmail-send/SKILL.md
@@ -24,14 +24,15 @@ gws gmail +send --to `, ``, ``, ``, ` Rich content Rich content Original Original FYI FYI FYI Original FYI FYI Original FYI Original message body FYI HTML only Rich HTML body Rich HTML body Hello Hello rich text rich text Nested HTML Nested HTML Real HTML Real HTML Body Body This is HTML but flag says plain Rich content Original My HTML reply My HTML reply Original My HTML reply Hello world Hello world
`, `/
/
`. No need for ``/``/`` wrappers.
+- For attachments, use the raw API instead: gws gmail users messages send --json '...'
> [!CAUTION]
> This is a **write** command — confirm with the user before executing.
diff --git a/src/helpers/gmail/forward.rs b/src/helpers/gmail/forward.rs
index 9200b48f..e9b76da6 100644
--- a/src/helpers/gmail/forward.rs
+++ b/src/helpers/gmail/forward.rs
@@ -43,7 +43,8 @@ pub(super) async fn handle_forward(
bcc: config.bcc.as_deref(),
from: config.from.as_deref(),
subject: &subject,
- body: config.body_text.as_deref(),
+ body: config.body.as_deref(),
+ html: config.html,
};
let raw = create_forward_raw_message(&envelope, &original);
@@ -65,7 +66,8 @@ pub(super) struct ForwardConfig {
pub from: Option
\r\n")
+ } else {
+ (format_forwarded_message(original), "\r\n\r\n")
+ };
let body = match envelope.body {
- Some(note) => format!("{}\r\n\r\n{}", note, forwarded_block),
+ Some(note) => format!("{}{}{}", note, separator, forwarded_block),
None => forwarded_block,
};
@@ -132,6 +140,40 @@ fn format_forwarded_message(original: &OriginalMessage) -> String {
)
}
+fn format_forwarded_message_html(original: &OriginalMessage) -> String {
+ let cc_line = if original.cc.is_empty() {
+ String::new()
+ } else {
+ format!("Cc: {}
", format_address_list_with_links(&original.cc))
+ };
+
+ let body = resolve_html_body(original);
+ let date = format_date_for_attribution(&original.date);
+ let from = format_forward_from(&original.from);
+ let to = format_address_list_with_links(&original.to);
+
+ format!(
+ "
\
+ From: {}
\
+ Date: {}
\
+ Subject: {}
\
+ To: {}
\
+ {}\
+
\
+ {}\
+ \nLine two".to_string(),
+ body_html: None,
+ };
+ let html = format_forwarded_message_html(&original);
+ assert!(html.contains("Line one & <stuff>
"));
+ assert!(html.contains("Line two"));
+ }
+
+ #[test]
+ fn test_format_forwarded_message_html_escapes_metadata() {
+ let original = OriginalMessage {
+ thread_id: "t1".to_string(),
+ message_id_header: "".to_string(),
+ references: "".to_string(),
+ from: "Tom & Jerry
\r\n
between note and forwarded block (not \r\n\r\n)
+ assert!(raw.contains("
\r\n
` tags.
+pub(super) fn resolve_html_body(original: &OriginalMessage) -> String {
+ match &original.body_html {
+ Some(html) => html.clone(),
+ None => {
+ eprintln!("Note: original message has no HTML body; plain text was converted to HTML.");
+ html_escape(&original.body_text)
+ .lines()
+ .collect::
\r\n")
+ }
+ }
+}
+
+/// Escape `&`, `<`, `>`, `"`, `'` for safe embedding in HTML.
+pub(super) fn html_escape(text: &str) -> String {
+ // `&` must be replaced first to avoid double-escaping the other replacements.
+ text.replace('&', "&")
+ .replace('<', "<")
+ .replace('>', ">")
+ .replace('"', """)
+ .replace('\'', "'")
+}
+
+/// Extract the bare email address from a header value like
+/// `"Alice
\r\nLine 2
\r\nLine 3"
+ );
+ }
+
+ #[test]
+ fn test_message_builder_html_content_type() {
+ let raw = MessageBuilder {
+ to: "test@example.com",
+ subject: "Hello",
+ from: None,
+ cc: None,
+ bcc: None,
+ threading: None,
+ html: true,
+ }
+ .build("
\r\n")
+ } else {
+ (format_quoted_original(original), "\r\n\r\n")
+ };
+ let body = format!("{}{}{}", envelope.body, separator, quoted);
builder.build(&body)
}
@@ -417,12 +375,33 @@ fn format_quoted_original(original: &OriginalMessage) -> String {
)
}
+fn format_quoted_original_html(original: &OriginalMessage) -> String {
+ let quoted_body = resolve_html_body(original);
+ let date = format_date_for_attribution(&original.date);
+ let sender = format_sender_for_attribution(&original.from);
+
+ format!(
+ "
\
+ \
+
\
+ Rich content"));
+ assert!(!html.contains("plain fallback"));
+ // Sender is a bare email — formatted as a mailto link
+ assert!(html.contains("alice@example.com wrote:"));
+ }
+
+ #[test]
+ fn test_format_quoted_original_html_fallback_plain_text() {
+ let original = 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: "".to_string(),
+ cc: "".to_string(),
+ subject: "".to_string(),
+ date: "Mon, 1 Jan 2026".to_string(),
+ body_text: "Line one &
"));
+ assert!(html.contains("Line two"));
+ }
+
+ #[test]
+ fn test_format_quoted_original_html_escapes_metadata() {
+ let original = OriginalMessage {
+ thread_id: "t1".to_string(),
+ message_id_header: "".to_string(),
+ references: "".to_string(),
+ from: "O'Brien & Associates
between reply and quoted block (not \r\n\r\n)
+ assert!(raw.contains(
+ "
\r\n