@kernhq/module-mail 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +662 -0
- package/dist/contract.d.ts +285 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +115 -0
- package/dist/contract.js.map +1 -0
- package/dist/server/index.d.ts +29 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +189 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/providers/index.d.ts +13 -0
- package/dist/server/providers/index.d.ts.map +1 -0
- package/dist/server/providers/index.js +36 -0
- package/dist/server/providers/index.js.map +1 -0
- package/dist/server/providers/mailgun.d.ts +9 -0
- package/dist/server/providers/mailgun.d.ts.map +1 -0
- package/dist/server/providers/mailgun.js +42 -0
- package/dist/server/providers/mailgun.js.map +1 -0
- package/dist/server/providers/postmark.d.ts +7 -0
- package/dist/server/providers/postmark.d.ts.map +1 -0
- package/dist/server/providers/postmark.js +38 -0
- package/dist/server/providers/postmark.js.map +1 -0
- package/dist/server/providers/resend.d.ts +7 -0
- package/dist/server/providers/resend.d.ts.map +1 -0
- package/dist/server/providers/resend.js +34 -0
- package/dist/server/providers/resend.js.map +1 -0
- package/dist/server/providers/ses.d.ts +9 -0
- package/dist/server/providers/ses.d.ts.map +1 -0
- package/dist/server/providers/ses.js +59 -0
- package/dist/server/providers/ses.js.map +1 -0
- package/dist/server/providers/smtp.d.ts +13 -0
- package/dist/server/providers/smtp.d.ts.map +1 -0
- package/dist/server/providers/smtp.js +36 -0
- package/dist/server/providers/smtp.js.map +1 -0
- package/dist/server/providers/types.d.ts +38 -0
- package/dist/server/providers/types.d.ts.map +1 -0
- package/dist/server/providers/types.js +2 -0
- package/dist/server/providers/types.js.map +1 -0
- package/dist/server/schema.d.ts +469 -0
- package/dist/server/schema.d.ts.map +1 -0
- package/dist/server/schema.js +40 -0
- package/dist/server/schema.js.map +1 -0
- package/dist/server/send.d.ts +32 -0
- package/dist/server/send.d.ts.map +1 -0
- package/dist/server/send.js +119 -0
- package/dist/server/send.js.map +1 -0
- package/dist/server/settings.d.ts +9 -0
- package/dist/server/settings.d.ts.map +1 -0
- package/dist/server/settings.js +35 -0
- package/dist/server/settings.js.map +1 -0
- package/dist/server/suppressions.d.ts +17 -0
- package/dist/server/suppressions.d.ts.map +1 -0
- package/dist/server/suppressions.js +46 -0
- package/dist/server/suppressions.js.map +1 -0
- package/dist/server/templates.d.ts +19 -0
- package/dist/server/templates.d.ts.map +1 -0
- package/dist/server/templates.js +55 -0
- package/dist/server/templates.js.map +1 -0
- package/migrations/0000_init.sql +39 -0
- package/migrations/0001_notes.sql +6 -0
- package/migrations/meta/0000_snapshot.json +288 -0
- package/migrations/meta/_journal.json +20 -0
- package/package.json +80 -0
- package/src/client/MailSettings.svelte +214 -0
- package/src/client/index.ts +29 -0
- package/templates/_layout.mjml +34 -0
- package/templates/invitation.mjml +16 -0
- package/templates/invitation.subject.txt +1 -0
- package/templates/invitation.txt +8 -0
- package/templates/magic-link.mjml +16 -0
- package/templates/magic-link.subject.txt +1 -0
- package/templates/magic-link.txt +6 -0
- package/templates/notification-digest.mjml +24 -0
- package/templates/notification-digest.subject.txt +1 -0
- package/templates/notification-digest.txt +8 -0
- package/templates/reset-password.mjml +16 -0
- package/templates/reset-password.subject.txt +1 -0
- package/templates/reset-password.txt +7 -0
- package/templates/test.mjml +12 -0
- package/templates/test.subject.txt +1 -0
- package/templates/test.txt +5 -0
- package/templates/verify-email.mjml +17 -0
- package/templates/verify-email.subject.txt +1 -0
- package/templates/verify-email.txt +7 -0
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kernhq/module-mail",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Kern mail module: outbound providers (SMTP/Mailgun/SES/Postmark/Resend), MJML templates, delivery queue, bounces & suppressions",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/KernAIO/modules",
|
|
10
|
+
"directory": "packages/mail"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"migrations",
|
|
18
|
+
"templates",
|
|
19
|
+
"src/client"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
"./contract": {
|
|
23
|
+
"types": "./dist/contract.d.ts",
|
|
24
|
+
"import": "./dist/contract.js"
|
|
25
|
+
},
|
|
26
|
+
"./server": {
|
|
27
|
+
"types": "./dist/server/index.d.ts",
|
|
28
|
+
"import": "./dist/server/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./client": {
|
|
31
|
+
"types": "./src/client/index.ts",
|
|
32
|
+
"svelte": "./src/client/index.ts",
|
|
33
|
+
"default": "./src/client/index.ts"
|
|
34
|
+
},
|
|
35
|
+
"./migrations": "./migrations",
|
|
36
|
+
"./templates": "./templates",
|
|
37
|
+
"./client/*": {
|
|
38
|
+
"types": "./src/client/*",
|
|
39
|
+
"svelte": "./src/client/*",
|
|
40
|
+
"default": "./src/client/*"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@aws-sdk/client-sesv2": "^3.1116.0",
|
|
45
|
+
"@kernhq/contracts": "^0.1.0",
|
|
46
|
+
"@kernhq/kernel": "^0.1.0",
|
|
47
|
+
"@orpc/contract": "^1.15.0",
|
|
48
|
+
"@orpc/server": "^1.15.0",
|
|
49
|
+
"drizzle-orm": "^0.45.0",
|
|
50
|
+
"eta": "^4.6.0",
|
|
51
|
+
"mjml": "^5.4.0",
|
|
52
|
+
"nodemailer": "^9.0.5",
|
|
53
|
+
"zod": "^4.1.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"fastify": "^5.0.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependenciesMeta": {
|
|
59
|
+
"fastify": {
|
|
60
|
+
"optional": true
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@kernhq/tsconfig": "^0.1.0",
|
|
65
|
+
"@types/mjml": "^5.0.0",
|
|
66
|
+
"@types/node": "^24.0.0",
|
|
67
|
+
"@types/nodemailer": "^8.0.1",
|
|
68
|
+
"drizzle-kit": "^0.31.0",
|
|
69
|
+
"fastify": "^5.6.0",
|
|
70
|
+
"typescript": "~5.9.3",
|
|
71
|
+
"vitest": "^4.0.0"
|
|
72
|
+
},
|
|
73
|
+
"scripts": {
|
|
74
|
+
"build": "tsc -p tsconfig.json",
|
|
75
|
+
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
76
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
77
|
+
"test": "vitest run",
|
|
78
|
+
"db:generate": "drizzle-kit generate"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { SECRET_PLACEHOLDER } from '../contract.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Workspace email settings.
|
|
6
|
+
*
|
|
7
|
+
* A workspace can send through its own provider; without one it falls back to the instance's SMTP
|
|
8
|
+
* configuration, so a fresh self-hosted install already delivers invitations and password resets.
|
|
9
|
+
*
|
|
10
|
+
* Stored secrets are never returned by the API — reads replace them with a placeholder, and sending
|
|
11
|
+
* the placeholder back leaves the stored value untouched. That is why the fields below start out
|
|
12
|
+
* showing the placeholder rather than empty when a provider is already configured.
|
|
13
|
+
*/
|
|
14
|
+
interface MailApi {
|
|
15
|
+
settings: {
|
|
16
|
+
get(input: { workspaceId: string }): Promise<{ config: Record<string, unknown> | null }>
|
|
17
|
+
set(input: { workspaceId: string; config: Record<string, unknown> | null }): Promise<{ ok: boolean }>
|
|
18
|
+
test(input: { workspaceId: string; to: string }): Promise<{ ok: boolean; error: string | null }>
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
workspaceId: string
|
|
24
|
+
/** the caller passes a client bound to /api/mail */
|
|
25
|
+
api: MailApi
|
|
26
|
+
/** address the test message goes to (defaults to the signed-in user) */
|
|
27
|
+
testAddress?: string
|
|
28
|
+
}
|
|
29
|
+
let { workspaceId, api, testAddress = '' }: Props = $props()
|
|
30
|
+
|
|
31
|
+
type Provider = 'platform' | 'smtp' | 'mailgun' | 'ses' | 'postmark' | 'resend'
|
|
32
|
+
|
|
33
|
+
const FIELDS: Record<Provider, Array<{ key: string; label: string; type?: string; hint?: string }>> = {
|
|
34
|
+
platform: [],
|
|
35
|
+
smtp: [
|
|
36
|
+
{ key: 'host', label: 'Host' },
|
|
37
|
+
{ key: 'port', label: 'Port', type: 'number' },
|
|
38
|
+
{ key: 'user', label: 'Username' },
|
|
39
|
+
{ key: 'pass', label: 'Password', type: 'password' },
|
|
40
|
+
{ key: 'from', label: 'From address', hint: 'Kern <no-reply@example.com>' },
|
|
41
|
+
],
|
|
42
|
+
mailgun: [
|
|
43
|
+
{ key: 'apiKey', label: 'API key', type: 'password' },
|
|
44
|
+
{ key: 'domain', label: 'Domain' },
|
|
45
|
+
{ key: 'region', label: 'Region', hint: 'us or eu' },
|
|
46
|
+
{ key: 'from', label: 'From address' },
|
|
47
|
+
],
|
|
48
|
+
ses: [
|
|
49
|
+
{ key: 'accessKeyId', label: 'Access key ID' },
|
|
50
|
+
{ key: 'secretAccessKey', label: 'Secret access key', type: 'password' },
|
|
51
|
+
{ key: 'region', label: 'Region' },
|
|
52
|
+
{ key: 'from', label: 'From address' },
|
|
53
|
+
],
|
|
54
|
+
postmark: [
|
|
55
|
+
{ key: 'serverToken', label: 'Server token', type: 'password' },
|
|
56
|
+
{ key: 'from', label: 'From address' },
|
|
57
|
+
],
|
|
58
|
+
resend: [
|
|
59
|
+
{ key: 'apiKey', label: 'API key', type: 'password' },
|
|
60
|
+
{ key: 'from', label: 'From address' },
|
|
61
|
+
],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const PROVIDER_LABELS: Record<Provider, string> = {
|
|
65
|
+
platform: 'Use the instance default',
|
|
66
|
+
smtp: 'SMTP',
|
|
67
|
+
mailgun: 'Mailgun',
|
|
68
|
+
ses: 'Amazon SES',
|
|
69
|
+
postmark: 'Postmark',
|
|
70
|
+
resend: 'Resend',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let provider = $state<Provider>('platform')
|
|
74
|
+
let values = $state<Record<string, string>>({})
|
|
75
|
+
let loading = $state(true)
|
|
76
|
+
let saving = $state(false)
|
|
77
|
+
let testing = $state(false)
|
|
78
|
+
let message = $state<{ kind: 'ok' | 'error'; text: string } | null>(null)
|
|
79
|
+
let recipient = $state(testAddress)
|
|
80
|
+
|
|
81
|
+
$effect(() => {
|
|
82
|
+
void (async () => {
|
|
83
|
+
const { config } = await api.settings.get({ workspaceId })
|
|
84
|
+
if (config && typeof config.provider === 'string') {
|
|
85
|
+
provider = config.provider as Provider
|
|
86
|
+
values = Object.fromEntries(
|
|
87
|
+
Object.entries(config)
|
|
88
|
+
.filter(([k]) => k !== 'provider')
|
|
89
|
+
.map(([k, v]) => [k, String(v ?? '')]),
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
loading = false
|
|
93
|
+
})()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const fields = $derived(FIELDS[provider])
|
|
97
|
+
|
|
98
|
+
async function save(event: SubmitEvent) {
|
|
99
|
+
event.preventDefault()
|
|
100
|
+
saving = true
|
|
101
|
+
message = null
|
|
102
|
+
try {
|
|
103
|
+
const config =
|
|
104
|
+
provider === 'platform'
|
|
105
|
+
? null
|
|
106
|
+
: { provider, ...Object.fromEntries(fields.map((f) => [f.key, values[f.key] ?? ''])) }
|
|
107
|
+
await api.settings.set({ workspaceId, config })
|
|
108
|
+
message = { kind: 'ok', text: 'Email settings saved' }
|
|
109
|
+
} catch (err) {
|
|
110
|
+
message = { kind: 'error', text: err instanceof Error ? err.message : 'Could not save settings' }
|
|
111
|
+
} finally {
|
|
112
|
+
saving = false
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function sendTest() {
|
|
117
|
+
if (!recipient) return
|
|
118
|
+
testing = true
|
|
119
|
+
message = null
|
|
120
|
+
try {
|
|
121
|
+
const result = await api.settings.test({ workspaceId, to: recipient })
|
|
122
|
+
message = result.ok
|
|
123
|
+
? { kind: 'ok', text: `Test message queued for ${recipient}` }
|
|
124
|
+
: { kind: 'error', text: result.error ?? 'The provider rejected the message' }
|
|
125
|
+
} catch (err) {
|
|
126
|
+
message = { kind: 'error', text: err instanceof Error ? err.message : 'Could not send the test' }
|
|
127
|
+
} finally {
|
|
128
|
+
testing = false
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<section class="grid gap-4">
|
|
134
|
+
<header>
|
|
135
|
+
<h2 class="text-[15px] font-semibold text-[var(--kern-ink-900)]">Email</h2>
|
|
136
|
+
<p class="mt-1 text-[12.5px] leading-relaxed text-[var(--kern-ink-500)]">
|
|
137
|
+
Choose how this workspace sends email. Without a provider it uses the instance default.
|
|
138
|
+
</p>
|
|
139
|
+
</header>
|
|
140
|
+
|
|
141
|
+
{#if loading}
|
|
142
|
+
<p class="text-[13px] text-[var(--kern-ink-400)]">Loading…</p>
|
|
143
|
+
{:else}
|
|
144
|
+
<form class="grid gap-4" onsubmit={save}>
|
|
145
|
+
<label class="grid gap-1.5">
|
|
146
|
+
<span class="text-[12.5px] font-medium text-[var(--kern-ink-700)]">Provider</span>
|
|
147
|
+
<select
|
|
148
|
+
bind:value={provider}
|
|
149
|
+
class="h-[34px] rounded-[9px] border border-[var(--kern-border)] bg-[var(--kern-surface-input)] px-2.5 text-[13px] text-[var(--kern-ink-900)]"
|
|
150
|
+
>
|
|
151
|
+
{#each Object.entries(PROVIDER_LABELS) as [value, label] (value)}
|
|
152
|
+
<option {value}>{label}</option>
|
|
153
|
+
{/each}
|
|
154
|
+
</select>
|
|
155
|
+
</label>
|
|
156
|
+
|
|
157
|
+
{#each fields as field (field.key)}
|
|
158
|
+
<label class="grid gap-1.5">
|
|
159
|
+
<span class="text-[12.5px] font-medium text-[var(--kern-ink-700)]">{field.label}</span>
|
|
160
|
+
<input
|
|
161
|
+
type={field.type ?? 'text'}
|
|
162
|
+
bind:value={values[field.key]}
|
|
163
|
+
placeholder={field.hint ?? ''}
|
|
164
|
+
autocomplete="off"
|
|
165
|
+
class="h-[34px] rounded-[9px] border border-[var(--kern-border)] bg-[var(--kern-surface-input)] px-2.5 text-[13px] text-[var(--kern-ink-900)]"
|
|
166
|
+
/>
|
|
167
|
+
{#if values[field.key] === SECRET_PLACEHOLDER}
|
|
168
|
+
<span class="text-[11.5px] text-[var(--kern-ink-400)]">
|
|
169
|
+
Stored securely — leave as is to keep it
|
|
170
|
+
</span>
|
|
171
|
+
{/if}
|
|
172
|
+
</label>
|
|
173
|
+
{/each}
|
|
174
|
+
|
|
175
|
+
{#if message}
|
|
176
|
+
<p
|
|
177
|
+
role="status"
|
|
178
|
+
class="rounded-[9px] px-3 py-2 text-[12.5px] {message.kind === 'ok'
|
|
179
|
+
? 'bg-[var(--kern-success-tint)] text-[var(--kern-success)]'
|
|
180
|
+
: 'bg-[var(--kern-danger-tint)] text-[var(--kern-danger)]'}"
|
|
181
|
+
>
|
|
182
|
+
{message.text}
|
|
183
|
+
</p>
|
|
184
|
+
{/if}
|
|
185
|
+
|
|
186
|
+
<div class="flex items-center gap-2">
|
|
187
|
+
<button
|
|
188
|
+
type="submit"
|
|
189
|
+
disabled={saving}
|
|
190
|
+
class="h-[34px] rounded-[9px] bg-[var(--kern-ink-900)] px-3.5 text-[13px] font-medium text-[var(--kern-ink-inverse)] disabled:opacity-50"
|
|
191
|
+
>
|
|
192
|
+
{saving ? 'Saving…' : 'Save'}
|
|
193
|
+
</button>
|
|
194
|
+
|
|
195
|
+
<span class="flex-1"></span>
|
|
196
|
+
|
|
197
|
+
<input
|
|
198
|
+
type="email"
|
|
199
|
+
bind:value={recipient}
|
|
200
|
+
placeholder="you@example.com"
|
|
201
|
+
class="h-[34px] w-[220px] rounded-[9px] border border-[var(--kern-border)] bg-[var(--kern-surface-input)] px-2.5 text-[13px]"
|
|
202
|
+
/>
|
|
203
|
+
<button
|
|
204
|
+
type="button"
|
|
205
|
+
onclick={sendTest}
|
|
206
|
+
disabled={testing || !recipient}
|
|
207
|
+
class="h-[34px] rounded-[9px] border border-[var(--kern-border)] px-3 text-[13px] text-[var(--kern-ink-700)] disabled:opacity-50"
|
|
208
|
+
>
|
|
209
|
+
{testing ? 'Sending…' : 'Send test'}
|
|
210
|
+
</button>
|
|
211
|
+
</div>
|
|
212
|
+
</form>
|
|
213
|
+
{/if}
|
|
214
|
+
</section>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineClientModule } from '@kernhq/kernel/client'
|
|
2
|
+
import { MODULE_ID } from '../contract.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mail's client surface. The module has no navigation of its own — outbound email is configuration,
|
|
6
|
+
* not a place people visit — so it contributes a workspace settings page where an admin chooses the
|
|
7
|
+
* provider and sends a test message.
|
|
8
|
+
*
|
|
9
|
+
* The settings component is still to be written; the entry is registered so the module appears in
|
|
10
|
+
* the settings navigation as soon as it lands.
|
|
11
|
+
*/
|
|
12
|
+
export const mailClient = defineClientModule({
|
|
13
|
+
id: MODULE_ID,
|
|
14
|
+
name: 'Mail',
|
|
15
|
+
icon: 'mail',
|
|
16
|
+
settingsPages: [
|
|
17
|
+
{
|
|
18
|
+
id: 'mail',
|
|
19
|
+
label: 'Email',
|
|
20
|
+
icon: 'mail',
|
|
21
|
+
scope: 'workspace',
|
|
22
|
+
permission: 'mail.settings.manage',
|
|
23
|
+
order: 40,
|
|
24
|
+
component: () => import('./MailSettings.svelte'),
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export default mailClient
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<mjml>
|
|
2
|
+
<mj-head>
|
|
3
|
+
<mj-attributes>
|
|
4
|
+
<mj-all font-family="Instrument Sans, -apple-system, Segoe UI, Helvetica, Arial, sans-serif" />
|
|
5
|
+
<mj-text color="#1C1A17" font-size="14px" line-height="1.6" />
|
|
6
|
+
<mj-button background-color="#B4661C" color="#FBFAF7" border-radius="6px" font-size="14px" font-weight="600" inner-padding="10px 22px" />
|
|
7
|
+
<mj-section padding="0" />
|
|
8
|
+
</mj-attributes>
|
|
9
|
+
<mj-style>
|
|
10
|
+
a { color: #A85A18; }
|
|
11
|
+
.muted div { color: #615B4F !important; }
|
|
12
|
+
.card { border: 1px solid #E4E0D6; border-radius: 10px; overflow: hidden; }
|
|
13
|
+
</mj-style>
|
|
14
|
+
</mj-head>
|
|
15
|
+
<mj-body background-color="#E9E6DD" width="560px">
|
|
16
|
+
<mj-section padding="28px 0 16px">
|
|
17
|
+
<mj-column>
|
|
18
|
+
<mj-text align="center" font-size="15px" font-weight="700" letter-spacing="0.08em" color="#1C1A17">
|
|
19
|
+
<%= it.__instanceName %>
|
|
20
|
+
</mj-text>
|
|
21
|
+
</mj-column>
|
|
22
|
+
</mj-section>
|
|
23
|
+
<mj-wrapper css-class="card" background-color="#FBFAF7" padding="8px 0">
|
|
24
|
+
<%~ it.__body %>
|
|
25
|
+
</mj-wrapper>
|
|
26
|
+
<mj-section padding="16px 0 28px">
|
|
27
|
+
<mj-column>
|
|
28
|
+
<mj-text css-class="muted" align="center" font-size="12px" color="#615B4F">
|
|
29
|
+
<%~ it.__footer %>
|
|
30
|
+
</mj-text>
|
|
31
|
+
</mj-column>
|
|
32
|
+
</mj-section>
|
|
33
|
+
</mj-body>
|
|
34
|
+
</mjml>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<mj-section padding="24px 32px 8px">
|
|
2
|
+
<mj-column>
|
|
3
|
+
<mj-text font-size="18px" font-weight="600">You're invited to <%= it.workspaceName %></mj-text>
|
|
4
|
+
<mj-text css-class="muted" color="#615B4F">
|
|
5
|
+
<%= it.inviterName %> invited you to join the <b><%= it.workspaceName %></b> workspace on <%= it.instanceName %>.
|
|
6
|
+
</mj-text>
|
|
7
|
+
</mj-column>
|
|
8
|
+
</mj-section>
|
|
9
|
+
<mj-section padding="8px 32px 24px">
|
|
10
|
+
<mj-column>
|
|
11
|
+
<mj-button href="<%= it.acceptUrl %>">Accept invitation</mj-button>
|
|
12
|
+
<mj-text css-class="muted" font-size="12px" color="#615B4F" padding-top="12px">
|
|
13
|
+
Or paste this link into your browser: <a href="<%= it.acceptUrl %>"><%= it.acceptUrl %></a>
|
|
14
|
+
</mj-text>
|
|
15
|
+
</mj-column>
|
|
16
|
+
</mj-section>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= it.inviterName %> invited you to <%= it.workspaceName %> on <%= it.instanceName %>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
You're invited to <%= it.workspaceName %>
|
|
2
|
+
|
|
3
|
+
<%= it.inviterName %> invited you to join the <%= it.workspaceName %> workspace on <%= it.instanceName %>.
|
|
4
|
+
|
|
5
|
+
Accept the invitation:
|
|
6
|
+
<%= it.acceptUrl %>
|
|
7
|
+
|
|
8
|
+
If you didn't expect this invitation you can ignore this email.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<mj-section padding="24px 32px 8px">
|
|
2
|
+
<mj-column>
|
|
3
|
+
<mj-text font-size="18px" font-weight="600">Sign in to <%= it.instanceName %></mj-text>
|
|
4
|
+
<mj-text css-class="muted" color="#615B4F">
|
|
5
|
+
Click the button below to sign in. This link is valid for a short time and can be used once.
|
|
6
|
+
</mj-text>
|
|
7
|
+
</mj-column>
|
|
8
|
+
</mj-section>
|
|
9
|
+
<mj-section padding="8px 32px 24px">
|
|
10
|
+
<mj-column>
|
|
11
|
+
<mj-button href="<%= it.url %>">Sign in</mj-button>
|
|
12
|
+
<mj-text css-class="muted" font-size="12px" color="#615B4F" padding-top="12px">
|
|
13
|
+
If you didn't request this email you can safely ignore it.
|
|
14
|
+
</mj-text>
|
|
15
|
+
</mj-column>
|
|
16
|
+
</mj-section>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Sign in to <%= it.instanceName %>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<mj-section padding="24px 32px 8px">
|
|
2
|
+
<mj-column>
|
|
3
|
+
<mj-text font-size="18px" font-weight="600">Your <%= it.periodLabel %> digest</mj-text>
|
|
4
|
+
<mj-text css-class="muted" color="#615B4F">
|
|
5
|
+
Hi <%= it.name %>, here is what happened in <b><%= it.workspaceName %></b> while you were away
|
|
6
|
+
(<%= it.items.length %> notification<%= it.items.length === 1 ? '' : 's' %>).
|
|
7
|
+
</mj-text>
|
|
8
|
+
</mj-column>
|
|
9
|
+
</mj-section>
|
|
10
|
+
<% it.items.forEach(function (item) { %>
|
|
11
|
+
<mj-section padding="4px 32px">
|
|
12
|
+
<mj-column border-left="3px solid #B4661C" padding-left="12px">
|
|
13
|
+
<mj-text font-weight="600" padding-bottom="0"><a href="<%= item.url %>" style="color:#1C1A17;text-decoration:none;"><%= item.title %></a></mj-text>
|
|
14
|
+
<% if (item.body) { %>
|
|
15
|
+
<mj-text css-class="muted" font-size="12px" color="#615B4F" padding-top="2px"><%= item.body %></mj-text>
|
|
16
|
+
<% } %>
|
|
17
|
+
</mj-column>
|
|
18
|
+
</mj-section>
|
|
19
|
+
<% }) %>
|
|
20
|
+
<mj-section padding="16px 32px 24px">
|
|
21
|
+
<mj-column>
|
|
22
|
+
<mj-button href="<%= it.inboxUrl %>">Open inbox</mj-button>
|
|
23
|
+
</mj-column>
|
|
24
|
+
</mj-section>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= it.items.length %> new notification<%= it.items.length === 1 ? '' : 's' %> in <%= it.workspaceName %>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<mj-section padding="24px 32px 8px">
|
|
2
|
+
<mj-column>
|
|
3
|
+
<mj-text font-size="18px" font-weight="600">Reset your password</mj-text>
|
|
4
|
+
<mj-text css-class="muted" color="#615B4F">
|
|
5
|
+
Hi <%= it.name %>, we received a request to reset the password for your <%= it.instanceName %> account.
|
|
6
|
+
</mj-text>
|
|
7
|
+
</mj-column>
|
|
8
|
+
</mj-section>
|
|
9
|
+
<mj-section padding="8px 32px 24px">
|
|
10
|
+
<mj-column>
|
|
11
|
+
<mj-button href="<%= it.url %>">Choose a new password</mj-button>
|
|
12
|
+
<mj-text css-class="muted" font-size="12px" color="#615B4F" padding-top="12px">
|
|
13
|
+
The link expires soon. If you didn't request a reset, your password is unchanged and you can ignore this email.
|
|
14
|
+
</mj-text>
|
|
15
|
+
</mj-column>
|
|
16
|
+
</mj-section>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Reset your <%= it.instanceName %> password
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Reset your password
|
|
2
|
+
|
|
3
|
+
Hi <%= it.name %>, we received a request to reset the password for your <%= it.instanceName %> account. Choose a new password here:
|
|
4
|
+
|
|
5
|
+
<%= it.url %>
|
|
6
|
+
|
|
7
|
+
The link expires soon. If you didn't request a reset, your password is unchanged and you can ignore this email.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<mj-section padding="24px 32px 24px">
|
|
2
|
+
<mj-column>
|
|
3
|
+
<mj-text font-size="18px" font-weight="600">Mail is working</mj-text>
|
|
4
|
+
<mj-text css-class="muted" color="#615B4F">
|
|
5
|
+
This is a test message from <b><%= it.instanceName %></b><% if (it.workspaceName) { %> for the
|
|
6
|
+
workspace <b><%= it.workspaceName %></b><% } %>, sent via the <b><%= it.provider %></b> provider.
|
|
7
|
+
</mj-text>
|
|
8
|
+
<mj-text css-class="muted" font-size="12px" color="#615B4F">
|
|
9
|
+
If you received this email, your outbound mail configuration is correct.
|
|
10
|
+
</mj-text>
|
|
11
|
+
</mj-column>
|
|
12
|
+
</mj-section>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Test email from <%= it.instanceName %>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<mj-section padding="24px 32px 8px">
|
|
2
|
+
<mj-column>
|
|
3
|
+
<mj-text font-size="18px" font-weight="600">Verify your email</mj-text>
|
|
4
|
+
<mj-text css-class="muted" color="#615B4F">
|
|
5
|
+
Hi <%= it.name %>, confirm that <%= it.email %> is your address to finish setting up your
|
|
6
|
+
<%= it.instanceName %> account.
|
|
7
|
+
</mj-text>
|
|
8
|
+
</mj-column>
|
|
9
|
+
</mj-section>
|
|
10
|
+
<mj-section padding="8px 32px 24px">
|
|
11
|
+
<mj-column>
|
|
12
|
+
<mj-button href="<%= it.url %>">Verify email</mj-button>
|
|
13
|
+
<mj-text css-class="muted" font-size="12px" color="#615B4F" padding-top="12px">
|
|
14
|
+
If you didn't create an account you can safely ignore this email.
|
|
15
|
+
</mj-text>
|
|
16
|
+
</mj-column>
|
|
17
|
+
</mj-section>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Verify your email for <%= it.instanceName %>
|