/**
* The MIT License (MIT)
*
* Copyright (c) 2018-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.
*/
/**
* Custom HTML elements that wrap the client-API for declarative use in markup.
* Each element is registered as an HTML custom element when the client-API
* script loads, so they can be authored directly as HTML tags — no JavaScript
* needed beyond optional event listeners.
*
* @module web-components
*/
/**
* Custom HTML element that renders a signed, encrypted form. Wraps
* {@link Mailvelope#createEncryptedFormContainer} so partners can embed an
* encrypted form declaratively in HTML instead of via JavaScript.
*
* The form definition is provided as the `innerText` of a single child
* `<script>` element. The signature attribute carries the OpenPGP signature
* over that definition. On submit, the element dispatches an `encrypt` event
* whose `detail.armoredData` is the encrypted form payload.
*
* @memberof module:web-components
* @hideconstructor
* @element openpgp-encrypted-form
*
* @attr {string} id - Required. Unique identifier used as the iframe container target.
* @attr {string} [signature] - OpenPGP signature over the embedded form definition.
*
* @fires connected - Dispatched once the element is attached to the document.
* @fires encrypt - Dispatched on successful encryption.
* `event.detail.armoredData` ({@link AsciiArmored}) holds the encrypted payload.
* @fires error - Standard `ErrorEvent`. `event.error.code` may be one of:
* `NO_FORM_ID` (id attribute missing), `NO_FORM_SCRIPT` (no child `<script>`
* with form template), or any error code raised by
* {@link Mailvelope#createEncryptedFormContainer} (e.g. `INVALID_FORM`).
*
* @example
* <openpgp-encrypted-form id="contact-form" signature="-----BEGIN PGP SIGNATURE-----...">
* <script type="text/template">
* <form data-recipient="contact@example.com">
* <input type="text" name="name">
* <input type="email" name="email">
* <textarea name="message"></textarea>
* <button type="submit">Send</button>
* </form>
* </script>
* </openpgp-encrypted-form>
* <script>
* document.getElementById('contact-form').addEventListener('encrypt', e => {
* console.log(e.detail.armoredData);
* });
* </script>
*/
class OpenPGPEncryptedForm extends HTMLElement {
/** @private */
connectedCallback() {
this.dispatchEvent(new Event('connected'));
const id = this.getAttribute('id');
if (!id) {
const error = new Error('No form id for openpgp-encrypted-tag. Please add a unique identifier.');
error.code = 'NO_FORM_ID';
return this.onError(error);
}
let html;
const scriptTags = this.getElementsByTagName('script');
if (scriptTags.length) {
html = scriptTags[0].innerText;
} else {
const error = new Error('No form template for openpgp-encrypted-tag. Please add a form template.');
error.code = 'NO_FORM_SCRIPT';
return this.onError(error);
}
window.mailvelope.createEncryptedFormContainer(`#${id}`, html, this.getAttribute('signature'))
.then(data => this.onEncrypt(data), error => this.onError(error));
}
/** @private */
onEncrypt(data) {
this.dispatchEvent(new CustomEvent('encrypt', {
detail: {armoredData: data.armoredData},
bubbles: true,
cancelable: true
}));
}
/** @private */
onError(error) {
this.dispatchEvent(new ErrorEvent('error', {
message: error.message,
error
}));
}
}
/**
* Custom HTML element that renders the decrypted content of an encrypted mail.
* Wraps {@link Mailvelope#createDisplayContainer} for declarative use.
*
* The armored PGP message is supplied either through a child
* `<template class="armored">` (preferred for multi-line bodies) or via the
* `data-armored` attribute.
*
* @memberof module:web-components
* @hideconstructor
* @element openpgp-email-read
*
* @attr {string} id - Required. Unique identifier used as the iframe container target.
* @attr {string} [data-armored] - {@link AsciiArmored} PGP message. Used when no
* `<template class="armored">` child is present.
* @attr {string} [data-sender-address] - Sender email address. Used to identify
* the key for signature verification.
*
* @fires ready - Dispatched once the decrypted container has been mounted.
* @fires error - Standard `ErrorEvent`. `event.error.code` may be any code
* raised by {@link Mailvelope#createDisplayContainer} — for example
* `DECRYPT_ERROR`, `ARMOR_PARSE_ERROR`, `PWD_DIALOG_CANCEL`, `NO_KEY_FOUND`.
*
* @example
* <openpgp-email-read id="msg" data-sender-address="alice@example.com">
* <template class="armored">-----BEGIN PGP MESSAGE-----
* ...
* -----END PGP MESSAGE-----</template>
* </openpgp-email-read>
*/
class OpenPGPEmailRead extends HTMLElement {
/** @private */
connectedCallback() {
const id = this.getAttribute('id');
if (!id) {
return this.onError(new Error('Missing id attribute on openpgp-email-read tag. Please add a unique identifier.'));
}
const [armoredElement] = this.getElementsByClassName('armored');
const armored = armoredElement ? armoredElement.textContent : this.dataset.armored;
if (!armored) {
return this.onError(new Error('Armored message required as <template class="armored"> child element or data-armored attribute.'));
}
const options = {senderAddress: this.dataset.senderAddress};
if (window.mailvelope) {
this.createContainer(id, armored, options);
} else {
window.addEventListener('mailvelope', () => this.createContainer(id, armored, options), {once: true});
}
}
/** @private */
async createContainer(id, armored, options) {
try {
const {error} = await window.mailvelope.createDisplayContainer(`#${id}`, armored, null, options);
if (error) {
return this.onError(error);
}
this.onReady();
} catch (e) {
this.onError(e);
}
}
/** @private */
onReady() {
this.dispatchEvent(new CustomEvent('ready', {bubbles: true, cancelable: true}));
}
/** @private */
onError(error) {
this.dispatchEvent(new ErrorEvent('error', {message: error.message, error}));
}
}
/**
* Custom HTML element that renders the Mailvelope editor for composing a new
* encrypted mail. Wraps {@link Mailvelope#createEditorContainer} for declarative use.
*
* An armored draft and/or a quoted mail can be supplied through child
* `<template>` elements. All `data-*` attributes are forwarded as editor
* options (see {@link EditorContainerOptions}); boolean options follow the
* standard HTML boolean-attribute convention (presence = true, regardless of value).
*
* @memberof module:web-components
* @hideconstructor
* @element openpgp-email-write
*
* @attr {string} id - Required. Unique identifier used as the iframe container target.
* @attr {string} [data-quota] - Mail content limit in kilobytes (default: 20480).
* @attr {string} [data-sign-msg] - Presence of the attribute enables signing
* (HTML boolean-attribute style; the attribute value is ignored).
* @attr {string} [data-keep-attachments] - Presence of the attribute keeps
* attachments from the quoted mail in the editor.
* @attr {string} [data-predefined-text] - Initial text to load into the editor.
* @attr {string} [data-quoted-mail-indent] - Indent quoted mail (default: true).
* @attr {string} [data-quoted-mail-header] - Header inserted before the quoted mail.
*
* @fires ready - Dispatched once the editor has been mounted.
* `event.detail.editor` is the {@link Editor} instance used for `encrypt()`
* and `createDraft()`.
* @fires error - Standard `ErrorEvent`. `event.error.code` may be any code
* raised by {@link Mailvelope#createEditorContainer} — for example
* `WRONG_ARMORED_TYPE` or `INVALID_OPTIONS`.
*
* @example
* <openpgp-email-write id="compose" data-sign-msg data-quota="10240">
* <template class="quoted-mail">-----BEGIN PGP MESSAGE-----
* ...
* -----END PGP MESSAGE-----</template>
* </openpgp-email-write>
* <script>
* document.getElementById('compose').addEventListener('ready', e => {
* const editor = e.detail.editor;
* document.getElementById('send').addEventListener('click', () => {
* editor.encrypt(['bob@example.com']).then(armored => console.log(armored));
* });
* });
* </script>
*/
class OpenPGPEmailWrite extends HTMLElement {
/** @private */
connectedCallback() {
const id = this.getAttribute('id');
if (!id) {
return this.onError(new Error('Missing id attribute on openpgp-email-write tag. Please add a unique identifier.'));
}
const [armoredDraftElement] = this.getElementsByClassName('armored-draft');
const armoredDraft = armoredDraftElement ? armoredDraftElement.textContent : undefined;
const [quotedMailElement] = this.getElementsByClassName('quoted-mail');
const quotedMail = quotedMailElement ? quotedMailElement.textContent : undefined;
let {quota, signMsg, keepAttachments} = this.dataset;
quota = quota ? Number(quota) : undefined;
signMsg = signMsg || signMsg === '' ? true : false;
keepAttachments = keepAttachments || keepAttachments === '' ? true : false;
const options = {armoredDraft, quotedMail, ...this.dataset, quota, signMsg, keepAttachments};
if (window.mailvelope) {
this.createEditor(id, options);
} else {
window.addEventListener('mailvelope', () => this.createEditor(id, options), {once: true});
}
}
/** @private */
async createEditor(id, options) {
try {
this.editor = await window.mailvelope.createEditorContainer(`#${id}`, null, options);
this.onReady(this.editor);
} catch (e) {
this.onError(e);
}
}
/** @private */
onReady(editor) {
this.dispatchEvent(new CustomEvent('ready', {bubbles: true, cancelable: true, detail: {editor}}));
}
/** @private */
onError(error) {
this.dispatchEvent(new ErrorEvent('error', {message: error.message, error}));
}
}
/** @private */
export function init() {
// See. https://developer.mozilla.org/en-US/docs/Web/API/Window/customElements#Specification#Browser_compatibility
if (!window.customElements) {
return;
}
window.customElements.define('openpgp-encrypted-form', OpenPGPEncryptedForm);
window.customElements.define('openpgp-email-read', OpenPGPEmailRead);
window.customElements.define('openpgp-email-write', OpenPGPEmailWrite);
}