@kernhq/module-mail 0.1.1 → 0.1.2
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/package.json +3 -2
- package/src/client/MailSettings.svelte +1 -1
- package/src/client/index.ts +1 -1
- package/src/contract.ts +142 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kernhq/module-mail",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Kern mail module: outbound providers (SMTP/Mailgun/SES/Postmark/Resend), MJML templates, delivery queue, bounces & suppressions",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"dist",
|
|
17
17
|
"migrations",
|
|
18
18
|
"templates",
|
|
19
|
-
"src/client"
|
|
19
|
+
"src/client",
|
|
20
|
+
"src/contract.ts"
|
|
20
21
|
],
|
|
21
22
|
"exports": {
|
|
22
23
|
"./contract": {
|
package/src/client/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineClientModule } from '@kernhq/kernel/client'
|
|
2
|
-
import { MODULE_ID } from '
|
|
2
|
+
import { MODULE_ID } from '../contract.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Mail's client surface. The module has no navigation of its own — outbound email is configuration,
|
package/src/contract.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
baseContract,
|
|
3
|
+
defineEvent,
|
|
4
|
+
definePermissions,
|
|
5
|
+
Email,
|
|
6
|
+
PageInput,
|
|
7
|
+
page,
|
|
8
|
+
Timestamp,
|
|
9
|
+
WorkspaceId,
|
|
10
|
+
} from '@kernhq/contracts'
|
|
11
|
+
import { z } from 'zod'
|
|
12
|
+
|
|
13
|
+
export const MODULE_ID = 'mail'
|
|
14
|
+
|
|
15
|
+
/** Value returned in place of a secret field when reading settings; sending it back keeps the stored value. */
|
|
16
|
+
export const SECRET_PLACEHOLDER = '__kern_secret__'
|
|
17
|
+
|
|
18
|
+
export const MailProviderKind = z.enum(['platform', 'smtp', 'mailgun', 'ses', 'postmark', 'resend'])
|
|
19
|
+
export type MailProviderKind = z.infer<typeof MailProviderKind>
|
|
20
|
+
|
|
21
|
+
export const MailAttachment = z
|
|
22
|
+
.object({
|
|
23
|
+
filename: z.string().min(1).max(255),
|
|
24
|
+
contentType: z.string().default('application/octet-stream'),
|
|
25
|
+
/** id of a file stored in core files (fetched at send time) */
|
|
26
|
+
fileId: z.uuid().optional(),
|
|
27
|
+
/** inline content, base64-encoded (small attachments only) */
|
|
28
|
+
base64: z.string().optional(),
|
|
29
|
+
})
|
|
30
|
+
.refine((a) => Boolean(a.fileId) !== Boolean(a.base64), {
|
|
31
|
+
message: 'exactly one of fileId or base64 is required',
|
|
32
|
+
})
|
|
33
|
+
export type MailAttachment = z.infer<typeof MailAttachment>
|
|
34
|
+
|
|
35
|
+
export const SendMailInput = z.object({
|
|
36
|
+
/** omitted for instance-level mail (signup verification…): the platform provider is used */
|
|
37
|
+
workspaceId: WorkspaceId.optional(),
|
|
38
|
+
to: z.array(Email).min(1).max(50),
|
|
39
|
+
cc: z.array(Email).max(50).optional(),
|
|
40
|
+
bcc: z.array(Email).max(50).optional(),
|
|
41
|
+
subject: z.string().min(1).max(500),
|
|
42
|
+
text: z.string().optional(),
|
|
43
|
+
html: z.string().optional(),
|
|
44
|
+
/** render a named MJML template with `data` instead of passing text/html */
|
|
45
|
+
template: z.object({ name: z.string(), data: z.record(z.string(), z.unknown()).default({}) }).optional(),
|
|
46
|
+
replyTo: Email.optional(),
|
|
47
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
48
|
+
attachments: z.array(MailAttachment).max(20).optional(),
|
|
49
|
+
/** free-form tags stored on the delivery (e.g. `digest`, `invite`) */
|
|
50
|
+
tags: z.array(z.string().max(64)).max(10).optional(),
|
|
51
|
+
})
|
|
52
|
+
export type SendMailInput = z.infer<typeof SendMailInput>
|
|
53
|
+
|
|
54
|
+
export const MailDeliveryStatus = z.enum(['queued', 'sent', 'failed', 'bounced'])
|
|
55
|
+
export type MailDeliveryStatus = z.infer<typeof MailDeliveryStatus>
|
|
56
|
+
|
|
57
|
+
export const MailDelivery = z.object({
|
|
58
|
+
id: z.uuid(),
|
|
59
|
+
workspaceId: WorkspaceId.nullable(),
|
|
60
|
+
to: z.array(z.string()),
|
|
61
|
+
subject: z.string(),
|
|
62
|
+
provider: MailProviderKind,
|
|
63
|
+
template: z.string().nullable(),
|
|
64
|
+
status: MailDeliveryStatus,
|
|
65
|
+
providerMessageId: z.string().nullable(),
|
|
66
|
+
error: z.string().nullable(),
|
|
67
|
+
tags: z.array(z.string()),
|
|
68
|
+
createdAt: Timestamp,
|
|
69
|
+
updatedAt: Timestamp,
|
|
70
|
+
})
|
|
71
|
+
export type MailDelivery = z.infer<typeof MailDelivery>
|
|
72
|
+
|
|
73
|
+
export const MailSuppression = z.object({
|
|
74
|
+
id: z.uuid(),
|
|
75
|
+
workspaceId: WorkspaceId.nullable(),
|
|
76
|
+
email: z.string(),
|
|
77
|
+
reason: z.enum(['bounce', 'complaint', 'manual']),
|
|
78
|
+
source: z.string().nullable(),
|
|
79
|
+
createdAt: Timestamp,
|
|
80
|
+
})
|
|
81
|
+
export type MailSuppression = z.infer<typeof MailSuppression>
|
|
82
|
+
|
|
83
|
+
const ws = z.object({ workspaceId: WorkspaceId })
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Workspace mail settings as exposed over the API. Secret fields are replaced with
|
|
87
|
+
* SECRET_PLACEHOLDER on read; sending the placeholder back keeps the stored secret.
|
|
88
|
+
* The stored shape is `core.MailProviderConfig` (contracts).
|
|
89
|
+
*/
|
|
90
|
+
export const mailContract = {
|
|
91
|
+
settings: {
|
|
92
|
+
get: baseContract
|
|
93
|
+
.route({ method: 'GET', path: '/settings', tags: ['mail'] })
|
|
94
|
+
.input(ws)
|
|
95
|
+
.output(z.object({ config: z.record(z.string(), z.unknown()).nullable() })),
|
|
96
|
+
set: baseContract
|
|
97
|
+
.route({ method: 'PUT', path: '/settings', tags: ['mail'] })
|
|
98
|
+
.input(ws.extend({ config: z.record(z.string(), z.unknown()).nullable() }))
|
|
99
|
+
.output(z.object({ ok: z.boolean() })),
|
|
100
|
+
test: baseContract
|
|
101
|
+
.route({ method: 'POST', path: '/settings/test', tags: ['mail'] })
|
|
102
|
+
.input(ws.extend({ to: Email }))
|
|
103
|
+
.output(z.object({ ok: z.boolean(), error: z.string().nullable() })),
|
|
104
|
+
},
|
|
105
|
+
deliveries: {
|
|
106
|
+
list: baseContract
|
|
107
|
+
.route({ method: 'GET', path: '/deliveries', tags: ['mail'] })
|
|
108
|
+
.input(ws.extend(PageInput.shape).extend({ status: MailDeliveryStatus.optional() }))
|
|
109
|
+
.output(page(MailDelivery)),
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
export type MailContract = typeof mailContract
|
|
113
|
+
|
|
114
|
+
const deliveryEvent = z.object({
|
|
115
|
+
deliveryId: z.uuid(),
|
|
116
|
+
workspaceId: WorkspaceId.nullable(),
|
|
117
|
+
to: z.array(z.string()),
|
|
118
|
+
providerMessageId: z.string().nullable(),
|
|
119
|
+
error: z.string().nullable(),
|
|
120
|
+
})
|
|
121
|
+
export const mailEvents = {
|
|
122
|
+
deliverySent: defineEvent('mail.delivery.sent', deliveryEvent),
|
|
123
|
+
deliveryFailed: defineEvent('mail.delivery.failed', deliveryEvent),
|
|
124
|
+
deliveryBounced: defineEvent('mail.delivery.bounced', deliveryEvent),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const mailPermissions = definePermissions([
|
|
128
|
+
{
|
|
129
|
+
key: 'mail.settings.manage',
|
|
130
|
+
label: 'Manage mail settings',
|
|
131
|
+
scope: 'workspace',
|
|
132
|
+
defaultRoles: ['owner', 'admin'],
|
|
133
|
+
dangerous: true,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
key: 'mail.deliveries.view',
|
|
137
|
+
label: 'View mail deliveries',
|
|
138
|
+
scope: 'workspace',
|
|
139
|
+
defaultRoles: ['owner', 'admin'],
|
|
140
|
+
dangerous: false,
|
|
141
|
+
},
|
|
142
|
+
])
|