Skip to content

Commit 8fff83a

Browse files
authored
[HOT-FIX] Download attachment is failed on iOS (#4405)
1 parent 6a34e27 commit 8fff83a

File tree

3 files changed

+130
-6
lines changed

3 files changed

+130
-6
lines changed

lib/features/email/data/network/email_api.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ class EmailAPI
277277
return _downloadManager.downloadFile(
278278
attachment.getDownloadUrl(baseDownloadUrl, accountId),
279279
getTemporaryDirectory(),
280-
attachment.name ?? '',
280+
attachment.generateFileName(),
281281
authentication,
282282
cancelToken: cancelToken);
283283
}

model/lib/email/attachment.dart

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class Attachment with EquatableMixin {
2929
static const String eventICSSubtype = 'ics';
3030
static const String eventCalendarSubtype = 'calendar';
3131
static const String applicationRTFType = 'application/rtf';
32+
static const String _defaultName = 'unknown-attachment';
3233

3334
final PartId? partId;
3435
final Id? blobId;
@@ -72,11 +73,17 @@ class Attachment with EquatableMixin {
7273
}
7374

7475
String generateFileName() {
75-
if (name?.isNotEmpty == true) {
76-
return name!;
77-
} else {
78-
return '${blobId?.value}.${type?.subtype}';
79-
}
76+
final rawName = (name?.trim().isNotEmpty == true)
77+
? name!.trim()
78+
: (blobId != null
79+
? (type?.subtype != null
80+
? '${blobId!.value}.${type!.subtype}'
81+
: blobId!.value)
82+
: _defaultName);
83+
84+
final sanitized = rawName.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_').trim();
85+
// Fall back if sanitized name has no meaningful content (e.g. '???' → '___')
86+
return sanitized.contains(RegExp(r'[^_\s]')) ? sanitized : _defaultName;
8087
}
8188

8289
factory Attachment.fromJson(Map<String, dynamic> json) => _$AttachmentFromJson(json);

model/test/email/attachment_test.dart

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,122 @@ void main() {
6363
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');
6464
});
6565
});
66+
67+
group('generateFileName:', () {
68+
test(
69+
'should return name when name is not empty',
70+
() {
71+
final attachment = Attachment(
72+
blobId: Id('some-blob-id'),
73+
name: 'document.pdf',
74+
type: MediaType.parse('application/pdf'),
75+
);
76+
77+
expect(attachment.generateFileName(), 'document.pdf');
78+
});
79+
80+
test(
81+
'should return name is trimmed when name has leading/trailing spaces',
82+
() {
83+
final attachment = Attachment(
84+
blobId: Id('some-blob-id'),
85+
name: ' document.pdf ',
86+
type: MediaType.parse('application/pdf'),
87+
);
88+
89+
expect(attachment.generateFileName(), 'document.pdf');
90+
});
91+
92+
test(
93+
'should return blobId with extension when name is null',
94+
() {
95+
final attachment = Attachment(
96+
blobId: Id('some-blob-id'),
97+
type: MediaType.parse('image/png'),
98+
);
99+
100+
expect(attachment.generateFileName(), 'some-blob-id.png');
101+
});
102+
103+
test(
104+
'should return blobId with extension when name is empty',
105+
() {
106+
final attachment = Attachment(
107+
blobId: Id('some-blob-id'),
108+
name: '',
109+
type: MediaType.parse('image/png'),
110+
);
111+
112+
expect(attachment.generateFileName(), 'some-blob-id.png');
113+
});
114+
115+
test(
116+
'should return blobId with extension when name is only whitespace',
117+
() {
118+
final attachment = Attachment(
119+
blobId: Id('some-blob-id'),
120+
name: ' ',
121+
type: MediaType.parse('image/jpeg'),
122+
);
123+
124+
expect(attachment.generateFileName(), 'some-blob-id.jpeg');
125+
});
126+
127+
test(
128+
'should return blobId without extension when name is null and type is null',
129+
() {
130+
final attachment = Attachment(
131+
blobId: Id('some-blob-id'),
132+
);
133+
134+
expect(attachment.generateFileName(), 'some-blob-id');
135+
});
136+
137+
test(
138+
'should return default name when both name and blobId are null',
139+
() {
140+
final attachment = Attachment(
141+
type: MediaType.parse('image/png'),
142+
);
143+
144+
expect(attachment.generateFileName(), 'unknown-attachment');
145+
});
146+
147+
test(
148+
'should sanitize name with question mark character',
149+
() {
150+
final attachment = Attachment(
151+
blobId: Id('some-blob-id'),
152+
name: 'doc?.pdf',
153+
type: MediaType.parse('application/pdf'),
154+
);
155+
156+
expect(attachment.generateFileName(), 'doc_.pdf');
157+
});
158+
159+
test(
160+
'should sanitize name with forward slash character',
161+
() {
162+
final attachment = Attachment(
163+
blobId: Id('some-blob-id'),
164+
name: 'folder/document.pdf',
165+
type: MediaType.parse('application/pdf'),
166+
);
167+
168+
expect(attachment.generateFileName(), 'folder_document.pdf');
169+
});
170+
171+
test(
172+
'should return default name when name consists entirely of illegal characters',
173+
() {
174+
final attachment = Attachment(
175+
blobId: Id('some-blob-id'),
176+
name: '???',
177+
type: MediaType.parse('application/pdf'),
178+
);
179+
180+
expect(attachment.generateFileName(), 'unknown-attachment');
181+
});
182+
});
66183
});
67184
}

0 commit comments

Comments
 (0)