2024-04-02 15:47:06 +02:00
|
|
|
|
import { Command, EnumType } from '@cliffy/command/mod.ts'
|
|
|
|
|
import { Input } from '@cliffy/prompt/mod.ts'
|
2024-03-28 22:32:46 +01:00
|
|
|
|
import { Contact } from '../src/contact.ts'
|
2024-03-29 15:58:19 +01:00
|
|
|
|
import { send } from '../src/send.ts'
|
2024-04-02 15:47:06 +02:00
|
|
|
|
import type { Mail, Template } from '../types.ts'
|
|
|
|
|
import type { JSX } from 'preact'
|
2024-04-03 17:11:03 +02:00
|
|
|
|
import { fromFileUrl } from '@std/path'
|
2024-04-02 15:47:06 +02:00
|
|
|
|
|
|
|
|
|
const templates: Map<
|
|
|
|
|
string,
|
|
|
|
|
Template<
|
|
|
|
|
(props: Record<string, unknown>) => JSX.Element,
|
|
|
|
|
Record<string, unknown>
|
|
|
|
|
>
|
|
|
|
|
> = new Map()
|
|
|
|
|
|
2024-04-03 17:11:03 +02:00
|
|
|
|
const templatesDirUrl = import.meta.resolve('../templates')
|
|
|
|
|
const templatesDir = fromFileUrl(templatesDirUrl)
|
2024-04-02 15:47:06 +02:00
|
|
|
|
|
|
|
|
|
//Load templates dynamicaly
|
|
|
|
|
for await (
|
|
|
|
|
const template of Deno.readDir(templatesDir)
|
|
|
|
|
) {
|
2024-04-03 17:15:23 +02:00
|
|
|
|
if (
|
|
|
|
|
template.isFile &&
|
|
|
|
|
template.name.endsWith('.tsx') &&
|
|
|
|
|
!template.name.startsWith('_')
|
|
|
|
|
) {
|
2024-04-02 15:47:06 +02:00
|
|
|
|
const modPath = new URL(
|
|
|
|
|
template.name,
|
|
|
|
|
`${import.meta.resolve('../templates')}/`,
|
|
|
|
|
)
|
|
|
|
|
const mod = await import(modPath.href)
|
|
|
|
|
templates.set(mod.default.name, mod.default)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-03 12:49:00 +02:00
|
|
|
|
//TODO completions for "--from"
|
|
|
|
|
//TODO require sudo for "--from !== !me"
|
|
|
|
|
|
2024-04-02 15:47:06 +02:00
|
|
|
|
const templateType = new EnumType([...templates.keys()])
|
2024-03-28 22:32:46 +01:00
|
|
|
|
|
|
|
|
|
export const cmd = new Command()
|
|
|
|
|
.name('send')
|
|
|
|
|
.description('Send a mail.')
|
2024-04-02 15:47:06 +02:00
|
|
|
|
.type('template', templateType)
|
2024-03-28 22:32:46 +01:00
|
|
|
|
.option(
|
|
|
|
|
'-f, --from <account:string>',
|
|
|
|
|
'From mail account or short name from config.',
|
|
|
|
|
{ default: '!me' },
|
|
|
|
|
)
|
2024-04-02 15:47:06 +02:00
|
|
|
|
.option('-r, --recipient <recipient:string>', 'Recipient (to) of the mail.', {
|
|
|
|
|
required: true,
|
|
|
|
|
collect: true,
|
|
|
|
|
})
|
2024-03-28 22:32:46 +01:00
|
|
|
|
.option('--cc <recipient:string>', 'Copy carbon.', { collect: true })
|
|
|
|
|
.option('--cci <recipient:string>', 'Copy carbon invisible.', {
|
|
|
|
|
collect: true,
|
|
|
|
|
})
|
|
|
|
|
.option('-a, --attachments <file:file>', 'Attachments.', { collect: true })
|
2024-04-02 15:47:06 +02:00
|
|
|
|
.option('-t, --template <name:template>', 'HTML template from config', {
|
|
|
|
|
default: 'message',
|
|
|
|
|
})
|
|
|
|
|
.arguments('<subject:string>')
|
2024-03-28 22:32:46 +01:00
|
|
|
|
.action(
|
2024-04-02 15:47:06 +02:00
|
|
|
|
async ({ from, recipient, cc, cci, attachments, template }, subject) => {
|
2024-03-28 22:32:46 +01:00
|
|
|
|
const fromContact: Contact = await (async () => {
|
|
|
|
|
if (from === '!me') {
|
|
|
|
|
const whoami = new Deno.Command('whoami', {
|
|
|
|
|
stderr: 'inherit',
|
|
|
|
|
})
|
|
|
|
|
const { stdout } = await whoami.output()
|
|
|
|
|
const rawName = new TextDecoder().decode(stdout).trim()
|
|
|
|
|
const name = encodeURIComponent(rawName)
|
|
|
|
|
|
2024-04-02 15:47:06 +02:00
|
|
|
|
return new Contact({
|
2024-03-28 22:32:46 +01:00
|
|
|
|
name: `${name} de Cohabit`,
|
|
|
|
|
address: `_${name}_@cohabit.fr`,
|
2024-04-02 15:47:06 +02:00
|
|
|
|
})
|
2024-03-28 22:32:46 +01:00
|
|
|
|
}
|
|
|
|
|
if (from.startsWith('!')) {
|
|
|
|
|
//@ts-ignore try expand
|
|
|
|
|
return expand(from.slice(1))
|
|
|
|
|
}
|
|
|
|
|
return Contact.fromString(from)
|
|
|
|
|
})()
|
|
|
|
|
|
2024-04-02 15:47:06 +02:00
|
|
|
|
const selectedTemplate = templates.get(template)!
|
|
|
|
|
type Props = typeof selectedTemplate
|
|
|
|
|
const props: Partial<Props> = {}
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-29 15:58:19 +01:00
|
|
|
|
const mail: Mail = {
|
|
|
|
|
from: fromContact,
|
2024-04-02 15:47:06 +02:00
|
|
|
|
to: recipient.map((to) => Contact.fromString(to)),
|
2024-03-29 15:58:19 +01:00
|
|
|
|
subject,
|
2024-04-02 15:47:06 +02:00
|
|
|
|
body: selectedTemplate.builder(props)!,
|
2024-03-29 15:58:19 +01:00
|
|
|
|
options: {
|
|
|
|
|
cc: cc?.map(Contact.fromString) ?? [],
|
|
|
|
|
cci: cci?.map(Contact.fromString) ?? [],
|
|
|
|
|
attachments: attachments ?? [],
|
|
|
|
|
},
|
|
|
|
|
}
|
2024-03-28 22:32:46 +01:00
|
|
|
|
|
|
|
|
|
await send(mail)
|
|
|
|
|
},
|
|
|
|
|
)
|