diff --git a/README.md b/README.md
index 6d685df62..590b21e06 100644
--- a/README.md
+++ b/README.md
@@ -75,23 +75,23 @@ Things might work in Edge 15-18, Firefox 47-62 and Chrome 54-68 due to one polyf
|js/* |1.14.0 |native |gzip 1.14 |gzip |
|----------- |--------: |--------: |--------: |--------: |
-|admin.js |2.130.942 |1.172.413 | 485.481 | 291.715 |
-|app.js |4.184.455 |2.888.668 | 932.725 | 682.438 |
-|boot.js | 671.522 | 44.394 | 169.502 | 15.460 |
+|admin.js |2.130.942 |1.133.275 | 485.481 | 274.632 |
+|app.js |4.184.455 |2.844.189 | 932.725 | 664.075 |
+|boot.js | 671.522 | 44.029 | 169.502 | 15.460 |
|libs.js | 647.614 | 431.049 | 194.728 | 132.315 |
|polyfills.js | 325.834 | 0 | 71.825 | 0 |
-|TOTAL js |7.960.367 |4.537.200 |1.854.261 |1.121.928 |
+|TOTAL js |7.960.367 |4.452.542 |1.854.261 |1.086.482 |
|js/min/* |1.14.0 |native |gzip 1.14 |gzip |
|--------------- |--------: |--------: |--------: |--------: |
-|admin.min.js | 252.147 | 153.689 | 73.657 | 44.000 |
-|app.min.js | 511.202 | 380.294 |140.462 |100.236 |
+|admin.min.js | 252.147 | 153.095 | 73.657 | 43.734 |
+|app.min.js | 511.202 | 378.695 |140.462 | 99.511 |
|boot.min.js | 66.007 | 5.589 | 22.567 | 2.333 |
|libs.min.js | 572.545 | 387.636 |176.720 |122.519 |
|polyfills.min.js | 32.452 | 0 | 11.312 | 0 |
-|TOTAL js/min |1.434.353 | 927.208 |424.718 |269.088 |
+|TOTAL js/min |1.434.353 | 925.015 |424.718 |268.097 |
-507.145 bytes (155.630 gzip) is not much, but it feels faster.
+509.338 bytes (156.621 gzip) is not much, but it feels faster.
|css/* |1.14.0 |native |
diff --git a/dev/Common/ClientStorageDriver/Cookie.js b/dev/Common/ClientStorageDriver/Cookie.js
index f14be4dc7..a90702c33 100644
--- a/dev/Common/ClientStorageDriver/Cookie.js
+++ b/dev/Common/ClientStorageDriver/Cookie.js
@@ -1,6 +1,114 @@
-import Cookies from 'js-cookie';
import { CLIENT_SIDE_STORAGE_INDEX_NAME } from 'Common/Consts';
+var Cookies = (() => {
+ function extend (...args) {
+ var result = {};
+ args.forEach(attributes => {
+ for (var key in attributes) {
+ result[key] = attributes[key];
+ }
+ });
+ return result;
+ }
+
+ function decode (s) {
+ return s.replace(/(%[0-9A-Z]{2})+/g, decodeURIComponent);
+ }
+
+ function set (key, value, attributes) {
+ if (typeof document === 'undefined') {
+ return;
+ }
+
+ attributes = extend({
+ path: '/'
+ }, attributes);
+
+ if (typeof attributes.expires === 'number') {
+ attributes.expires = new Date(new Date() * 1 + attributes.expires * 864e+5);
+ }
+
+ // We're using "expires" because "max-age" is not supported by IE
+ attributes.expires = attributes.expires ? attributes.expires.toUTCString() : '';
+
+ try {
+ var result = JSON.stringify(value);
+ if (/^[{[]/.test(result)) {
+ value = result;
+ }
+ } catch (e) {} // eslint-disable-line no-empty
+
+ value = encodeURIComponent(String(value))
+ .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent);
+
+ key = encodeURIComponent(String(key))
+ .replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent)
+ .replace(/[()]/g, escape);
+
+ var stringifiedAttributes = '';
+ for (var attributeName in attributes) {
+ if (!attributes[attributeName]) {
+ continue;
+ }
+ stringifiedAttributes += '; ' + attributeName;
+ if (attributes[attributeName] === true) {
+ continue;
+ }
+
+ // Considers RFC 6265 section 5.2:
+ // ...
+ // 3. If the remaining unparsed-attributes contains a %x3B (";")
+ // character:
+ // Consume the characters of the unparsed-attributes up to,
+ // not including, the first %x3B (";") character.
+ // ...
+ stringifiedAttributes += '=' + attributes[attributeName].split(';')[0];
+ }
+
+ return (document.cookie = key + '=' + value + stringifiedAttributes);
+ }
+
+ function get (key) {
+ if (typeof document === 'undefined') {
+ return;
+ }
+
+ var jar = {};
+ // To prevent the for loop in the first place assign an empty array
+ // in case there are no cookies at all.
+ var cookies = document.cookie ? document.cookie.split('; ') : [];
+ var i = 0;
+
+ for (; i < cookies.length; i++) {
+ var parts = cookies[i].split('=');
+ var cookie = parts.slice(1).join('=');
+
+ try {
+ var name = decode(parts[0]);
+ cookie = decode(cookie);
+
+ try {
+ cookie = JSON.parse(cookie);
+ } catch (e) {} // eslint-disable-line no-empty
+
+ jar[name] = cookie;
+
+ if (key === name) {
+ break;
+ }
+ } catch (e) {} // eslint-disable-line no-empty
+ }
+
+ return key ? jar[key] : jar;
+ }
+
+ return {
+ set: set,
+ getJSON: key => get(key)
+ };
+})();
+
+
class CookieDriver {
/**
* @param {string} key
diff --git a/dev/Model/Email.js b/dev/Model/Email.js
index a0a0dedc8..147b29cc1 100644
--- a/dev/Model/Email.js
+++ b/dev/Model/Email.js
@@ -1,6 +1,267 @@
-import addressparser from 'emailjs-addressparser';
import { encodeHtml, isNonEmptyArray } from 'Common/Utils';
+'use strict';
+
+/**
+ * Parses structured e-mail addresses from an address field
+ *
+ * Example:
+ *
+ * "Name
"
+ *
+ * will be converted to
+ *
+ * [{name: "Name", address: "address@domain"}]
+ *
+ * @param {String} str Address field
+ * @return {Array} An array of address objects
+ */
+function addressparser(str) {
+ var tokenizer = new Tokenizer(str);
+ var tokens = tokenizer.tokenize();
+ var addresses = [];
+ var address = [];
+ var parsedAddresses = [];
+
+ tokens.forEach(token => {
+ if (token.type === 'operator' && (token.value === ',' || token.value === ';')) {
+ if (address.length) {
+ addresses.push(address);
+ }
+ address = [];
+ } else {
+ address.push(token);
+ }
+ });
+
+ if (address.length) {
+ addresses.push(address);
+ }
+
+ addresses.forEach(address => {
+ address = _handleAddress(address);
+ if (address.length) {
+ parsedAddresses = parsedAddresses.concat(address);
+ }
+ });
+
+ return parsedAddresses;
+}
+
+/**
+ * Converts tokens for a single address into an address object
+ *
+ * @param {Array} tokens Tokens object
+ * @return {Object} Address object
+ */
+function _handleAddress(tokens) {
+ var isGroup = false;
+ var state = 'text';
+ var address = void 0;
+ var addresses = [];
+ var data = {
+ address: [],
+ comment: [],
+ group: [],
+ text: []
+
+ // Filter out , (comments) and regular text
+ };
+ for (var i = 0, len = tokens.length; i < len; i++) {
+ var token = tokens[i];
+
+ if (token.type === 'operator') {
+ switch (token.value) {
+ case '<':
+ state = 'address';
+ break;
+ case '(':
+ state = 'comment';
+ break;
+ case ':':
+ state = 'group';
+ isGroup = true;
+ break;
+ default:
+ state = 'text';
+ }
+ } else {
+ if (token.value) {
+ data[state].push(token.value);
+ }
+ }
+ }
+
+ // If there is no text but a comment, replace the two
+ if (!data.text.length && data.comment.length) {
+ data.text = data.comment;
+ data.comment = [];
+ }
+
+ if (isGroup) {
+ // http://tools.ietf.org/html/rfc2822#appendix-A.1.3
+ data.text = data.text.join(' ');
+ addresses.push({
+ name: data.text || address && address.name,
+ group: data.group.length ? addressparser(data.group.join(',')) : []
+ });
+ } else {
+ // If no address was found, try to detect one from regular text
+ if (!data.address.length && data.text.length) {
+ for (var _i = data.text.length - 1; _i >= 0; _i--) {
+ if (data.text[_i].match(/^[^@\s]+@[^@\s]+$/)) {
+ data.address = data.text.splice(_i, 1);
+ break;
+ }
+ }
+
+ var _regexHandler = function _regexHandler(address) {
+ if (!data.address.length) {
+ data.address = [address.trim()];
+ return ' ';
+ }
+ return address;
+ };
+
+ // still no address
+ if (!data.address.length) {
+ for (var _i2 = data.text.length - 1; _i2 >= 0; _i2--) {
+ data.text[_i2] = data.text[_i2].replace(/\s*\b[^@\s]+@[^@\s]+\b\s*/, _regexHandler).trim();
+ if (data.address.length) {
+ break;
+ }
+ }
+ }
+ }
+
+ // If there's still is no text but a comment exixts, replace the two
+ if (!data.text.length && data.comment.length) {
+ data.text = data.comment;
+ data.comment = [];
+ }
+
+ // Keep only the first address occurence, push others to regular text
+ if (data.address.length > 1) {
+ data.text = data.text.concat(data.address.splice(1));
+ }
+
+ // Join values with spaces
+ data.text = data.text.join(' ');
+ data.address = data.address.join(' ');
+
+ if (!data.address && isGroup) {
+ return [];
+ }
+ address = {
+ address: data.address || data.text || '',
+ name: data.text || data.address || ''
+ };
+
+ if (address.address === address.name) {
+ if ((address.address || '').match(/@/)) {
+ address.name = '';
+ } else {
+ address.address = '';
+ }
+ }
+
+ addresses.push(address);
+ }
+
+ return addresses;
+}
+
+/*
+ * Operator tokens and which tokens are expected to end the sequence
+ */
+var OPERATORS = {
+ '"': '"',
+ '(': ')',
+ '<': '>',
+ ',': '',
+ // Groups are ended by semicolons
+ ':': ';',
+ // Semicolons are not a legal delimiter per the RFC2822 grammar other
+ // than for terminating a group, but they are also not valid for any
+ // other use in this context. Given that some mail clients have
+ // historically allowed the semicolon as a delimiter equivalent to the
+ // comma in their UI, it makes sense to treat them the same as a comma
+ // when used outside of a group.
+ ';': ''
+};
+
+class Tokenizer
+{
+ constructor(str) {
+ this.str = (str || '').toString();
+ this.operatorCurrent = '';
+ this.operatorExpecting = '';
+ this.node = null;
+ this.escaped = false;
+ this.list = [];
+ }
+
+ tokenize() {
+ var list = [], i = this.str.length;
+ while (i--) this.checkChar(this.str[i]);
+
+ this.list.forEach(node => {
+ node.value = (node.value || '').toString().trim();
+ if (node.value) {
+ list.push(node);
+ }
+ });
+
+ return list;
+ }
+
+ checkChar(chr) {
+ if ((chr in OPERATORS || chr === '\\') && this.escaped) {
+ this.escaped = false;
+ } else if (this.operatorExpecting && chr === this.operatorExpecting) {
+ this.node = {
+ type: 'operator',
+ value: chr
+ };
+ this.list.push(this.node);
+ this.node = null;
+ this.operatorExpecting = '';
+ this.escaped = false;
+ return;
+ } else if (!this.operatorExpecting && chr in OPERATORS) {
+ this.node = {
+ type: 'operator',
+ value: chr
+ };
+ this.list.push(this.node);
+ this.node = null;
+ this.operatorExpecting = OPERATORS[chr];
+ this.escaped = false;
+ return;
+ }
+
+ if (!this.escaped && chr === '\\') {
+ this.escaped = true;
+ return;
+ }
+
+ if (!this.node) {
+ this.node = {
+ type: 'text',
+ value: ''
+ };
+ this.list.push(this.node);
+ }
+
+ if (this.escaped && chr !== '\\') {
+ this.node.value += '\\';
+ }
+
+ this.node.value += chr;
+ this.escaped = false;
+ }
+}
+
class EmailModel {
email = '';
name = '';
diff --git a/dev/Model/Message.js b/dev/Model/Message.js
index 68217189e..b61fedcf0 100644
--- a/dev/Model/Message.js
+++ b/dev/Model/Message.js
@@ -1,5 +1,4 @@
import ko from 'ko';
-import classnames from 'classnames';
import { MessagePriority, SignedVerifyStatus } from 'Common/Enums';
import { i18n } from 'Common/Translator';
@@ -456,7 +455,8 @@ class MessageModel extends AbstractModel {
* @return string
*/
lineAsCss() {
- return classnames({
+ let classes = [];
+ Object.entries({
'deleted': this.deleted(),
'deleted-mark': this.deletedMark(),
'selected': this.selected(),
@@ -473,7 +473,8 @@ class MessageModel extends AbstractModel {
// 'hasChildrenMessage': 1 < this.threadsLen(),
'hasUnseenSubMessage': this.hasUnseenSubMessage(),
'hasFlaggedSubMessage': this.hasFlaggedSubMessage()
- });
+ }).forEach(([key, value]) => value && classes.push(key));
+ return classes.join(' ');
}
/**
diff --git a/package.json b/package.json
index 8d29446aa..e4d48e4ad 100644
--- a/package.json
+++ b/package.json
@@ -55,10 +55,8 @@
"autolinker": "3.13.0",
"babel-eslint": "10.1.0",
"babel-loader": "8.0.6",
- "classnames": "2.2.6",
"copy-webpack-plugin": "5.1.1",
"css-loader": "3.4.2",
- "emailjs-addressparser": "2.0.2",
"eslint": "6.8.0",
"eslint-config-prettier": "6.10.0",
"eslint-plugin-prettier": "3.1.2",
@@ -89,7 +87,6 @@
"gulp-zip": "5.0.1",
"ifvisible.js": "1.0.6",
"jquery": "^3.5.1",
- "js-cookie": "2.2.1",
"knockout": "3.4.2",
"knockout-sortable": "1.2.0",
"moment": "2.24.0",
diff --git a/yarn.lock b/yarn.lock
index baa60c0d1..8344847bc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1856,10 +1856,6 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
-classnames@2.2.6:
- version "2.2.6"
- resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
-
clean-css@4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"