Repository: squareboat/nest-mailman Branch: main Commit: c4dcbd453e77 Files: 37 Total size: 30.0 KB Directory structure: gitextract_6x4a5o5l/ ├── .gitignore ├── .npmignore ├── CONTRIBUTING.MD ├── LICENSE.md ├── README.md ├── copymailman.js ├── jest.json ├── lib/ │ ├── constants.ts │ ├── index.ts │ ├── interfaces/ │ │ ├── MailCompiler.ts │ │ ├── index.ts │ │ ├── mjml.ts │ │ └── options.ts │ ├── mailman.ts │ ├── message.ts │ ├── module.ts │ ├── provider.map.ts │ └── service.ts ├── package.json ├── tsconfig.json └── views/ ├── assets/ │ └── style.css ├── components/ │ ├── body.tsx │ ├── bodyBuilder.tsx │ ├── button.tsx │ ├── description.tsx │ ├── divider.tsx │ ├── footer.tsx │ ├── greeting.tsx │ ├── head.tsx │ ├── header.tsx │ ├── html.tsx │ ├── image.tsx │ ├── regards.tsx │ ├── table.tsx │ └── text.tsx └── mail/ ├── generic.tsx └── index.tsx ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # dependencies /node_modules # IDE /.idea /.awcache /.vscode # misc npm-debug.log .DS_Store # tests /test /coverage /.nyc_output # dist dist ================================================ FILE: .npmignore ================================================ # source lib tests index.ts package-lock.json tslint.json tsconfig.json .prettierrc # github .github CONTRIBUTING.MD # misc .commitlintrc.json .release-it.json .eslintignore .eslintrc.js ================================================ FILE: CONTRIBUTING.MD ================================================ ================================================ FILE: LICENSE.md ================================================ # The MIT License (MIT) Copyright © 2020 [SquareBoat](https://squareboat.com) > 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. ================================================ FILE: README.md ================================================ # Nest Mailman 📮 The mailer package for your NestJS Applications. ### Features - ✅ Build mails programmtically - ✅ Supports MJML + React templating - ✅ Use JSX to easily create clean components - ✅ Comes with built-in template to quickly send mails without creating templates. - ✅ Uses nodemailer internally For complete documentation, head over to [our site](https://squareboat.com/open-source/nest-mailman/). --- ## About Us We are a bunch of dreamers, designers, and futurists. We are high on collaboration, low on ego, and take our happy hours seriously. We'd love to hear more about your product. Let's talk and turn your great ideas into something even greater! We have something in store for everyone. [☎️ 📧 Connect with us!](https://squareboat.com/contact) --- ## License The MIT License. Please see License File for more information. Copyright © 2020 SquareBoat. Made with ❤️ by [Squareboat](https://squareboat.com) ================================================ FILE: copymailman.js ================================================ const fs = require("fs-extra"); var appRoot = require("app-root-path"); const picocolors = require("picocolors"); function handle() { const path = `${appRoot.path}/resources/views`; if (fs.existsSync(path)) { console.log(picocolors.blue`➡️ ${path} already exists. Returning...`); console.log( picocolors.blue`➡️ To copy file, copy from ${picocolors.white( process.cwd() + "/cli.js" )} to ${picocolors.white(path)}` ); return; } fs.copySync("./views", `${appRoot.path}/resources/views`, { overwrite: true | false, }); // (err) => { // if (err) throw err; // console.log( // picocolors.green`🚀 Copying cli.js file to ${appRoot.path}/cli`, // ); // console.log( // picocolors.yellow`❓ To know more about on how to change default module and path in cli, go to https://github.com/squareboat/nest-console`, // ); } handle(); ================================================ FILE: jest.json ================================================ { "moduleFileExtensions": ["ts", "tsx", "js", "json"], "transform": { "^.+\\.tsx?$": "ts-jest" }, "testRegex": "/lib/.*\\.(test|spec).(ts|tsx|js)$", "collectCoverageFrom": [ "lib/**/*.{js,jsx,tsx,ts}", "!**/node_modules/**", "!**/vendor/**" ], "coverageReporters": ["json", "lcov","text-summary"] } ================================================ FILE: lib/constants.ts ================================================ export const MAILMAN_QUEUE = "MAILMAN_QUEUE"; export const SEND_MAIL = "SEND_MAIL"; // Mail Formats export const RAW_MAIL = "RAW"; export const VIEW_BASED_MAIL = "VIEW_BASED"; export const GENERIC_MAIL = "GENERIC"; export class MailmanConstant {} ================================================ FILE: lib/index.ts ================================================ export * from "./module"; export * from "./service"; export * from "./mailman"; export * from "./interfaces"; export * from "./message"; ================================================ FILE: lib/interfaces/MailCompiler.ts ================================================ // All mail compilers(Handlebars, Markdown, etc.) will implement this interface. export interface MailCompiler { filePath: string; compileMail(options: Record | undefined ): string; } ================================================ FILE: lib/interfaces/index.ts ================================================ export * from './options'; export * from './MailCompiler'; export * from './mjml'; ================================================ FILE: lib/interfaces/mjml.ts ================================================ export interface MJMLParsingOpts { fonts?: { [key: string]: string }; keepComments?: boolean; beautify?: boolean; minify?: boolean; validationLevel?: 'strict' | 'soft' | 'skip'; filePath?: string; minifyOptions?: MJMLMinifyOptions; } interface MJMLMinifyOptions { collapseWhitespace?: boolean; minifyCSS?: boolean; removeEmptyAttributes?: boolean; } ================================================ FILE: lib/interfaces/options.ts ================================================ import { ModuleMetadata, Type } from "@nestjs/common/interfaces"; import { Attachment } from "nodemailer/lib/mailer"; import { MailMessage } from "../message"; import { MJMLParsingOpts } from "./mjml"; export interface MailmanBaseTemplateOptions { appName?: string; appLogoSrc?: string; socialMedia?: { name: string; href: string; }[]; contactEmail?: string; } export interface MailmanMetaPayload { title?: string; preview?: string; } export interface MailmanPayload { _templateConfig?: MailmanBaseTemplateOptions; meta?: MailmanMetaPayload; genericFields?: Record[]; } export interface MailmanOptions { host: string; port: number; username: string; password: string; from: string; ignoreTLS?: boolean; replyTo?: string; path?: string; mjml?: MJMLParsingOpts; templateConfig: { baseComponent: (payload: Record) => JSX.Element; templateOptions?: MailmanBaseTemplateOptions; }; } export type CompilerOptions = { configPath: string; mjml?: MJMLParsingOpts; }; export interface MailmanOptionsFactory { createMailmanOptions(): Promise | MailmanOptions; } export interface MailmanAsyncOptions extends Pick { name?: string; useExisting?: Type; useClass?: Type; useFactory?: (...args: any[]) => Promise | MailmanOptions; inject?: any[]; } export interface MailData { subject?: string; html: string; attachments: Attachment[]; } export interface SendMailOptions { sender: string; replyTo?: string; inReplyTo?: string; mail: MailMessage; cc: string | string[]; bcc: string | string[]; receipents: string | string[]; } export type MailType = "RAW" | "VIEW_BASED" | "GENERIC"; ================================================ FILE: lib/mailman.ts ================================================ import { MailmanService } from './service'; import { MailMessage } from './message'; import { MailData } from './interfaces'; export class Mailman { private receipents: string | string[]; private ccReceipents: string | string[]; private bccReceipents: string | string[]; private sender: string; private _replyTo: string; private _inReplyTo: string; private constructor() { this.sender = ''; this._replyTo = ''; this._inReplyTo = ''; this.receipents = ''; this.ccReceipents = ''; this.bccReceipents = ''; } /** * Returns new instance */ static init() { return new Mailman(); } /** * `FROM` in email address. * Use this method to override the `from` address provided in configuration. * @param sender */ from(sender: string): this { this.sender = sender; return this; } /** * `REPLY_TO` in email address. * Use this method to override the `reply_to` address provided in configuration or to add one. * @param replyToEmail */ replyTo(replyToEmail: string): this { this._replyTo = replyToEmail; return this; } /** * `IN_REPLY_TO` in email address. * Use this method to provide the `in_reply_to` header. * @param replyToEmail */ inReplyTo(messageId: string): this { this._inReplyTo = messageId; return this; } /** * `TO` in email address * @param receipents */ to(receipents: string | string[]): this { this.receipents = receipents; return this; } /** * `CC` in email addres * @param ccreceipents */ cc(ccReceipents: string | string[]): this { this.ccReceipents = ccReceipents; return this; } /** * `BCC` in email address * @param bccReceipents */ bcc(bccReceipents: string | string[]): this { this.bccReceipents = bccReceipents; return this; } /** * Send mail * @param mail */ send(mail: MailMessage) { return MailmanService.send({ mail, cc: this.ccReceipents, bcc: this.bccReceipents, sender: this.sender, replyTo: this._replyTo, inReplyTo: this._inReplyTo, receipents: this.receipents, }); } } ================================================ FILE: lib/message.ts ================================================ import { Attachment } from "nodemailer/lib/mailer"; import { GENERIC_MAIL, RAW_MAIL, VIEW_BASED_MAIL } from "./constants"; import { MailData, MailType, MailmanMetaPayload, MailmanPayload, } from "./interfaces"; import { MailmanService } from "./service"; import { renderToMjml } from "@faire/mjml-react/utils/renderToMjml"; import mjml2html from "mjml"; export class MailMessage { private mailSubject?: string; private viewFile?: (payload: Record) => JSX.Element; private templateString?: string; private payload: MailmanPayload = {}; private mailType: MailType; private compiledHtml: string; private attachments: Record; constructor() { this.attachments = {}; this.compiledHtml = ""; this.mailType = RAW_MAIL; } /** * static method to create new instance of the MailMessage class */ static init(): MailMessage { return new MailMessage(); } /** * Define subject of the mail * @param subject */ subject(subject: string): this { this.mailSubject = subject; return this; } /** * Define the view to be used for the mail * @param viewFile * @param payload */ view( component: (payload: Record) => JSX.Element, payload?: Record ): this { this.mailType = VIEW_BASED_MAIL; this.viewFile = component; this.payload = payload || {}; return this; } /** * Define the template string to be used for the mail * @param template * @param payload */ raw(template: string, payload?: Record): this { this.mailType = RAW_MAIL; this.templateString = template; this.payload = payload || {}; return this; } /** * Add attachment to the mail * @param greeting */ attach(filename: string, content: Omit): this { this.attachments[filename] = { ...content, filename }; return this; } /** * ==> Generic Template Method <== * Use this method for adding the greeting to the generic mail * @param greeting */ greeting(greeting: string): this { this._setGenericMailProperties(); if (this.payload.genericFields) { this.payload?.genericFields.push({ greeting }); } return this; } /** * ==> Generic Template Method <== * Use this method for adding a text line to the generic mail * @param line */ line(line: string): this { this._setGenericMailProperties(); if (this.payload.genericFields) { this.payload?.genericFields.push({ line }); } return this; } html(html: string): this { this._setGenericMailProperties(); if (this.payload.genericFields) { this.payload?.genericFields.push({ html }); } return this; } /** * ==> Generic Template Method <== * Use this method for adding a url action to the generic mail * @param text * @param link */ action(text: string, link: string): this { this._setGenericMailProperties(); if (this.payload.genericFields) { this.payload.genericFields.push({ action: { text, link } }); } return this; } /** * ==> Generic Template Method <== * Use this method for adding a table to the generic mail * @param data */ table(data: Record[], showHeading = true, vertical = false): this { this._setGenericMailProperties(); if (this.payload.genericFields) { this.payload.genericFields.push({ table: data, showHeading, vertical: vertical }); } return this; } meta(payload: MailmanMetaPayload): this { this.payload.meta = payload; return this; } /** * ==> Generic Template Method <== * @param greeting */ private _setGenericMailProperties() { this.mailType = GENERIC_MAIL; if (!this.payload || !this.payload.genericFields) { this.payload.genericFields = []; } } /** * Method to compile templates */ private _compileTemplate(): string { if (this.compiledHtml) return this.compiledHtml; const config = MailmanService.getConfig(); const componentData = { ...this.payload, _templateConfig: config.templateConfig.templateOptions, }; if (this.mailType === GENERIC_MAIL) { const component = config.templateConfig?.baseComponent; if (!component) { throw new Error( "BaseComponent not found for generic view, please check if you have set the baseComponent attribute in config correctly." ); } const { html } = mjml2html( renderToMjml(component(componentData)), config.mjml ); this.compiledHtml = html; return this.compiledHtml; } if (this.mailType === VIEW_BASED_MAIL && this.viewFile) { const component = this.viewFile; const { html } = mjml2html( renderToMjml(component(componentData)), config.mjml ); this.compiledHtml = html; return this.compiledHtml; } if (this.mailType === RAW_MAIL && this.templateString) { return this.compiledHtml; } return this.compiledHtml; } /** * Returns the maildata payload */ getMailData(): MailData { if (typeof (this as any).handle === "function") { (this as any)["handle"](); } return { subject: this.mailSubject, html: this._compileTemplate(), attachments: Object.values(this.attachments), }; } /** * Render the email template. * Returns the complete html of the mail. */ render(): string { return this._compileTemplate(); } } ================================================ FILE: lib/module.ts ================================================ import { map } from "./provider.map"; import { Module, DynamicModule, Provider, Type } from "@nestjs/common"; import { MailmanService } from "./service"; import { MailmanOptions, MailmanAsyncOptions, MailmanOptionsFactory, } from "./interfaces"; @Module({}) export class MailmanModule { /** * Register options * @param options */ static register(options: MailmanOptions): DynamicModule { return { global: true, module: MailmanModule, providers: [ MailmanService, { provide: map.MAILABLE_OPTIONS, useValue: options }, ], }; } /** * Register Async Options */ static registerAsync(options: MailmanAsyncOptions): DynamicModule { return { global: true, module: MailmanModule, imports: [], providers: [MailmanService, this.createStorageOptionsProvider(options)], }; } private static createStorageOptionsProvider( options: MailmanAsyncOptions ): Provider { if (options.useFactory) { return { provide: map.MAILABLE_OPTIONS, useFactory: options.useFactory, inject: options.inject || [], }; } const inject = [ (options.useClass || options.useExisting) as Type, ]; return { provide: map.MAILABLE_OPTIONS, useFactory: async (optionsFactory: MailmanOptionsFactory) => await optionsFactory.createMailmanOptions(), inject, }; } } ================================================ FILE: lib/provider.map.ts ================================================ export const map = { MAILABLE_OPTIONS: 'MAILABLE_OPTIONS', }; ================================================ FILE: lib/service.ts ================================================ import { map } from "./provider.map"; import * as nodemailer from "nodemailer"; import { Injectable, Inject } from "@nestjs/common"; import { MailmanOptions, MailData, SendMailOptions } from "./interfaces"; @Injectable() export class MailmanService { private static options: MailmanOptions; private static transporter: any; constructor(@Inject(map.MAILABLE_OPTIONS) options: MailmanOptions) { MailmanService.options = options; MailmanService.transporter = nodemailer.createTransport( { host: options.host, port: options.port, ignoreTLS: options.ignoreTLS, auth: { user: options.username, pass: options.password }, }, { from: options.from } ); } static getConfig(): MailmanOptions { return MailmanService.options; } static async send(options: SendMailOptions) { const config = MailmanService.options; const mailData: MailData = options.mail.getMailData(); const mail: Record = { to: options.receipents, cc: options.cc, bcc: options.bcc, from: options.sender || config.from, html: mailData.html, subject: mailData.subject, attachments: mailData.attachments, }; if (options.replyTo || config.replyTo) { mail.replyTo = options.replyTo || config.replyTo; } if (options.inReplyTo) { mail.inReplyTo = options.inReplyTo; } await MailmanService.transporter.sendMail(mail); } } ================================================ FILE: package.json ================================================ { "name": "@squareboat/nest-mailman", "version": "1.0.5", "description": "📮 The mailer package for your NestJS Applications", "main": "dist/index.js", "types": "dist/index.d.ts", "keywords": [ "nestjs", "nodemailer", "nestjs-mail", "nestjs-mailing", "nestjs-mailer", "nestjs-mailman" ], "repository": { "type": "git", "url": "https://github.com/squareboat/nest-mailman.git" }, "bugs": { "url": "https://github.com/squareboat/nest-mailman/issues" }, "homepage": "https://github.com/squareboat/nest-mailman", "author": "Vinayak Sarawagi ", "private": false, "license": "MIT", "scripts": { "postinstall": "node copymailman.js", "build": "rm -rf dist && tsc -p tsconfig.json", "format": "prettier --write \"**/*.ts\"", "lint": "eslint 'lib/**/*.ts' --fix", "prepublish:npm": "npm run build", "publish:npm": "npm publish --access public", "prepublish:next": "npm run build", "publish:next": "npm publish --access public --tag next", "test": "jest --config=jest.json", "test:cov": "jest --config=jest.json --coverage", "test:e2e": "jest --config ./tests/jest-e2e.json --runInBand", "test:e2e:dev": "jest --config ./tests/jest-e2e.json --runInBand --watch" }, "devDependencies": { "@nestjs/common": "^9.3.9", "@nestjs/core": "^9.3.9", "@types/mjml": "^4.7.0", "@types/nodemailer": "^6.4.7", "@types/react": "^18.0.28", "typescript": "^4.9.5" }, "dependencies": { "@faire/mjml-react": "^3.1.2", "app-root-path": "^3.1.0", "fs-extra": "^11.1.0", "mjml": "^4.7.1", "nodemailer": "^6.9.1", "picocolors": "^1.0.0", "reflect-metadata": "^0.1.13" }, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0", "@nestjs/core": "^8.0.0 || ^9.0.0" } } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "module": "commonjs", "declaration": true, "strict": true, "removeComments": true, "noLib": false, "emitDecoratorMetadata": true, "experimentalDecorators": true, "target": "es6", "sourceMap": false, "outDir": "./dist", "rootDir": "./lib", "skipLibCheck": true, "esModuleInterop": true, "allowJs": true, "jsx": "react" }, "include": ["lib/**/*"], "exclude": ["node_modules", "**/*.spec.ts", "tests", "views"] } ================================================ FILE: views/assets/style.css ================================================ .default-bg { background-color: #d1deec; } .header { background-color: #f5f6f6; } .footer { background-color: #f5f6f6; } .card { background-color: #f5f6f6; border-radius: 8px; margin-left: 20px; margin-right: 20px; } .styled-table { /* border-collapse: collapse; */ font-size: 0.9em; font-family: sans-serif; border-radius: 10em; overflow: hidden; } .styled-table thead tr { background-color: #0275d8; color: #ffffff; text-align: left; } .styled-table th { background-color: #0275d8; color: #ffffff; text-align: left; } .styled-table th, .styled-table td { padding: 12px 15px; } .styled-table tbody tr { border-bottom: 2px solid #dddddd; } .styled-table tbody tr:nth-of-type(even) { background-color: #f3f3f3; } .styled-table tbody tr:last-of-type { border-bottom: 2px solid #0275d8; } .styled-table tbody tr.active-row { font-weight: bold; color: #2d94f3; } ================================================ FILE: views/components/body.tsx ================================================ import { MjmlBody } from "@faire/mjml-react"; import React from "react"; export const MailmanBody = ({ children }: { children: JSX.Element }) => { return {children}; }; ================================================ FILE: views/components/bodyBuilder.tsx ================================================ import { MjmlColumn, MjmlSection } from "@faire/mjml-react"; import { MailmanButton } from "./button"; import { MailmanDivider } from "./divider"; import { Greeting } from "./greeting"; import { TextLine } from "./text"; import React from "react"; import { MailmanTable } from "./table"; import { RawHtml } from "./html"; export const MailmanBodyBuilder = (payload: Record) => { return ( {payload.fields.map((obj) => { return ComponentView(obj); })} ); }; const ComponentView = (payload: Record) => { if (payload.greeting) { return ; } if (payload.line) { return ; } if (payload.divider) { return ; } if (payload.html) { return ; } if (payload.action) { return ; } if (payload.table) { return ; } return <>; }; ================================================ FILE: views/components/button.tsx ================================================ import { MjmlButton } from "@faire/mjml-react"; import React from "react"; export const MailmanButton = (payload: Record) => { return ( <> {payload.value.text} ); }; ================================================ FILE: views/components/description.tsx ================================================ import { MjmlColumn, MjmlText } from "@faire/mjml-react"; import React from "react"; export const Description = (payload: Record) => { return ( {payload.value} ); }; ================================================ FILE: views/components/divider.tsx ================================================ import { MjmlDivider } from "@faire/mjml-react"; import React from "react"; export const MailmanDivider = (payload: Record) => { return ; }; ================================================ FILE: views/components/footer.tsx ================================================ import { MjmlColumn, MjmlDivider, MjmlImage, MjmlSection, MjmlSocial, MjmlSocialElement, MjmlSpacer, MjmlText, MjmlWrapper, } from "@faire/mjml-react"; import React from "react"; export const MailmanFooter = (payload: Record) => { const appLogoSrc = payload.config.appLogoSrc; const appName = payload.config.appName; const socialMedia = payload.config.socialMedia; const contactEmail = payload.config.contactEmail; return ( <> {socialMedia?.map((a) => ( <> ))} {contactEmail && ( Contact Us: {contactEmail} )} © {new Date().getFullYear()} {appName} ); }; ================================================ FILE: views/components/greeting.tsx ================================================ import { MjmlText } from "@faire/mjml-react"; import React from "react"; export const Greeting = (payload: Record) => { return (

{payload.value}

); }; ================================================ FILE: views/components/head.tsx ================================================ import { MjmlHead, MjmlPreview, MjmlStyle, MjmlTitle } from "@faire/mjml-react"; import { readFileSync } from "fs"; import React from "react"; const css = readFileSync("resources/views/assets/style.css").toString(); export const MailmanHead = (payload: Record) => { return ( {payload.title} {payload.preview || payload.title} {css} ); }; ================================================ FILE: views/components/header.tsx ================================================ import { MjmlColumn, MjmlImage, MjmlSection, MjmlSpacer, } from "@faire/mjml-react"; import React from "react"; { /* */ } export const MailmanHeader = (payload: Record) => { const appLogoSrc = payload.config.appLogoSrc; const appName = payload.config.appName; return ( <> ); }; ================================================ FILE: views/components/html.tsx ================================================ import { MjmlText } from "@faire/mjml-react"; import React from "react"; export const RawHtml = (payload: Record) => { return ( ); }; ================================================ FILE: views/components/image.tsx ================================================ import { MjmlImage } from "@faire/mjml-react"; import React from "react"; export const MailmanImage = (payload: Record) => { return ( ); }; ================================================ FILE: views/components/regards.tsx ================================================ import { MjmlColumn, MjmlSection, MjmlText } from "@faire/mjml-react"; import React from "react"; export const MailmanRegards = (payload: Record) => { return ( For any help or support, please feel free to reach out to us at{" "} support@atadel.ca Thank you, Team Atadel ); }; ================================================ FILE: views/components/table.tsx ================================================ import { MjmlColumn, MjmlSection, MjmlTable } from "@faire/mjml-react"; import React from "react"; function toTitleCase(str) { return str .toLowerCase() .split(" ") .map(function (word) { return word.charAt(0).toUpperCase() + word.slice(1); }) .join(" "); } export const MailmanTable = (payload: Record) => { const headings = Object.keys(payload.value[0]); const tableHeader = () => { return ( {headings.map((h) => ( {toTitleCase(h)} ))} ); }; if (payload.vertical) { return ( <> {payload.value.map((obj) => { const values = Object.values(obj) as string[]; return ( {values.map((v, index) => ( {toTitleCase(headings[index])} {v} ))} ); })} ); } else { return ( {payload.heading && tableHeader()} {payload.value.map((obj) => { const values = Object.values(obj) as string[]; return ( {values.map((v) => ( {v} ))} ); })} ); } }; ================================================ FILE: views/components/text.tsx ================================================ import { MjmlText } from "@faire/mjml-react"; import React from "react"; export const TextLine = (payload: Record) => { return ( <> {payload.value} ); }; ================================================ FILE: views/mail/generic.tsx ================================================ import { Mjml } from "@faire/mjml-react"; import { MailmanHead } from "../components/head"; import { MailmanBody } from "../components/body"; import { MailmanHeader } from "../components/header"; import { MailmanFooter } from "../components/footer"; import { MailmanBodyBuilder } from "../components/bodyBuilder"; import React from "react"; export const GenericMail = (payload: Record) => { return ( <> ); }; ================================================ FILE: views/mail/index.tsx ================================================ export * from "./generic";