From befff95eb5ba7dee91d7008a4e50c46f457134e6 Mon Sep 17 00:00:00 2001 From: Julien Oculi Date: Tue, 2 Apr 2024 15:47:06 +0200 Subject: [PATCH] feat(chore): add support of custom mail templates --- cli/send.ts | 90 +++++++++++++++++++++++++++++++++++++++++++++++------ deno.lock | 34 ++++++++++++++++++++ src/send.ts | 9 ++++-- 3 files changed, 121 insertions(+), 12 deletions(-) diff --git a/cli/send.ts b/cli/send.ts index 07a0050..1a8afaa 100644 --- a/cli/send.ts +++ b/cli/send.ts @@ -1,25 +1,60 @@ -import { Command } from '@cliffy/command/mod.ts' +import { Command, EnumType } from '@cliffy/command/mod.ts' +import { Input } from '@cliffy/prompt/mod.ts' import { Contact } from '../src/contact.ts' import { send } from '../src/send.ts' -import type { Mail } from '../types.ts' +import type { Mail, Template } from '../types.ts' +import type { JSX } from 'preact' + +const templates: Map< + string, + Template< + (props: Record) => JSX.Element, + Record + > +> = new Map() + +const templatesDir = import.meta.resolve('../templates').slice('file://'.length) + +//Load templates dynamicaly +for await ( + const template of Deno.readDir(templatesDir) +) { + if (template.isFile && template.name.endsWith('.tsx')) { + const modPath = new URL( + template.name, + `${import.meta.resolve('../templates')}/`, + ) + const mod = await import(modPath.href) + templates.set(mod.default.name, mod.default) + } +} + +const templateType = new EnumType([...templates.keys()]) export const cmd = new Command() .name('send') .description('Send a mail.') + .type('template', templateType) .option( '-f, --from ', 'From mail account or short name from config.', { default: '!me' }, ) + .option('-r, --recipient ', 'Recipient (to) of the mail.', { + required: true, + collect: true, + }) .option('--cc ', 'Copy carbon.', { collect: true }) .option('--cci ', 'Copy carbon invisible.', { collect: true, }) .option('-a, --attachments ', 'Attachments.', { collect: true }) - //.option('-t, --template ', 'HTML template from config', { default: 'message' }) - .arguments(' ') + .option('-t, --template ', 'HTML template from config', { + default: 'message', + }) + .arguments('') .action( - async ({ from, cc, cci, attachments }, to, subject, body) => { + async ({ from, recipient, cc, cci, attachments, template }, subject) => { const fromContact: Contact = await (async () => { if (from === '!me') { const whoami = new Deno.Command('whoami', { @@ -29,10 +64,10 @@ export const cmd = new Command() const rawName = new TextDecoder().decode(stdout).trim() const name = encodeURIComponent(rawName) - return { + return new Contact({ name: `${name} de Cohabit`, address: `_${name}_@cohabit.fr`, - } + }) } if (from.startsWith('!')) { //@ts-ignore try expand @@ -41,11 +76,48 @@ export const cmd = new Command() return Contact.fromString(from) })() + const selectedTemplate = templates.get(template)! + type Props = typeof selectedTemplate + const props: Partial = {} + for (const prop of selectedTemplate.props) { + if (prop.multiline) { + console.log( + `%c?%c ${prop.description} %c[end input with "!EOL" on a new line] %c›`, + 'color: yellow;', + 'font-weight: bold', + 'color: white', + 'color: blue', + ) + + const decoder = new TextDecoder() + for await (const chunk of Deno.stdin.readable) { + const text = decoder.decode(chunk) + if (text.startsWith('!EOF\n')) { + break + } + //@ts-ignore TODO fix type inference + props[prop.tag] += text + } + //@ts-ignore TODO fix type inference + if (props[prop.tag].startsWith('undefined')) { + //@ts-ignore TODO fix type inference + props[prop.tag] = props[prop.tag].slice('undefined'.length) + } + } else { + const value = await Input.prompt({ + message: prop.description, + }) + + //@ts-ignore TODO fix type inference + props[prop.tag] = value + } + } + const mail: Mail = { from: fromContact, - to: [Contact.fromString(to)], + to: recipient.map((to) => Contact.fromString(to)), subject, - body, + body: selectedTemplate.builder(props)!, options: { cc: cc?.map(Contact.fromString) ?? [], cci: cci?.map(Contact.fromString) ?? [], diff --git a/deno.lock b/deno.lock index ac903bd..4901d62 100644 --- a/deno.lock +++ b/deno.lock @@ -3862,15 +3862,30 @@ "https://esm.sh/@types/nodemailer": "https://esm.sh/v135/@types/nodemailer@6.4.14/index.d.ts" }, "remote": { + "https://deno.land/std@0.196.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", "https://deno.land/std@0.196.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", "https://deno.land/std@0.196.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", "https://deno.land/std@0.196.0/console/_data.json": "cf2cc9d039a192b3adbfe64627167c7e6212704c888c25c769fc8f1709e1e1b8", "https://deno.land/std@0.196.0/console/_rle.ts": "56668d5c44f964f1b4ff93f21c9896df42d6ee4394e814db52d6d13f5bb247c7", "https://deno.land/std@0.196.0/console/unicode_width.ts": "10661c0f2eeab802d16b8b85ed8825bbc573991bbfb6affed32dc1ff994f54f9", + "https://deno.land/std@0.196.0/encoding/base64.ts": "144ae6234c1fbe5b68666c711dc15b1e9ee2aef6d42b3b4345bf9a6c91d70d0d", "https://deno.land/std@0.196.0/fmt/colors.ts": "a7eecffdf3d1d54db890723b303847b6e0a1ab4b528ba6958b8f2e754cf1b3bc", + "https://deno.land/std@0.196.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", + "https://deno.land/std@0.196.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", + "https://deno.land/std@0.196.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", + "https://deno.land/std@0.196.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", + "https://deno.land/std@0.196.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", + "https://deno.land/std@0.196.0/path/mod.ts": "f065032a7189404fdac3ad1a1551a9ac84751d2f25c431e101787846c86c79ef", + "https://deno.land/std@0.196.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", + "https://deno.land/std@0.196.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", + "https://deno.land/std@0.196.0/path/win32.ts": "4fca292f8d116fd6d62f243b8a61bd3d6835a9f0ede762ba5c01afe7c3c0aa12", "https://deno.land/std@0.203.0/dotenv/load.ts": "0636983549b98f29ab75c9a22a42d9723f0a389ece5498fe971e7bb2556a12e2", "https://deno.land/std@0.203.0/dotenv/mod.ts": "1da8c6d0e7f7d8a5c2b19400b763bc11739df24acec235dda7ea2cfd3d300057", "https://deno.land/x/cliffy@v1.0.0-rc.3/_utils/distance.ts": "02af166952c7c358ac83beae397aa2fbca4ad630aecfcd38d92edb1ea429f004", + "https://deno.land/x/cliffy@v1.0.0-rc.3/ansi/ansi_escapes.ts": "193b3c3a4e520274bd8322ca4cab1c3ce38070bed1898cb2ade12a585dddd7c9", + "https://deno.land/x/cliffy@v1.0.0-rc.3/ansi/cursor_position.ts": "caa008d29f7a904908bda514f9839bfbb7a93f2d5f5580501675b646d26a87ff", + "https://deno.land/x/cliffy@v1.0.0-rc.3/ansi/deps.ts": "f48ae5d066684793f4a203524db2a9fd61f514527934b458006f3e57363c0215", + "https://deno.land/x/cliffy@v1.0.0-rc.3/ansi/tty.ts": "155aacdcb7dc00f3f95352616a2415c622ffb88db51c5934e5d2e8341eab010b", "https://deno.land/x/cliffy@v1.0.0-rc.3/command/_argument_types.ts": "ab269dacea2030f865a07c2a1e953ec437a64419a05bad1f1ddaab3f99752ead", "https://deno.land/x/cliffy@v1.0.0-rc.3/command/_errors.ts": "12d513ff401020287a344e0830e1297ce1c80c077ecb91e0ac5db44d04a6019c", "https://deno.land/x/cliffy@v1.0.0-rc.3/command/_spread.ts": "0cc6eb70a6df97b5d7d26008822d39f3e8a1232ee0a27f395aa19e68de738245", @@ -3920,6 +3935,25 @@ "https://deno.land/x/cliffy@v1.0.0-rc.3/flags/types/integer.ts": "b60d4d590f309ddddf066782d43e4dc3799f0e7d08e5ede7dc62a5ee94b9a6d9", "https://deno.land/x/cliffy@v1.0.0-rc.3/flags/types/number.ts": "610936e2d29de7c8c304b65489a75ebae17b005c6122c24e791fbed12444d51e", "https://deno.land/x/cliffy@v1.0.0-rc.3/flags/types/string.ts": "e89b6a5ce322f65a894edecdc48b44956ec246a1d881f03e97bbda90dd8638c5", + "https://deno.land/x/cliffy@v1.0.0-rc.3/keycode/_key_codes.ts": "917f0a2da0dbace08cf29bcfdaaa2257da9fe7e705fff8867d86ed69dfb08cfe", + "https://deno.land/x/cliffy@v1.0.0-rc.3/keycode/key_code.ts": "730fa675ca12fc2a99ba718aa8dbebb1f2c89afd47484e30ef3cb705ddfca367", + "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/_figures.ts": "e22413ddd51bb271b6b861a058742e83aaa3f62c14e8162cb73ae6f047062f51", + "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/_generic_input.ts": "870dad97077582439cee26cb19aec123b4850376331338abdc64a91224733cdc", + "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/_generic_list.ts": "8b0bea4521b1e2f62c564e0d3764a63264043694f4228bb0bc0b63ce129ef33b", + "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/_generic_prompt.ts": "4c9d9cdeda749620a3f5332524df13d083e2d59b1ed90a003f43cd0991a75a10", + "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/_generic_suggestions.ts": "5e6ee1190b4dd5af261ae2ff0196dec7f1988ea9c41c6288cfaece293703002c", + "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/_utils.ts": "498ae639d7666599d612b615ee85de9103b3c3a913d5196f6b265072674258c7", + "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/checkbox.ts": "9cfd71f1e278d0ef76054be103d956b66995593902c149380d01b1a1647025f3", + "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/confirm.ts": "ff892331f6de281079421fe2f57f1d56acb38f28bc48678f87a3fc11ef4a5f7c", + "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/deps.ts": "2560142f070bb2668e2e8a74683c799461648b9aad01bbf36b3cad3851d712e6", + "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/input.ts": "81821244f895cc4db32c2511c17e21fb48fd7606e300605aeb2a231ab1680544", + "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/list.ts": "e5d3e1a6d931b9736d03eec2425fb7b4d2b8d1461a84e210b4787edda414dca4", + "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/mod.ts": "f8789193742daf3aba93b543a2ea099383284d60fcccc03567102e28c0d61927", + "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/number.ts": "5421bf1b6411a6f02c44da4e867f19e02315450769e0feacab3c1c88cc1b06d6", + "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/prompt.ts": "f10e1c8a0c2ca093a485f7f1156342210b27a8cffc96fe0b4cff60007cabab30", + "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/secret.ts": "cece271c7ce01e12b249c31c2f9cea9e53b6e6be7621a478dac902bd8f288b61", + "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/select.ts": "c10902aeaca02a55d9b846934958dd166ee39c741faebdaa9800689e402186cf", + "https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/toggle.ts": "028f80de31750e7b5479727a64b4878f090ecd783fe3bb0d286e2e1c29f0eee3", "https://deno.land/x/cliffy@v1.0.0-rc.3/table/_layout.ts": "e4a518da28333de95ad791208b9930025987c8b93d5f8b7f30b377b3e26b24e1", "https://deno.land/x/cliffy@v1.0.0-rc.3/table/_utils.ts": "fd48d1a524a42e72aa3ad2eec858a92f5a00728d306c7e8436fba6c34314fee6", "https://deno.land/x/cliffy@v1.0.0-rc.3/table/border.ts": "5c6e9ef5078c6930169aacb668b274bdbb498461c724a7693ac9270fe9d3f5d5", diff --git a/src/send.ts b/src/send.ts index f8dba5f..84568aa 100644 --- a/src/send.ts +++ b/src/send.ts @@ -1,15 +1,18 @@ import type { Mail } from '../types.ts' +import { renderTemplate } from './template.tsx' import { transporter } from './transporter.ts' -export function send(mail: Mail) { +export async function send(mail: Mail) { + const { html, text } = await renderTemplate(mail.body) + return transporter.sendMail({ from: mail.from.toString(), to: mail.to.map((contact) => contact.toString()), cc: mail.options.cc.map((cc) => cc.toString()), bcc: mail.options.cci.map((cci) => cci.toString()), subject: mail.subject, - text: 'Text only', - html: mail.body.toString(), + text, + html, attachments: mail.options.attachments.map((path) => ({ path })), }) }