0.0.0-0 • Published 3 years ago

@plurid/action-mail-parser v0.0.0-0

Weekly downloads
-
License
SEE LICENSE IN LI...
Repository
github
Last release
3 years ago

action mail provides a specification, parsing tools, and mail client utilities to obtain action entities and/or action variables from mails, messages, or other user input.

action entities have true or false values. action variables have string values.

action mails are intended to be used for accountless interactions, however the parsing functionality can be used to extract and perform actions for fully authenticated users using any realtime communication channel.

An user is accountless when interacting with an internet service without being expressly identified to/for that service.

An user could desire to be accountless and at the same time be interested in a particular product offered by an internet service. They could even desire to purchase that particular product, but without going through the hassle of setting up an account, or exposing their favorite login with provider for just another, simple, one-time buy event. They want a flea market-like economic transaction, not to engage in a 12 year relationship with a prenuptial agreement and a tough divorce.

Contents

Syntax

An action mail field content is enclosed in { and }, or in [ and ], or in other characters that have a general delimiting semantic, customizable and specified by the service implementing action mails. The action mail field content can consist of one or more action mail field tokens.

An action mail field token can be an action mail entity or an action mail variable.

The action mail entity expresses the truth or falseness of a concept, e.g. {send}, {generate}. action mail entities can be negated by prefixing the token with common language words: don't, no, none; e.g. {don't send}. The negations can be extended and specified by the service implementing action mails.

The action mail variables expect a string value after the colon, e.g. {name: one}, {zip code: 012345}. The value is generally left to be filled by the user, but it can contain predefined values. The parsing of the string value into types, i.e. numbers, is left to the service implementing action mails, e.g. { one: 123 } is parsed as { one: '123' }.

An action mail field can have multiple entities and/or variables, using a spacer to distinguish them, e.g. {send · name: one}, {generate, don't send}, with · and , acting as spacers. The spacers are customizable and specified by the service implementing action mails.

The leading and trailing whitespace is trimmed from action mail field entities and variables, e.g.

{     pay  }

is parsed as { pay: true } and

{   one:   two  }

is parsed as { one: 'two' }.

Usage

Parser

In-browser action mails are composed on the client-side using an anchor a tag with the href attribute set to mailto, where the query values of subject and body are encodeURIComponent strings with the action mail syntax interspersing the text.

Dolphin Example

Consider the following dummy example with a jsx BuyWithoutAccount button

const mail = 'address@example.com';

const requestSubject = encodeURIComponent(
    `Hello from subject text`,
);

const requestBody = encodeURIComponent(
    `Hello,\n\nfrom body text\n{using} an {action: mail} to {specify: variables} and {entities}.\n`,
);

const BuyWithoutAccount = () => (
    <a
        href={`mailto:${mail}?subject=${requestSubject}&body=${requestBody}`}
    >
        <button>
            Buy without Account
        </button>
    </a>
);

When the user clicks the button, the browser will open their default mail client with the destination, subject, and body text already filled, such as

To: address@example.com
Subject: Hello from subject text
Body: Hello,

from body text
{using} an {action: mail} to {specify: variables} and {entities}.

After the mail reaches the destination, it will be parsed and the following data structures is obtained

{
    "using": true,
    "action": "mail",
    "specify": "variables",
    "entities": true
}

The data structure can then be used to POST an API endpoint which will take care of responding accordingly to the action mail.

The mail client add-ons handle parsing and calling the adequate API endpoint, with the appropriate authorization token.

World Example

A sales action mail could use the following subject and body.

const requestSubject = encodeURIComponent(
    `[Order] Product x1 for $100`,
);

const requestBody = encodeURIComponent(
`Hello,


I would like to order

    {Product: x1 · specifications: of product}

Please {send} me a payment link

and {do not generate} an account using this email.

Deliver with the following shipment details

    {name: }
    {country: }
    {city: }
    {street: }


Thanks
`);

which when parsed with the following settings

import {
    parser,
} from '@plurid/action-mail-parser';


const data = requestSubject + requestBody;

const values = parser(
    data,
    {
        spacer: '·',
        fielders: [
            ['{', '}'],
            ['[', ']'],
        ],
        camelCaseKeys: true,
    },
);

obtains the data structure

{
    "order": true,
    "send": true,
    "generate": false,
    "name": "",
    "country": "",
    "city": "",
    "street": "",
    "groups": [
        {
            "product": "x1",
            "specifications": "of product"
        }
    ]
}

The user only has to complete the name, country, city, street fields, or they could be autofilled with predefined values if they also use the action mail mail client.

Advanced

The parser options are

interface ParserOptions {
    /**
     * Delimiting string between multiple fields within the same action mail group.
     *
     * e.g. `{one: two · three: four}` with `·` as spacer.
     *
     * Default: none
     */
    spacer: string;

    /**
     * The name of the groups key.
     *
     * Default: `'groups'`
     */
    groupsKey: string;

    /**
     * Use `camelCase` for all the keys.
     *
     * Default: `false`
     */
    camelCaseKeys: boolean;

    /**
     * Field start-end pairs.
     *
     * The first element marks field start, the second element marks field end.
     *
     * Default: `[ ['{', '}'] ]`
     */
    fielders: string[][];

    /**
     * Negation words (strings or regular expressions) to negate the value of a field.
     *
     * e.g. `{no foo}` gives `{ foo: false }`.
     *
     * Default: `[ 'no', 'not', 'none', 'don\'t', 'do not' ]`
     */
    negations: (string | RegExp)[];
}

When parsing multiple types of data, a Registry can be used to easily determine the data structure type.

import {
    Registry,
} from '@plurid/action-mail-parser';


const main = () => {
    const registry = new Registry();

    registry.register({
        type: 'one',
        shape: {
            two: 'string',
            three: 'boolean',
        },
    });


    interface DataOne {
        two: string;
        three: boolean;
    }

    const dataOne = `one {two: data one} {three}`;
    /**
     * {
     *   type: 'one',
     *   values: {
     *     two: 'data one',
     *     three: true,
     *   },
     * }
     */
    const parseOne = registry.parse<DataOne>(dataOne);
}

main();

Clients

The action mail client listens for new mails, parses them accordingly, and sends the parsed data to the appropriate API endpoint.

Gmail

The Gmail Action Mail client is a Google Workspace Add-on which can be installed in Gmail.

To use the Action Mail, Add Mail providing the required information: to mail, endpoint, token, public key.

The to mail is the mail which receives action mails, if multiple mails configured in Gmail, e.g. example@gmail.com.

The endpoint is a https API endpoint, e.g. https://api.example.com. The endpoint can be REST or GraphQL.

The GraphQL types are

mutation ActionMailCall(input: ActionMailCallInput!): ActionMailResponse!


input ActionMailCallInput {
    data: ActionMailCallInputCrypted!
    metadata: ActionMailCallInputCrypted!
    token: String
}

input ActionMailCallInputCrypted {
    aes: String!
    text: String!
}


type ActionMailResponse {
    status: Boolean!
}

The token is generated and will be checked on the endpoint-side and can be attached to the payload or added as a bearer header, e.g. Authorization: Bearer <token>.

Due to a limitation in dynamically whitelisting URLs for the URL Fetch Google Apps Script Service, the configured endpoint will be called from https://api.plurid.com.

The public key will be used to encrypt the data and metadata on the add-on side.

Generate private/public key pairs using

# generate private key
openssl genrsa -des3 -out action-mail.private.pem 4096

# extract public key
openssl rsa -in action-mail.private.pem -outform PEM -pubout -out action-mail.public.pem

The encryption scheme is hybrid:

  • the data and the metadata objects are converted to strings,
  • unique AES keys are generated to encrypt the values,
  • the generated AES keys are encrypted with the public key and sent together to the endpoint.

The call interface is

interface ActionMailCall {
    data: {
        aes: string;
        text: string;
    };
    metadata: {
        aes: string;
        text: string;
    };
    token: string;
}

The metadata interface is

interface ActionMailMetadata {
    id: string;
    parsedAt: number;
    message: ActionMailMetadataMessage;
}

interface ActionMailMetadataMessage {
    id: string;
    sender: string;
    receiver: string;
    subject: string
    body: string
    date: number;
    attachments: ActionMailMetadataAttachment[];
}

interface ActionMailMetadataAttachment {
    name: string;
    hash: string;
    size: number;
    contentType: string;
    bytes: number[];
}

A decryption implementation in NodeJS could look like this (see more in fixtures/test-endpoint).

import crypto from 'crypto';
import CryptoJS from 'crypto-js';


interface Crypted {
    text: string;
    aes: string;
}


const privateKey = `-----BEGIN RSA PRIVATE KEY-----...`;
const keyPassphrase = 'secretPassphrase';


const decryptAes = (
    aes: string,
    privateKey: string,
    keyPassphrase: string,
) => {
    const aesKey = crypto.privateDecrypt(
        {
            key: privateKey,
            padding: crypto.constants.RSA_PKCS1_PADDING,
            passphrase: keyPassphrase,
        },
        Buffer.from(aes, 'base64'),
    );

    return aesKey.toString();
}

const decryptLoad = (
    aesKey: string,
    load: string,
) => {
    const aesBytes = CryptoJS.AES.decrypt(load, aesKey.toString());
    const clearText = JSON.parse(aesBytes.toString(CryptoJS.enc.Utf8));

    return clearText;
}

const decrypt = (
    crypted: Crypted,
): any => {
    const {
        aes,
        text,
    } = crypted;

    const aesKey = decryptAes(
        aes,
        privateKey,
        keyPassphrase,
    );

    const load = decryptLoad(
        aesKey,
        text,
    );

    return load;
}

The gateway token is required to use extended features, such as increased bandwidth to account for attachments. See more on plurid.com/action-mail.

The attachments, if any, can be saved to the filesystem, given the correctly JSON.parsed metadata object.

import {
    promises as fs,
} from 'fs';

for (const attachment of metadata.message.attachments) {
    const {
        name,
        bytes,
    } = attachment;

    fs.writeFile(
        name,
        Buffer.from(bytes),
    );
}

Packages

@plurid/action-mail-parser • parser

@plurid/action-mail-gmail • gmail mail client

Codeophon