Compare commits
28 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a090186c54 | ||
|
|
9c948f1f3e | ||
|
|
ee0c106a2c | ||
|
|
2f095b79c5 | ||
|
|
52f9438f6c | ||
|
|
64fe0ab3d7 | ||
|
|
d33dd4539a | ||
|
|
486a3da213 | ||
|
|
34764ad6b2 | ||
|
|
7006664fc4 | ||
|
|
b4954163b2 | ||
|
|
b033031d39 | ||
|
|
6659fe5ee6 | ||
|
|
cbb26e87ce | ||
|
|
54ceae3e71 | ||
|
|
b437863f8b | ||
|
|
6eaf3b6083 | ||
|
|
a88178c2ce | ||
|
|
09f84fc493 | ||
|
|
27519378e5 | ||
|
|
2b29a4c9e8 | ||
|
|
dc000ac4de | ||
|
|
5e126ebcaf | ||
|
|
92b8ece1c3 | ||
|
|
495e5e2e7e | ||
|
|
901c8fd672 | ||
|
|
fd1011619c | ||
|
|
2f663f0e08 |
19
README.md
19
README.md
|
|
@ -8,6 +8,20 @@ Mail cli for Coh@bit.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
### From [jsr](https://jsr.io)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
deno install \
|
||||||
|
--allow-read \
|
||||||
|
--allow-env \
|
||||||
|
--allow-net=0.0.0.0 \
|
||||||
|
--allow-sys=osRelease,networkInterfaces \
|
||||||
|
--allow-run=/usr/sbin/sendmail,whoami \
|
||||||
|
--global --force --name=cohamail jsr:@cohabit/mailer/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
### From sources
|
||||||
|
|
||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
>
|
>
|
||||||
> Currently bin manually added to [releases](./releases) tab. Prefer `git clone`
|
> Currently bin manually added to [releases](./releases) tab. Prefer `git clone`
|
||||||
|
|
@ -28,3 +42,8 @@ Mail cli for Coh@bit.
|
||||||
# OR
|
# OR
|
||||||
./cohamail <subcommand> -h
|
./cohamail <subcommand> -h
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
You can override default config and templates with your own in
|
||||||
|
`/etc/cohabit/mailer/(config|templates)/`.
|
||||||
|
|
|
||||||
22
cli.ts
22
cli.ts
|
|
@ -1,14 +1,34 @@
|
||||||
import config from './deno.json' with { type: 'json' }
|
import config from './deno.json' with { type: 'json' }
|
||||||
import { Command } from '@cliffy/command/command.ts'
|
import { Command } from '@cliffy/command'
|
||||||
|
import { UpgradeCommand } from '@cliffy/command/upgrade'
|
||||||
|
import { JsrProvider } from '@cliffy/command/upgrade/provider/jsr'
|
||||||
import { cmd as send } from './cli/send.ts'
|
import { cmd as send } from './cli/send.ts'
|
||||||
import { cmd as preview } from './cli/preview.ts'
|
import { cmd as preview } from './cli/preview.ts'
|
||||||
|
|
||||||
|
const upgradeCommand = new UpgradeCommand({
|
||||||
|
args: [
|
||||||
|
'--allow-read',
|
||||||
|
'--allow-env',
|
||||||
|
'--allow-net=0.0.0.0',
|
||||||
|
'--allow-sys=osRelease,networkInterfaces',
|
||||||
|
'--allow-run=/usr/sbin/sendmail,whoami',
|
||||||
|
'--name=cohamail',
|
||||||
|
],
|
||||||
|
provider: [
|
||||||
|
new JsrProvider({
|
||||||
|
package: config.name as `@${string}/${string}`,
|
||||||
|
main: 'cli',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
const cli = new Command()
|
const cli = new Command()
|
||||||
.name('cohamail')
|
.name('cohamail')
|
||||||
.description('Mail cli for coh@bit.')
|
.description('Mail cli for coh@bit.')
|
||||||
.version(config.version)
|
.version(config.version)
|
||||||
.command('preview', preview)
|
.command('preview', preview)
|
||||||
.command('send', send)
|
.command('send', send)
|
||||||
|
.command('upgrade', upgradeCommand)
|
||||||
|
|
||||||
if (import.meta.main) {
|
if (import.meta.main) {
|
||||||
if (Deno.args.length) {
|
if (Deno.args.length) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Input } from '@cliffy/prompt/input.ts'
|
import { Input } from '@cliffy/prompt'
|
||||||
import type { Template } from '../types.ts'
|
import type { Template } from '../types.ts'
|
||||||
import type { JSX } from 'preact'
|
import type { JSX } from 'preact'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { fromFileUrl } from '@std/path'
|
import { exists } from '@std/fs/exists'
|
||||||
import type { JSX } from 'preact'
|
import type { JSX } from 'preact'
|
||||||
import { EnumType } from '@cliffy/command/mod.ts'
|
import { EnumType } from '@cliffy/command'
|
||||||
import type { Template } from '../types.ts'
|
import type { Template } from '../types.ts'
|
||||||
|
import * as defaultTemplates from '../templates/mod.ts'
|
||||||
|
|
||||||
export const templates: Map<
|
export const templates: Map<
|
||||||
string,
|
string,
|
||||||
|
|
@ -11,25 +12,38 @@ export const templates: Map<
|
||||||
>
|
>
|
||||||
> = new Map()
|
> = new Map()
|
||||||
|
|
||||||
const templatesDirUrl = import.meta.resolve('../templates')
|
//Load default templates
|
||||||
const templatesDir = fromFileUrl(templatesDirUrl)
|
for (const template of Object.values(defaultTemplates)) {
|
||||||
|
//@ts-expect-error types are checked at runtime later
|
||||||
|
templates.set(template.name, template)
|
||||||
|
}
|
||||||
|
|
||||||
//Load templates dynamicaly
|
//Load local templates
|
||||||
for await (
|
const basePath = '/etc/cohabit/mailer/templates'
|
||||||
const template of Deno.readDir(templatesDir)
|
|
||||||
) {
|
if (await exists(basePath)) {
|
||||||
if (
|
for await (const template of Deno.readDir(basePath)) {
|
||||||
template.isFile &&
|
if (!template.isFile) continue
|
||||||
template.name.endsWith('.tsx') &&
|
if (!template.name.endsWith('.tsx')) continue
|
||||||
!template.name.startsWith('_')
|
if (template.name.startsWith('_')) continue
|
||||||
) {
|
|
||||||
const modPath = new URL(
|
const mod = await import(`${basePath}/${template.name}`).catch(
|
||||||
template.name,
|
checkFsErrors,
|
||||||
`${import.meta.resolve('../templates')}/`,
|
|
||||||
)
|
)
|
||||||
const mod = await import(modPath.href)
|
|
||||||
templates.set(mod.default.name, mod.default)
|
templates.set(mod.default.name, mod.default)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkFsErrors(error: Error) {
|
||||||
|
if (error instanceof Deno.errors.PermissionDenied) {
|
||||||
|
throw new Error(
|
||||||
|
'unable to load config file due to read access permissions issues',
|
||||||
|
{
|
||||||
|
cause: error,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
export const templateType = new EnumType([...templates.keys()])
|
export const templateType = new EnumType([...templates.keys()])
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Command } from '@cliffy/command/mod.ts'
|
import { Command } from '@cliffy/command'
|
||||||
import { renderTemplate } from '../src/template.tsx'
|
import { renderTemplate } from '../src/template.tsx'
|
||||||
import { templates, templateType } from './_templates_loader.ts'
|
import { templates, templateType } from './_templates_loader.ts'
|
||||||
import { promptProps } from './_prompt_template.ts'
|
import { promptProps } from './_prompt_template.ts'
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Command } from '@cliffy/command/mod.ts'
|
import { Command } from '@cliffy/command'
|
||||||
import { Contact } from '../src/contact.ts'
|
import { Contact } from '../src/contact.ts'
|
||||||
import { send } from '../src/send.ts'
|
import { send } from '../src/send.ts'
|
||||||
import type { Mail } from '../types.ts'
|
import type { Mail } from '../types.ts'
|
||||||
|
|
|
||||||
5
config/dkim.json
Normal file
5
config/dkim.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"domainName": "cohabit.fr",
|
||||||
|
"keySelector": "sendmailY2024M03",
|
||||||
|
"privateKey": "/etc/ssl/certs/dkim_keys/dkimMailKey.pem"
|
||||||
|
}
|
||||||
49
config/loader.ts
Normal file
49
config/loader.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import type { JsonValue } from '@std/json'
|
||||||
|
import defaultAccounts from './account.json' with { type: 'json' }
|
||||||
|
import defaultDkim from './dkim.json' with { type: 'json' }
|
||||||
|
|
||||||
|
function checkFsErrors(error: Error) {
|
||||||
|
if (error instanceof Deno.errors.NotFound) {
|
||||||
|
//use default config file
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (error instanceof Deno.errors.PermissionDenied) {
|
||||||
|
throw new Error(
|
||||||
|
'unable to load config file due to read access permissions issues',
|
||||||
|
{
|
||||||
|
cause: error,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
type JsonRecord = Record<string, JsonValue>
|
||||||
|
export type DkimRecord = {
|
||||||
|
domainName: string
|
||||||
|
keySelector: string
|
||||||
|
privateKey: string
|
||||||
|
}
|
||||||
|
export type AccountRecord = Record<string, {
|
||||||
|
name: string
|
||||||
|
address: string
|
||||||
|
}>
|
||||||
|
|
||||||
|
async function readJsonFile<
|
||||||
|
T extends JsonRecord,
|
||||||
|
>(path: string, defaultValue: T): Promise<T> {
|
||||||
|
const file = await Deno.readTextFile(path).catch(checkFsErrors)
|
||||||
|
const json = JSON.parse(file ?? '{}')
|
||||||
|
return { ...defaultValue, ...json }
|
||||||
|
}
|
||||||
|
|
||||||
|
const basePath = '/ect/cohabit/mailer/config'
|
||||||
|
|
||||||
|
export const accounts = await readJsonFile<AccountRecord>(
|
||||||
|
`${basePath}/account.json`,
|
||||||
|
defaultAccounts,
|
||||||
|
)
|
||||||
|
export const dkim = await readJsonFile<DkimRecord>(
|
||||||
|
`${basePath}/dkim.json`,
|
||||||
|
defaultDkim,
|
||||||
|
)
|
||||||
22
deno.json
22
deno.json
|
|
@ -1,15 +1,15 @@
|
||||||
{
|
{
|
||||||
"name": "@cohabit/cohamail",
|
"name": "@cohabit/mailer",
|
||||||
"version": "0.1.2",
|
"version": "0.5.2",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./mod.ts",
|
".": "./mod.ts",
|
||||||
"./cli": "./cli.ts",
|
"./cli": "./cli.ts",
|
||||||
"./types": "./types.ts"
|
"./types": "./types.ts",
|
||||||
|
"./templates": "./templates/mod.ts"
|
||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"compile": "deno compile --allow-read --allow-env --allow-net=0.0.0.0 --allow-sys=osRelease,networkInterfaces --allow-run=/usr/sbin/sendmail,whoami --output=bin/cohamail --target=x86_64-unknown-linux-gnu ./cli.ts",
|
"compile": "deno compile --allow-read --allow-env --allow-net=0.0.0.0,jsr.io --allow-sys=osRelease,networkInterfaces --allow-run=/usr/sbin/sendmail,whoami --output=bin/mailer --target=x86_64-unknown-linux-gnu ./cli.ts",
|
||||||
"cli": "deno run --allow-read --allow-env --allow-net=0.0.0.0 --allow-sys=osRelease,networkInterfaces --allow-run=/usr/sbin/sendmail,whoami ./cli.ts",
|
"cli": "deno run --allow-read --allow-env --allow-net=0.0.0.0,jsr.io --allow-sys=osRelease,networkInterfaces --allow-run=/usr/sbin/sendmail,whoami ./cli.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": {
|
"fmt": {
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
|
|
@ -17,8 +17,11 @@
|
||||||
"useTabs": true
|
"useTabs": true
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"@cliffy/": "https://deno.land/x/cliffy@v1.0.0-rc.3/",
|
"@cliffy/command": "jsr:@cliffy/command@^1.0.0-rc.5",
|
||||||
"@std/path": "jsr:@std/path@^0.221.0",
|
"@cliffy/prompt": "jsr:@cliffy/prompt@^1.0.0-rc.5",
|
||||||
|
"@std/fs": "jsr:@std/fs@^1.0.13",
|
||||||
|
"@std/json": "jsr:@std/json@^1.0.1",
|
||||||
|
"@types/nodemailer": "npm:@types/nodemailer@^6.4.15",
|
||||||
"jsx-email": "npm:jsx-email@^1.10.12",
|
"jsx-email": "npm:jsx-email@^1.10.12",
|
||||||
"nodemailer": "npm:nodemailer@^6.9.13",
|
"nodemailer": "npm:nodemailer@^6.9.13",
|
||||||
"nodemailer-smime": "npm:nodemailer-smime@^1.1.0",
|
"nodemailer-smime": "npm:nodemailer-smime@^1.1.0",
|
||||||
|
|
@ -28,5 +31,6 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "preact"
|
"jsxImportSource": "preact"
|
||||||
}
|
},
|
||||||
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import custom from '../config/account.json' with { type: 'json' }
|
import { accounts, dkim } from '../config/loader.ts'
|
||||||
|
|
||||||
|
export type AddressString = `${string}@${string}.${string}`
|
||||||
|
|
||||||
export class Contact {
|
export class Contact {
|
||||||
#name: string
|
#name: string
|
||||||
#address: `${string}@${string}.${string}`
|
#address: AddressString
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
{ name, address }: {
|
{ name, address }: {
|
||||||
name: string
|
name: string
|
||||||
address: `${string}@${string}.${string}`
|
address: AddressString
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
this.#name = name
|
this.#name = name
|
||||||
|
|
@ -15,11 +17,11 @@ export class Contact {
|
||||||
}
|
}
|
||||||
|
|
||||||
static expand(shortName: string): Contact {
|
static expand(shortName: string): Contact {
|
||||||
if (!(shortName in custom)) {
|
if (!(shortName in accounts)) {
|
||||||
throw new Error('unknown short name contact')
|
throw new Error('unknown short name contact')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, address } = custom[shortName as keyof typeof custom]
|
const { name, address } = accounts[shortName]
|
||||||
|
|
||||||
if (typeof name !== 'string') {
|
if (typeof name !== 'string') {
|
||||||
throw new SyntaxError(
|
throw new SyntaxError(
|
||||||
|
|
@ -31,7 +33,9 @@ export class Contact {
|
||||||
`missing key "address" in contact short name config for "${shortName}"`,
|
`missing key "address" in contact short name config for "${shortName}"`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!(/\w+@(\w+\.)?cohabit\.fr/.test(address))) {
|
|
||||||
|
const addressRegExp = new RegExp(String.raw`\w+@(\w+\.)?${dkim.domainName}`)
|
||||||
|
if (!(addressRegExp.test(address))) {
|
||||||
throw new SyntaxError(
|
throw new SyntaxError(
|
||||||
`invalid "address" in contact short name config for "${shortName}"`,
|
`invalid "address" in contact short name config for "${shortName}"`,
|
||||||
)
|
)
|
||||||
|
|
@ -57,18 +61,18 @@ export class Contact {
|
||||||
return `${this.#name} <${this.#address}>`
|
return `${this.#name} <${this.#address}>`
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON(): { name: string; address: AddressString } {
|
||||||
return {
|
return {
|
||||||
name: this.#name,
|
name: this.#name,
|
||||||
address: this.#address,
|
address: this.#address,
|
||||||
} as const
|
} as const
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
get name(): string {
|
||||||
return this.#name
|
return this.#name
|
||||||
}
|
}
|
||||||
|
|
||||||
get address() {
|
get address(): AddressString {
|
||||||
return this.#address
|
return this.#address
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
|
import type SendmailTransport from 'nodemailer'
|
||||||
import type { Mail } from '../types.ts'
|
import type { Mail } from '../types.ts'
|
||||||
import { renderTemplate } from './template.tsx'
|
import { renderTemplate } from './template.tsx'
|
||||||
import { transporter } from './transporter.ts'
|
import { transporter } from './transporter.ts'
|
||||||
|
|
||||||
export async function send(mail: Mail) {
|
export async function send(
|
||||||
|
mail: Mail,
|
||||||
|
): Promise<SendmailTransport.SentMessageInfo> {
|
||||||
const { html, text } = await renderTemplate(mail.body)
|
const { html, text } = await renderTemplate(mail.body)
|
||||||
|
|
||||||
return (await transporter()).sendMail({
|
return (await transporter()).sendMail({
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import React from 'preact/compat' //for jsx-email
|
import React from 'preact/compat' //for jsx-email
|
||||||
import { render } from 'jsx-email'
|
import { render } from 'jsx-email'
|
||||||
// @deno-types="npm:turndown"
|
// @deno-types="npm:turndown@^7.1.3"
|
||||||
import Turndown from 'turndown'
|
import Turndown from 'turndown'
|
||||||
import type { JSX } from 'preact'
|
import type { JSX } from 'preact'
|
||||||
|
|
||||||
|
console.assert(React !== undefined)
|
||||||
|
|
||||||
const htmlToMd = new Turndown({
|
const htmlToMd = new Turndown({
|
||||||
headingStyle: 'atx',
|
headingStyle: 'atx',
|
||||||
codeBlockStyle: 'fenced',
|
codeBlockStyle: 'fenced',
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
// @deno-types="npm:@types/nodemailer"
|
// @deno-types="npm:@types/nodemailer@^6.4.15"
|
||||||
import nodemailer from 'nodemailer'
|
import nodemailer from 'nodemailer'
|
||||||
|
import { dkim } from '../config/loader.ts'
|
||||||
|
|
||||||
export async function transporter() {
|
export async function transporter() {
|
||||||
const dkimPath =
|
const dkimPath = dkim.privateKey
|
||||||
'/home/julien/dkim_sendmail_keys/dkim_sendmail_cohabit_fr.pem'
|
|
||||||
const dkimPrivateKey = await Deno.readTextFile(dkimPath).catch((cause) => {
|
const dkimPrivateKey = await Deno.readTextFile(dkimPath).catch((cause) => {
|
||||||
throw new Error(`unable to load DKIM private key from "${dkimPath}"`, {
|
throw new Error(`unable to load DKIM private key from "${dkimPath}"`, {
|
||||||
cause,
|
cause,
|
||||||
|
|
@ -15,8 +15,8 @@ export async function transporter() {
|
||||||
newline: 'unix',
|
newline: 'unix',
|
||||||
path: '/usr/sbin/sendmail',
|
path: '/usr/sbin/sendmail',
|
||||||
dkim: {
|
dkim: {
|
||||||
domainName: 'cohabit.fr',
|
domainName: dkim.domainName,
|
||||||
keySelector: 'sendmailY2024M03',
|
keySelector: dkim.keySelector,
|
||||||
privateKey: dkimPrivateKey,
|
privateKey: dkimPrivateKey,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
140
templates/MagicLink.tsx
Normal file
140
templates/MagicLink.tsx
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Heading,
|
||||||
|
Html,
|
||||||
|
Preview,
|
||||||
|
Section,
|
||||||
|
Text,
|
||||||
|
} from 'jsx-email'
|
||||||
|
import { Signature } from './components/Signature.tsx'
|
||||||
|
import type { Template } from '../types.ts'
|
||||||
|
import type { JSX } from 'preact'
|
||||||
|
import {
|
||||||
|
bodyCss,
|
||||||
|
buttonCss,
|
||||||
|
headingCss,
|
||||||
|
messageCss,
|
||||||
|
rootCss,
|
||||||
|
textCss,
|
||||||
|
} from './styles/base.tsx'
|
||||||
|
import { BaseStyle } from './styles/base.tsx'
|
||||||
|
|
||||||
|
function MagicLink(
|
||||||
|
{ message, device, ip, endpoint }: {
|
||||||
|
message?: string
|
||||||
|
device?: string
|
||||||
|
ip?: string
|
||||||
|
endpoint: string
|
||||||
|
},
|
||||||
|
): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Html lang='fr' style={{ fontSize: '14px' }}>
|
||||||
|
<BaseStyle />
|
||||||
|
<Preview>Nouveau lien de connection</Preview>
|
||||||
|
<Body style={bodyCss}>
|
||||||
|
<Container style={messageCss}>
|
||||||
|
<Section>
|
||||||
|
<Heading as='h1' style={headingCss}>
|
||||||
|
Nouveau lien de connection
|
||||||
|
</Heading>
|
||||||
|
<Text style={textCss}>
|
||||||
|
Une nouvelle demande de connection à été effectuée sur votre
|
||||||
|
compte.
|
||||||
|
</Text>
|
||||||
|
<Text style={detailsCss}>
|
||||||
|
<span>
|
||||||
|
<span style={{ fontWeight: 'bolder' }}>{'Date : '}</span>
|
||||||
|
{dateNow()}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<span>
|
||||||
|
<span style={{ fontWeight: 'bolder' }}>{'Appareil : '}</span>
|
||||||
|
{device?.length ? device : 'Iconnue'}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<span>
|
||||||
|
<span style={{ fontWeight: 'bolder' }}>{'Ip : '}</span>
|
||||||
|
{ip?.length ? ip : 'Inconnue'}
|
||||||
|
</span>
|
||||||
|
</Text>
|
||||||
|
{message?.length ? <Text style={textCss}>{message}</Text> : ''}
|
||||||
|
<Text style={infoCss}>
|
||||||
|
Si vous n'êtes pas à l'origine de cette demande vous pouvez
|
||||||
|
ignorer ce mail en toute sécurité.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Container
|
||||||
|
style={{ width: '100%', textAlign: 'center', padding: '1rem' }}
|
||||||
|
>
|
||||||
|
<Button href={endpoint} style={buttonCss}>Me connecter</Button>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
<Signature />
|
||||||
|
</Body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateNow() {
|
||||||
|
return new Date().toLocaleString('fr', {
|
||||||
|
weekday: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const infoCss: JSX.CSSProperties = {
|
||||||
|
...textCss,
|
||||||
|
opacity: 0.7,
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailsCss: JSX.CSSProperties = {
|
||||||
|
...textCss,
|
||||||
|
backgroundColor: rootCss.backgroundColor,
|
||||||
|
padding: '0.5rem',
|
||||||
|
borderRadius: '0.4rem',
|
||||||
|
}
|
||||||
|
|
||||||
|
const template: Template<typeof MagicLink, Parameters<typeof MagicLink>[0]> = {
|
||||||
|
props: [
|
||||||
|
{
|
||||||
|
name: 'Message',
|
||||||
|
description: "Message à afficher à l'utilisateur.",
|
||||||
|
required: false,
|
||||||
|
multiline: false,
|
||||||
|
tag: 'message',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Device',
|
||||||
|
description: 'User-Agent (ou equiv.) de la demande.',
|
||||||
|
required: false,
|
||||||
|
multiline: false,
|
||||||
|
tag: 'device',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Ip',
|
||||||
|
description: 'Addresse ip de la demande.',
|
||||||
|
required: false,
|
||||||
|
multiline: false,
|
||||||
|
tag: 'ip',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Endpoint',
|
||||||
|
description: 'Endpoint du lien "Me connecter".',
|
||||||
|
required: true,
|
||||||
|
multiline: false,
|
||||||
|
tag: 'endpoint',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
name: 'magic-link',
|
||||||
|
description: 'Coh@bit connection magic link.',
|
||||||
|
builder: MagicLink,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default template
|
||||||
|
|
@ -2,10 +2,11 @@ import { Body, Container, Html, Markdown, Preview } from 'jsx-email'
|
||||||
import { Signature } from './components/Signature.tsx'
|
import { Signature } from './components/Signature.tsx'
|
||||||
import type { Template } from '../types.ts'
|
import type { Template } from '../types.ts'
|
||||||
import { BaseStyle, bodyCss, messageCss, textCss } from './styles/base.tsx'
|
import { BaseStyle, bodyCss, messageCss, textCss } from './styles/base.tsx'
|
||||||
|
import type { JSX } from 'preact'
|
||||||
|
|
||||||
function Message(
|
function Message(
|
||||||
{ summary, body }: { summary?: string; body: string },
|
{ summary, body }: { summary?: string; body: string },
|
||||||
) {
|
): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Html lang='fr' style={{ fontSize: '14px' }}>
|
<Html lang='fr' style={{ fontSize: '14px' }}>
|
||||||
<BaseStyle />
|
<BaseStyle />
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,14 @@ import {
|
||||||
import { Signature } from './components/Signature.tsx'
|
import { Signature } from './components/Signature.tsx'
|
||||||
import type { Template } from '../types.ts'
|
import type { Template } from '../types.ts'
|
||||||
import type { JSX } from 'preact'
|
import type { JSX } from 'preact'
|
||||||
import { bodyCss, messageCss, rootCss, textCss } from './styles/base.tsx'
|
import {
|
||||||
|
bodyCss,
|
||||||
|
buttonCss,
|
||||||
|
headingCss,
|
||||||
|
messageCss,
|
||||||
|
rootCss,
|
||||||
|
textCss,
|
||||||
|
} from './styles/base.tsx'
|
||||||
import { BaseStyle } from './styles/base.tsx'
|
import { BaseStyle } from './styles/base.tsx'
|
||||||
|
|
||||||
function Welcome(
|
function Welcome(
|
||||||
|
|
@ -22,7 +29,7 @@ function Welcome(
|
||||||
login: string
|
login: string
|
||||||
endpoint?: string
|
endpoint?: string
|
||||||
},
|
},
|
||||||
) {
|
): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Html lang='fr' style={{ fontSize: '14px' }}>
|
<Html lang='fr' style={{ fontSize: '14px' }}>
|
||||||
<BaseStyle />
|
<BaseStyle />
|
||||||
|
|
@ -90,9 +97,7 @@ function Welcome(
|
||||||
style={{ width: '100%', textAlign: 'center', padding: '1rem' }}
|
style={{ width: '100%', textAlign: 'center', padding: '1rem' }}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
href={(endpoint && endpoint.length > 1)
|
href={endpoint?.length ? endpoint : 'https://cohabit.fr/profil'}
|
||||||
? endpoint
|
|
||||||
: 'https://cohabit.fr/profil'}
|
|
||||||
style={buttonCss}
|
style={buttonCss}
|
||||||
>
|
>
|
||||||
Accéder à mon compte
|
Accéder à mon compte
|
||||||
|
|
@ -106,14 +111,6 @@ function Welcome(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const headingCss: JSX.CSSProperties = {
|
|
||||||
fontFamily: 'Garamond, serif',
|
|
||||||
color: rootCss.accentColor,
|
|
||||||
textAlign: 'center',
|
|
||||||
margin: '-1rem 0 3rem',
|
|
||||||
fontSize: '2.5rem',
|
|
||||||
}
|
|
||||||
|
|
||||||
const preCss: JSX.CSSProperties = {
|
const preCss: JSX.CSSProperties = {
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
fontSize: '1rem',
|
fontSize: '1rem',
|
||||||
|
|
@ -122,18 +119,11 @@ const preCss: JSX.CSSProperties = {
|
||||||
backgroundColor: rootCss.backgroundColor,
|
backgroundColor: rootCss.backgroundColor,
|
||||||
}
|
}
|
||||||
|
|
||||||
const buttonCss: JSX.CSSProperties = {
|
|
||||||
color: 'white',
|
|
||||||
padding: '1rem',
|
|
||||||
borderRadius: '0.4rem',
|
|
||||||
fontSize: '1.2rem',
|
|
||||||
backgroundColor: rootCss.accentColor,
|
|
||||||
}
|
|
||||||
|
|
||||||
const imgCss: JSX.CSSProperties = {
|
const imgCss: JSX.CSSProperties = {
|
||||||
display: 'inline',
|
display: 'inline',
|
||||||
padding: '1rem',
|
padding: '1rem',
|
||||||
transform: 'translateY(40%)',
|
transform: 'translateY(40%)',
|
||||||
|
maxHeight: '3rem',
|
||||||
}
|
}
|
||||||
|
|
||||||
const template: Template<typeof Welcome, Parameters<typeof Welcome>[0]> = {
|
const template: Template<typeof Welcome, Parameters<typeof Welcome>[0]> = {
|
||||||
|
|
|
||||||
3
templates/mod.ts
Normal file
3
templates/mod.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { default as magicLinkTemplate } from './MagicLink.tsx'
|
||||||
|
export { default as messageTemplate } from './Message.tsx'
|
||||||
|
export { default as welcomeTemplate } from './Welcome.tsx'
|
||||||
|
|
@ -19,10 +19,26 @@ export const messageCss: JSX.CSSProperties = {
|
||||||
textWrap: 'balance',
|
textWrap: 'balance',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const headingCss: JSX.CSSProperties = {
|
||||||
|
fontFamily: 'Garamond, serif',
|
||||||
|
color: rootCss.accentColor,
|
||||||
|
textAlign: 'center',
|
||||||
|
margin: '-1rem 0 3rem',
|
||||||
|
fontSize: '2.5rem',
|
||||||
|
}
|
||||||
|
|
||||||
export const textCss: JSX.CSSProperties = {
|
export const textCss: JSX.CSSProperties = {
|
||||||
fontSize: '1rem',
|
fontSize: '1rem',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const buttonCss: JSX.CSSProperties = {
|
||||||
|
color: 'white',
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '0.4rem',
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
backgroundColor: rootCss.accentColor,
|
||||||
|
}
|
||||||
|
|
||||||
export const rawCss = `
|
export const rawCss = `
|
||||||
a {
|
a {
|
||||||
color: ${rootCss.accentColor};
|
color: ${rootCss.accentColor};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue