Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
02a680a
update emailjs-mime-codec.js
sosnovsky Aug 3, 2023
a202c8f
Merge branch 'master' into 5310-attachment-emoji
sosnovsky Aug 8, 2023
6544030
Merge branch 'master' into 5310-attachment-emoji
sosnovsky Aug 21, 2023
c192157
Merge branch 'master' into 5310-attachment-emoji
sosnovsky Aug 21, 2023
05d8b4d
Merge branch 'master' into 5310-attachment-emoji
sosnovsky Aug 22, 2023
4ac45a6
replace emoji images with text emojis
sosnovsky Aug 22, 2023
2a78117
add test for sending attachment with emoji
sosnovsky Aug 22, 2023
9c338ba
wip
sosnovsky Aug 23, 2023
7d82aa3
wip
sosnovsky Aug 23, 2023
79f4479
add live test
sosnovsky Aug 24, 2023
bd480c2
Merge branch 'master' into 5310-attachment-emoji-live-test
sosnovsky Aug 24, 2023
754c0b1
wip
sosnovsky Aug 24, 2023
a9ce2a9
wip
sosnovsky Aug 24, 2023
d01ae3a
wip
sosnovsky Aug 25, 2023
2961fc9
wip
sosnovsky Aug 25, 2023
d5c1f95
Merge branch 'master' into 5310-attachment-emoji-live-test
sosnovsky Aug 28, 2023
8b5cb78
wip
sosnovsky Aug 28, 2023
210ab78
wip
sosnovsky Aug 29, 2023
12cec88
Merge branch 'master' into 5310-attachment-emoji-live-test
sosnovsky Aug 29, 2023
405b31b
puppeteer 21.0.3
sosnovsky Aug 30, 2023
ee729f8
wip
sosnovsky Aug 30, 2023
c04a464
wip
sosnovsky Aug 30, 2023
0b4bb54
wip
sosnovsky Aug 31, 2023
4264d35
wip
sosnovsky Aug 31, 2023
3051ca5
wip
sosnovsky Aug 31, 2023
fbae72f
use updated methods from libmime
sosnovsky Sep 1, 2023
f3d2ceb
fix
sosnovsky Sep 1, 2023
9f5d682
update tests
sosnovsky Sep 1, 2023
9a6823e
Merge branch 'master' into 5310-attachment-emoji-live-test
sosnovsky Sep 4, 2023
37f3ab0
Merge branch 'master' into 5310-attachment-emoji-live-test
sosnovsky Sep 7, 2023
b1e4285
fix
sosnovsky Sep 7, 2023
0db40ae
Merge branch 'master' into 5310-attachment-emoji-live-test
sosnovsky Sep 8, 2023
7b86beb
wip
sosnovsky Sep 8, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,11 @@ export class GmailElementReplacer implements WebmailElementReplacer {
.filter('span.aZo:visible, span.a5r:visible')
.find('span.aV3')
.filter(function () {
const name = $(this).text().trim();
// replace emoji images with text emojis
const emojiRegex = /<img data-emoji="([^\"]+)"[^>]*>/g;
const name = $(this)
.html()
.replace(emojiRegex, (_, emoji) => emoji as string);
return regExp.test(name);
})
.closest('span.aZo, span.a5r');
Expand Down
209 changes: 197 additions & 12 deletions extension/lib/emailjs/emailjs-mime-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -539,28 +539,213 @@

/**
* Joins parsed header value together as 'value; param1=value1; param2=value2'
*
* PS: We are following RFC 822 for the list of special characters that we need to keep in quotes.
* Refer: https://www.w3.org/Protocols/rfc1341/4_Content-Type.html
* @param {Object} structured Parsed header value
* @return {String} joined header value
*/
MimeNode.prototype._buildHeaderValue = function (structured) {
var paramsArray = [];
// copied from https://github.com/nodemailer/libmime/
MimeNode.prototype._buildHeaderValue = function(structured) {
let paramsArray = [];

Object.keys(structured.params || {}).forEach(function (param) {
Object.keys(structured.params || {}).forEach(param => {
// filename might include unicode characters so it is a special case
if (param === 'filename') {
mimecodec.continuationEncode(param, structured.params[param], 50).forEach(function (encodedParam) {
// continuation encoded strings are always escaped, so no need to use enclosing quotes
// in fact using quotes might end up with invalid filenames in some clients
paramsArray.push(encodedParam.key + '=' + encodedParam.value);
let value = structured.params[param];
if (!this._isPlainText(value) || value.length >= 75) {
this._buildHeaderParam(param, value, 50).forEach(encodedParam => {
if (!/[\s"\\;:/=(),<>@[\]?]|^[-']|'$/.test(encodedParam.value) || encodedParam.key.substr(-1) === '*') {
paramsArray.push(encodedParam.key + '=' + encodedParam.value);
} else {
paramsArray.push(encodedParam.key + '=' + JSON.stringify(encodedParam.value));
}
});
} else if (/[\s'"\\;:/=(),<>@[\]?]|^-/.test(value)) {
paramsArray.push(param + '=' + JSON.stringify(value));
} else {
paramsArray.push(param + '=' + this._escapeHeaderArgument(structured.params[param]));
paramsArray.push(param + '=' + value);
}
}.bind(this));
});

return structured.value + (paramsArray.length ? '; ' + paramsArray.join('; ') : '');
};
}

/**
* Encodes a string or an Buffer to an UTF-8 Parameter Value Continuation encoding (rfc2231)
* Useful for splitting long parameter values.
*
* For example
* title="unicode string"
* becomes
* title*0*=utf-8''unicode
* title*1*=%20string
*
* @param {String|Buffer} data String to be encoded
* @param {Number} [maxLength=50] Max length for generated chunks
* @param {String} [fromCharset='UTF-8'] Source sharacter set
* @return {Array} A list of encoded keys and headers
*/
// copied from https://github.com/nodemailer/libmime/
MimeNode.prototype._buildHeaderParam = function(key, data, maxLength, fromCharset) {
let list = [];
let encodedStr = typeof data === 'string' ? data : mimecodec.decode(data, fromCharset);
let encodedStrArr;
let chr, ord;
let line;
let startPos = 0;
let isEncoded = false;
let i, len;

maxLength = maxLength || 50;

// process ascii only text
if (this._isPlainText(data)) {
// check if conversion is even needed
if (encodedStr.length <= maxLength) {
return [
{
key,
value: encodedStr
}
];
}

encodedStr = encodedStr.replace(new RegExp('.{' + maxLength + '}', 'g'), str => {
list.push({
line: str
});
return '';
});

if (encodedStr) {
list.push({
line: encodedStr
});
}
} else {
if (/[\uD800-\uDBFF]/.test(encodedStr)) {
// string containts surrogate pairs, so normalize it to an array of bytes
encodedStrArr = [];
for (i = 0, len = encodedStr.length; i < len; i++) {
chr = encodedStr.charAt(i);
ord = chr.charCodeAt(0);
if (ord >= 0xd800 && ord <= 0xdbff && i < len - 1) {
chr += encodedStr.charAt(i + 1);
encodedStrArr.push(chr);
i++;
} else {
encodedStrArr.push(chr);
}
}
encodedStr = encodedStrArr;
}

// first line includes the charset and language info and needs to be encoded
// even if it does not contain any unicode characters
line = "utf-8''";
isEncoded = true;
startPos = 0;

// process text with unicode or special chars
for (i = 0, len = encodedStr.length; i < len; i++) {
chr = encodedStr[i];

if (isEncoded) {
chr = this._safeEncodeURIComponent(chr);
} else {
// try to urlencode current char
chr = chr === ' ' ? chr : this._safeEncodeURIComponent(chr);
// By default it is not required to encode a line, the need
// only appears when the string contains unicode or special chars
// in this case we start processing the line over and encode all chars
if (chr !== encodedStr[i]) {
// Check if it is even possible to add the encoded char to the line
// If not, there is no reason to use this line, just push it to the list
// and start a new line with the char that needs encoding
if ((this._safeEncodeURIComponent(line) + chr).length >= maxLength) {
list.push({
line,
encoded: isEncoded
});
line = '';
startPos = i - 1;
} else {
isEncoded = true;
i = startPos;
line = '';
continue;
}
}
}

// if the line is already too long, push it to the list and start a new one
if ((line + chr).length >= maxLength) {
list.push({
line,
encoded: isEncoded
});
line = chr = encodedStr[i] === ' ' ? ' ' : this._safeEncodeURIComponent(encodedStr[i]);
if (chr === encodedStr[i]) {
isEncoded = false;
startPos = i - 1;
} else {
isEncoded = true;
}
} else {
line += chr;
}
}

if (line) {
list.push({
line,
encoded: isEncoded
});
}
}

return list.map((item, i) => ({
// encoded lines: {name}*{part}*
// unencoded lines: {name}*{part}
// if any line needs to be encoded then the first line (part==0) is always encoded
key: key + '*' + i + (item.encoded ? '*' : ''),
value: item.line
}));
}

// copied from https://github.com/nodemailer/libmime/
MimeNode.prototype._safeEncodeURIComponent = function(str) {
str = (str || '').toString();

try {
// might throw if we try to encode invalid sequences, eg. partial emoji
str = encodeURIComponent(str);
} catch (E) {
// should never run
return str.replace(/[^\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]+/g, '');
}

// ensure chars that are not handled by encodeURICompent are converted as well
return str.replace(/[\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]/g, chr => this._encodeURICharComponent(chr));
}

MimeNode.prototype._encodeURICharComponent = function(chr) {
let res = '';
let ord = chr.charCodeAt(0).toString(16).toUpperCase();

if (ord.length % 2) {
ord = '0' + ord;
}

if (ord.length > 2) {
for (let i = 0, len = ord.length / 2; i < len; i++) {
res += '%' + ord.substr(i, 2);
}
} else {
res += '%' + ord;
}

return res;
}

/**
* Escapes a header argument value (eg. boundary value for content type),
Expand Down
30 changes: 20 additions & 10 deletions extension/lib/emailjs/emailjs-mime-codec.js
Original file line number Diff line number Diff line change
Expand Up @@ -507,15 +507,15 @@
*/
parseHeaderValue: function(str) {
var response = {
value: false,
params: {}
},
key = false,
value = '',
type = 'value',
quote = false,
escaped = false,
chr;
value: false,
params: {}
},
key = false,
value = '',
type = 'value',
quote = false,
escaped = false,
chr;

for (var i = 0, len = str.length; i < len; i++) {
chr = str.charAt(i);
Expand Down Expand Up @@ -697,12 +697,22 @@
}

} else {

// first line includes the charset and language info and needs to be encoded
// even if it does not contain any unicode characters
line = 'utf-8\'\'';
isEncoded = true;
startPos = 0;

// fix for attachments with emoji in filenames
if (typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function') {
// Intl.Segmenter() currently not available on Firefox
// https://caniuse.com/mdn-javascript_builtins_intl_segmenter
encodedStr = [...new Intl.Segmenter().segment(encodedStr)].map(x => x.segment)
} else {
// regex from https://stackoverflow.com/a/69661174/3091318
encodedStr = encodedStr.replace(/(?![*#0-9]+)[\p{Emoji}\p{Emoji_Modifier}\p{Emoji_Component}\p{Emoji_Modifier_Base}\p{Emoji_Presentation}]/gu, '')
}
Comment on lines +711 to +714
Copy link
Collaborator

@martgil martgil Aug 26, 2023

Choose a reason for hiding this comment

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

This is nice - I tried to find a reference for regex in Kotlin but I do not see any relevant documentation/reference material that Kotlin supports \p{Emoji_Presentation} or similar, unlike javascript which has rich support with it. So with that, we might end up using emoji-unicode ranges which again may contain an incomplete range of emojis.

@sosnovsky Do you allow me to fix both #5308 and #5309 by reusing the regex on line 713?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, I tried to use the same regex on FES for allowing emojis, but unfortunately Kotlin doesn't support these \p keywords.

Let me finish this PR, then I'll check possible solution for #5308 and #5309 on FES side, and if there won't be a simple way to allow emojis and other symbols in recipient names - I'll notify you that you can work on stripping emojis from recipient names on browser extension side.


// process text with unicode or special chars
for (var i = 0, len = encodedStr.length; i < len; i++) {

Expand Down
File renamed without changes.
23 changes: 20 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/samples/attac👍hment!🔸.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
some text
2 changes: 2 additions & 0 deletions test/source/mock/google/strategies/send-message-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,8 @@ export class TestBySubjectStrategyContext {
this.strategy = new SaveMessageInStorageStrategy();
} else if (subject.includes('FlowCrypt OpenPGP Private Key backup')) {
this.strategy = new SaveMessageInStorageStrategy();
} else if (subject.includes('Test Sending Message With Attachment Which Contains Emoji in Filename')) {
this.strategy = new SaveMessageInStorageStrategy();
} else if (subject.includes('Re: FROM: flowcrypt.compatibility@gmail.com, TO: flowcrypt.compatibility@gmail.com + vladimir@flowcrypt.com')) {
this.strategy = new NoopTestStrategy();
} else {
Expand Down
Loading