Skip to content

fix: keep attachment file extension when saving#11268

Open
mvanhorn wants to merge 1 commit into
thunderbird:mainfrom
mvanhorn:fix/7968-attachment-extra-extension
Open

fix: keep attachment file extension when saving#11268
mvanhorn wants to merge 1 commit into
thunderbird:mainfrom
mvanhorn:fix/7968-attachment-extra-extension

Conversation

@mvanhorn

Copy link
Copy Markdown
Contributor

Contribution Summary

Linked Issue/Ticket: Closes #7968
RFC / Technical Design (if applicable): N/A (small bug fix)

Description

When saving a mail attachment to the filesystem, an extra file extension was appended to the file on disk: appointment.ics was written as appointment.ics.txt (and a corroborating report showed something.pkpass written with a spurious trailing extension). The save dialog showed the correct name, but the saved file gained an extra extension.

Root cause

MessageViewFragment.onSaveAttachment launches Intent.ACTION_CREATE_DOCUMENT (via CreateDocumentResultContract) passing the attachment's declared MIME type, which comes from the mail Content-Type header (e.g. text/plain), together with the display name (appointment.ics). Android's Storage Access Framework (FileSystemProvider.splitFileName) checks whether the display name's extension round-trips to the supplied MIME type. Because .ics does not map back to text/plain, the provider appends the MIME type's preferred extension (text/plain -> .txt), producing appointment.ics.txt.

Fix

Pass a MIME type that is consistent with the display name's extension, so the SAF provider leaves the filename unchanged. This mirrors the pattern the VIEW flow already uses (ViewIntentFinder.getBestViewIntent derives MimeTypeUtil.getMimeTypeByExtension(displayName)).

  • Added a small, unit-testable helper MimeTypeUtil.getMimeTypeForFilename(displayName, declaredMimeType) that returns the extension-derived MIME type when the display name has an extension, and otherwise falls back to the declared MIME type (or application/octet-stream when that is null).
  • onSaveAttachment now builds the CreateDocumentResultContract.Input with mimeType = MimeTypeUtil.getMimeTypeForFilename(attachment.displayName, attachment.mimeType).

Because the derived MIME type round-trips with the extension Android computes, the provider no longer appends an extra extension. The actual byte copy in AttachmentController.writeAttachment is unaffected, and the unrelated EML-export path (which passes its own explicit message/rfc822) is untouched.

Notes on edge cases (all covered by the new unit test):

  • Known extension (appointment.ics, boardingpass.pkpass): resolves to the extension-consistent type (text/calendar, application/vnd.apple.pkpass) so the filename is preserved.
  • No extension (noname): falls back to the declared type, and to application/octet-stream when the declared type is null — the provider may then add a suitable extension, which is the desired behaviour.
  • Unknown extension (mystery.unknownext): resolves to application/octet-stream. This is deliberate: SAF treats octet-stream as matching any unrecognised extension, so it keeps the filename as-is rather than appending a spurious extension. (Returning the declared type here would reintroduce the double-extension bug.)

This also removes a latent requireNotNull(attachment.mimeType) that could throw for an attachment with a null declared MIME type; the new helper handles that case gracefully.

Screen Shots

No UI changes (the fix affects the filename passed to the create-document intent, not any visible screen).

AI Disclosure

Select one of the following (mandatory)

  • This contribution does not include any changes created or assisted by AI.
  • This contribution includes changes assisted by AI.
  • This contribution includes changes created by AI.

Contribution Checklist

  • I have read and affirm that my contribution adheres to Mozilla’s Community Participation Guidelines
  • This contribution is in Kotlin where possible (the caller change and the new test are Kotlin; the helper is added to the existing MimeTypeUtil Java utility class alongside the closely related getMimeTypeByExtension it builds on, to keep the change minimal and consistent)
  • This contribution does not use merge commits
  • This contribution adheres to the existing codestyle (./gradlew :legacy:core:spotlessCheck :legacy:ui:legacy:spotlessCheck passes; ./gradlew :legacy:core:detekt :legacy:ui:legacy:detekt passes)
  • This contribution does not break existing unit tests (./gradlew :legacy:core:testDebugUnitTest :legacy:ui:legacy:testDebugUnitTest passes: 751 and 245 tests respectively, 0 failures)
  • This contribution includes tests for any new functionality, and maintains tests for any updated functionality (new MimeTypeUtilTest with 6 cases covering known/unknown/missing extensions)
  • This contribution adheres to our Engineering process (RFC/Technical Design/ADR) — N/A for a self-contained bug fix
  • This PR has a descriptive title and body that accurately outlines all changes made, and contains a reference to any issues that it fixes (Closes Saving an attachment (appointment.ics ) adds an extra file extension #7968)

Testing

Run locally with JDK 21 and Android SDK 36:

  • ./gradlew :legacy:core:testDebugUnitTest --tests "com.fsck.k9.helper.MimeTypeUtilTest" — 6 passed, 0 failed
  • ./gradlew :legacy:core:testDebugUnitTest :legacy:ui:legacy:testDebugUnitTest — 996 passed (6 skipped, pre-existing), 0 failed
  • ./gradlew :legacy:core:spotlessCheck :legacy:ui:legacy:spotlessCheck — passed
  • ./gradlew :legacy:core:detekt :legacy:ui:legacy:detekt — passed

connectedAndroidTest (instrumented) was not run; there is no device/emulator in this environment. The SAF filename behaviour itself is provided by the Android platform and is exercised via the new unit test on the MIME-resolution helper that drives it.

AI was used for assistance.

Saving an attachment launched ACTION_CREATE_DOCUMENT with the MIME type
declared in the mail Content-Type header. When that type did not
round-trip to the display name's extension, the Storage Access Framework
appended the declared type's preferred extension, so appointment.ics
declared as text/plain was written as appointment.ics.txt.

Derive a MIME type that is consistent with the display name's extension
(the same approach the VIEW flow already uses in ViewIntentFinder) via a
new MimeTypeUtil.getMimeTypeForFilename helper and pass it to the
create-document intent, so the provider keeps the filename unchanged.
When the name has no extension, the declared type (or the default) is
used and the provider may add a suitable extension as before.

Fixes thunderbird#7968
@github-actions

Copy link
Copy Markdown
Contributor

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

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.

Saving an attachment (appointment.ics ) adds an extra file extension

2 participants