Skip to content

feat(signature): add HTML signature support - #10876

Merged
rafaeltonholo merged 12 commits into
thunderbird:mainfrom
joshua-grimmett:feat/8841/html-signature
Aug 20, 2026
Merged

feat(signature): add HTML signature support#10876
rafaeltonholo merged 12 commits into
thunderbird:mainfrom
joshua-grimmett:feat/8841/html-signature

Conversation

@joshua-grimmett

@joshua-grimmett joshua-grimmett commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Closes #11405

Summary

Adds a per-identity "Format signature as HTML" toggle so users can write rich-text signatures and have them sent as HTML in outgoing mail. Refs #8841.

When the flag is on, the signature is embedded verbatim in outgoing HTML messages (no text-to-HTML conversion), converted via Jsoup for outgoing plain-text messages, and rendered as a live WebView preview in the compose screen so drafts and new composes show the rendered result instead of raw tags.

Scope (vs #8841)

In scope ✅

  • Users can input signatures as HTML tags
  • HTML signatures sent with outgoing email

Stretch (not in this PR)

  • Import HTML from file — can follow up in a separate PR if desired

Out of scope per #8841

  • Full visual / WYSIWYG editor
  • Attaching vCards as signatures

Changes

  • Data model: Identity.signatureIsHtml: Boolean added, persisted via LegacyAccountStorageHandler under key signatureIsHtml.$ident. IdentitySettingsDescriptions registers the new setting at version 111.
  • UI: "Format signature as HTML" checkbox added to both the Edit Identity screen and the Composition defaults screen, with a summary line explaining the sanitisation behaviour.
  • Outgoing mail: TextBodyBuilder.getSignatureHtml() short-circuits HtmlConverter.textToHtmlFragment() when the flag is set. For plain-text messages, TextBodyBuilder.getSignature() runs the HTML signature through HtmlConverter.htmlToText() to produce a plain-text fallback.
  • Sanitisation: HtmlSignatureSanitizer wraps Jsoup.clean() with a Safelist.relaxed() baseline that strips <script>, event handler attributes, javascript: URLs, <iframe>, <object>, and <embed> elements.
  • Compose preview: MessageCompose renders the HTML signature in a MessageWebView (reusing the component already used for quoted HTML) instead of showing raw tags in the plain EditText. Remote images and natural device-size rendering are enabled since the signature is the user's own content.
  • Draft reload: When an existing draft is reopened, account.findIdentity(email) looks up the matching account identity and inherits its signatureIsHtml value, preserving the flag across drafts for multi-identity accounts.

Tests

  • New HtmlSignatureSanitizerTest — 10 cases covering sanitisation (preserves basic formatting, strips scripts/event handlers / javascript: URLs / iframes, passes plain text unchanged).
  • New TextBodyBuilderHtmlSignatureTest — 4 cases covering the HTML short-circuit in both the HTML and plain-text body paths.
  • Existing TextBodyBuilderTest, MessageBuilderTest, ReplyToPresenterTest, IdentityHeaderBuilderTest updated for the new Identity field and continue to pass.
  • ./gradlew spotlessCheck detekt pass locally.

Screenshots

Edit Identity screen — new HTML checkbox:

pr-edit-identity

Compose screen — rendered HTML signature preview:

pr-compose

Test plan

  • Set HTML signature in Edit Identity → compose new message → recipient sees rendered HTML
  • Save message as draft → reopen draft → signature renders correctly
  • Multi-identity: drafts preserve per-identity HTML flag correctly
  • Sanitiser strips <script> / onclick / javascript: URLs
  • Unchecking HTML flag → compose falls back to plain-text signature EditText
  • Sending plain-text message with HTML signature produces a text/plain fallback via HtmlConverter.htmlToText()

@github-actions

Copy link
Copy Markdown
Contributor

Missing report label. Set exactly one of: report: include, report: exclude OR report: highlight.

@Alecaddd Alecaddd added the report: highlight Highlight changes (major feature, breaking, or noticeable to users) in user-facing reports. label Apr 16, 2026
@ryanleesipes

Copy link
Copy Markdown
Contributor

Thank you for this @joshua-grimmett - very cool stuff.

@jbott-tbird

Copy link
Copy Markdown
Collaborator

hey @joshua-grimmett, we haven't forgotten about this and appreciate you doing this (especially since it's one of our main roadmap items this year). @rafaeltonholo was put on some last minute changes for our Thundermail project, but will be back on this in the next week or so.

@joshua-grimmett

Copy link
Copy Markdown
Contributor Author

No worries, @ryanleesipes glad to contribute. I love the work you guys are doing.

@joshua-grimmett

Copy link
Copy Markdown
Contributor Author

Thanks @jbott-tbird, look forward to seeing how it goes!

@rafaeltonholo rafaeltonholo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hi @joshua-grimmett, thanks for this contribution and sorry for the delay in reviewing this PR.

Before merging this, we have some points we need to address first.

Comment thread legacy/core/src/main/java/com/fsck/k9/message/TextBodyBuilder.java
Comment thread legacy/core/src/test/java/com/fsck/k9/message/html/HtmlSignatureSanitizerTest.kt Outdated
Comment thread legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java Outdated
Comment thread legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java Outdated
Comment thread legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java Outdated
Comment thread legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java Outdated
// multi-identity accounts preserve each identity's setting. If no match, fall back
// to the currently-loaded default identity.
Identity matchedIdentity = null;
String draftEmail = newIdentity.getEmail();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: could be final

Suggested change
String draftEmail = newIdentity.getEmail();
final String draftEmail = newIdentity.getEmail();

// Ignore — fall back to default identity below.
}
}
boolean signatureIsHtml = matchedIdentity != null

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: could be final

Suggested change
boolean signatureIsHtml = matchedIdentity != null
final boolean signatureIsHtml = matchedIdentity != null

@jbott-tbird

Copy link
Copy Markdown
Collaborator

Hi @joshua-grimmett, thank you again for working on this. It is a feature on our roadmap for this year, and I would love to get this in. Are you able to make the change that @rafaeltonholo suggested? We would love to work with you on getting this through!

@guardian-groot

Copy link
Copy Markdown

@joshua-grimmett thank you for your work! Are there any ways to support this PR?

@apidemy

apidemy commented Aug 14, 2026

Copy link
Copy Markdown

Hi! I’m interested in helping get HTML signature support completed and merged into Thunderbird for Android.

I’d be happy to contribute code to this PR, address outstanding review comments, add or improve tests, and help with any remaining work needed to make the feature ready for merging.

Please let me know if it would be helpful for me to pick up any specific remaining tasks on this PR, or if you’re happy for me to start working through the open review feedback.

@rafaeltonholo

Copy link
Copy Markdown
Member

Hi! I’m interested in helping get HTML signature support completed and merged into Thunderbird for Android.

I’d be happy to contribute code to this PR, address outstanding review comments, add or improve tests, and help with any remaining work needed to make the feature ready for merging.

Please let me know if it would be helpful for me to pick up any specific remaining tasks on this PR, or if you’re happy for me to start working through the open review feedback.

Hi @apidemy,

Unfortunately, I have already begun working on this PR to ensure we can deliver it as planned. However, we will soon have additional items related to HTML signature support, and we would be glad to have your assistance if you're still interested.

Add a per-identity "Format signature as HTML" toggle so users can set
rich-text signatures and have them sent as HTML in outgoing mail,
addressing issue thunderbird#8841.

- Identity.signatureIsHtml persisted alongside the existing signature
  fields; LegacyAccountStorageHandler reads/writes/deletes the new key;
  IdentitySettingsDescriptions bumps to settings version 111.
- Edit Identity and Account Composition defaults screens gain a
  "Format signature as HTML" checkbox.
- TextBodyBuilder short-circuits HtmlConverter.textToHtmlFragment when
  the flag is set, and runs the signature through HtmlConverter.htmlToText
  for the plain-text message path.
- HtmlSignatureSanitizer sanitizes user HTML via a Jsoup Safelist.relaxed
  baseline, stripping scripts, event handlers, and javascript: URLs.
- MessageCompose renders the HTML signature in a MessageWebView preview
  instead of raw text, so drafts and new composes show the rendered
  result. Remote images and natural device-size rendering are enabled
  for the user's own signature content.
- Draft reload looks up the matching identity by email via
  account.findIdentity() to preserve signatureIsHtml for multi-identity
  accounts, since the X-K9mail-Identity header does not encode the flag.
- Tests: HtmlSignatureSanitizerTest (10 cases) and
  TextBodyBuilderHtmlSignatureTest (4 cases).
- Convert legacy tests to use AAA pattern with explicit sections
- Replace inline assertions with descriptive test names using backticks
- Add testSubject property for clarity
- Reorder tests logically (basic → allowed → removed content)
- Add language hints to HTML string literals
- Add missing assertions for sanitize tests
- Replace mock() with FakeGeneralSettingsManager in TextBodyBuilderHtmlSignatureTest
… implementation

- Extract HtmlSignatureSanitizer to interface in feature.mail.message.composer
- Move implementation from legacy.core to DefaultHtmlSignatureSanitizer
- Replace jsoup with ksoup for Kotlin Multiplatform compatibility
- Inject HtmlSignatureSanitizer via DI into MessageBuilder and TextBodyBuilder
- Move tests to new package and convert to kotlin.test
…essageWebView

- Add ThunderbirdWebViewSettings interface in webkit api module
- Move WebViewConfig from legacy.ui.legacy to core.android.webkit.api
- Deprecate legacy WebViewConfig with replacement directive
- Make MessageWebView implement ThunderbirdWebViewSettings
- Expose loadWithOverviewMode property in interface
- Add KDoc to interface methods
…to reusable function

- Move SignaturePreviewWebView configuration logic from MessageCompose to new SignaturePreviewWebView.kt
- Add configureForSignaturePreview extension function on ThunderbirdWebViewSettings
- Include KDoc explaining network data unblocking and overview mode behavior
- Add comprehensive tests covering config application and signature-specific overrides
… content

- Replace manual HTML document construction with displayHtml.wrapMessageContent
- Remove duplicate HtmlSignatureSanitizer import
- Remove fully qualified class names for MessageWebView and WebViewConfigProvider
- Add saveSignatureAsHtml to AccountSetupComposition state and OnFormatSignatureAsHtmlCheck event, persisted via LegacyAccount.signatureIsHtml.
- Render a live signature preview with RichText when a signature is set.
- Pass the whole state object to AccountSetupCompositionScreen instead of individual parameters.
- Replace the hand-rolled checkbox row with CheckboxInput and adjust signature labels.
… header

- Add SIGNATURE_IS_HTML field to IdentityField enum
- Persist signatureIsHtml flag in identity blob when signature is customized
- Parse and apply signature HTML setting from identity header on draft restore
- Remove workaround that matched identity by email to infer HTML signature setting
…on and edit identity

- Extract LegacyIdentitySignatureWebViewConfigurator to share WebView config and HTML preview logic
- Add signature preview WebView to EditIdentity layout with debounced updates
- Adjust AccountSetupComposition signature UI spacing and label styling
- Remove unused account_setup_composition.xml layout file
- Add edit_identity_screen dimension resources for consistent padding
@rafaeltonholo
rafaeltonholo force-pushed the feat/8841/html-signature branch from fdc3210 to fc71357 Compare August 19, 2026 12:02
@rafaeltonholo
rafaeltonholo dismissed their stale review August 19, 2026 12:08

I've applied all the required changes

@rafaeltonholo

Copy link
Copy Markdown
Member

Hi all!

I've applied all the required changes and also included a few others to make this PR mergeable. Aside from the changes I've asked for in the PR comments, the new changes also include:

  • A preview of the signature on both "Composition defaults" and "Edit Identity" screens; see videos below
    Composition defaults Edit Identity
    Screen.Recording.2026-08-19.at.9.12.11.AM.mp4
    Screen.Recording.2026-08-19.at.9.14.16.AM.mp4
  • A button to clear the signature when editing it in the Composition defaults screen; see screenshot below image
  • Removed new code from the legacy module to a dedicated module called :core:android:webkit
  • Properly saved the identity signature when saving a draft

Important

It is important to note that this isn't the final version of the signature feature. There are still some improvements to be made, such as:

  • Include a WYSIWYG editor for the signature, so that users can format their signature text without needing to know HTML
  • Migrate the Edit Identity screen to Compose, so we can have a consistent experience across the app
  • And other improvements that will be discussed in future with the team.

I really appreciate all your patience and feedback on this PR. We know this is a very important feature for our users, and we want to make sure we get it right.

Please let me know if you have any questions or concerns regarding the changes made.

cc: @jbott-tbird, @Alecaddd, @ryanleesipes, @dani-zilla

@dani-zilla dani-zilla left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good! Tested locally and sent some beautiful HTML signatures to test it out. Great work!


@Test
fun `sanitize should return empty string when input is empty`() {
// Arrange

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a reason for the arrange/act/assert labels and language as well? Feels like something AI may have done, but I don't know if it has value to analog intelligence 😆

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've always used the comments Arrange/Act/Assert to organize the test cases by blocks, so it is easier to understand:

  • Where I'm preparing the test case (Arrange)
  • Where I'm actually triggering the test subject action (Act), and
  • Where I'm verifying the test subject output (Assert).

This is a well-known test pattern that predates AI; see more: "Arrange-Act-Assert" and Arrange-Act-Assert: A Pattern for Writing Good Tests

Regarding the language feature, it is just my OCD of seeing code inside a string and not having it show in the syntax colour. The language comment makes AS understand that this is code and show the syntax colour.

@rafaeltonholo
rafaeltonholo merged commit 88212e4 into thunderbird:main Aug 20, 2026
17 checks passed
@thunderbird-botmobile

Copy link
Copy Markdown
Contributor

Thanks for your contribution! Your pull request has been merged and will be part of Thunderbird 24. We appreciate the time and effort you put into improving Thunderbird. If you haven’t already, you’re welcome to join our Matrix chat for contributors. It’s where we discuss development and help each other out. https://matrix.to/#/#tb-android-dev:mozilla.org
Hope to see you there! 🚀📱🐦

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

report: highlight Highlight changes (major feature, breaking, or noticeable to users) in user-facing reports.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shepherd Signature PR

8 participants