Skip to content

Commit b89fc54

Browse files
authored
#5839 Fix parsing noname attachment with "message/global" content type (#5844)
* #5839 Skip “message/global” noname attachments * wip * add ui test * wip * wip * wip * wip * rename isHidden to shoudBeHidden
1 parent a59d88d commit b89fc54

File tree

9 files changed

+454
-152
lines changed

9 files changed

+454
-152
lines changed

extension/chrome/elements/attachment_preview.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,8 @@ View.run(
9898
const extension = nameSplit[nameSplit.length - 1].toLowerCase();
9999
if (['jpg', 'jpeg', 'png', 'gif'].includes(extension)) {
100100
return 'img';
101-
} else if (extension === 'txt') {
102-
return 'txt';
103-
} else if (extension === 'pdf') {
104-
return 'pdf';
101+
} else if (['txt', 'pdf'].includes(extension)) {
102+
return extension as AttachmentType;
105103
}
106104
return undefined;
107105
};

extension/js/common/api/email-provider/gmail/gmail-parser.ts

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -154,43 +154,44 @@ export class GmailParser {
154154
internalResults: Attachment[] = [],
155155
{ pgpEncryptedIndex }: { pgpEncryptedIndex?: number } = {}
156156
) => {
157-
if (msgOrPayloadOrPart.hasOwnProperty('payload')) {
158-
internalMsgId = (msgOrPayloadOrPart as GmailRes.GmailMsg).id;
159-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
160-
GmailParser.findAttachments((msgOrPayloadOrPart as GmailRes.GmailMsg).payload!, internalMsgId, internalResults);
157+
if ('payload' in msgOrPayloadOrPart && msgOrPayloadOrPart.payload) {
158+
internalMsgId = msgOrPayloadOrPart.id;
159+
GmailParser.findAttachments(msgOrPayloadOrPart.payload, internalMsgId, internalResults);
161160
}
162-
if (msgOrPayloadOrPart.hasOwnProperty('parts')) {
163-
const payload = msgOrPayloadOrPart as GmailRes.GmailMsg$payload;
164-
const contentType = payload.headers?.find(x => x.name.toLowerCase() === 'content-type');
165-
const parts = payload.parts!; // eslint-disable-line @typescript-eslint/no-non-null-assertion
166-
// are we dealing with a PGP/MIME encrypted message?
167-
const pgpEncrypted = Boolean(
168-
parts.length === 2 &&
169-
contentType?.value?.startsWith('multipart/encrypted') &&
170-
(contentType.value.includes('protocol="application/pgp-encrypted"') || parts[0].mimeType === 'application/pgp-encrypted')
171-
);
172-
for (const [i, part] of parts.entries()) {
173-
GmailParser.findAttachments(part, internalMsgId, internalResults, {
174-
pgpEncryptedIndex: pgpEncrypted ? i : undefined,
175-
});
161+
if ('parts' in msgOrPayloadOrPart) {
162+
const contentType = msgOrPayloadOrPart.headers?.find(header => header.name.toLowerCase() === 'content-type');
163+
const parts = msgOrPayloadOrPart.parts ?? [];
164+
const hasMultipartAlternativePart = parts.find(part => part.mimeType === 'multipart/alternative');
165+
// ignore plain inline attachments
166+
if (!contentType?.value.startsWith('multipart/related') || !hasMultipartAlternativePart) {
167+
// are we dealing with a PGP/MIME encrypted message?
168+
const pgpEncrypted = Boolean(
169+
parts.length === 2 &&
170+
contentType?.value.startsWith('multipart/encrypted') &&
171+
(contentType.value.includes('protocol="application/pgp-encrypted"') || parts[0].mimeType === 'application/pgp-encrypted')
172+
);
173+
for (const [i, part] of parts.entries()) {
174+
GmailParser.findAttachments(part, internalMsgId, internalResults, {
175+
pgpEncryptedIndex: pgpEncrypted ? i : undefined,
176+
});
177+
}
176178
}
177179
}
178-
/* eslint-disable @typescript-eslint/no-non-null-assertion */
179-
if (msgOrPayloadOrPart.hasOwnProperty('body') && (msgOrPayloadOrPart as GmailRes.GmailMsg$payload$part).body!.hasOwnProperty('attachmentId')) {
180-
const payload = msgOrPayloadOrPart as GmailRes.GmailMsg$payload;
180+
if ('body' in msgOrPayloadOrPart && msgOrPayloadOrPart.body?.hasOwnProperty('attachmentId')) {
181+
const payload = msgOrPayloadOrPart as GmailRes.GmailMsg$payload$part;
181182
const treatAs = Attachment.treatAsForPgpEncryptedAttachments(payload.mimeType, pgpEncryptedIndex);
183+
const inline = (GmailParser.findHeader(payload, 'content-disposition') || '').toLowerCase().startsWith('inline');
182184
internalResults.push(
183185
new Attachment({
184186
msgId: internalMsgId,
185-
id: (msgOrPayloadOrPart as GmailRes.GmailMsg$payload$part).body!.attachmentId,
186-
length: (msgOrPayloadOrPart as GmailRes.GmailMsg$payload$part).body!.size,
187-
name: (msgOrPayloadOrPart as GmailRes.GmailMsg$payload$part).filename,
188-
type: (msgOrPayloadOrPart as GmailRes.GmailMsg$payload$part).mimeType,
187+
id: payload.body?.attachmentId ?? '',
188+
length: payload.body?.size ?? 0,
189+
name: payload.filename,
190+
type: payload.mimeType,
189191
treatAs,
190-
inline: (GmailParser.findHeader(msgOrPayloadOrPart, 'content-disposition') || '').toLowerCase().startsWith('inline'),
192+
inline,
191193
})
192194
);
193-
/* eslint-enable @typescript-eslint/no-non-null-assertion */
194195
}
195196
return internalResults;
196197
};

extension/js/common/core/attachment.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -148,15 +148,6 @@ export class Attachment {
148148
);
149149
};
150150

151-
public isPrivateKey = (): boolean => {
152-
return (
153-
Boolean(this.name.match(/(cryptup|flowcrypt)-backup-([a-z0-9]+(?:\-[A-F0-9]{40})?)\.(key|asc)$/g)) ||
154-
(/\.(asc|key)$/.test(this.name) &&
155-
this.hasData() &&
156-
Buf.with(this.getData().subarray(0, 100)).toUtfStr().includes('-----BEGIN PGP PRIVATE KEY BLOCK-----'))
157-
);
158-
};
159-
160151
public hasData = () => {
161152
return this.bytes instanceof Uint8Array;
162153
};
@@ -178,7 +169,7 @@ export class Attachment {
178169
throw new Error('Attachment has no data set');
179170
};
180171

181-
public isAttachmentAnImage = () => {
172+
public isImage = () => {
182173
return this.type.startsWith('image/') || this.type.startsWith('img/');
183174
};
184175

@@ -201,9 +192,9 @@ export class Attachment {
201192
}
202193
}
203194
return 'signature';
204-
} else if (this.inline && this.isAttachmentAnImage()) {
195+
} else if (this.inline && this.isImage()) {
205196
return 'inlineImage';
206-
} else if (!this.name && !this.isAttachmentAnImage() && this.type !== 'application/octet-stream' && this.type !== 'multipart/mixed') {
197+
} else if (!this.name && !this.isImage() && !['application/octet-stream', 'multipart/mixed', 'message/global'].includes(this.type)) {
207198
// this.name may be '' or undefined - catch either
208199
return this.length < 100 ? 'hidden' : 'encryptedMsg';
209200
} else if (this.name === 'msg.asc' && this.length < 100 && this.type === 'application/pgp-encrypted') {
@@ -222,11 +213,10 @@ export class Attachment {
222213
} else if (this.isPrivateKey()) {
223214
return 'privateKey';
224215
} else {
225-
// && !Attachment.encryptedMsgNames.includes(this.name) -- already checked above
226216
const isAmbiguousAscFile = this.name.endsWith('.asc'); // ambiguous .asc name
227217
const isAmbiguousNonameFile = !this.name || this.name === 'noname'; // may not even be OpenPGP related
228-
if (!this.inline && this.length < 100000 && (isAmbiguousAscFile || isAmbiguousNonameFile) && !this.isAttachmentAnImage()) {
229-
if (isAmbiguousNonameFile && this.type === 'application/octet-stream') {
218+
if (!this.inline && this.length < 100000 && (isAmbiguousAscFile || isAmbiguousNonameFile) && !this.isImage()) {
219+
if (isAmbiguousNonameFile && ['application/octet-stream', 'message/global'].includes(this.type)) {
230220
return 'plainFile';
231221
}
232222
return this.hasData() ? 'maybePgp' : 'needChunk';
@@ -239,6 +229,12 @@ export class Attachment {
239229
return this.type === 'application/pgp-encrypted' && this.name.length === 0 && this.getData().toUtfStr() === 'Version: 1';
240230
};
241231

232+
public shouldBeHidden = () => {
233+
return (
234+
this.type === 'application/pgp-keys' || this.isPublicKey() || this.inline || Attachment.encryptedMsgNames.some(filename => this.name.includes(filename))
235+
);
236+
};
237+
242238
public isExecutableFile = () => {
243239
return [
244240
'ade',
@@ -296,4 +292,13 @@ export class Attachment {
296292
'xll',
297293
].some(exeFileExtension => this.name.endsWith('.' + exeFileExtension));
298294
};
295+
296+
private isPrivateKey = (): boolean => {
297+
return (
298+
Boolean(this.name.match(/(cryptup|flowcrypt)-backup-([a-z0-9]+(?:\-[A-F0-9]{40})?)\.(key|asc)$/g)) ||
299+
(/\.(asc|key)$/.test(this.name) &&
300+
this.hasData() &&
301+
Buf.with(this.getData().subarray(0, 100)).toUtfStr().includes('-----BEGIN PGP PRIVATE KEY BLOCK-----'))
302+
);
303+
};
299304
}

extension/js/content_scripts/webmail/gmail/gmail-element-replacer.ts

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class GmailElementReplacer extends WebmailElementReplacer {
5959
settingsBtnContainer: 'div.aeH > div > .fY',
6060
standardComposeRecipient: 'div.az9 span[email][data-hovercard-id]',
6161
numberOfAttachments: '.aVW',
62-
numberOfAttachmentsDigit: '.aVW span:first-child',
62+
numberOfAttachmentsLabel: '.aVW span:first-child',
6363
attachmentsButtons: '.aZi',
6464
draftsList: '.ae4',
6565
};
@@ -475,16 +475,6 @@ export class GmailElementReplacer extends WebmailElementReplacer {
475475
}
476476
};
477477

478-
private isAttachmentHideable = (attachment: Attachment, renderStatus: string) => {
479-
return (
480-
renderStatus === 'hidden' ||
481-
attachment.type === 'application/pgp-keys' ||
482-
attachment.isPublicKey() ||
483-
attachment.inline ||
484-
Attachment.encryptedMsgNames.some(filename => attachment.name.includes(filename))
485-
);
486-
};
487-
488478
private processAttachments = async (
489479
msgId: string,
490480
attachments: Attachment[],
@@ -519,7 +509,7 @@ export class GmailElementReplacer extends WebmailElementReplacer {
519509
messageInfo,
520510
skipGoogleDrive
521511
);
522-
if (this.isAttachmentHideable(a, renderStatus)) {
512+
if (renderStatus === 'hidden' || a.shouldBeHidden()) {
523513
nRenderedAttachments--;
524514
}
525515
}
@@ -528,22 +518,14 @@ export class GmailElementReplacer extends WebmailElementReplacer {
528518
$(this.sel.attachmentsButtons).hide();
529519
}
530520
if (nRenderedAttachments === 0) {
521+
attachmentsContainerInner.parents(this.sel.attachmentsContainerOuter).first().hide();
531522
$(this.sel.attachmentsContainerOuter).children('.hp').hide();
532523
if ($('.pgp_block').length === 0) {
533524
attachmentsContainerInner.parents(this.sel.attachmentsContainerOuter).first().hide();
534525
}
535526
} else {
536-
const elementsToClone = ['span.a2H', 'div.a2b'];
537-
const scannedByGmailLabel = $(elementsToClone[0]).first().clone();
538-
const scannedByGmailPopover = $(elementsToClone[1]).first().clone(true);
539-
// for uniformity reasons especially when Google used "One" for single attachment and numeric representation for multiple.
540-
const gmailAttachmentLabelReplacement = $(
541-
`<span>${nRenderedAttachments}</span>&nbsp;<span>${nRenderedAttachments > 1 ? 'Attachments' : 'Attachment'}</span>`
542-
);
543-
attachmentsContainerInner.parent().find(this.sel.numberOfAttachments).empty();
544-
gmailAttachmentLabelReplacement.appendTo(attachmentsContainerInner.parent().find(this.sel.numberOfAttachments));
545-
scannedByGmailLabel.appendTo(attachmentsContainerInner.parent().find(this.sel.numberOfAttachments));
546-
scannedByGmailPopover.appendTo(attachmentsContainerInner.parent().find(this.sel.numberOfAttachments));
527+
const attachmentsLabel = nRenderedAttachments > 1 ? `${nRenderedAttachments} Attachments` : 'One Attachment';
528+
attachmentsContainerInner.parent().find(this.sel.numberOfAttachmentsLabel).text(attachmentsLabel);
547529
attachmentsContainerInner.parent().find(this.sel.numberOfAttachments).show();
548530
}
549531
if (!skipGoogleDrive) {

0 commit comments

Comments
 (0)