@kernhq/module-mail 0.2.2 → 0.3.1
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/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/package.json +22 -6
- package/src/client/api-instance.ts +35 -0
- package/src/client/i18n.ts +282 -0
- package/src/client/index.ts +10 -5
- package/src/client/mock.ts +125 -0
- package/src/client/module.ts +106 -0
- package/src/client/permissions.ts +30 -0
- package/src/client/settings/MailSettings.svelte +382 -0
- package/src/client/widgets/DeliveriesWidget.svelte +103 -0
- package/src/client/widgets/FailedWidget.svelte +55 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,MAAM,EAKZ,MAAM,gBAAgB,CAAA;AAGvB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAEL,SAAS,EACT,YAAY,EACZ,UAAU,EACV,eAAe,EACf,kBAAkB,EAClB,aAAa,EACd,MAAM,gBAAgB,CAAA;AAOvB,cAAc,sBAAsB,CAAA;AACpC,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,mBAAmB,CAAA;AACjC,cAAc,gBAAgB,CAAA;AAK9B;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,aAAa,GACnB,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAiBjD;AA2ED;;;;GAIG;AACH,eAAO,MAAM,UAAU,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,MAAM,EAKZ,MAAM,gBAAgB,CAAA;AAGvB,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAEL,SAAS,EACT,YAAY,EACZ,UAAU,EACV,eAAe,EACf,kBAAkB,EAClB,aAAa,EACd,MAAM,gBAAgB,CAAA;AAOvB,cAAc,sBAAsB,CAAA;AACpC,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,mBAAmB,CAAA;AACjC,cAAc,gBAAgB,CAAA;AAK9B;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,aAAa,GACnB,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAiBjD;AA2ED;;;;GAIG;AACH,eAAO,MAAM,UAAU,gHA8DrB,CAAA;AAEF,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,kBAAkB,EAAE,CAAA;AACnF,eAAe,UAAU,CAAA;AAEzB,yFAAyF;AACzF,wBAAgB,eAAe,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,SAAS,GAAG,IAAI,GAAG,CAAC,CAG/D"}
|
package/dist/server/index.js
CHANGED
|
@@ -132,6 +132,8 @@ export const mailModule = defineServerModule({
|
|
|
132
132
|
permissions: mailPermissions,
|
|
133
133
|
events: mailEvents,
|
|
134
134
|
}),
|
|
135
|
+
/** Attached so the developer panel can check the router against what was promised. */
|
|
136
|
+
contract: mailContract,
|
|
135
137
|
schema,
|
|
136
138
|
migrationsFolder: join(dirname(fileURLToPath(import.meta.url)), '../../migrations'),
|
|
137
139
|
router: mailRouter,
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kernhq/module-mail",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Kern mail module: outbound providers (SMTP/Mailgun/SES/Postmark/Resend), MJML templates, delivery queue, bounces & suppressions",
|
|
5
5
|
"homepage": "https://kernaio.com",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@aws-sdk/client-sesv2": "^3.1116.0",
|
|
49
|
-
"@kernhq/contracts": "^0.
|
|
50
|
-
"@kernhq/kernel": "^0.
|
|
51
|
-
"@kernhq/sdk": "^0.1.
|
|
49
|
+
"@kernhq/contracts": "^0.5.1",
|
|
50
|
+
"@kernhq/kernel": "^0.7.0",
|
|
51
|
+
"@kernhq/sdk": "^0.1.5",
|
|
52
52
|
"@orpc/contract": "^1.15.0",
|
|
53
53
|
"@orpc/server": "^1.15.0",
|
|
54
54
|
"drizzle-orm": "^0.45.0",
|
|
@@ -58,27 +58,43 @@
|
|
|
58
58
|
"zod": "^4.1.0"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"
|
|
61
|
+
"@kernhq/ui": "^0.8.0",
|
|
62
|
+
"@tanstack/svelte-query": "^6.1.0",
|
|
63
|
+
"fastify": "^5.0.0",
|
|
64
|
+
"svelte": "^5.46.0"
|
|
62
65
|
},
|
|
63
66
|
"peerDependenciesMeta": {
|
|
67
|
+
"@kernhq/ui": {
|
|
68
|
+
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"@tanstack/svelte-query": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
64
73
|
"fastify": {
|
|
65
74
|
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"svelte": {
|
|
77
|
+
"optional": true
|
|
66
78
|
}
|
|
67
79
|
},
|
|
68
80
|
"devDependencies": {
|
|
69
81
|
"@kernhq/tsconfig": "^0.1.0",
|
|
82
|
+
"@kernhq/ui": "^0.8.0",
|
|
83
|
+
"@tanstack/svelte-query": "^6.1.0",
|
|
70
84
|
"@types/mjml": "^5.0.0",
|
|
71
85
|
"@types/node": "^24.0.0",
|
|
72
86
|
"@types/nodemailer": "^8.0.1",
|
|
73
87
|
"drizzle-kit": "^0.31.0",
|
|
74
88
|
"fastify": "^5.6.0",
|
|
89
|
+
"svelte": "^5.46.0",
|
|
90
|
+
"svelte-check": "^4.0.0",
|
|
75
91
|
"typescript": "~5.9.3",
|
|
76
92
|
"vitest": "^4.0.0"
|
|
77
93
|
},
|
|
78
94
|
"scripts": {
|
|
79
95
|
"build": "tsc -p tsconfig.json",
|
|
80
96
|
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
81
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
97
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && svelte-check --tsconfig ./tsconfig.client.json --threshold error",
|
|
82
98
|
"test": "vitest run",
|
|
83
99
|
"db:generate": "drizzle-kit generate"
|
|
84
100
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getHost } from '@kernhq/ui'
|
|
2
|
+
import { createMailClient, type MailApi } from './api.js'
|
|
3
|
+
import { createMockMailApi } from './mock.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The mail API client.
|
|
7
|
+
*
|
|
8
|
+
* Mail runs in its own service because it holds provider connections and a delivery queue, but its
|
|
9
|
+
* REST surface is reached like any other module's: `/api/mail`, same origin, same session cookie.
|
|
10
|
+
* The dev server proxies the prefix to 4200 and Caddy does the same in production.
|
|
11
|
+
*
|
|
12
|
+
* The base URL comes from the shell, not from an env var read here: same-origin in every real
|
|
13
|
+
* deployment, so this module never needs to know that mail listens on 4200.
|
|
14
|
+
*
|
|
15
|
+
* The shell also decides whether it is running against the in-memory implementation, which
|
|
16
|
+
* satisfies the same contract types — so the settings screen has no second code path for demos and
|
|
17
|
+
* end-to-end tests.
|
|
18
|
+
*/
|
|
19
|
+
export type { MailApi }
|
|
20
|
+
|
|
21
|
+
let cached: MailApi | null = null
|
|
22
|
+
|
|
23
|
+
export function getMailApi(): MailApi {
|
|
24
|
+
if (cached) return cached
|
|
25
|
+
const host = getHost()
|
|
26
|
+
cached = host.isMock
|
|
27
|
+
? (createMockMailApi() as unknown as MailApi)
|
|
28
|
+
: createMailClient({ baseUrl: host.apiBaseUrl })
|
|
29
|
+
return cached
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Test seam: lets tests install a fake without touching module state elsewhere. */
|
|
33
|
+
export function __setMailApi(api: MailApi | null) {
|
|
34
|
+
cached = api
|
|
35
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mail's own strings, in every locale the platform ships.
|
|
3
|
+
*
|
|
4
|
+
* A module ships separately from the app, so Paraglide cannot compile these — the shell merges
|
|
5
|
+
* them into the framework's message runtime when it registers this module, and `t()` resolves
|
|
6
|
+
* against the merged map. Keys are namespaced by module id, which is what keeps two modules from
|
|
7
|
+
* colliding in that one map.
|
|
8
|
+
*
|
|
9
|
+
* Bundles are thunks so a locale is only fetched when it is the one in use; English is the
|
|
10
|
+
* fallback and is therefore always loaded.
|
|
11
|
+
*/
|
|
12
|
+
import { scopedT } from '@kernhq/ui'
|
|
13
|
+
|
|
14
|
+
export const en = {
|
|
15
|
+
'mail.field_access_key': 'Access key ID',
|
|
16
|
+
'mail.field_api_key': 'API key',
|
|
17
|
+
'mail.field_domain': 'Domain',
|
|
18
|
+
'mail.field_from': 'From address',
|
|
19
|
+
'mail.field_host': 'Host',
|
|
20
|
+
'mail.field_password': 'Password',
|
|
21
|
+
'mail.field_port': 'Port',
|
|
22
|
+
'mail.field_region': 'Region',
|
|
23
|
+
'mail.field_secret_key': 'Secret access key',
|
|
24
|
+
'mail.field_server_token': 'Server token',
|
|
25
|
+
'mail.field_user': 'Username',
|
|
26
|
+
'mail.log_all': 'Everything',
|
|
27
|
+
'mail.log_empty': 'Nothing has been sent yet.',
|
|
28
|
+
'mail.log_hint': 'Every message this workspace sent, and why any of them did not arrive.',
|
|
29
|
+
'mail.log_title': 'What has been sent',
|
|
30
|
+
'mail.provider_hint': "Without one, this workspace sends through the instance's own configuration.",
|
|
31
|
+
'mail.provider_label': 'Send through',
|
|
32
|
+
'mail.provider_mailgun': 'Mailgun',
|
|
33
|
+
'mail.provider_platform': 'The instance default',
|
|
34
|
+
'mail.provider_postmark': 'Postmark',
|
|
35
|
+
'mail.provider_resend': 'Resend',
|
|
36
|
+
'mail.provider_ses': 'Amazon SES',
|
|
37
|
+
'mail.provider_smtp': 'SMTP',
|
|
38
|
+
'mail.provider_title': 'Provider',
|
|
39
|
+
'mail.saved': 'Email settings saved',
|
|
40
|
+
'mail.secret_kept': 'Stored — leave this as it is to keep it.',
|
|
41
|
+
'mail.settings_hint': "Where this workspace's email comes from, and what it has sent.",
|
|
42
|
+
'mail.settings_nav': 'Email',
|
|
43
|
+
'mail.settings_title': 'Email',
|
|
44
|
+
'mail.status_bounced': 'Bounced',
|
|
45
|
+
'mail.status_failed': 'Failed',
|
|
46
|
+
'mail.status_queued': 'Queued',
|
|
47
|
+
'mail.status_sent': 'Sent',
|
|
48
|
+
'mail.test_hint': 'The only way to find out whether the credentials work is to use them.',
|
|
49
|
+
'mail.test_refused': 'The provider refused the message.',
|
|
50
|
+
'mail.test_send': 'Send test',
|
|
51
|
+
'mail.test_sent': 'Test message queued for {to}',
|
|
52
|
+
'mail.test_title': 'Send a test',
|
|
53
|
+
'mail.widget_bounced': 'Bounced',
|
|
54
|
+
'mail.widget_desc': 'What this workspace has been sending.',
|
|
55
|
+
'mail.widget_empty': 'Nothing sent yet',
|
|
56
|
+
'mail.widget_failed': 'Failed',
|
|
57
|
+
'mail.widget_failed_desc': 'Delivery problems in the recent sends.',
|
|
58
|
+
'mail.widget_failed_note': 'of the last {count}',
|
|
59
|
+
'mail.widget_failed_title': 'Failed sends',
|
|
60
|
+
'mail.widget_queued': 'Queued',
|
|
61
|
+
'mail.widget_sent': 'Sent',
|
|
62
|
+
'mail.widget_title': 'Recent deliveries',
|
|
63
|
+
} as const
|
|
64
|
+
|
|
65
|
+
export type MailMessageKey = keyof typeof en
|
|
66
|
+
|
|
67
|
+
const ar: Record<string, string> = {
|
|
68
|
+
'mail.field_access_key': 'معرّف مفتاح الوصول',
|
|
69
|
+
'mail.field_api_key': 'مفتاح API',
|
|
70
|
+
'mail.field_domain': 'النطاق',
|
|
71
|
+
'mail.field_from': 'عنوان المُرسِل',
|
|
72
|
+
'mail.field_host': 'المضيف',
|
|
73
|
+
'mail.field_password': 'كلمة المرور',
|
|
74
|
+
'mail.field_port': 'المنفذ',
|
|
75
|
+
'mail.field_region': 'المنطقة',
|
|
76
|
+
'mail.field_secret_key': 'مفتاح الوصول السري',
|
|
77
|
+
'mail.field_server_token': 'رمز الخادم',
|
|
78
|
+
'mail.field_user': 'اسم المستخدم',
|
|
79
|
+
'mail.log_all': 'الكل',
|
|
80
|
+
'mail.log_empty': 'لم يُرسل شيء بعد.',
|
|
81
|
+
'mail.log_hint': 'كل رسالة أرسلتها مساحة العمل هذه، وسبب عدم وصول ما لم يصل.',
|
|
82
|
+
'mail.log_title': 'ما أُرسل',
|
|
83
|
+
'mail.provider_hint': 'بدون مزوّد خاص، ترسل مساحة العمل هذه عبر إعدادات النسخة نفسها.',
|
|
84
|
+
'mail.provider_label': 'الإرسال عبر',
|
|
85
|
+
'mail.provider_mailgun': 'Mailgun',
|
|
86
|
+
'mail.provider_platform': 'الإعداد الافتراضي للنسخة',
|
|
87
|
+
'mail.provider_postmark': 'Postmark',
|
|
88
|
+
'mail.provider_resend': 'Resend',
|
|
89
|
+
'mail.provider_ses': 'Amazon SES',
|
|
90
|
+
'mail.provider_smtp': 'SMTP',
|
|
91
|
+
'mail.provider_title': 'المزوّد',
|
|
92
|
+
'mail.saved': 'حُفظت إعدادات البريد',
|
|
93
|
+
'mail.secret_kept': 'مخزَّن — اتركه كما هو للإبقاء عليه.',
|
|
94
|
+
'mail.settings_hint': 'من أين يأتي بريد مساحة العمل هذه، وماذا أرسلت.',
|
|
95
|
+
'mail.settings_nav': 'البريد',
|
|
96
|
+
'mail.settings_title': 'البريد',
|
|
97
|
+
'mail.status_bounced': 'مرتدّة',
|
|
98
|
+
'mail.status_failed': 'فشل',
|
|
99
|
+
'mail.status_queued': 'في الانتظار',
|
|
100
|
+
'mail.status_sent': 'أُرسلت',
|
|
101
|
+
'mail.test_hint': 'الطريقة الوحيدة لمعرفة أن بيانات الاعتماد تعمل هي استخدامها.',
|
|
102
|
+
'mail.test_refused': 'رفض المزوّد الرسالة.',
|
|
103
|
+
'mail.test_send': 'إرسال اختبار',
|
|
104
|
+
'mail.test_sent': 'وُضعت رسالة اختبار في الانتظار إلى {to}',
|
|
105
|
+
'mail.test_title': 'إرسال اختبار',
|
|
106
|
+
'mail.widget_bounced': 'مرتدّة',
|
|
107
|
+
'mail.widget_desc': 'ما الذي أرسلته مساحة العمل هذه.',
|
|
108
|
+
'mail.widget_empty': 'لم يُرسل شيء بعد',
|
|
109
|
+
'mail.widget_failed': 'فاشلة',
|
|
110
|
+
'mail.widget_failed_desc': 'مشكلات التسليم في عمليات الإرسال الأخيرة.',
|
|
111
|
+
'mail.widget_failed_note': 'من آخر {count}',
|
|
112
|
+
'mail.widget_failed_title': 'عمليات إرسال فاشلة',
|
|
113
|
+
'mail.widget_queued': 'في الانتظار',
|
|
114
|
+
'mail.widget_sent': 'مُرسَلة',
|
|
115
|
+
'mail.widget_title': 'عمليات الإرسال الأخيرة',
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const de: Record<string, string> = {
|
|
119
|
+
'mail.field_access_key': 'Access Key ID',
|
|
120
|
+
'mail.field_api_key': 'API-Schlüssel',
|
|
121
|
+
'mail.field_domain': 'Domain',
|
|
122
|
+
'mail.field_from': 'Absenderadresse',
|
|
123
|
+
'mail.field_host': 'Host',
|
|
124
|
+
'mail.field_password': 'Passwort',
|
|
125
|
+
'mail.field_port': 'Port',
|
|
126
|
+
'mail.field_region': 'Region',
|
|
127
|
+
'mail.field_secret_key': 'Secret Access Key',
|
|
128
|
+
'mail.field_server_token': 'Server-Token',
|
|
129
|
+
'mail.field_user': 'Benutzername',
|
|
130
|
+
'mail.log_all': 'Alles',
|
|
131
|
+
'mail.log_empty': 'Es wurde noch nichts gesendet.',
|
|
132
|
+
'mail.log_hint': 'Jede Nachricht, die dieser Workspace gesendet hat — und warum manche nicht ankamen.',
|
|
133
|
+
'mail.log_title': 'Was gesendet wurde',
|
|
134
|
+
'mail.provider_hint': 'Ohne eigenen Anbieter sendet dieser Workspace über die Konfiguration der Instanz.',
|
|
135
|
+
'mail.provider_label': 'Senden über',
|
|
136
|
+
'mail.provider_mailgun': 'Mailgun',
|
|
137
|
+
'mail.provider_platform': 'Standard der Instanz',
|
|
138
|
+
'mail.provider_postmark': 'Postmark',
|
|
139
|
+
'mail.provider_resend': 'Resend',
|
|
140
|
+
'mail.provider_ses': 'Amazon SES',
|
|
141
|
+
'mail.provider_smtp': 'SMTP',
|
|
142
|
+
'mail.provider_title': 'Anbieter',
|
|
143
|
+
'mail.saved': 'E-Mail-Einstellungen gespeichert',
|
|
144
|
+
'mail.secret_kept': 'Gespeichert — lass es so, um es zu behalten.',
|
|
145
|
+
'mail.settings_hint': 'Woher die E-Mail dieses Workspace kommt und was er gesendet hat.',
|
|
146
|
+
'mail.settings_nav': 'E-Mail',
|
|
147
|
+
'mail.settings_title': 'E-Mail',
|
|
148
|
+
'mail.status_bounced': 'Unzustellbar',
|
|
149
|
+
'mail.status_failed': 'Fehlgeschlagen',
|
|
150
|
+
'mail.status_queued': 'In der Warteschlange',
|
|
151
|
+
'mail.status_sent': 'Gesendet',
|
|
152
|
+
'mail.test_hint': 'Ob die Zugangsdaten stimmen, findet man nur heraus, indem man sie benutzt.',
|
|
153
|
+
'mail.test_refused': 'Der Anbieter hat die Nachricht abgelehnt.',
|
|
154
|
+
'mail.test_send': 'Test senden',
|
|
155
|
+
'mail.test_sent': 'Testnachricht für {to} eingereiht',
|
|
156
|
+
'mail.test_title': 'Test senden',
|
|
157
|
+
'mail.widget_bounced': 'Unzustellbar',
|
|
158
|
+
'mail.widget_desc': 'Was dieser Arbeitsbereich versendet hat.',
|
|
159
|
+
'mail.widget_empty': 'Noch nichts versendet',
|
|
160
|
+
'mail.widget_failed': 'Fehlgeschlagen',
|
|
161
|
+
'mail.widget_failed_desc': 'Zustellprobleme bei den letzten Sendungen.',
|
|
162
|
+
'mail.widget_failed_note': 'von den letzten {count}',
|
|
163
|
+
'mail.widget_failed_title': 'Fehlgeschlagene Sendungen',
|
|
164
|
+
'mail.widget_queued': 'In der Warteschlange',
|
|
165
|
+
'mail.widget_sent': 'Gesendet',
|
|
166
|
+
'mail.widget_title': 'Letzte Zustellungen',
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const fa: Record<string, string> = {
|
|
170
|
+
'mail.field_access_key': 'شناسه کلید دسترسی',
|
|
171
|
+
'mail.field_api_key': 'کلید API',
|
|
172
|
+
'mail.field_domain': 'دامنه',
|
|
173
|
+
'mail.field_from': 'نشانی فرستنده',
|
|
174
|
+
'mail.field_host': 'میزبان',
|
|
175
|
+
'mail.field_password': 'گذرواژه',
|
|
176
|
+
'mail.field_port': 'درگاه',
|
|
177
|
+
'mail.field_region': 'ناحیه',
|
|
178
|
+
'mail.field_secret_key': 'کلید دسترسی محرمانه',
|
|
179
|
+
'mail.field_server_token': 'توکن سرور',
|
|
180
|
+
'mail.field_user': 'نام کاربری',
|
|
181
|
+
'mail.log_all': 'همه',
|
|
182
|
+
'mail.log_empty': 'هنوز چیزی فرستاده نشده است.',
|
|
183
|
+
'mail.log_hint': 'هر پیامی که این فضای کاری فرستاده، و اینکه چرا برخی نرسیدهاند.',
|
|
184
|
+
'mail.log_title': 'آنچه فرستاده شده',
|
|
185
|
+
'mail.provider_hint': 'بدون آن، این فضای کاری با پیکربندی خودِ نمونه ارسال میکند.',
|
|
186
|
+
'mail.provider_label': 'ارسال از طریق',
|
|
187
|
+
'mail.provider_mailgun': 'Mailgun',
|
|
188
|
+
'mail.provider_platform': 'پیشفرض نمونه',
|
|
189
|
+
'mail.provider_postmark': 'Postmark',
|
|
190
|
+
'mail.provider_resend': 'Resend',
|
|
191
|
+
'mail.provider_ses': 'Amazon SES',
|
|
192
|
+
'mail.provider_smtp': 'SMTP',
|
|
193
|
+
'mail.provider_title': 'ارائهدهنده',
|
|
194
|
+
'mail.saved': 'تنظیمات ایمیل ذخیره شد',
|
|
195
|
+
'mail.secret_kept': 'ذخیره شده — برای نگهداشتنش دست نزنید.',
|
|
196
|
+
'mail.settings_hint': 'اینکه ایمیل این فضای کاری از کجا میآید و چه چیزی فرستاده است.',
|
|
197
|
+
'mail.settings_nav': 'ایمیل',
|
|
198
|
+
'mail.settings_title': 'ایمیل',
|
|
199
|
+
'mail.status_bounced': 'برگشتخورده',
|
|
200
|
+
'mail.status_failed': 'ناموفق',
|
|
201
|
+
'mail.status_queued': 'در صف',
|
|
202
|
+
'mail.status_sent': 'فرستادهشده',
|
|
203
|
+
'mail.test_hint': 'تنها راه دانستن اینکه اطلاعات ورود کار میکند، استفاده از آن است.',
|
|
204
|
+
'mail.test_refused': 'ارائهدهنده پیام را نپذیرفت.',
|
|
205
|
+
'mail.test_send': 'ارسال آزمایشی',
|
|
206
|
+
'mail.test_sent': 'پیام آزمایشی برای {to} در صف قرار گرفت',
|
|
207
|
+
'mail.test_title': 'ارسال آزمایشی',
|
|
208
|
+
'mail.widget_bounced': 'برگشتخورده',
|
|
209
|
+
'mail.widget_desc': 'این فضای کاری چه فرستاده است.',
|
|
210
|
+
'mail.widget_empty': 'هنوز چیزی فرستاده نشده',
|
|
211
|
+
'mail.widget_failed': 'ناموفق',
|
|
212
|
+
'mail.widget_failed_desc': 'مشکلات تحویل در ارسالهای اخیر.',
|
|
213
|
+
'mail.widget_failed_note': 'از {count} مورد اخیر',
|
|
214
|
+
'mail.widget_failed_title': 'ارسالهای ناموفق',
|
|
215
|
+
'mail.widget_queued': 'در صف',
|
|
216
|
+
'mail.widget_sent': 'فرستادهشده',
|
|
217
|
+
'mail.widget_title': 'ارسالهای اخیر',
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const tr: Record<string, string> = {
|
|
221
|
+
'mail.field_access_key': 'Erişim anahtarı kimliği',
|
|
222
|
+
'mail.field_api_key': 'API anahtarı',
|
|
223
|
+
'mail.field_domain': 'Alan adı',
|
|
224
|
+
'mail.field_from': 'Gönderen adresi',
|
|
225
|
+
'mail.field_host': 'Sunucu',
|
|
226
|
+
'mail.field_password': 'Parola',
|
|
227
|
+
'mail.field_port': 'Bağlantı noktası',
|
|
228
|
+
'mail.field_region': 'Bölge',
|
|
229
|
+
'mail.field_secret_key': 'Gizli erişim anahtarı',
|
|
230
|
+
'mail.field_server_token': 'Sunucu belirteci',
|
|
231
|
+
'mail.field_user': 'Kullanıcı adı',
|
|
232
|
+
'mail.log_all': 'Tümü',
|
|
233
|
+
'mail.log_empty': 'Henüz bir şey gönderilmedi.',
|
|
234
|
+
'mail.log_hint': 'Bu çalışma alanının gönderdiği her ileti ve ulaşmayanların nedeni.',
|
|
235
|
+
'mail.log_title': 'Gönderilenler',
|
|
236
|
+
'mail.provider_hint':
|
|
237
|
+
'Kendi sağlayıcısı yoksa bu çalışma alanı sunucunun kendi yapılandırmasıyla gönderir.',
|
|
238
|
+
'mail.provider_label': 'Şunun üzerinden gönder',
|
|
239
|
+
'mail.provider_mailgun': 'Mailgun',
|
|
240
|
+
'mail.provider_platform': 'Sunucu varsayılanı',
|
|
241
|
+
'mail.provider_postmark': 'Postmark',
|
|
242
|
+
'mail.provider_resend': 'Resend',
|
|
243
|
+
'mail.provider_ses': 'Amazon SES',
|
|
244
|
+
'mail.provider_smtp': 'SMTP',
|
|
245
|
+
'mail.provider_title': 'Sağlayıcı',
|
|
246
|
+
'mail.saved': 'E-posta ayarları kaydedildi',
|
|
247
|
+
'mail.secret_kept': 'Saklanıyor — korumak için olduğu gibi bırakın.',
|
|
248
|
+
'mail.settings_hint': 'Bu çalışma alanının e-postası nereden geliyor ve neler göndermiş.',
|
|
249
|
+
'mail.settings_nav': 'E-posta',
|
|
250
|
+
'mail.settings_title': 'E-posta',
|
|
251
|
+
'mail.status_bounced': 'Geri döndü',
|
|
252
|
+
'mail.status_failed': 'Başarısız',
|
|
253
|
+
'mail.status_queued': 'Kuyrukta',
|
|
254
|
+
'mail.status_sent': 'Gönderildi',
|
|
255
|
+
'mail.test_hint': 'Kimlik bilgilerinin çalışıp çalışmadığını öğrenmenin tek yolu onları kullanmaktır.',
|
|
256
|
+
'mail.test_refused': 'Sağlayıcı iletiyi reddetti.',
|
|
257
|
+
'mail.test_send': 'Test gönder',
|
|
258
|
+
'mail.test_sent': '{to} için test iletisi kuyruğa alındı',
|
|
259
|
+
'mail.test_title': 'Test gönder',
|
|
260
|
+
'mail.widget_bounced': 'Geri döndü',
|
|
261
|
+
'mail.widget_desc': 'Bu çalışma alanının neler gönderdiği.',
|
|
262
|
+
'mail.widget_empty': 'Henüz bir şey gönderilmedi',
|
|
263
|
+
'mail.widget_failed': 'Başarısız',
|
|
264
|
+
'mail.widget_failed_desc': 'Son gönderimlerdeki iletim sorunları.',
|
|
265
|
+
'mail.widget_failed_note': 'son {count} içinden',
|
|
266
|
+
'mail.widget_failed_title': 'Başarısız gönderimler',
|
|
267
|
+
'mail.widget_queued': 'Kuyrukta',
|
|
268
|
+
'mail.widget_sent': 'Gönderildi',
|
|
269
|
+
'mail.widget_title': 'Son gönderimler',
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** In the shape `defineClientModule().messages` expects. */
|
|
273
|
+
export const mailMessageBundles = {
|
|
274
|
+
ar: async () => ar,
|
|
275
|
+
de: async () => de,
|
|
276
|
+
en: async () => en as Record<string, string>,
|
|
277
|
+
fa: async () => fa,
|
|
278
|
+
tr: async () => tr,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** `t('settings_nav')` — the module id is implied. */
|
|
282
|
+
export const t = scopedT('mail')
|
package/src/client/index.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
export { MODULE_ID, SECRET_PLACEHOLDER } from '../contract.js'
|
|
2
2
|
export { createMailClient, type MailApi } from './api.js'
|
|
3
|
+
export { __setMailApi, getMailApi } from './api-instance.js'
|
|
4
|
+
export { type MailMessageKey, mailMessageBundles, t } from './i18n.js'
|
|
5
|
+
export { mailClientModule, mailClientModule as default } from './module.js'
|
|
6
|
+
export { canMail, MAIL_PERMISSIONS, type MailPermission } from './permissions.js'
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
|
-
* Mail's client
|
|
9
|
+
* Mail's client half: the typed API client, its own strings, its permissions, and the client module
|
|
10
|
+
* the shell registers.
|
|
6
11
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
12
|
+
* The screens live here too — `settings/MailSettings.svelte` and the two dashboard cards — rather
|
|
13
|
+
* than in the app. That is what makes this a module rather than a server half plus a hand-written
|
|
14
|
+
* page somebody has to remember to keep in step: the shell mounts whatever the manifest declares,
|
|
15
|
+
* and deleting this package removes the feature completely.
|
|
11
16
|
*/
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { WorkspaceId } from '@kernhq/contracts'
|
|
2
|
+
import type { MailDelivery } from '../contract.js'
|
|
3
|
+
import { SECRET_PLACEHOLDER } from './index.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The in-memory mail API, for `pnpm dev:mock` and the end-to-end suite.
|
|
7
|
+
*
|
|
8
|
+
* It keeps the one behaviour that is easy to get wrong and impossible to see: a stored secret is
|
|
9
|
+
* never returned, reads replace it with a placeholder, and writing the placeholder back leaves the
|
|
10
|
+
* stored value alone. A mock that echoed the real secret would let a screen that leaks it pass
|
|
11
|
+
* every test.
|
|
12
|
+
*/
|
|
13
|
+
const DAY = 86_400_000
|
|
14
|
+
const iso = (msAgo: number) => new Date(Date.now() - msAgo).toISOString()
|
|
15
|
+
|
|
16
|
+
const SECRET_KEYS = new Set(['pass', 'apiKey', 'secretAccessKey', 'serverToken'])
|
|
17
|
+
|
|
18
|
+
/** The demo workspace, branded the way the other mocks brand theirs. */
|
|
19
|
+
const WORKSPACE = '01920000-0000-7000-8000-000000000010' as string & WorkspaceId
|
|
20
|
+
|
|
21
|
+
export function createMockMailApi() {
|
|
22
|
+
let config: Record<string, unknown> | null = {
|
|
23
|
+
provider: 'smtp',
|
|
24
|
+
host: 'smtp.example.com',
|
|
25
|
+
port: 587,
|
|
26
|
+
user: 'kern',
|
|
27
|
+
pass: 'hunter2',
|
|
28
|
+
from: 'Northstar <no-reply@northstar.example>',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const deliveries: MailDelivery[] = [
|
|
32
|
+
{
|
|
33
|
+
id: '0192aa00-0000-7000-8000-00000000d001',
|
|
34
|
+
workspaceId: WORKSPACE,
|
|
35
|
+
to: ['maya@northstar.example'],
|
|
36
|
+
subject: 'You were mentioned in KRN-6',
|
|
37
|
+
provider: 'smtp',
|
|
38
|
+
template: 'mention',
|
|
39
|
+
status: 'sent',
|
|
40
|
+
providerMessageId: '<a1b2@smtp.example.com>',
|
|
41
|
+
error: null,
|
|
42
|
+
tags: ['notification'],
|
|
43
|
+
createdAt: iso(2 * 3600e3),
|
|
44
|
+
updatedAt: iso(2 * 3600e3),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: '0192aa00-0000-7000-8000-00000000d002',
|
|
48
|
+
workspaceId: WORKSPACE,
|
|
49
|
+
to: ['tomas@northstar.example'],
|
|
50
|
+
subject: 'Your weekly digest',
|
|
51
|
+
provider: 'smtp',
|
|
52
|
+
template: 'digest',
|
|
53
|
+
status: 'sent',
|
|
54
|
+
providerMessageId: '<c3d4@smtp.example.com>',
|
|
55
|
+
error: null,
|
|
56
|
+
tags: ['digest'],
|
|
57
|
+
createdAt: iso(DAY),
|
|
58
|
+
updatedAt: iso(DAY),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: '0192aa00-0000-7000-8000-00000000d003',
|
|
62
|
+
workspaceId: WORKSPACE,
|
|
63
|
+
to: ['left@example.com'],
|
|
64
|
+
subject: 'You have been invited to Northstar',
|
|
65
|
+
provider: 'smtp',
|
|
66
|
+
template: 'invite',
|
|
67
|
+
status: 'bounced',
|
|
68
|
+
providerMessageId: null,
|
|
69
|
+
error: '550 5.1.1 The email account that you tried to reach does not exist',
|
|
70
|
+
tags: ['invite'],
|
|
71
|
+
createdAt: iso(3 * DAY),
|
|
72
|
+
updatedAt: iso(3 * DAY),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: '0192aa00-0000-7000-8000-00000000d004',
|
|
76
|
+
workspaceId: WORKSPACE,
|
|
77
|
+
to: ['dan@northstar.example'],
|
|
78
|
+
subject: 'Reset your password',
|
|
79
|
+
provider: 'smtp',
|
|
80
|
+
template: 'password-reset',
|
|
81
|
+
status: 'failed',
|
|
82
|
+
providerMessageId: null,
|
|
83
|
+
error: 'Connection timed out after 30s',
|
|
84
|
+
tags: ['account'],
|
|
85
|
+
createdAt: iso(5 * DAY),
|
|
86
|
+
updatedAt: iso(5 * DAY),
|
|
87
|
+
},
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
/** What a read returns: the shape of the config with every secret masked. */
|
|
91
|
+
const masked = () =>
|
|
92
|
+
config === null
|
|
93
|
+
? null
|
|
94
|
+
: Object.fromEntries(
|
|
95
|
+
Object.entries(config).map(([k, v]) => [k, SECRET_KEYS.has(k) && v ? SECRET_PLACEHOLDER : v]),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
settings: {
|
|
100
|
+
get: async () => ({ config: masked() }),
|
|
101
|
+
set: async ({ config: next }: { config: Record<string, unknown> | null }) => {
|
|
102
|
+
if (next === null) {
|
|
103
|
+
config = null
|
|
104
|
+
return { ok: true as const }
|
|
105
|
+
}
|
|
106
|
+
// The placeholder means "leave it alone", which is the whole point of masking on read.
|
|
107
|
+
const merged: Record<string, unknown> = { ...next }
|
|
108
|
+
for (const key of SECRET_KEYS)
|
|
109
|
+
if (merged[key] === SECRET_PLACEHOLDER) merged[key] = config?.[key] ?? ''
|
|
110
|
+
config = merged
|
|
111
|
+
return { ok: true as const }
|
|
112
|
+
},
|
|
113
|
+
test: async ({ to }: { to: string }) =>
|
|
114
|
+
to.endsWith('@example.com') || to.endsWith('.example')
|
|
115
|
+
? { ok: true as const, error: null }
|
|
116
|
+
: { ok: false as const, error: 'The provider refused the recipient' },
|
|
117
|
+
},
|
|
118
|
+
deliveries: {
|
|
119
|
+
list: async ({ status }: { status?: MailDelivery['status'] }) => ({
|
|
120
|
+
items: deliveries.filter((d) => !status || d.status === status),
|
|
121
|
+
nextCursor: null,
|
|
122
|
+
}),
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { defineClientModule } from '@kernhq/ui'
|
|
2
|
+
import { mailMessageBundles, t } from './i18n.js'
|
|
3
|
+
import { MAIL_PERMISSIONS } from './permissions.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Mail as the shell sees it.
|
|
7
|
+
*
|
|
8
|
+
* No navigation: where a workspace's email comes from is configuration, not a place people visit.
|
|
9
|
+
* The module contributes one settings page and two dashboard cards, and turning mail off in
|
|
10
|
+
* workspace settings removes all three with no conditionals anywhere in the shell.
|
|
11
|
+
*
|
|
12
|
+
* Labels are getters because a module is defined once at import time while the interface language
|
|
13
|
+
* can change afterwards; reading them on render keeps them in the language actually chosen.
|
|
14
|
+
*
|
|
15
|
+
* The settings page's `id` is its URL. The shell mounts a workspace-scope page whose id equals the
|
|
16
|
+
* module id at `/<ws>/settings/mail` — declaring it is the whole wiring, and there is no route file
|
|
17
|
+
* in the app to keep in step with it.
|
|
18
|
+
*/
|
|
19
|
+
export const mailClientModule = defineClientModule({
|
|
20
|
+
id: 'mail',
|
|
21
|
+
name: 'Mail',
|
|
22
|
+
icon: 'mail',
|
|
23
|
+
messages: mailMessageBundles,
|
|
24
|
+
|
|
25
|
+
widgets: [
|
|
26
|
+
{
|
|
27
|
+
id: 'mail.deliveries',
|
|
28
|
+
get title() {
|
|
29
|
+
return t('widget_title')
|
|
30
|
+
},
|
|
31
|
+
get description() {
|
|
32
|
+
return t('widget_desc')
|
|
33
|
+
},
|
|
34
|
+
icon: 'mail',
|
|
35
|
+
permission: MAIL_PERMISSIONS.deliveriesView,
|
|
36
|
+
sizes: ['m', 'l', 'xl'],
|
|
37
|
+
defaultSize: 'l',
|
|
38
|
+
order: 10,
|
|
39
|
+
settings: [
|
|
40
|
+
{
|
|
41
|
+
kind: 'select',
|
|
42
|
+
key: 'status',
|
|
43
|
+
get label() {
|
|
44
|
+
return t('common.setting_status')
|
|
45
|
+
},
|
|
46
|
+
default: null,
|
|
47
|
+
nullable: true,
|
|
48
|
+
get nullLabel() {
|
|
49
|
+
return t('common.any')
|
|
50
|
+
},
|
|
51
|
+
get options() {
|
|
52
|
+
return [
|
|
53
|
+
{ value: 'queued', label: t('widget_queued') },
|
|
54
|
+
{ value: 'sent', label: t('widget_sent') },
|
|
55
|
+
{ value: 'failed', label: t('widget_failed') },
|
|
56
|
+
{ value: 'bounced', label: t('widget_bounced') },
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
kind: 'number',
|
|
62
|
+
key: 'limit',
|
|
63
|
+
get label() {
|
|
64
|
+
return t('common.setting_rows')
|
|
65
|
+
},
|
|
66
|
+
default: 8,
|
|
67
|
+
min: 3,
|
|
68
|
+
max: 20,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
component: () => import('./widgets/DeliveriesWidget.svelte'),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'mail.stat-failed',
|
|
75
|
+
get title() {
|
|
76
|
+
return t('widget_failed_title')
|
|
77
|
+
},
|
|
78
|
+
get description() {
|
|
79
|
+
return t('widget_failed_desc')
|
|
80
|
+
},
|
|
81
|
+
icon: 'circle-alert',
|
|
82
|
+
permission: MAIL_PERMISSIONS.deliveriesView,
|
|
83
|
+
sizes: ['s'],
|
|
84
|
+
defaultSize: 's',
|
|
85
|
+
compact: true,
|
|
86
|
+
order: 20,
|
|
87
|
+
component: () => import('./widgets/FailedWidget.svelte'),
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
|
|
91
|
+
settingsPages: [
|
|
92
|
+
{
|
|
93
|
+
id: 'mail',
|
|
94
|
+
get label() {
|
|
95
|
+
return t('settings_nav')
|
|
96
|
+
},
|
|
97
|
+
icon: 'mail',
|
|
98
|
+
scope: 'workspace',
|
|
99
|
+
permission: MAIL_PERMISSIONS.settingsManage,
|
|
100
|
+
order: 45,
|
|
101
|
+
component: () => import('./settings/MailSettings.svelte'),
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
export default mailClientModule
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { session } from '@kernhq/ui'
|
|
2
|
+
import { mailPermissions } from '../contract.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What mail lets somebody do.
|
|
6
|
+
*
|
|
7
|
+
* Derived from the contract rather than re-typed. The app used to keep its own copy of these
|
|
8
|
+
* strings, which type-checks perfectly while being wrong: a mistyped key silently hides a control
|
|
9
|
+
* or offers one the server refuses, and nothing anywhere reports it.
|
|
10
|
+
*
|
|
11
|
+
* Hide what a person may never do; disable — with a reason — what they cannot do right now. The
|
|
12
|
+
* server checks again on every call regardless; this only stops the interface offering a door that
|
|
13
|
+
* will not open.
|
|
14
|
+
*/
|
|
15
|
+
const key = (suffix: string) => {
|
|
16
|
+
const found = mailPermissions.find((p) => p.key === `mail.${suffix}`)
|
|
17
|
+
if (!found) throw new Error(`mail: no permission declared for mail.${suffix}`)
|
|
18
|
+
return found.key
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const MAIL_PERMISSIONS = {
|
|
22
|
+
settingsManage: key('settings.manage'),
|
|
23
|
+
deliveriesView: key('deliveries.view'),
|
|
24
|
+
} as const
|
|
25
|
+
|
|
26
|
+
export type MailPermission = keyof typeof MAIL_PERMISSIONS
|
|
27
|
+
|
|
28
|
+
export function canMail(permission: MailPermission): boolean {
|
|
29
|
+
return session.can(MAIL_PERMISSIONS[permission])
|
|
30
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
Badge,
|
|
4
|
+
Button,
|
|
5
|
+
Input,
|
|
6
|
+
messageLocale,
|
|
7
|
+
Select,
|
|
8
|
+
SettingsPage,
|
|
9
|
+
SettingsSection,
|
|
10
|
+
Spinner,
|
|
11
|
+
session,
|
|
12
|
+
toast,
|
|
13
|
+
} from '@kernhq/ui'
|
|
14
|
+
import { createMutation, createQuery, useQueryClient } from '@tanstack/svelte-query'
|
|
15
|
+
import type { MailDelivery } from '../../contract.js'
|
|
16
|
+
import { getMailApi } from '../api-instance.js'
|
|
17
|
+
import { t } from '../i18n.js'
|
|
18
|
+
import { SECRET_PLACEHOLDER } from '../index.js'
|
|
19
|
+
import { canMail } from '../permissions.js'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Where this workspace's email comes from, and what it has sent.
|
|
23
|
+
*
|
|
24
|
+
* The module has been complete and unreachable: settings, a test send and a delivery log all had a
|
|
25
|
+
* server and nothing that called them.
|
|
26
|
+
*
|
|
27
|
+
* Without a provider the workspace falls back to the instance's own configuration, which is why a
|
|
28
|
+
* fresh self-hosted install already delivers invitations and password resets — so "use the instance
|
|
29
|
+
* default" is a real choice rather than an unconfigured state.
|
|
30
|
+
*/
|
|
31
|
+
const api = getMailApi()
|
|
32
|
+
const queryClient = useQueryClient()
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The shell passes these; this component does not read the router.
|
|
36
|
+
*
|
|
37
|
+
* `$app/state` is a SvelteKit alias, and a module package is type-checked on its own — reaching for
|
|
38
|
+
* the router here fails standalone even though it resolves inside the app. `ModuleRoute` already
|
|
39
|
+
* hands every module page the workspace it is rendering, which is the only thing this needed it for.
|
|
40
|
+
*/
|
|
41
|
+
interface Props {
|
|
42
|
+
workspaceId: string
|
|
43
|
+
workspaceSlug: string
|
|
44
|
+
}
|
|
45
|
+
const { workspaceId }: Props = $props()
|
|
46
|
+
const canManage = $derived(canMail('settingsManage'))
|
|
47
|
+
const canSeeLog = $derived(canMail('deliveriesView'))
|
|
48
|
+
|
|
49
|
+
type Provider = 'platform' | 'smtp' | 'mailgun' | 'ses' | 'postmark' | 'resend'
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What each provider needs. Carried over from the module package, which is where this screen used
|
|
53
|
+
* to live before it could be rendered.
|
|
54
|
+
*/
|
|
55
|
+
const FIELDS: Record<Provider, Array<{ key: string; label: () => string; type?: string; hint?: string }>> = {
|
|
56
|
+
platform: [],
|
|
57
|
+
smtp: [
|
|
58
|
+
{ key: 'host', label: () => t('field_host') },
|
|
59
|
+
{ key: 'port', label: () => t('field_port'), type: 'number' },
|
|
60
|
+
{ key: 'user', label: () => t('field_user') },
|
|
61
|
+
{ key: 'pass', label: () => t('field_password'), type: 'password' },
|
|
62
|
+
{ key: 'from', label: () => t('field_from'), hint: 'Kern <no-reply@example.com>' },
|
|
63
|
+
],
|
|
64
|
+
mailgun: [
|
|
65
|
+
{ key: 'apiKey', label: () => t('field_api_key'), type: 'password' },
|
|
66
|
+
{ key: 'domain', label: () => t('field_domain') },
|
|
67
|
+
{ key: 'region', label: () => t('field_region'), hint: 'us / eu' },
|
|
68
|
+
{ key: 'from', label: () => t('field_from') },
|
|
69
|
+
],
|
|
70
|
+
ses: [
|
|
71
|
+
{ key: 'accessKeyId', label: () => t('field_access_key') },
|
|
72
|
+
{ key: 'secretAccessKey', label: () => t('field_secret_key'), type: 'password' },
|
|
73
|
+
{ key: 'region', label: () => t('field_region'), hint: 'eu-west-1' },
|
|
74
|
+
{ key: 'from', label: () => t('field_from') },
|
|
75
|
+
],
|
|
76
|
+
postmark: [
|
|
77
|
+
{ key: 'serverToken', label: () => t('field_server_token'), type: 'password' },
|
|
78
|
+
{ key: 'from', label: () => t('field_from') },
|
|
79
|
+
],
|
|
80
|
+
resend: [
|
|
81
|
+
{ key: 'apiKey', label: () => t('field_api_key'), type: 'password' },
|
|
82
|
+
{ key: 'from', label: () => t('field_from') },
|
|
83
|
+
],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const PROVIDER_LABELS: Record<Provider, () => string> = {
|
|
87
|
+
platform: () => t('provider_platform'),
|
|
88
|
+
smtp: () => t('provider_smtp'),
|
|
89
|
+
mailgun: () => t('provider_mailgun'),
|
|
90
|
+
ses: () => t('provider_ses'),
|
|
91
|
+
postmark: () => t('provider_postmark'),
|
|
92
|
+
resend: () => t('provider_resend'),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let provider = $state<Provider>('platform')
|
|
96
|
+
let values = $state<Record<string, string>>({})
|
|
97
|
+
/** Set once from the server, so re-renders do not fight what is being typed. */
|
|
98
|
+
let loaded = $state(false)
|
|
99
|
+
let recipient = $state('')
|
|
100
|
+
let statusFilter = $state('')
|
|
101
|
+
|
|
102
|
+
const settingsQuery = createQuery(() => ({
|
|
103
|
+
queryKey: ['mail', 'settings', workspaceId],
|
|
104
|
+
queryFn: () => api.settings.get({ workspaceId }),
|
|
105
|
+
enabled: Boolean(workspaceId),
|
|
106
|
+
}))
|
|
107
|
+
|
|
108
|
+
$effect(() => {
|
|
109
|
+
const config = settingsQuery.data?.config
|
|
110
|
+
if (loaded || settingsQuery.isPending) return
|
|
111
|
+
if (config && typeof config.provider === 'string') {
|
|
112
|
+
provider = config.provider as Provider
|
|
113
|
+
values = Object.fromEntries(
|
|
114
|
+
Object.entries(config)
|
|
115
|
+
.filter(([k]) => k !== 'provider')
|
|
116
|
+
.map(([k, v]) => [k, String(v ?? '')]),
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
loaded = true
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const fields = $derived(FIELDS[provider])
|
|
123
|
+
|
|
124
|
+
const save = createMutation(() => ({
|
|
125
|
+
mutationFn: () =>
|
|
126
|
+
api.settings.set({
|
|
127
|
+
workspaceId,
|
|
128
|
+
config:
|
|
129
|
+
provider === 'platform'
|
|
130
|
+
? null
|
|
131
|
+
: { provider, ...Object.fromEntries(fields.map((f) => [f.key, values[f.key] ?? ''])) },
|
|
132
|
+
}),
|
|
133
|
+
onSuccess: () => {
|
|
134
|
+
void queryClient.invalidateQueries({ queryKey: ['mail', 'settings', workspaceId] })
|
|
135
|
+
toast.success(t('saved'))
|
|
136
|
+
},
|
|
137
|
+
onError: (error: Error) => toast.error(error.message),
|
|
138
|
+
}))
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* A test send, which is the only way to find out whether the credentials work.
|
|
142
|
+
*
|
|
143
|
+
* The provider's refusal comes back as a result rather than an error, so it is shown as one — a
|
|
144
|
+
* rejected recipient is an answer, not a failure of the request.
|
|
145
|
+
*/
|
|
146
|
+
const test = createMutation(() => ({
|
|
147
|
+
mutationFn: () => api.settings.test({ workspaceId, to: recipient }),
|
|
148
|
+
onSuccess: (result: { ok: boolean; error: string | null }) => {
|
|
149
|
+
if (result.ok) toast.success(t('test_sent', { to: recipient }))
|
|
150
|
+
else toast.error(result.error ?? t('test_refused'))
|
|
151
|
+
},
|
|
152
|
+
onError: (error: Error) => toast.error(error.message),
|
|
153
|
+
}))
|
|
154
|
+
|
|
155
|
+
const deliveriesQuery = createQuery(() => ({
|
|
156
|
+
queryKey: ['mail', 'deliveries', workspaceId, statusFilter],
|
|
157
|
+
queryFn: () =>
|
|
158
|
+
api.deliveries.list({
|
|
159
|
+
workspaceId,
|
|
160
|
+
...(statusFilter ? { status: statusFilter as MailDelivery['status'] } : {}),
|
|
161
|
+
}),
|
|
162
|
+
enabled: Boolean(workspaceId) && canSeeLog,
|
|
163
|
+
}))
|
|
164
|
+
const deliveries = $derived(deliveriesQuery.data?.items ?? [])
|
|
165
|
+
|
|
166
|
+
const STATUS_LABELS: Record<string, () => string> = {
|
|
167
|
+
queued: () => t('status_queued'),
|
|
168
|
+
sent: () => t('status_sent'),
|
|
169
|
+
failed: () => t('status_failed'),
|
|
170
|
+
bounced: () => t('status_bounced'),
|
|
171
|
+
}
|
|
172
|
+
const statusLabel = (status: string) => STATUS_LABELS[status]?.() ?? status
|
|
173
|
+
/** Only a failure has a tone: colouring every row makes the one that needs attention disappear. */
|
|
174
|
+
const statusTone = (status: string) =>
|
|
175
|
+
status === 'bounced' || status === 'failed' ? 'danger' : status === 'queued' ? 'grey' : 'success'
|
|
176
|
+
|
|
177
|
+
const when = $derived(new Intl.DateTimeFormat(messageLocale(), { dateStyle: 'medium', timeStyle: 'short' }))
|
|
178
|
+
</script>
|
|
179
|
+
|
|
180
|
+
<SettingsPage title={t('settings_title')} description={t('settings_hint')}>
|
|
181
|
+
<SettingsSection title={t('provider_title')} description={t('provider_hint')}>
|
|
182
|
+
{#if settingsQuery.isPending}
|
|
183
|
+
<div class="state"><Spinner /></div>
|
|
184
|
+
{:else}
|
|
185
|
+
<div class="grid">
|
|
186
|
+
<label class="row" data-testid="mail-provider">
|
|
187
|
+
<span class="lbl">{t('provider_label')}</span>
|
|
188
|
+
<Select
|
|
189
|
+
value={provider}
|
|
190
|
+
options={(Object.keys(PROVIDER_LABELS) as Provider[]).map((p) => ({
|
|
191
|
+
value: p,
|
|
192
|
+
label: PROVIDER_LABELS[p](),
|
|
193
|
+
}))}
|
|
194
|
+
disabled={!canManage}
|
|
195
|
+
onValueChange={(v: string) => (provider = v as Provider)}
|
|
196
|
+
/>
|
|
197
|
+
</label>
|
|
198
|
+
|
|
199
|
+
{#each fields as field (field.key)}
|
|
200
|
+
<label class="row">
|
|
201
|
+
<span class="lbl">{field.label()}</span>
|
|
202
|
+
<Input
|
|
203
|
+
type={field.type ?? 'text'}
|
|
204
|
+
value={values[field.key] ?? ''}
|
|
205
|
+
placeholder={field.hint ?? ''}
|
|
206
|
+
autocomplete="off"
|
|
207
|
+
disabled={!canManage}
|
|
208
|
+
data-testid="mail-{field.key}"
|
|
209
|
+
oninput={(e: Event) =>
|
|
210
|
+
(values = { ...values, [field.key]: (e.currentTarget as HTMLInputElement).value })}
|
|
211
|
+
/>
|
|
212
|
+
{#if values[field.key] === SECRET_PLACEHOLDER}
|
|
213
|
+
<!-- The server never returns a stored secret; sending the placeholder back keeps it.
|
|
214
|
+
Saying so is the difference between "already set" and "about to be blanked". -->
|
|
215
|
+
<span class="hint">{t('secret_kept')}</span>
|
|
216
|
+
{/if}
|
|
217
|
+
</label>
|
|
218
|
+
{/each}
|
|
219
|
+
</div>
|
|
220
|
+
{/if}
|
|
221
|
+
|
|
222
|
+
{#snippet footer()}
|
|
223
|
+
<Button
|
|
224
|
+
size="sm"
|
|
225
|
+
disabled={!canManage}
|
|
226
|
+
loading={save.isPending}
|
|
227
|
+
onclick={() => save.mutate()}
|
|
228
|
+
data-testid="mail-save"
|
|
229
|
+
>
|
|
230
|
+
{t('common.save')}
|
|
231
|
+
</Button>
|
|
232
|
+
{/snippet}
|
|
233
|
+
</SettingsSection>
|
|
234
|
+
|
|
235
|
+
<SettingsSection title={t('test_title')} description={t('test_hint')}>
|
|
236
|
+
<div class="testrow">
|
|
237
|
+
<Input
|
|
238
|
+
type="email"
|
|
239
|
+
bind:value={recipient}
|
|
240
|
+
placeholder={session.user?.email ?? 'you@example.com'}
|
|
241
|
+
disabled={!canManage}
|
|
242
|
+
data-testid="mail-test-to"
|
|
243
|
+
/>
|
|
244
|
+
<Button
|
|
245
|
+
size="sm"
|
|
246
|
+
variant="secondary"
|
|
247
|
+
disabled={!canManage || !recipient.trim()}
|
|
248
|
+
loading={test.isPending}
|
|
249
|
+
onclick={() => test.mutate()}
|
|
250
|
+
data-testid="mail-test-send"
|
|
251
|
+
>
|
|
252
|
+
{t('test_send')}
|
|
253
|
+
</Button>
|
|
254
|
+
</div>
|
|
255
|
+
</SettingsSection>
|
|
256
|
+
|
|
257
|
+
{#if canSeeLog}
|
|
258
|
+
<SettingsSection title={t('log_title')} description={t('log_hint')}>
|
|
259
|
+
<div class="filter" data-testid="mail-log-filter">
|
|
260
|
+
<Select
|
|
261
|
+
value={statusFilter}
|
|
262
|
+
options={[
|
|
263
|
+
{ value: '', label: t('log_all') },
|
|
264
|
+
...['sent', 'queued', 'failed', 'bounced'].map((s) => ({ value: s, label: statusLabel(s) })),
|
|
265
|
+
]}
|
|
266
|
+
onValueChange={(v: string) => (statusFilter = v)}
|
|
267
|
+
/>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
{#if deliveriesQuery.isPending}
|
|
271
|
+
<div class="state"><Spinner /></div>
|
|
272
|
+
{:else if !deliveries.length}
|
|
273
|
+
<p class="empty">{t('log_empty')}</p>
|
|
274
|
+
{:else}
|
|
275
|
+
<ul class="log" data-testid="mail-log">
|
|
276
|
+
{#each deliveries as delivery (delivery.id)}
|
|
277
|
+
<li>
|
|
278
|
+
<div class="top">
|
|
279
|
+
<Badge tone={statusTone(delivery.status)}>{statusLabel(delivery.status)}</Badge>
|
|
280
|
+
<span class="subject">{delivery.subject}</span>
|
|
281
|
+
<span class="when">{when.format(new Date(delivery.createdAt))}</span>
|
|
282
|
+
</div>
|
|
283
|
+
<div class="meta">
|
|
284
|
+
<span>{delivery.to.join(', ')}</span>
|
|
285
|
+
{#if delivery.template}<span>{delivery.template}</span>{/if}
|
|
286
|
+
</div>
|
|
287
|
+
{#if delivery.error}
|
|
288
|
+
<!-- The provider's own words. A message that could not be delivered is only
|
|
289
|
+
actionable if you can read why the other end refused it. -->
|
|
290
|
+
<p class="err">{delivery.error}</p>
|
|
291
|
+
{/if}
|
|
292
|
+
</li>
|
|
293
|
+
{/each}
|
|
294
|
+
</ul>
|
|
295
|
+
{/if}
|
|
296
|
+
</SettingsSection>
|
|
297
|
+
{/if}
|
|
298
|
+
</SettingsPage>
|
|
299
|
+
|
|
300
|
+
<style>
|
|
301
|
+
.grid {
|
|
302
|
+
display: grid;
|
|
303
|
+
gap: 12px;
|
|
304
|
+
}
|
|
305
|
+
.row {
|
|
306
|
+
display: grid;
|
|
307
|
+
gap: 4px;
|
|
308
|
+
}
|
|
309
|
+
.lbl {
|
|
310
|
+
font-size: 12px;
|
|
311
|
+
font-weight: 500;
|
|
312
|
+
color: var(--kern-ink-600);
|
|
313
|
+
}
|
|
314
|
+
.hint {
|
|
315
|
+
font-size: 11.5px;
|
|
316
|
+
color: var(--kern-ink-400);
|
|
317
|
+
}
|
|
318
|
+
.testrow {
|
|
319
|
+
display: grid;
|
|
320
|
+
grid-template-columns: minmax(0, 260px) auto;
|
|
321
|
+
align-items: center;
|
|
322
|
+
gap: 8px;
|
|
323
|
+
}
|
|
324
|
+
.filter {
|
|
325
|
+
max-width: 200px;
|
|
326
|
+
margin-bottom: 10px;
|
|
327
|
+
}
|
|
328
|
+
.log {
|
|
329
|
+
list-style: none;
|
|
330
|
+
margin: 0;
|
|
331
|
+
padding: 0;
|
|
332
|
+
}
|
|
333
|
+
.log li {
|
|
334
|
+
padding: 8px 0;
|
|
335
|
+
border-bottom: 1px solid var(--kern-border-hairline);
|
|
336
|
+
}
|
|
337
|
+
.log li:last-child {
|
|
338
|
+
border-bottom: 0;
|
|
339
|
+
}
|
|
340
|
+
.top {
|
|
341
|
+
display: flex;
|
|
342
|
+
align-items: center;
|
|
343
|
+
gap: 8px;
|
|
344
|
+
}
|
|
345
|
+
.subject {
|
|
346
|
+
flex: 1;
|
|
347
|
+
min-width: 0;
|
|
348
|
+
font-size: 13px;
|
|
349
|
+
overflow: hidden;
|
|
350
|
+
text-overflow: ellipsis;
|
|
351
|
+
white-space: nowrap;
|
|
352
|
+
}
|
|
353
|
+
.when {
|
|
354
|
+
font-size: 12px;
|
|
355
|
+
color: var(--kern-ink-400);
|
|
356
|
+
white-space: nowrap;
|
|
357
|
+
}
|
|
358
|
+
.meta {
|
|
359
|
+
display: flex;
|
|
360
|
+
flex-wrap: wrap;
|
|
361
|
+
gap: 4px 10px;
|
|
362
|
+
margin-top: 2px;
|
|
363
|
+
font-size: 12px;
|
|
364
|
+
color: var(--kern-ink-400);
|
|
365
|
+
}
|
|
366
|
+
.err {
|
|
367
|
+
margin: 4px 0 0;
|
|
368
|
+
font-size: 12px;
|
|
369
|
+
color: var(--kern-danger);
|
|
370
|
+
overflow-wrap: anywhere;
|
|
371
|
+
}
|
|
372
|
+
.empty {
|
|
373
|
+
margin: 0;
|
|
374
|
+
font-size: 13px;
|
|
375
|
+
color: var(--kern-ink-400);
|
|
376
|
+
}
|
|
377
|
+
.state {
|
|
378
|
+
display: grid;
|
|
379
|
+
place-items: center;
|
|
380
|
+
padding: 24px;
|
|
381
|
+
}
|
|
382
|
+
</style>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { WidgetProps } from '@kernhq/ui'
|
|
3
|
+
import { Badge, relativeTime, settingsScope, WidgetState } from '@kernhq/ui'
|
|
4
|
+
import { createQuery } from '@tanstack/svelte-query'
|
|
5
|
+
import { getMailApi } from '../api-instance.js'
|
|
6
|
+
import { t } from '../i18n.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* What this workspace has been sending.
|
|
10
|
+
*
|
|
11
|
+
* The mail module is outbound only — there is no inbox to show, and `ROADMAP.md` defers one to
|
|
12
|
+
* v1.1 — so this is about delivery health rather than about reading anything.
|
|
13
|
+
*/
|
|
14
|
+
let { workspaceId, workspaceSlug, settings }: WidgetProps = $props()
|
|
15
|
+
|
|
16
|
+
const api = getMailApi()
|
|
17
|
+
const limit = $derived(Number(settings.limit ?? 8))
|
|
18
|
+
const status = $derived((settings.status as string | null) ?? null)
|
|
19
|
+
|
|
20
|
+
const query = createQuery(() => ({
|
|
21
|
+
queryKey: ['mail', 'delivery', workspaceId, 'widget', settingsScope(settings)],
|
|
22
|
+
queryFn: () =>
|
|
23
|
+
api.deliveries.list({
|
|
24
|
+
workspaceId,
|
|
25
|
+
limit,
|
|
26
|
+
status: (status ?? undefined) as never,
|
|
27
|
+
}),
|
|
28
|
+
enabled: Boolean(workspaceId),
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
const tone = (s: string) =>
|
|
32
|
+
s === 'failed' || s === 'bounced' ? 'danger' : s === 'sent' || s === 'delivered' ? 'done' : 'grey'
|
|
33
|
+
|
|
34
|
+
const items = $derived(query.data?.items ?? [])
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<WidgetState
|
|
38
|
+
pending={query.isPending}
|
|
39
|
+
error={query.error}
|
|
40
|
+
empty={items.length === 0}
|
|
41
|
+
emptyTitle={t('widget_empty')}
|
|
42
|
+
emptyIcon="mail"
|
|
43
|
+
onRetry={() => query.refetch()}
|
|
44
|
+
>
|
|
45
|
+
<ul>
|
|
46
|
+
{#each items as delivery (delivery.id)}
|
|
47
|
+
<li>
|
|
48
|
+
<a href="/{workspaceSlug}/settings/mail">
|
|
49
|
+
<span class="text">
|
|
50
|
+
<span class="subject">{delivery.subject}</span>
|
|
51
|
+
<span class="to">{delivery.to.join(', ')}</span>
|
|
52
|
+
</span>
|
|
53
|
+
<Badge tone={tone(delivery.status)} variant="chip">{delivery.status}</Badge>
|
|
54
|
+
<time datetime={delivery.createdAt}>{relativeTime(delivery.createdAt)}</time>
|
|
55
|
+
</a>
|
|
56
|
+
</li>
|
|
57
|
+
{/each}
|
|
58
|
+
</ul>
|
|
59
|
+
</WidgetState>
|
|
60
|
+
|
|
61
|
+
<style>
|
|
62
|
+
li {
|
|
63
|
+
border-block-end: 1px solid var(--kern-border-hairline);
|
|
64
|
+
}
|
|
65
|
+
li:last-child {
|
|
66
|
+
border-block-end: 0;
|
|
67
|
+
}
|
|
68
|
+
a {
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
gap: 9px;
|
|
72
|
+
padding: 8px 14px;
|
|
73
|
+
color: inherit;
|
|
74
|
+
}
|
|
75
|
+
a:hover {
|
|
76
|
+
background: var(--kern-surface-hover);
|
|
77
|
+
}
|
|
78
|
+
.text {
|
|
79
|
+
flex: 1;
|
|
80
|
+
min-width: 0;
|
|
81
|
+
display: grid;
|
|
82
|
+
}
|
|
83
|
+
.subject,
|
|
84
|
+
.to {
|
|
85
|
+
overflow: hidden;
|
|
86
|
+
text-overflow: ellipsis;
|
|
87
|
+
white-space: nowrap;
|
|
88
|
+
}
|
|
89
|
+
.subject {
|
|
90
|
+
font-size: 13px;
|
|
91
|
+
color: var(--kern-ink-900);
|
|
92
|
+
}
|
|
93
|
+
.to {
|
|
94
|
+
font-size: 12px;
|
|
95
|
+
color: var(--kern-ink-450);
|
|
96
|
+
}
|
|
97
|
+
time {
|
|
98
|
+
flex-shrink: 0;
|
|
99
|
+
font-family: var(--kern-font-mono);
|
|
100
|
+
font-size: 11px;
|
|
101
|
+
color: var(--kern-ink-400);
|
|
102
|
+
}
|
|
103
|
+
</style>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { WidgetProps } from '@kernhq/ui'
|
|
3
|
+
import { formatCount, StatTile } from '@kernhq/ui'
|
|
4
|
+
import { createQuery } from '@tanstack/svelte-query'
|
|
5
|
+
import { getMailApi } from '../api-instance.js'
|
|
6
|
+
import { t } from '../i18n.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Failed sends, of the last fifty.
|
|
10
|
+
*
|
|
11
|
+
* `deliveries.list` returns a page and no total, and has no date filter, so a bare number here would
|
|
12
|
+
* be a claim the API cannot support. The note says exactly what the figure covers instead — a
|
|
13
|
+
* smaller true statement beats a bigger invented one. A real total needs a `deliveries.stats`
|
|
14
|
+
* procedure in the mail module first.
|
|
15
|
+
*/
|
|
16
|
+
let { workspaceId, workspaceSlug }: WidgetProps = $props()
|
|
17
|
+
|
|
18
|
+
const WINDOW = 50
|
|
19
|
+
|
|
20
|
+
const api = getMailApi()
|
|
21
|
+
const query = createQuery(() => ({
|
|
22
|
+
queryKey: ['mail', 'delivery', workspaceId, 'failed', WINDOW],
|
|
23
|
+
queryFn: () => api.deliveries.list({ workspaceId, limit: WINDOW }),
|
|
24
|
+
enabled: Boolean(workspaceId),
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
const failed = $derived(
|
|
28
|
+
(query.data?.items ?? []).filter((d) => d.status === 'failed' || d.status === 'bounced').length,
|
|
29
|
+
)
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<div class="wrap">
|
|
33
|
+
<StatTile
|
|
34
|
+
label={t('widget_failed_title')}
|
|
35
|
+
value={query.isPending ? '—' : formatCount(failed)}
|
|
36
|
+
note={t('widget_failed_note', { count: formatCount(WINDOW) })}
|
|
37
|
+
href="/{workspaceSlug}/settings/mail"
|
|
38
|
+
size="md"
|
|
39
|
+
class="tile"
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<style>
|
|
44
|
+
.wrap {
|
|
45
|
+
display: grid;
|
|
46
|
+
align-content: center;
|
|
47
|
+
height: 100%;
|
|
48
|
+
padding: 14px 16px;
|
|
49
|
+
}
|
|
50
|
+
.wrap :global(.tile) {
|
|
51
|
+
border: 0;
|
|
52
|
+
background: transparent;
|
|
53
|
+
padding: 0;
|
|
54
|
+
}
|
|
55
|
+
</style>
|