diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index 43e5d3352..81652d852 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -277,7 +277,7 @@ class EmailAPI return _downloadManager.downloadFile( attachment.getDownloadUrl(baseDownloadUrl, accountId), getTemporaryDirectory(), - attachment.name ?? '', + attachment.generateFileName(), authentication, cancelToken: cancelToken); } diff --git a/model/lib/email/attachment.dart b/model/lib/email/attachment.dart index db1f91f37..010ad2205 100644 --- a/model/lib/email/attachment.dart +++ b/model/lib/email/attachment.dart @@ -29,6 +29,7 @@ class Attachment with EquatableMixin { static const String eventICSSubtype = 'ics'; static const String eventCalendarSubtype = 'calendar'; static const String applicationRTFType = 'application/rtf'; + static const String _defaultName = 'unknown-attachment'; final PartId? partId; final Id? blobId; @@ -72,11 +73,17 @@ class Attachment with EquatableMixin { } String generateFileName() { - if (name?.isNotEmpty == true) { - return name!; - } else { - return '${blobId?.value}.${type?.subtype}'; - } + final rawName = (name?.trim().isNotEmpty == true) + ? name!.trim() + : (blobId != null + ? (type?.subtype != null + ? '${blobId!.value}.${type!.subtype}' + : blobId!.value) + : _defaultName); + + final sanitized = rawName.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_').trim(); + // Fall back if sanitized name has no meaningful content (e.g. '???' → '___') + return sanitized.contains(RegExp(r'[^_\s]')) ? sanitized : _defaultName; } factory Attachment.fromJson(Map json) => _$AttachmentFromJson(json); diff --git a/model/test/email/attachment_test.dart b/model/test/email/attachment_test.dart index b4f43a002..b4acc4733 100644 --- a/model/test/email/attachment_test.dart +++ b/model/test/email/attachment_test.dart @@ -63,5 +63,122 @@ void main() { expect(result, 'http://localhost/download/1/some-blob-id/ti%C3%AAu%20%C4%91%E1%BB%81%20attachment?name=ti%C3%AAu%20%C4%91%E1%BB%81%20attachment&type=application%2Foctet-stream'); }); }); + + group('generateFileName:', () { + test( + 'should return name when name is not empty', + () { + final attachment = Attachment( + blobId: Id('some-blob-id'), + name: 'document.pdf', + type: MediaType.parse('application/pdf'), + ); + + expect(attachment.generateFileName(), 'document.pdf'); + }); + + test( + 'should return name is trimmed when name has leading/trailing spaces', + () { + final attachment = Attachment( + blobId: Id('some-blob-id'), + name: ' document.pdf ', + type: MediaType.parse('application/pdf'), + ); + + expect(attachment.generateFileName(), 'document.pdf'); + }); + + test( + 'should return blobId with extension when name is null', + () { + final attachment = Attachment( + blobId: Id('some-blob-id'), + type: MediaType.parse('image/png'), + ); + + expect(attachment.generateFileName(), 'some-blob-id.png'); + }); + + test( + 'should return blobId with extension when name is empty', + () { + final attachment = Attachment( + blobId: Id('some-blob-id'), + name: '', + type: MediaType.parse('image/png'), + ); + + expect(attachment.generateFileName(), 'some-blob-id.png'); + }); + + test( + 'should return blobId with extension when name is only whitespace', + () { + final attachment = Attachment( + blobId: Id('some-blob-id'), + name: ' ', + type: MediaType.parse('image/jpeg'), + ); + + expect(attachment.generateFileName(), 'some-blob-id.jpeg'); + }); + + test( + 'should return blobId without extension when name is null and type is null', + () { + final attachment = Attachment( + blobId: Id('some-blob-id'), + ); + + expect(attachment.generateFileName(), 'some-blob-id'); + }); + + test( + 'should return default name when both name and blobId are null', + () { + final attachment = Attachment( + type: MediaType.parse('image/png'), + ); + + expect(attachment.generateFileName(), 'unknown-attachment'); + }); + + test( + 'should sanitize name with question mark character', + () { + final attachment = Attachment( + blobId: Id('some-blob-id'), + name: 'doc?.pdf', + type: MediaType.parse('application/pdf'), + ); + + expect(attachment.generateFileName(), 'doc_.pdf'); + }); + + test( + 'should sanitize name with forward slash character', + () { + final attachment = Attachment( + blobId: Id('some-blob-id'), + name: 'folder/document.pdf', + type: MediaType.parse('application/pdf'), + ); + + expect(attachment.generateFileName(), 'folder_document.pdf'); + }); + + test( + 'should return default name when name consists entirely of illegal characters', + () { + final attachment = Attachment( + blobId: Id('some-blob-id'), + name: '???', + type: MediaType.parse('application/pdf'), + ); + + expect(attachment.generateFileName(), 'unknown-attachment'); + }); + }); }); } \ No newline at end of file