initial commit
This commit is contained in:
commit
5e4d833378
67
cli/send.ts
Normal file
67
cli/send.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { Command } from '@cliffy/command/mod.ts'
|
||||||
|
import { format } from '../src/format.ts'
|
||||||
|
import { Contact } from '../src/contact.ts'
|
||||||
|
// import { send } from '../src/send.ts'
|
||||||
|
import { send } from '../src/transporter.ts'
|
||||||
|
|
||||||
|
export const cmd = new Command()
|
||||||
|
.name('send')
|
||||||
|
.description('Send a mail.')
|
||||||
|
.option(
|
||||||
|
'-f, --from <account:string>',
|
||||||
|
'From mail account or short name from config.',
|
||||||
|
{ default: '!me' },
|
||||||
|
)
|
||||||
|
.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 })
|
||||||
|
.option('--content-type <type:string>', 'Mail body Content-Type.', {
|
||||||
|
default: 'text/html; charset=utf-8',
|
||||||
|
})
|
||||||
|
//.option('-t, --template <name:template>', 'HTML template from config', { default: 'basic' })
|
||||||
|
.arguments('<to:string> <subject:string> <body:string>')
|
||||||
|
.action(
|
||||||
|
async ({ from, cc, cci, attachments, contentType }, to, subject, body) => {
|
||||||
|
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)
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: `${name} de Cohabit`,
|
||||||
|
address: `_${name}_@cohabit.fr`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (from.startsWith('!')) {
|
||||||
|
//@ts-ignore try expand
|
||||||
|
return expand(from.slice(1))
|
||||||
|
}
|
||||||
|
return Contact.fromString(from)
|
||||||
|
})()
|
||||||
|
|
||||||
|
const toContact = Contact.fromString(to)
|
||||||
|
|
||||||
|
const mail = format(fromContact, toContact, subject, body, {
|
||||||
|
cc: cc?.map(Contact.fromString) ?? [],
|
||||||
|
cci: cci?.map(Contact.fromString) ?? [],
|
||||||
|
attachments: attachments ?? [],
|
||||||
|
contentType,
|
||||||
|
})
|
||||||
|
|
||||||
|
await send(mail)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
if (Deno.args.length) {
|
||||||
|
cmd.parse(Deno.args)
|
||||||
|
} else {
|
||||||
|
cmd.showHelp()
|
||||||
|
}
|
||||||
|
}
|
22
config/account.json
Normal file
22
config/account.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"contact": {
|
||||||
|
"name": "FabLab Cohabit",
|
||||||
|
"address": "contact@cohabit.fr"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"name": "Admin Cohabit",
|
||||||
|
"address": "admin@cohabit.fr"
|
||||||
|
},
|
||||||
|
"dev": {
|
||||||
|
"name": "Dev Cohabit",
|
||||||
|
"address": "dev@cohabit.fr"
|
||||||
|
},
|
||||||
|
"bot": {
|
||||||
|
"name": "Bot Cohabit",
|
||||||
|
"address": "bot@cohabit.fr"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"name": "Test Cohabit",
|
||||||
|
"address": "test@cohabit.fr"
|
||||||
|
}
|
||||||
|
}
|
23
deno.json
Normal file
23
deno.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"tasks": {
|
||||||
|
"compile:send": "deno compile --allow-read --allow-env --allow-sys=osRelease --allow-run=mail,whoami ./cli/send.ts",
|
||||||
|
"cli:send": "deno run --allow-read --allow-env --allow-sys=osRelease,networkInterfaces --allow-run=/usr/sbin/sendmail,whoami ./cli/send.ts",
|
||||||
|
"utils:scp": "scp -O -P 55555 -i C:/Users/Julien/.ssh/id_ed25519_cohabit -r $(pwd) julien@cohabit.fr:/home/julien/cohabit_mail"
|
||||||
|
},
|
||||||
|
"fmt": {
|
||||||
|
"singleQuote": true,
|
||||||
|
"semiColons": false,
|
||||||
|
"useTabs": true
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"@cliffy/": "https://deno.land/x/cliffy@v1.0.0-rc.3/",
|
||||||
|
"jsx-email": "npm:jsx-email@^1.10.12",
|
||||||
|
"nodemailer": "npm:nodemailer@^6.9.13",
|
||||||
|
"nodemailer-smime": "npm:nodemailer-smime@^1.1.0",
|
||||||
|
"preact": "npm:preact@^10.20.1"
|
||||||
|
},
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "preact"
|
||||||
|
}
|
||||||
|
}
|
74
src/contact.ts
Normal file
74
src/contact.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import custom from '../config/account.json' with { type: 'json' }
|
||||||
|
|
||||||
|
export class Contact {
|
||||||
|
#name: string
|
||||||
|
#address: `${string}@${string}.${string}`
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
{ name, address }: {
|
||||||
|
name: string
|
||||||
|
address: `${string}@${string}.${string}`
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
this.#name = name
|
||||||
|
this.#address = address
|
||||||
|
}
|
||||||
|
|
||||||
|
static expand(shortName: string): Contact {
|
||||||
|
if (!(shortName in custom)) {
|
||||||
|
throw new Error('unknown short name contact')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, address } = custom[shortName as keyof typeof custom]
|
||||||
|
|
||||||
|
if (name !== 'string') {
|
||||||
|
throw new SyntaxError(
|
||||||
|
`missing key "name" in contact short name config for "${shortName}"`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (address !== 'string') {
|
||||||
|
throw new SyntaxError(
|
||||||
|
`missing key "address" in contact short name config for "${shortName}"`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!(/\w+@(\w+\.)?cohabit\.fr/.test(address))) {
|
||||||
|
throw new SyntaxError(
|
||||||
|
`invalid "address" in contact short name config for "${shortName}"`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//@ts-ignore checked against regex
|
||||||
|
return new Contact({ name, address })
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromString(raw: string): Contact {
|
||||||
|
const [_, name, address] =
|
||||||
|
raw.match(/(.*)<(\S+@\S+\.\S+)>/)?.map((match) => match.trim()) ?? []
|
||||||
|
|
||||||
|
if (typeof address !== 'string') {
|
||||||
|
throw new Error('address is empty')
|
||||||
|
}
|
||||||
|
|
||||||
|
//@ts-ignore checked against regex
|
||||||
|
return new Contact({ name, address })
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return `${this.#name} <${this.#address}>`
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
name: this.#name,
|
||||||
|
address: this.#address,
|
||||||
|
} as const
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.#name
|
||||||
|
}
|
||||||
|
|
||||||
|
get address() {
|
||||||
|
return this.#address
|
||||||
|
}
|
||||||
|
}
|
25
src/format.ts
Normal file
25
src/format.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { Mail, Options } from '../types.ts'
|
||||||
|
import { Contact } from './contact.ts'
|
||||||
|
|
||||||
|
export function format(
|
||||||
|
from: Contact,
|
||||||
|
to: Contact | Contact[],
|
||||||
|
subject: string,
|
||||||
|
body: string,
|
||||||
|
options: Partial<Options>,
|
||||||
|
): Mail {
|
||||||
|
const defaultOptions: Options = {
|
||||||
|
from,
|
||||||
|
cc: [],
|
||||||
|
cci: [],
|
||||||
|
attachments: [],
|
||||||
|
contentType: 'text/html; charset=utf-8',
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
to: Array.isArray(to) ? to : [to],
|
||||||
|
subject,
|
||||||
|
body,
|
||||||
|
options: { ...defaultOptions, ...options },
|
||||||
|
}
|
||||||
|
}
|
50
src/send.ts
Normal file
50
src/send.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { render } from 'jsx-email'
|
||||||
|
import { Mail } from '../types.ts'
|
||||||
|
|
||||||
|
export async function send(mail: Mail) {
|
||||||
|
const args: string[][] = [
|
||||||
|
['-s', mail.subject],
|
||||||
|
['-a', `From: ${mail.options.from}`],
|
||||||
|
['-a', `To: ${mail.to.map((contact) => contact.toString()).join(', ')}`],
|
||||||
|
['-a', 'MIME-Version: 1.0'],
|
||||||
|
['-a', `Content-Type: ${mail.options.contentType}`],
|
||||||
|
]
|
||||||
|
|
||||||
|
if (mail.options.cc.length) {
|
||||||
|
args.push([
|
||||||
|
'-a',
|
||||||
|
`CC:${mail.options.cc.map((cc) => cc.toString()).join(', ')}`,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
if (mail.options.cci.length) {
|
||||||
|
args.push([
|
||||||
|
'-a',
|
||||||
|
`BCC:${mail.options.cci.map((cci) => cci.toString()).join(', ')}`,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
if (mail.options.attachments.length) {
|
||||||
|
mail.options.attachments.forEach((attachment) =>
|
||||||
|
args.push([`-A=${attachment}`])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
args.push([mail.to.map((account) => account.address).join(', ')])
|
||||||
|
|
||||||
|
const cmd = new Deno.Command('/usr/bin/mail', {
|
||||||
|
args: args.flat(),
|
||||||
|
stdin: 'piped',
|
||||||
|
stderr: 'inherit',
|
||||||
|
stdout: 'inherit',
|
||||||
|
})
|
||||||
|
|
||||||
|
const process = cmd.spawn()
|
||||||
|
|
||||||
|
const _body = typeof mail.body === 'string'
|
||||||
|
? mail.body
|
||||||
|
: await render(mail.body)
|
||||||
|
|
||||||
|
const writer = process.stdin.getWriter()
|
||||||
|
await writer.write(new TextEncoder().encode(mail.body.toString()))
|
||||||
|
await writer.close()
|
||||||
|
|
||||||
|
await process.output()
|
||||||
|
}
|
27
src/template.tsx
Normal file
27
src/template.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { Body, Column, Html, render, Section } from 'jsx-email'
|
||||||
|
|
||||||
|
function Email() {
|
||||||
|
return (
|
||||||
|
<Html lang='en'>
|
||||||
|
<Body style={{ backgroundColor: '#61dafb' }}>
|
||||||
|
<Section>
|
||||||
|
<Column style={{ width: '50%' }}>First column</Column>
|
||||||
|
<Column style={{ width: '50%' }}>Second column</Column>
|
||||||
|
</Section>
|
||||||
|
</Body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(await render(Email))
|
||||||
|
// import { render } from 'npm:preact-render-to-string'
|
||||||
|
|
||||||
|
// function Email() {
|
||||||
|
// return (
|
||||||
|
// <html>
|
||||||
|
// <p>Test</p>
|
||||||
|
// </html>
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
console.log(await render(<Email />))
|
36
src/transporter.ts
Normal file
36
src/transporter.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// @deno-types="npm:@types/nodemailer"
|
||||||
|
import nodemailer from 'nodemailer'
|
||||||
|
// import smime from 'nodemailer-smime'
|
||||||
|
import { Mail } from '../types.ts'
|
||||||
|
|
||||||
|
const dkimPrivateKey = await Deno.readTextFile(
|
||||||
|
'/home/julien/dkim_sendmail_keys/dkim_sendmail_cohabit_fr.pem',
|
||||||
|
)
|
||||||
|
|
||||||
|
// const smimeOptions = {
|
||||||
|
// cert: '',
|
||||||
|
// chain: [''],
|
||||||
|
// key: '',
|
||||||
|
// }
|
||||||
|
|
||||||
|
export const transporter = nodemailer.createTransport({
|
||||||
|
sendmail: true,
|
||||||
|
newline: 'unix',
|
||||||
|
path: '/usr/sbin/sendmail',
|
||||||
|
dkim: {
|
||||||
|
domainName: 'cohabit.fr',
|
||||||
|
keySelector: 'sendmailY2024M03',
|
||||||
|
privateKey: dkimPrivateKey,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// .use('stream', smime(smimeOptions))
|
||||||
|
|
||||||
|
export function send(mail: Mail) {
|
||||||
|
return transporter.sendMail({
|
||||||
|
from: mail.options.from.toString(),
|
||||||
|
to: mail.to.map((contact) => contact.toString()).join(', '),
|
||||||
|
subject: mail.subject,
|
||||||
|
text: 'Text only',
|
||||||
|
html: mail.body.toString(),
|
||||||
|
})
|
||||||
|
}
|
17
types.ts
Normal file
17
types.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import type { JSX } from 'preact'
|
||||||
|
import { Contact } from './src/contact.ts'
|
||||||
|
|
||||||
|
export type Options = {
|
||||||
|
from: Contact
|
||||||
|
cc: Contact[]
|
||||||
|
cci: Contact[]
|
||||||
|
attachments: string[]
|
||||||
|
contentType: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Mail = {
|
||||||
|
to: Contact[]
|
||||||
|
subject: string
|
||||||
|
body: string | JSX.Element
|
||||||
|
options: Options
|
||||||
|
}
|
Loading…
Reference in a new issue