/**
* The MIT License (MIT)
*
* Copyright (c) 2014-2019 Mailvelope GmbH
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* @type {Mailvelope}
*/
class Mailvelope {
/**
* Gives access to the mailvelope extension version
* @returns {Promise.<String, Error>}
*/
getVersion() {
return send('get-version');
}
/**
* Retrieves the Keyring for the given identifier
* @param {String} identifier - the identifier of the keyring, if empty the main keyring is returned
* @returns {Promise.<Keyring, Error>}
* @throws {Error} error.code = 'NO_KEYRING_FOR_ID'
*/
getKeyring(identifier) {
return send('get-keyring', {identifier}).then(options => new Keyring(identifier, options));
}
/**
* Creates a Keyring for the given identifier
* @param {String} identifier - the identifier of the new keyring
* @returns {Promise.<Keyring, Error>}
* @throws {Error} error.code = 'KEYRING_ALREADY_EXISTS'
* @example
* mailvelope.createKeyring('Account-ID-4711').then(function(keyring) {
* // continue to display the settings container and start the setup wizard
* mailvelope.createSettingsContainer('#mailvelope-settings', keyring);
* });
*/
createKeyring(identifier) {
return send('create-keyring', {identifier}).then(options => new Keyring(identifier, options));
}
/**
* Ascii Armored PGP Text Block
* @typedef {String} AsciiArmored
*/
/**
* CSS Selector String
* @typedef {String} CssSelector
*/
/**
* @typedef {Object} DisplayContainerOptions
* @property {String} senderAddress - email address of sender, used to indentify key for signature verification
*/
/**
* @typedef {Object} DisplayContainer
* @property {Error} error - Error object with code and message attribute
* error.code = 'DECRYPT_ERROR' - generic decrypt error
* error.code = 'ARMOR_PARSE_ERROR' - error while parsing the armored message
* error.code = 'PWD_DIALOG_CANCEL' - user canceled password dialog
* error.code = 'NO_KEY_FOUND' - no private key found to decrypt this message
*/
/**
* Creates an iframe to display the decrypted content of the encrypted mail.
* The iframe will be injected into the container identified by selector.
* @param {CssSelector} selector - target container
* @param {AsciiArmored} armored - the encrypted mail to display
* @param {Keyring} [keyring] - the keyring to use for this operation
* @param {DisplayContainerOptions} options
* @returns {Promise.<DisplayContainer, Error>}
*/
createDisplayContainer(selector, armored, keyring, options) {
try {
checkTypeKeyring(keyring);
} catch (e) {
return Promise.reject(e);
}
return send('display-container', {selector, armored, identifier: keyring && keyring.identifier, options}).then(display => {
if (display && display.error) {
display.error = mapError(display.error);
}
return display;
});
}
/**
* @typedef {Object} EditorContainerOptions
* @property {number} quota - mail content (text + attachments) limit in kilobytes (default: 20480)
* @property {boolean} signMsg - if true then the mail will be signed (default: false)
* @property {AsciiArmored} armoredDraft - a PGP message, signed and encrypted with the default key of the user, will be used to restore a draft in the editor
* The armoredDraft parameter can't be combined with the parameters: predefinedText, quotedMail... parameters, keepAttachments
* @property {String} predefinedText - text that will be added to the editor
* @property {AsciiArmored} quotedMail - mail that should be quoted
* @property {boolean} quotedMailIndent - if true the quoted mail will be indented (default: true)
* @property {String} quotedMailHeader - header to be added before the quoted mail
* @property {boolean} keepAttachments - add attachments of quotedMail to editor (default: false)
*/
/**
* Creates an iframe with an editor for a new encrypted mail.
* The iframe will be injected into the container identified by selector.
* @param {CssSelector} selector - target container
* @param {Keyring} [keyring] - the keyring to use for this operation
* @param {EditorContainerOptions} options
* @returns {Promise.<Editor, Error>}
* @throws {Error} error.code = 'WRONG_ARMORED_TYPE' - parameters of type AsciiArmored do not have the correct armor type <br>
error.code = 'INVALID_OPTIONS' - invalid combination of options parameter
* @example
* mailvelope.createEditorContainer('#editor-element', keyring).then(function(editor) {
* // register event handler for mail client send button
* $('#mailer-send').click(function() {
* // encrypt current content of editor for array of recipients
* editor.encrypt(['info@mailvelope.com', 'abc@web.de']).then(function(armored) {
* console.log('encrypted message', armored);
* });
* });
* });
*/
createEditorContainer(selector, keyring, options) {
try {
checkTypeKeyring(keyring);
} catch (e) {
return Promise.reject(e);
}
return send('editor-container', {selector, identifier: keyring && keyring.identifier, options}).then(editorId => new Editor(editorId));
}
/**
* @typedef {Object} SettingsContainerOptions
* @property {String} email - the email address of the current user
* @property {String} fullName - the full name of the current user
*/
/**
* Creates an iframe to display the keyring settings.
* The iframe will be injected into the container identified by selector.
* @param {CssSelector} selector - target container
* @param {Keyring} [keyring] - the keyring to use for the setup
* @param {SettingsContainerOptions} options
* @returns {Promise.<undefined, Error>}
*/
createSettingsContainer(selector, keyring, options) {
try {
checkTypeKeyring(keyring);
} catch (e) {
return Promise.reject(e);
}
return send('settings-container', {selector, identifier: keyring && keyring.identifier, options});
}
/**
* Creates an iframe to display an encrypted form
* The iframe will be injected into the container identified by selector.
* @param @param {String} selector - the id of target container
* @param @param {String} formHtml - the form definition
* @param @param {String} signature - the OpenPGP signature
* @returns {Promise.<Object, Error>} an object that includes armoredData
* @throws {Error} error.code = 'INVALID_FORM' the form definition is not valid
*/
createEncryptedFormContainer(selector, formHtml, signature) {
return send('encrypted-form-container', {selector, formHtml, signature});
}
}
// connection to content script is alive
let connected = true;
let syncHandler = null;
function checkTypeKeyring(keyring) {
if (keyring && !(keyring instanceof Keyring)) {
const error = new Error('Type mismatch: keyring should be instance of Keyring.');
error.code = 'TYPE_MISMATCH';
throw error;
}
}
/**
* Not accessible, instance can be obtained using {@link Mailvelope#getKeyring}
* or {@link Mailvelope#createKeyring}.
* @param {String} identifier - the keyring identifier
* @param {object} options - the options
* @property {number} logoRev - revision number of the keyring logo, initial value: 0
*/
class Keyring {
constructor(identifier, options) {
this.identifier = identifier;
this.logoRev = options.revision || 0;
}
/**
* @typedef {Object} LookupResult
* @property {String} fingerprint - Fingerprint of the key
* @property {Date} lastModified - last time the key was modified
* @property {String} source - Source the key was found at <br>
* Currently available: <br>
* * 'LOC' - local key ring <br>
* * 'WKD' - web key directory <br>
* * 'MKS' - mailvelope key server <br>
* * 'AC' - autocrypt
* @property {Date} [lastSeen] - last time the key was seen <br>
* (not set for local keys)
* @property {String} [armored] - Armored key that can be imported <br>
* (not set for local keys)
*/
/**
* Checks for valid key in the keyring for provided email addresses
* If none is found also checks in other sources (see LookupResult).
* @param {Array} recipients - list of email addresses for key lookup
* @returns {Promise.<Object, Error>} The object maps email addresses to: <br>
* false: no valid key <br>
* {keys: [LookupResult]}: valid keys
*
* @example
* keyring.validKeyForAddress(['abc@web.de', 'info@mailvelope.com']).then(function(result) {
* console.log(result);
* // {
* // 'abc@web.de': false,
* // 'info@mailvelope.com': {
* // keys: [
* // {
* // fingerprint: 'f37377c39898d05ffd39157a98bbec557ce08def',
* // lastModified: Tue May 19 2015 10:36:53 GMT+0200 (CEST),
* // source: 'LOC'
* // }
* // ]
* // }
* // }
* });
*/
validKeyForAddress(recipients) {
return send('query-valid-key', {identifier: this.identifier, recipients}).then(keyMap => {
for (const address in keyMap) {
if (keyMap[address]) {
keyMap[address].keys.forEach(key => {
key.lastModified = new Date(key.lastModified);
});
}
}
return keyMap;
});
}
/**
* Exports the public key as an ascii armored string.
* Only keys belonging to the user (corresponding private key exists) can be exported.
* @param {String} emailAddr - email address to identify the public+private key
* @returns {Promise.<AsciiArmored, Error>}
* @throws {Error} error.code = 'NO_KEY_FOR_ADDRESS'
* @example
* keyring.exportOwnPublicKey('abc@web.de').then(function(armoredPublicKey) {
* console.log('exportOwnPublicKey', armoredPublicKey);
* // prints: "-----BEGIN PGP PUBLIC KEY BLOCK..."
* });
*/
exportOwnPublicKey(emailAddr) {
return send('export-own-pub-key', {identifier: this.identifier, emailAddr});
}
/**
* @typedef {Object} additionalMailHeaders
* @property {String} autocrypt - the Autocrypt header that should be <br>
* added to the outgoing mail<br>
*/
/**
* @typedef {Object} outgoingMailHeaders
* @property {String} from - the From header
*/
/**
* Returns headers that should be added to an outgoing email.
* So far this is only the `autocrypt` header.
* @param {outgoingMailHeaders} headers - headers of the outgoing mail. <br>
* In particular `from` to select the key
* @returns {Promise.<additionalMailHeaders, Error>}
* @throws {Error} error.code = 'NO_KEY_FOR_ADDRESS'
* @example
* keyring.additionalHeadersForOutgoingEmail(from: 'abc@web.de').then(function(additional) {
* console.log('additionalHeadersForOutgoingEmail', additional);
* // logs: {autocrypt: "addr=abc@web.de; prefer-encrypt=mutual; keydata=..."}
* });
*/
additionalHeadersForOutgoingEmail(headers) {
return send('additional-headers-for-outgoing', {identifier: this.identifier, headers});
}
/**
* Asks the user if they want to import the public key.
* @param {AsciiArmored} armored - public key to import
* @returns {Promise.<String, Error>} 'IMPORTED' - key has been imported <br>
'UPDATED' - key already in keyring, new key merged with existing key <br>
'INVALIDATED' - key has been updated, new status of key is 'invalid' (e.g. revoked) <br>
'REJECTED' - key import rejected by user
* @throws {Error} error.code = 'IMPORT_ERROR' <br>
error.code = 'WRONG_ARMORED_TYPE'
*/
importPublicKey(armored) {
return send('import-pub-key', {identifier: this.identifier, armored});
}
/**
* @typedef {Object} AutocryptMailHeaders
* @property {String} autocrypt - the Autocrypt header to process
* @property {String} from - the From header
* @property {String} date - the Date header
*/
/**
* Process Autocrypt header from message being read.
* @param {AutocryptMailHeaders} headers - the relevant mail headers
* @returns {Promise.<undefined, Error>}
* @throws {Error} error.code = 'INVALID_HEADER' <br>
error.code = 'STORAGE_ERROR'
*/
processAutocryptHeader(headers) {
return send('process-autocrypt-header', {identifier: this.identifier, headers});
}
/**
* Set logo for keyring. The image is persisted in Mailvelope with a revision number,
* therefore the method is only required after new keyring generation or if logo and revision number changes.
* @param {String} dataURL - data-URL representing the logo, max. file size: ~10KB, max. image size: 192x96px, content-type: image/png
* @param {number} revision - revision number
* @returns {Promise.<undefined, Error>}
* @throws {Error} error.code = 'LOGO_INVALID' <br>
* error.code = 'REVISION_INVALID'
* @example
* keyring.setLogo('data:image/png;base64,iVBORS==', 3).then(function() {
* // keyring.logoRev == 3
* }).catch(function(error) {
* // logo update failed
* });
*
*/
setLogo(dataURL, revision) {
return send('set-logo', {identifier: this.identifier, dataURL, revision}).then(() => {
this.logoRev = revision;
});
}
/**
* @typedef {Object} UserId
* @property {String} email - the email address of the current user
* @property {String} fullName - the full name of the current user
*/
/**
* @typedef {Object} KeyGenContainerOptions
* @property {Array.<UserId>} userIds - array of user IDs. The first entry in the array is set as the primary user ID.
* @property {number} keySize - key size in bit, optional, default: 2048, valid values: 2048, 4096.
*/
/**
* Creates an iframe to display the key generation container.
* The iframe will be injected into the container identified by selector.
* @param {CssSelector} selector - target container
* @param {KeyGenContainerOptions} options
* @returns {Promise.<Generator, Error>}
* @throws {Error} error.code = 'INPUT_NOT_VALID'
*/
createKeyGenContainer(selector, options) {
return send('key-gen-container', {selector, identifier: this.identifier, options}).then(generatorId => new Generator(generatorId));
}
/**
* @typedef {Object} KeyBackupContainerOptions
* @param {Boolean} initialSetup (default: true)
*/
/**
* Creates an iframe to initiate the key backup process.
* @param {CssSelector} selector - target container
* @param {KeyBackupContainerOptions} options
* @returns {Promise.<KeyBackupPopup, Error>}
*/
createKeyBackupContainer(selector, options) {
return send('key-backup-container', {selector, identifier: this.identifier, options}).then(popupId => new KeyBackupPopup(popupId));
}
/**
* @typedef {Object} PrivateKeyContainerOptions
* @property {boolean} restorePassword (default: false)
*/
/**
* Creates an iframe to restore the backup.
* @param {CssSelector} selector - target container
* @param {PrivateKeyContainerOptions} options
* @returns {Promise.<undefined, Error>}
*/
restoreBackupContainer(selector, options) {
return send('restore-backup-container', {selector, identifier: this.identifier, options}).then(restoreId => new RestoreBackup(restoreId));
}
/**
* Check if keyring contains valid private key with given fingerprint
* @param {String|{fingerprint: String, email: String}} fingerprint or Object with fingerprint or email property
* @returns {Promise.<boolean, Error>}
*/
hasPrivateKey(query = {}) {
const fingerprint = typeof query === 'string' ? query : query.fingerprint;
const {email} = query;
return send('has-private-key', {identifier: this.identifier, fingerprint, email}).then(result => result);
}
/**
* @typedef {Object} UploadSyncReply
* @property {String} eTag - entity tag for the uploaded encrypted keyring
*/
/**
* @typedef {Function} UploadSyncHandler
* @param {Object} uploadObj - object with upload data
* @param {String} uploadObj.eTag - entity tag for the uploaded encrypted keyring, or null if initial upload
* @param {AsciiArmored} uploadObj.keyringMsg - encrypted keyring as PGP armored message
* @returns {Promise.<UploadSyncReply, Error>} - if version on server has different eTag, then the promise is rejected
* if server is initial and uploadObj.eTag is not null, then the promise is rejected
*/
/**
* @typedef {Object} DownloadSyncReply
* @property {AsciiArmored} keyringMsg - encrypted keyring as PGP armored message, or null if no newer version available
* @property {String} eTag - entity tag for the current encrypted keyring message, or null if server is intial
*/
/**
* @typedef {Function} DownloadSyncHandler
* @param {Object} downloadObj - meta info for download
* @param {String} downloadObj.eTag - entity tag for the current local keyring, or null if no local eTag
* @returns {Promise.<DownloadSyncReply, Error>} - if version on server has same eTag, then keyringMsg property of reply is empty, but eTag in reply has to be set
* if server is initial and downloadObj.eTag is not null, then the promise is resolved with empty eTag
*/
/**
* @typedef {Object} BackupSyncPacket
* @property {AsciiArmored} backup - encrypted key backup as PGP armored message
*/
/**
* @typedef {Function} BackupSyncHandler
* @param {BackupSyncPacket} - object with backup data
* @returns {Promise.<undefined, Error>}
*/
/**
* @typedef {Function} RestoreSyncHandler
* @returns {Promise.<BackupSyncPacket, Error>}
*/
/**
* @typedef {Object} SyncHandlerObject
* @property {UploadSyncHandler} uploadSync - function called by Mailvelope to upload the keyring (public keys), the message is encrypted with the default private key
* @property {DownloadSyncHandler} downloadSync - function called by Mailvelope to download the encrypted keyring (public keys)
* @property {BackupSyncHandler} backup - function called by Mailvelope to upload a symmetrically encrypted private key backup
* @property {RestoreSyncHandler} restore - function called by Mailvelope to restore a private key backup
*/
/**
* Add various functions for keyring synchronization
* @param {SyncHandlerObject} syncHandlerObj
* @returns {Promise.<undefined, Error>}
*/
addSyncHandler(syncHandlerObj) {
if (typeof syncHandlerObj.uploadSync !== typeof syncHandlerObj.downloadSync) {
return Promise.reject(new Error('uploadSync and downloadSync Handler cannot be set exclusively.'));
}
return send('add-sync-handler', {identifier: this.identifier}).then(syncHandlerId => {
if (syncHandler) {
syncHandler.update(syncHandlerObj);
} else {
syncHandler = new SyncHandler(syncHandlerId, syncHandlerObj);
}
});
}
/**
* @typedef {Object} OpenSettingsOptions
* @param {Boolean} showDefaultKey (default: false)
*/
/**
* Open the extension settings in a new browser tab
* @param {OpenSettingsOptions} [options]
* @returns {Promise.<undefined, Error>}
*/
openSettings(options) {
return send('open-settings', {identifier: this.identifier, options});
}
}
/**
* Not accessible, instance can be obtained using {@link Keyring#createKeyBackupContainer}
* @param {String} popupId
*/
class KeyBackupPopup {
constructor(popupId) {
this.popupId = popupId;
}
/**
* @returns {Promise.<undefined, Error>} - key backup ready or error
* @throws {Error}
*/
isReady() {
return send('keybackup-popup-isready', {popupId: this.popupId});
}
}
/**
* Not accessible, instance can be obtained using {@link Keyring#createKeyGenContainer}.
* @param {String} generatorId - the internal id of the generator
*/
class Generator {
constructor(generatorId) {
this.generatorId = generatorId;
}
/**
* Generate a private key
* @param {Promise.<undefined, Error>} [confirm] - newly generate key is only persisted if Promise resolves,
* in the reject or timeout case the generated key is rejected
* @returns {Promise.<AsciiArmored, Error>} - the newly generated key (public part)
* @throws {Error}
*/
generate(confirm) {
return send('generator-generate', {generatorId: this.generatorId, confirmRequired: Boolean(confirm)}).then(armored => {
if (confirm) {
confirm.then(() => {
emit('generator-generate-confirm', {generatorId: this.generatorId});
}).catch(e => {
emit('generator-generate-reject', {generatorId: this.generatorId, error: objError(e)});
});
}
return armored;
});
}
}
/**
* Not accessible, instance can be obtained using {@link Keyring#restoreBackupContainer}.
* @param {String} restoreId - the internal id of the restore backup
*/
class RestoreBackup {
constructor(restoreId) {
this.restoreId = restoreId;
}
/**
* @returns {Promise.<undefined, Error>} - key restore ready or error
* @throws {Error}
*/
isReady() {
return send('restore-backup-isready', {restoreId: this.restoreId});
}
}
/**
* Not accessible, instance can be obtained using {@link Mailvelope#createEditorContainer}.
* @param {String} editorId - the internal id of the editor
*/
class Editor {
constructor(editorId) {
this.editorId = editorId;
}
/**
* Requests the encryption of the editor content for the given recipients.
* @param {Array.<String>} recipients - list of email addresses for public key lookup and encryption
* @returns {Promise.<AsciiArmored, Error>}
* @throws {Error} error.code = 'ENCRYPT_IN_PROGRESS' <br>
* error.code = 'NO_KEY_FOR_RECIPIENT' <br>
* error.code = 'ENCRYPT_QUOTA_SIZE'
* @example
* editor.encrypt(['abc@web.de', 'info@com']).then(function (armoredMessage) {
* console.log('encrypt', armoredMessage); // prints: "-----BEGIN PGP MESSAGE..."
* }
*/
encrypt(recipients) {
return send('editor-encrypt', {recipients, editorId: this.editorId});
}
/**
* Encrypt and sign the content of the editor with the default key of the user.
* To be used to save drafts. To restore drafts use the options.armoredDraft parameter of the createEditorContainer method.
* @returns {Promise.<AsciiArmored, Error>}
* @throws {Error} error.code = 'ENCRYPT_IN_PROGRESS' <br>
* error.code = 'NO_KEY_FOR_ENCRYPTION' <br>
* error.code = 'ENCRYPT_QUOTA_SIZE'
*/
createDraft() {
return send('editor-create-draft', {editorId: this.editorId});
}
}
const callbacks = Object.create(null);
class SyncHandler {
constructor(syncHandlerId, handlers) {
this.syncHandlerId = syncHandlerId;
this.handlers = handlers;
}
update(handlers) {
for (const handle in handlers) {
this.handlers[handle] = handlers[handle];
}
}
}
function handleSyncEvent({type, id, data}) {
let handler = null;
switch (type) {
case 'upload':
handler = syncHandler.handlers.uploadSync;
break;
case 'download':
handler = syncHandler.handlers.downloadSync;
break;
case 'backup':
handler = syncHandler.handlers.backup;
break;
case 'restore':
handler = syncHandler.handlers.restore;
break;
default:
console.log('mailvelope-client-api unknown sync event', type);
}
if (!handler) {
emit('sync-handler-done', {syncHandlerId: syncHandler.syncHandlerId, syncType: type, error: {message: 'Sync handler not available'}, id});
return;
}
handler(data)
.then(result => {
emit('sync-handler-done', {syncHandlerId: syncHandler.syncHandlerId, syncType: type, syncData: result, id});
})
.catch(error => {
if (!error) {
error = new Error('Unknown Error');
}
emit('sync-handler-done', {syncHandlerId: syncHandler.syncHandlerId, syncType: type, error: objError(error), id});
});
}
function eventListener(msg) {
if (msg.origin !== window.location.origin ||
msg.data.mvelo_client ||
!msg.data.mvelo_extension) {
return;
}
//console.log('clientAPI eventListener', event.data);
switch (msg.data.event) {
case 'sync-event':
handleSyncEvent(msg.data);
break;
case '_reply': {
let error;
if (msg.data.error) {
error = mapError(msg.data.error);
if (!callbacks[msg.data._reply]) {
throw error;
}
}
callbacks[msg.data._reply](error, msg.data.result);
delete callbacks[msg.data._reply];
break;
}
default:
console.log('mailvelope-client-api unknown event', msg.data.event);
}
}
function disconnectListener() {
window.removeEventListener('message', eventListener);
connected = false;
}
function getHash() {
let result = '';
const buf = new Uint16Array(6);
window.crypto.getRandomValues(buf);
for (let i = 0; i < buf.length; i++) {
result += buf[i].toString(16);
}
return result;
}
function mapError(obj) {
const error = new Error(obj.message);
error.code = obj.code;
return error;
}
function objError(error) {
if (error instanceof Error || typeof error === 'string') {
error = {message: error.message || String(error)};
}
return error;
}
function checkConnection() {
if (!connected) {
const error = new Error('Connection to Mailvelope extension is no longer alive.');
error.code = 'NO_CONNECTION';
throw error;
}
}
function emit(event, data) {
checkConnection();
const message = {...data, event, mvelo_client: true};
window.postMessage(message, window.location.origin);
}
function send(event, data) {
checkConnection();
return new Promise((resolve, reject) => {
const message = {...data, event, mvelo_client: true, _reply: getHash()};
callbacks[message._reply] = (err, data) => err ? reject(err) : resolve(data);
window.postMessage(message, window.location.origin);
});
}
export function init() {
window.mailvelope = new Mailvelope();
window.addEventListener('message', eventListener);
window.addEventListener('mailvelope-disconnect', disconnectListener);
window.setTimeout(() => {
window.dispatchEvent(new CustomEvent('mailvelope', {detail: window.mailvelope}));
}, 1);
}