# RFC: Email service ## Ventura email API - Integrated with admin templates defined in GO - Support for nested templates and translations - Can be consumed from external services like Make via Hasura actions - Allow basic customization ------------------------------------------------------------------------------------------ #### Sending email <details> <summary><code>POST</code> <code><b>/send</b></code> <code>(using a GO template)</code></summary> ##### Parameters | name | type | data type | description | |-----------|-----------|-------------------------|-----------------------------------------------------------------------| | templateId | required | string | UUID of `email_template` entity in GO | | from | required | string | sender email address | to | required | string | receiver email address | bcc | optional | string | blind carbon copy email address | lang | optional | string | `locale_id` if it's a multilang template. By default, will be the master language (en). | variables | optional | object | variables to be injected in the template | attachments | optional | string[] | list of object names (file key) stored in Google Cloud Storage. These will be independent to the static attachments the template may have. | brandStyles | optional | boolean | `false` by default. Include branding header and footer. This option will overwrite the value defined in the original template. | customStyle | optional | string | custom CSS string ##### Responses | http code | content-type | response | |---------------|-----------------------------------|---------------------------------------------------------------------| | `200` | `application/json` | <pre>{<br> "status": "success",<br> "message": "Email queued successfully",<br> "data": {<br> "emailId": "23e4567-e89b-12d3-a456-426614174000",<br> "templateId": "23e4567-e89b-12d3-a456-426614174000",<br> "from": "karen@gmail.com",<br> "to": "sophia@example.com",<br> "bcc": "jhon@example.com",<br> "attachments": [<br> "mybucket/pfs/something.pdf"<br> ],<br> "body": "Hello Sophia, this is a <b>test</b>"<br> }<br>}</pre> | | | `400` | `application/json` | <pre>{<br> "status": "error",<br> "message": "invalid request",<br> "error": "Template `23e4567-e89b-12d3-a456-426614174000` not found"<br>}</pre> | | `400` | `application/json` | <pre>{<br> "status": "error",<br> "message": "invalid request",<br> "error": "Variable `FAKE_VAR` not found in template `23e4567-e89b-12d3-a456-426614174000`"<br>}</pre> | | `400` | `application/json` | <pre>{<br> "status": "error",<br> "message": "invalid request",<br> "error": "`witherror@something` is not a valid email address"<br>}</pre> | | `400` | `application/json` | <pre>{<br> "status": "error",<br> "message": "invalid request",<br> "error": "attachment `mybucket/pfs/something.pdf` not found"<br>}</pre> | | `500` | `application/json` | <pre>{<br> "status": "error",<br> "message": "server error",<br> "error": "unknown error: something went wrong, please check the logs"<br>}</pre> | | </details> ------------------------------------------------------------------------------------------ <details> <summary>Implementation details</summary> The most important entities that hold complex logic are templates, variables and attachments. Here explained in context: #### Template Template entity holds the main data about the email, such as: - Subject and Body with conditional blocks handling - Translations - Linked brands with this new service, we have to add support for: - A `boolean` flag to enable/disable branding decoration (logos, header, footer). e.g: `is_brand_style_enabled` #### Variables It's kay-value where the key must match the names of the variables defined in the template. The GO must extend the current functionality to support new types of variables. There are 3 types of variables: ##### Simple text The value is a string or a number, most common variables will be just like the following example: ```typescript export const standard: SendEmailRequest = { templateId: "23e4567-e89b-12d3-a456-426614174000", from: "sophia@galapatours.com", to: "karen@gmail.com", bcc: "jhon@example.com", attachments: ["mybucket/pfs/something.pdf"], variables: { VAR_1: "test", CLIENT_NAME: "Karen", REMAINING_TICKETS: 11, }, }; ``` ##### Formatted text The value is an object with 2 keys: ```typescript export type FormattedVar = { format: "html" | "draft" | "text"; content: string; }; ``` By default, all variables saved in GO are `"draft"` which is a tree-like format but services that consume this API could use html as well: ```typescript export const standardWithHtmlVar: SendEmailRequest = { templateId: "23e4567-e89b-12d3-a456-426614174000", from: "sophia@galapatours.com", to: "karen@gmail.com", bcc: "jhon@example.com", attachments: ["mybucket/pfs/something.pdf"], variables: { VAR_1: { format: "html", content: "<b>test</b>", }, CLIENT_NAME: "Karen", // this is equivalent to { format: "text", content: "Karen"} }, }; ``` ##### Nested Template This type of variable unlocks the possibility to have nested templates, as the previous, it is also an object (or array of objects) but with the following shape: ```typescript export type NestedTemplateVar = { templateId: string; variables?: Record<string, string | number>; }[]; ``` The `templateId` represents a different template than the original and `variables` represents variables of simple text or formatted text and are scoped in to only this template. The API will support just one (1) level of nested TemplateVariables. Example: ```typescript export const withNested: SendEmailRequest = { templateId: "23e4567-e89b-12d3-a456-426614174000", from: "sophia@galapatours.com", to: "karen@gmail.com", bcc: "jhon@example.com", attachments: ["mybucket/pfs/something.pdf"], variables: { VAR_1: "test", CLIENT_NAME: "Karen", MY_NESTED_VAR: { templateId: "23e4567-e89b-12d3-a456-426614174000", variables: { VAR_1: "test nested", }, }, }, }; ``` You can also pass an array of `NestedTemplateVar` to handle an iterable block of templates: ```typescript export const withNestedIterable: SendEmailRequest = { templateId: "23e4567-e89b-12d3-a456-426614174000", from: "sophia@galapatours.com", to: "karen@gmail.com", bcc: "jhon@example.com", attachments: ["mybucket/pfs/something.pdf"], variables: { VAR_1: "test", CLIENT_NAME: "Karen", MY_NESTED_VAR: [ { templateId: "23e4567-e89b-12d3-a456-426614174000", variables: { VAR_1: "test nested 1", }, }, { templateId: "23e4567-e89b-12d3-a456-426614174000", variables: { VAR_1: "test nested 2", }, }, ], }, }; ``` This is useful to render structured lists in the email at scale since all the features of email template admin applies here, with some limitations: - **Attachments**: they will be ignored - **Lang**: will be use the same specified in the request or in the root template, make sense, right? - **Nested template variables**: only support one(1) level of nesting. ###### Impact in GO The **email variables admin** needs to have a way to link an existing template as a variable, and should be filtreable within the variables admin. And the **template admin** should be able to filter templates that are used as variables. This can be achieved with a new foreign field `template_id` in the `email_variable` table. Addionally, the **template admin** the variables chooser (dropdown) and the embed variable table (in Email tab) should distinguish the if the type of variable is a normal one (simple text) or a nested one (email template var). This can be done using different icons for each one. #### Attachments Attachments must include the bucket in the path because the file could be retrieved from anywhere in the Ventura's cloud workspace. ``` ["mybucket/pfs/103849.pdf", "mybucket/imgs/xmas_card_japan.jpg"] ``` </details> ### Flow ![email flow](https://hackmd.io/_uploads/SyNs7B5Hn.png) #### Interfaces <details> <summary>Typescript</summary> ```typescript /* Request */ export interface SendEmailRequest { templateId: string; from: string; to: string; bcc?: string; attachments?: string[]; variables?: Record< string, string | number | FormattedVar | NestedTemplateVar | NestedTemplateVar[] >; } export type NestedTemplateVar = { templateId: string; variables?: Record<string, string | number>; }; export type FormattedVar = { format: "html" | "draft" | "text"; content: string; }; /* Response */ export interface SendEmailResponse { status: "success" | "error"; message: string; data?: { emailId: string; templateId: string; from: string; to: string; bcc: string; attachments: string[]; body: string; }; error?: string; } ``` </details> ------------------------------------------------------------------------------------------