Skip to content

Replace 'post reply' with '--replying-to' option on 'post draft'#12

Merged
odrobnik merged 16 commits intomainfrom
feature/replying-to
Mar 17, 2026
Merged

Replace 'post reply' with '--replying-to' option on 'post draft'#12
odrobnik merged 16 commits intomainfrom
feature/replying-to

Conversation

@odrobnik
Copy link
Copy Markdown
Contributor

Summary

Replaces the separate post reply command with --replying-to <uid> and --reply-mailbox <name> options on post draft.

Changes

1. Remove post reply command

  • Deleted entire Reply struct (~145 lines)
  • Removed from subcommands array

2. Add reply functionality to post draft

  • --replying-to <uid> - UID of message to reply to
  • --reply-mailbox <name> - Mailbox containing original (default: INBOX)
  • Fetches original message and sets threading headers:
    • In-Reply-To from original.messageId
    • References chain (appends original's Message-ID)

3. Fix blockquote CSS

  • Added blockquote p { margin: 0; } to remove excessive vertical padding
  • Makes Markdown quotes more compact

Why This Design?

Single Unified Command

  • One command for all drafts (simple or replies)
  • No duplicate logic between draft and reply

Full Format Support

  • ✅ Markdown with selective quoting
  • ✅ HTML for rich replies
  • ✅ Plain text for simple replies

AI-Friendly Workflow

AI agents can now:

  1. Fetch original: post fetch 12345 --json
  2. Generate Markdown with selective quotes (> quoted text)
  3. Create threaded draft: post draft --replying-to 12345 --body reply.md

Markdown handles the quoting style — no CLI auto-quote logic needed.

Usage Examples

# Reply to message in INBOX (default)
post draft --replying-to 12345 --body reply.md \
  --from me@example.com --to them@example.com \
  --subject "Re: Original Subject"

# Reply to message in Archive
post draft --replying-to 5678 --reply-mailbox Archive --body reply.md

# Markdown with selective quote
cat > reply.md << 'EOF'
Thanks for reaching out!

> Would you consider moving to California?

I'm settled in Austria for now, but happy to discuss remote!
EOF

post draft --replying-to 12345 --body reply.md

Testing

✅ Created reply draft to UID 13106 with:

  • Markdown body with selective quote
  • Threading headers (In-Reply-To, References)
  • Compact blockquote rendering

Breaking Change

Removes post reply command.

Migration:

# Old
post reply 12345 --body "My reply"

# New
post draft --replying-to 12345 --body "My reply"

Closes

#11

- Remove 'Reply' command (~145 lines)
- Add --replying-to <uid> and --reply-mailbox <name> options to Draft
- Fetch original message and set In-Reply-To/References headers
- Default --reply-mailbox to INBOX (UIDs are mailbox-scoped)
- Fix blockquote CSS: remove paragraph margins for compact quotes

Closes #11
@odrobnik odrobnik self-assigned this Mar 17, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0951bb632

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@@ -113,7 +113,6 @@ struct PostCLI: AsyncParsableCommand {
Quota.self,
Attachment.self,
Draft.self,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore a complete reply workflow after removing post reply

Removing Reply.self from the command list drops the only path that auto-derived sender, recipients, and Re: subject from the original message, but the new --replying-to support in post draft only adds threading headers and still depends on explicit --from, --to, and --subject. In practice, existing post reply <uid> --body ... scripts now have no equivalent one-command replacement and the documented migration to post draft --replying-to <uid> --body ... fails with missing required options before execution.

Useful? React with 👍 / 👎.

Make --from, --to, and --subject optional when --replying-to is set.
Auto-derive from original message:
- from: original.to.first (sender becomes original recipient)
- to: original.from (reply to original sender)
- subject: 'Re: ' + original.subject (add Re: prefix if missing)

Restores the UX convenience of the old 'post reply' command.

Usage:
  post draft --replying-to 13106 --body reply.md

No need to specify --from, --to, or --subject explicitly.
@odrobnik
Copy link
Copy Markdown
Contributor Author

Update: Auto-Derive from/to/subject

Good catch by the bot! The initial implementation required explicit --from, --to, --subject even when using --replying-to, which broke the convenience of the old post reply command.

Fixed in commit 3c8dba3

Now --from, --to, and --subject are optional when --replying-to is used. They auto-derive from the original message:

Field Auto-derived from
--from original.to.first (your address from original's recipients)
--to original.from (reply to original sender)
--subject "Re: " + original.subject (add Re: prefix if missing)

New Usage

# Minimal reply (auto-derives from/to/subject)
post draft --replying-to 13106 --body reply.md

# Explicit override (if needed)
post draft --replying-to 13106 --body reply.md --to custom@example.com

# Old workflow still works
post draft --from me@example.com --to them@example.com --subject "Test" --body draft.md

Migration Now Complete

# Old
post reply 13106 --body "My reply"

# New (exact equivalent)
post draft --replying-to 13106 --body "My reply"

✅ Tested with UID 1592 — auto-derived from/to/subject correctly.

- Add --reply-all flag to post draft
- When used with --replying-to, includes all original recipients in CC
  (excludes sender and primary recipient)
- Add cc field to MessageDetail struct
- Populate cc from SwiftMail MessageInfo.cc

Usage:
  post draft --replying-to 13106 --reply-all --body reply.md
@odrobnik
Copy link
Copy Markdown
Contributor Author

Update: Added --reply-all Flag

Now supports reply-all workflow!

Changes in commit a73a252

  1. Added --reply-all flag to post draft

    • When used with --replying-to, includes all original recipients in CC
    • Automatically excludes sender and primary recipient from CC list
  2. Added cc field to MessageDetail struct

    • Populated from SwiftMail's MessageInfo.cc
    • Exposed in JSON output and CLI fetch

Usage

# Simple reply (only original sender)
post draft --replying-to 13106 --body reply.md

# Reply-all (includes all original recipients in CC)
post draft --replying-to 13106 --reply-all --body reply.md

# Explicit CC override
post draft --replying-to 13106 --cc custom@example.com --body reply.md

Logic

When --reply-all is set:

  • to: Original sender (original.from)
  • cc: All addresses from original.to + original.cc, excluding:
    • Your address (auto-derived from)
    • Original sender (already in to)

✅ Tested with UID 1593 — no CC added (Flo's email had no other recipients)

Allow creating empty reply drafts for inline editing in email client.

Usage:
  post draft --replying-to 13106

Creates a threaded reply with:
- Auto-derived from/to/subject
- In-Reply-To and References headers
- Empty body for inline composition in Mail.app

Body is still required for non-reply drafts.
@odrobnik
Copy link
Copy Markdown
Contributor Author

Update: Made --body Optional for Empty Reply Drafts

You can now create empty reply drafts for inline editing in your email client (like the Mail.app screenshot).

Changes in commit 8363993

  • --body is now optional when --replying-to is used
  • Creates empty body for inline composition
  • Still required for non-reply drafts

Usage

# Empty reply draft (for inline editing)
post draft --replying-to 13106

# Creates draft with:
# ✅ Auto-derived from/to/subject
# ✅ In-Reply-To and References headers
# ✅ Empty body (compose in Mail.app)

Why This Is Useful

Matches the workflow in the screenshot:

  1. Create empty reply with threading
  2. Open in Mail.app
  3. Compose inline with full UI (formatting, attachments, etc.)

Perfect for when you want the threading headers but prefer to write in the native email client.

✅ Tested with UID 1598

When creating a reply draft without --body, auto-quote the entire
original message (like Mail.app Reply behavior).

Format:
- Two blank lines at top (cursor position for inline editing)
- Attribution header: 'On DD.MM.YYYY, at HH:MM, sender wrote:'
- Full quoted original (Markdown, each line prefixed with '>')

Usage:
  post draft --replying-to 12731
  # → Creates draft with empty top + full quoted original

This matches native email client behavior when you press Reply.
@odrobnik
Copy link
Copy Markdown
Contributor Author

Update: Auto-Quote Original When Body Omitted

Now when you omit --body, it auto-quotes the entire original message (exactly like Mail.app Reply button).

Changes in commit e323c49

When --body is omitted, the draft includes:

  1. Two blank lines at top (cursor position)
  2. Attribution header: On DD.MM.YYYY, at HH:MM, sender wrote:
  3. Full quoted original (Markdown, each line prefixed with >)

Usage

# Auto-quote entire original (like Mail.app Reply)
post draft --replying-to 12731

# Custom body with selective quotes
post draft --replying-to 12731 --body reply.md

Example Output

On 04.03.2026, at 14:50, "Dr. Andrea Weihsengruber" <tophaut@ymail.com> wrote:
>
> Sehr geehrter Herr Drobnik,
>
> Herr Dr. Weihsengruber könnte ihnen das entfernen.
> ...

The blank lines at top give you space to type your reply inline in Mail.app, exactly like the native Reply workflow.

✅ Tested with UID 1601 — matches Mail.app Reply behavior!

Detect format AFTER resolving final body (including derivedBody).

Previously:
- Format detected from empty string when body was nil
- Always treated as plain text
- Markdown quote markers (>) rendered literally

Now:
- Format detected from final resolved body
- Auto-quoted body (with >) correctly detected as Markdown
- Renders as proper HTML blockquotes in email client

Result: Auto-quoted replies now have gray blockquote bars instead
of literal '>' characters.
@odrobnik
Copy link
Copy Markdown
Contributor Author

Fix: Markdown Format Detection for Auto-Quoted Replies

Fixed the format detection so auto-quoted replies render as proper HTML blockquotes instead of literal > characters.

Changes in commit 14d8027

Problem: Format was detected before creating the auto-quoted body, so it always detected as plain text.

Solution: Moved format detection to after resolving the final body:

// Resolve final body first
let finalBody = body ?? derivedBody ?? ""

// Then detect format from resolved content
let format: PostServer.BodyFormat
switch detectDraftBodyInputFormat(finalBody) {
case .markdown: format = .markdown  // Now correctly detected!
...
}

Result

Before: Literal > characters in email
After: Proper blockquote with gray left border

Auto-quoted replies now render correctly in Mail.app! 🎉

✅ Tested with UID 1602

- Move attribution header ('On ... wrote:') inside the blockquote
- Add two blank lines with spaces at top (prevents trimming)
- Creates empty paragraph space for cursor in Mail.app

Result matches Mail.app native Reply:
1. Two empty lines at top
2. > On DD.MM.YYYY, at HH:MM, sender wrote:
3. >
4. > [quoted message]
@odrobnik
Copy link
Copy Markdown
Contributor Author

Fix: Attribution Header Inside Blockquote + Empty Top Lines

Fixed the quote formatting to match Mail.app's native Reply exactly.

Changes in commit a1a8e56

  1. Attribution header now inside blockquote:

    > On 04.03.2026, at 14:50, "Dr. Andrea..." wrote:
  2. Two blank lines at top (with spaces to prevent trimming):

     
     
    > On ... wrote:

Result

Now renders exactly like Mail.app Reply:

  • ✅ Empty paragraphs at top (cursor position)
  • ✅ Attribution inside gray blockquote bar
  • ✅ Full quoted original below

Perfect for inline editing! 🎉

✅ Tested with UID 1606

Use > <br> for empty lines in quoted text to preserve paragraph
spacing in HTML rendering.

Standard Markdown blockquotes collapse consecutive lines, so empty
lines (>) don't create visual paragraph breaks. Using <br> tags
ensures proper spacing between paragraphs in the quoted original.

Result: Quoted text now has proper paragraph breaks matching the
original email formatting.
@odrobnik
Copy link
Copy Markdown
Contributor Author

Fix: Preserve Paragraph Breaks in Quoted Text

Fixed paragraph spacing in quoted replies using <br> tags.

Changes in commit 88f57a4

Problem: Standard Markdown blockquotes collapse consecutive lines, so empty lines (>) don't create visual paragraph breaks.

Solution: Use > <br> for empty lines in quoted text:

> Para 1
> <br>
> Para 2
> <br>
> Para 3

This ensures proper paragraph spacing in the HTML rendering.

Result

Quoted text now has proper paragraph breaks matching the original email formatting, instead of being compressed into a single block.

✅ Tested with UID 1610

Switch from original.markdown() to original.textBody for quoting.

Problem: HTMLToMarkdown converter collapses paragraph breaks when
converting HTML to Markdown, losing blank lines between paragraphs.

Solution: Use plain text body directly, which preserves original
formatting including blank lines.

Trade-off: Links are plain text instead of Markdown formatted, but
paragraph breaks are preserved correctly.

Mail.app will still render plain URLs as clickable links.
Use plain '>' for empty lines instead of '> <br>'.

Let Markdown-to-HTML handle blockquote rendering naturally.
@odrobnik
Copy link
Copy Markdown
Contributor Author

Known Issue: Blockquote Paragraph Breaks

The Markdown we generate is correct with proper > lines for blank lines:

> Para 1
>
> Para 2

However, SwiftText's MarkdownToHTML converter collapses consecutive blockquote lines, losing paragraph breaks in the HTML output.

Filed SwiftText Issue

SwiftText #8: MarkdownToHTML collapses blockquote paragraphs
Cocoanetics/SwiftText#8

Current Workaround

None yet. The quoted text will have compressed paragraphs until SwiftText is fixed.

Long-term Fix

Once SwiftText #8 is resolved, the paragraph breaks will render correctly without any changes to Post code.

Changed from ' \n \n' to '\n\n' for leading blank lines.

Note: Leading whitespace is still being trimmed somewhere (possibly
SwiftMail or email body normalization). Once SwiftText #8 is fixed
to preserve leading blank lines as empty <p></p> tags, this will
work correctly.

For now, the main reply functionality works - just missing the
empty paragraphs at the top.
- SwiftText 1.1.4: Fixes blockquote paragraph breaks and leading blank lines
- SwiftMCP 1.4.0: Stability improvements

This should fix:
- Paragraph breaks in quoted replies
- Empty paragraphs at top of reply drafts
Removed the '\n>\n' blank quote line after the attribution header.

Before:
> On ... wrote:
>
> Body text

After:
> On ... wrote:
> Body text

This matches Mail.app's native Reply formatting more closely.
Reverted previous commit - the blank line after 'On ... wrote:' should
be preserved for proper Mail.app formatting.

Format:
> On ... wrote:
>
> Body text
Changed blockquote p CSS:
- margin: 0.5em 0 0 0 (top spacing between paragraphs)
- margin-top: 0 on first-child (no space before first para)

This creates visible spacing for empty <p></p> tags (like the blank
line after 'On ... wrote:') while keeping the first paragraph tight
against the attribution header.
Use original.markdown() instead of original.textBody for quoting.

Benefits:
- HTML emails: Preserves formatting, links, lists, etc.
- Plain text emails: Falls back to plain text automatically
- Paragraph breaks: Preserved correctly in both cases

The markdown() method handles both HTML and plain text emails:
1. If HTML exists: HTMLToMarkdown conversion
2. If plain text only: Returns text as-is
3. Empty: Returns empty string

Result: Quoted replies now preserve original formatting for all
email types.
@odrobnik odrobnik merged commit 287a43a into main Mar 17, 2026
2 checks passed
@odrobnik odrobnik deleted the feature/replying-to branch March 17, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant