initial commit

This commit is contained in:
Julien Oculi 2024-03-28 22:32:46 +01:00
commit 5e4d833378
12 changed files with 4238 additions and 0 deletions

67
cli/send.ts Normal file
View 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
View 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
View 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"
}
}

3896
deno.lock Normal file

File diff suppressed because it is too large Load diff

1
mod.ts Normal file
View file

@ -0,0 +1 @@

0
send.exe Normal file
View file

74
src/contact.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
}