@open-workspace/email 0.0.0-stage → 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.
Files changed (96) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +29 -2
  3. package/alchemy.ts +123 -0
  4. package/containers/office-preview/Dockerfile +10 -0
  5. package/index.ts +3 -0
  6. package/migrations/0000_email.sql +53 -0
  7. package/migrations/meta/0000_snapshot.json +408 -0
  8. package/migrations/meta/_journal.json +13 -0
  9. package/package.json +91 -4
  10. package/shared/attachment-preview.ts +67 -0
  11. package/shared/mail-authentication.ts +145 -0
  12. package/shared/mail-feedback.ts +90 -0
  13. package/shared/mail-headers.ts +250 -0
  14. package/shared/mail-received.ts +298 -0
  15. package/shared/mail-remote.ts +17 -0
  16. package/shared/mail-rules.ts +156 -0
  17. package/shared/mail-sender-intel.ts +125 -0
  18. package/shared/mail-translation.ts +2 -0
  19. package/shared/mail.ts +179 -0
  20. package/shared/office-preview.ts +26 -0
  21. package/worker/admin/report-routes.ts +164 -0
  22. package/worker/bindings.d.ts +11 -0
  23. package/worker/db/schema.ts +97 -0
  24. package/worker/mail/access.ts +30 -0
  25. package/worker/mail/ai-classification.ts +119 -0
  26. package/worker/mail/ai-translation.ts +31 -0
  27. package/worker/mail/auth-headers.ts +557 -0
  28. package/worker/mail/bimi.ts +172 -0
  29. package/worker/mail/classification-signals.ts +116 -0
  30. package/worker/mail/conversation-cursor.ts +43 -0
  31. package/worker/mail/delivery-events.ts +99 -0
  32. package/worker/mail/delivery-facts.ts +122 -0
  33. package/worker/mail/demo-routes.ts +33 -0
  34. package/worker/mail/doh.ts +74 -0
  35. package/worker/mail/downloads.ts +47 -0
  36. package/worker/mail/header-metadata.ts +77 -0
  37. package/worker/mail/impersonation.ts +175 -0
  38. package/worker/mail/inbound-delivery.ts +22 -0
  39. package/worker/mail/inbound-threading.ts +48 -0
  40. package/worker/mail/inbound.ts +255 -0
  41. package/worker/mail/ip-intel.ts +135 -0
  42. package/worker/mail/link-domains.ts +57 -0
  43. package/worker/mail/mailbox-deletion.ts +18 -0
  44. package/worker/mail/mailbox-directory.ts +73 -0
  45. package/worker/mail/mailing-list.ts +120 -0
  46. package/worker/mail/message-security.ts +190 -0
  47. package/worker/mail/message-view.ts +63 -0
  48. package/worker/mail/office-conversion.ts +38 -0
  49. package/worker/mail/office-preview-container.ts +57 -0
  50. package/worker/mail/office-preview-queue.ts +49 -0
  51. package/worker/mail/org-domain.ts +15 -0
  52. package/worker/mail/outbound-content.ts +208 -0
  53. package/worker/mail/outbound-delivery.ts +83 -0
  54. package/worker/mail/outbound-service.ts +145 -0
  55. package/worker/mail/outbound-threading.ts +70 -0
  56. package/worker/mail/outbound.ts +446 -0
  57. package/worker/mail/outgoing-context.ts +122 -0
  58. package/worker/mail/participants.ts +115 -0
  59. package/worker/mail/push-notifications.ts +484 -0
  60. package/worker/mail/quote-parser.ts +50 -0
  61. package/worker/mail/r2-presigned-urls.ts +69 -0
  62. package/worker/mail/receive.ts +59 -0
  63. package/worker/mail/recipients.ts +34 -0
  64. package/worker/mail/remote-proxy.ts +140 -0
  65. package/worker/mail/reply-plan.ts +136 -0
  66. package/worker/mail/reported-messages.ts +68 -0
  67. package/worker/mail/rfc.ts +73 -0
  68. package/worker/mail/routes.ts +1434 -0
  69. package/worker/mail/safety-routes.ts +350 -0
  70. package/worker/mail/schemas.ts +339 -0
  71. package/worker/mail/search.ts +14 -0
  72. package/worker/mail/security-settings.ts +38 -0
  73. package/worker/mail/sender-intel.ts +189 -0
  74. package/worker/mail/spamhaus.ts +97 -0
  75. package/worker/mail/unsubscribe.ts +118 -0
  76. package/worker/mail/uploads.ts +537 -0
  77. package/worker/mail/webhook-source.ts +71 -0
  78. package/worker/mail/workspace-safety.ts +92 -0
  79. package/worker/mailbox/demo-html.ts +136 -0
  80. package/worker/mailbox/demo-seed.ts +79 -0
  81. package/worker/mailbox/demo.ts +978 -0
  82. package/worker/mailbox/folder-model.ts +72 -0
  83. package/worker/mailbox/index.ts +2508 -0
  84. package/worker/mailbox/legacy-migrations.ts +41 -0
  85. package/worker/mailbox/migrations.ts +10 -0
  86. package/worker/mailbox/model.ts +65 -0
  87. package/worker/mailbox/office-preview-jobs.ts +73 -0
  88. package/worker/mailbox/rules.ts +110 -0
  89. package/worker/mailbox/schema.ts +353 -0
  90. package/worker/mailbox/senders.ts +240 -0
  91. package/worker/mcp/mail-action-tools.ts +210 -0
  92. package/worker/mcp/mail-compose-tools.ts +196 -0
  93. package/worker/mcp/mail-query-tools.ts +196 -0
  94. package/worker/mcp/mail-safety-tools.ts +452 -0
  95. package/worker/mcp/tools.ts +15 -0
  96. package/worker/module.ts +59 -0
@@ -0,0 +1,408 @@
1
+ {
2
+ "version": "6",
3
+ "dialect": "sqlite",
4
+ "id": "cf90d43b-4927-4d8c-aad6-9d1182a8e66a",
5
+ "prevId": "00000000-0000-0000-0000-000000000000",
6
+ "tables": {
7
+ "blocked_senders": {
8
+ "name": "blocked_senders",
9
+ "columns": {
10
+ "pattern": {
11
+ "name": "pattern",
12
+ "type": "text",
13
+ "primaryKey": true,
14
+ "notNull": true,
15
+ "autoincrement": false
16
+ },
17
+ "report_id": {
18
+ "name": "report_id",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": false,
22
+ "autoincrement": false
23
+ },
24
+ "created_by_user_id": {
25
+ "name": "created_by_user_id",
26
+ "type": "text",
27
+ "primaryKey": false,
28
+ "notNull": false,
29
+ "autoincrement": false
30
+ },
31
+ "created_at": {
32
+ "name": "created_at",
33
+ "type": "integer",
34
+ "primaryKey": false,
35
+ "notNull": true,
36
+ "autoincrement": false
37
+ }
38
+ },
39
+ "indexes": {},
40
+ "foreignKeys": {
41
+ "blocked_senders_report_id_reported_messages_id_fk": {
42
+ "name": "blocked_senders_report_id_reported_messages_id_fk",
43
+ "tableFrom": "blocked_senders",
44
+ "tableTo": "reported_messages",
45
+ "columnsFrom": [
46
+ "report_id"
47
+ ],
48
+ "columnsTo": [
49
+ "id"
50
+ ],
51
+ "onDelete": "set null",
52
+ "onUpdate": "no action"
53
+ },
54
+ "blocked_senders_created_by_user_id_users_id_fk": {
55
+ "name": "blocked_senders_created_by_user_id_users_id_fk",
56
+ "tableFrom": "blocked_senders",
57
+ "tableTo": "users",
58
+ "columnsFrom": [
59
+ "created_by_user_id"
60
+ ],
61
+ "columnsTo": [
62
+ "id"
63
+ ],
64
+ "onDelete": "set null",
65
+ "onUpdate": "no action"
66
+ }
67
+ },
68
+ "compositePrimaryKeys": {},
69
+ "uniqueConstraints": {},
70
+ "checkConstraints": {}
71
+ },
72
+ "mailbox_notification_preferences": {
73
+ "name": "mailbox_notification_preferences",
74
+ "columns": {
75
+ "mailbox_id": {
76
+ "name": "mailbox_id",
77
+ "type": "text",
78
+ "primaryKey": false,
79
+ "notNull": true,
80
+ "autoincrement": false
81
+ },
82
+ "user_id": {
83
+ "name": "user_id",
84
+ "type": "text",
85
+ "primaryKey": false,
86
+ "notNull": true,
87
+ "autoincrement": false
88
+ },
89
+ "enabled": {
90
+ "name": "enabled",
91
+ "type": "integer",
92
+ "primaryKey": false,
93
+ "notNull": true,
94
+ "autoincrement": false,
95
+ "default": true
96
+ },
97
+ "updated_at": {
98
+ "name": "updated_at",
99
+ "type": "integer",
100
+ "primaryKey": false,
101
+ "notNull": true,
102
+ "autoincrement": false,
103
+ "default": "(unixepoch() * 1000)"
104
+ }
105
+ },
106
+ "indexes": {
107
+ "mailbox_notification_preferences_user_idx": {
108
+ "name": "mailbox_notification_preferences_user_idx",
109
+ "columns": [
110
+ "user_id"
111
+ ],
112
+ "isUnique": false
113
+ }
114
+ },
115
+ "foreignKeys": {
116
+ "mailbox_notification_preferences_mailbox_id_mailboxes_id_fk": {
117
+ "name": "mailbox_notification_preferences_mailbox_id_mailboxes_id_fk",
118
+ "tableFrom": "mailbox_notification_preferences",
119
+ "tableTo": "mailboxes",
120
+ "columnsFrom": [
121
+ "mailbox_id"
122
+ ],
123
+ "columnsTo": [
124
+ "id"
125
+ ],
126
+ "onDelete": "cascade",
127
+ "onUpdate": "no action"
128
+ },
129
+ "mailbox_notification_preferences_user_id_users_id_fk": {
130
+ "name": "mailbox_notification_preferences_user_id_users_id_fk",
131
+ "tableFrom": "mailbox_notification_preferences",
132
+ "tableTo": "users",
133
+ "columnsFrom": [
134
+ "user_id"
135
+ ],
136
+ "columnsTo": [
137
+ "id"
138
+ ],
139
+ "onDelete": "cascade",
140
+ "onUpdate": "no action"
141
+ }
142
+ },
143
+ "compositePrimaryKeys": {
144
+ "mailbox_notification_preferences_mailbox_id_user_id_pk": {
145
+ "columns": [
146
+ "mailbox_id",
147
+ "user_id"
148
+ ],
149
+ "name": "mailbox_notification_preferences_mailbox_id_user_id_pk"
150
+ }
151
+ },
152
+ "uniqueConstraints": {},
153
+ "checkConstraints": {}
154
+ },
155
+ "reported_messages": {
156
+ "name": "reported_messages",
157
+ "columns": {
158
+ "id": {
159
+ "name": "id",
160
+ "type": "text",
161
+ "primaryKey": true,
162
+ "notNull": true,
163
+ "autoincrement": false
164
+ },
165
+ "mailbox_id": {
166
+ "name": "mailbox_id",
167
+ "type": "text",
168
+ "primaryKey": false,
169
+ "notNull": true,
170
+ "autoincrement": false
171
+ },
172
+ "email_id": {
173
+ "name": "email_id",
174
+ "type": "text",
175
+ "primaryKey": false,
176
+ "notNull": true,
177
+ "autoincrement": false
178
+ },
179
+ "reporter_user_id": {
180
+ "name": "reporter_user_id",
181
+ "type": "text",
182
+ "primaryKey": false,
183
+ "notNull": false,
184
+ "autoincrement": false
185
+ },
186
+ "sender_address": {
187
+ "name": "sender_address",
188
+ "type": "text",
189
+ "primaryKey": false,
190
+ "notNull": true,
191
+ "autoincrement": false
192
+ },
193
+ "sender_domain": {
194
+ "name": "sender_domain",
195
+ "type": "text",
196
+ "primaryKey": false,
197
+ "notNull": true,
198
+ "autoincrement": false
199
+ },
200
+ "subject": {
201
+ "name": "subject",
202
+ "type": "text",
203
+ "primaryKey": false,
204
+ "notNull": true,
205
+ "autoincrement": false
206
+ },
207
+ "received_at": {
208
+ "name": "received_at",
209
+ "type": "integer",
210
+ "primaryKey": false,
211
+ "notNull": true,
212
+ "autoincrement": false
213
+ },
214
+ "snapshot_json": {
215
+ "name": "snapshot_json",
216
+ "type": "text",
217
+ "primaryKey": false,
218
+ "notNull": true,
219
+ "autoincrement": false
220
+ },
221
+ "status": {
222
+ "name": "status",
223
+ "type": "text",
224
+ "primaryKey": false,
225
+ "notNull": true,
226
+ "autoincrement": false,
227
+ "default": "'open'"
228
+ },
229
+ "resolved_by_user_id": {
230
+ "name": "resolved_by_user_id",
231
+ "type": "text",
232
+ "primaryKey": false,
233
+ "notNull": false,
234
+ "autoincrement": false
235
+ },
236
+ "resolved_at": {
237
+ "name": "resolved_at",
238
+ "type": "integer",
239
+ "primaryKey": false,
240
+ "notNull": false,
241
+ "autoincrement": false
242
+ },
243
+ "created_at": {
244
+ "name": "created_at",
245
+ "type": "integer",
246
+ "primaryKey": false,
247
+ "notNull": true,
248
+ "autoincrement": false
249
+ }
250
+ },
251
+ "indexes": {
252
+ "reported_messages_email_unique": {
253
+ "name": "reported_messages_email_unique",
254
+ "columns": [
255
+ "mailbox_id",
256
+ "email_id"
257
+ ],
258
+ "isUnique": true
259
+ },
260
+ "reported_messages_status_idx": {
261
+ "name": "reported_messages_status_idx",
262
+ "columns": [
263
+ "status",
264
+ "created_at"
265
+ ],
266
+ "isUnique": false
267
+ }
268
+ },
269
+ "foreignKeys": {
270
+ "reported_messages_mailbox_id_mailboxes_id_fk": {
271
+ "name": "reported_messages_mailbox_id_mailboxes_id_fk",
272
+ "tableFrom": "reported_messages",
273
+ "tableTo": "mailboxes",
274
+ "columnsFrom": [
275
+ "mailbox_id"
276
+ ],
277
+ "columnsTo": [
278
+ "id"
279
+ ],
280
+ "onDelete": "cascade",
281
+ "onUpdate": "no action"
282
+ },
283
+ "reported_messages_reporter_user_id_users_id_fk": {
284
+ "name": "reported_messages_reporter_user_id_users_id_fk",
285
+ "tableFrom": "reported_messages",
286
+ "tableTo": "users",
287
+ "columnsFrom": [
288
+ "reporter_user_id"
289
+ ],
290
+ "columnsTo": [
291
+ "id"
292
+ ],
293
+ "onDelete": "set null",
294
+ "onUpdate": "no action"
295
+ },
296
+ "reported_messages_resolved_by_user_id_users_id_fk": {
297
+ "name": "reported_messages_resolved_by_user_id_users_id_fk",
298
+ "tableFrom": "reported_messages",
299
+ "tableTo": "users",
300
+ "columnsFrom": [
301
+ "resolved_by_user_id"
302
+ ],
303
+ "columnsTo": [
304
+ "id"
305
+ ],
306
+ "onDelete": "set null",
307
+ "onUpdate": "no action"
308
+ }
309
+ },
310
+ "compositePrimaryKeys": {},
311
+ "uniqueConstraints": {},
312
+ "checkConstraints": {}
313
+ },
314
+ "sender_reports": {
315
+ "name": "sender_reports",
316
+ "columns": {
317
+ "mailbox_id": {
318
+ "name": "mailbox_id",
319
+ "type": "text",
320
+ "primaryKey": false,
321
+ "notNull": true,
322
+ "autoincrement": false
323
+ },
324
+ "email_id": {
325
+ "name": "email_id",
326
+ "type": "text",
327
+ "primaryKey": false,
328
+ "notNull": true,
329
+ "autoincrement": false
330
+ },
331
+ "address": {
332
+ "name": "address",
333
+ "type": "text",
334
+ "primaryKey": false,
335
+ "notNull": true,
336
+ "autoincrement": false
337
+ },
338
+ "domain": {
339
+ "name": "domain",
340
+ "type": "text",
341
+ "primaryKey": false,
342
+ "notNull": true,
343
+ "autoincrement": false
344
+ },
345
+ "label": {
346
+ "name": "label",
347
+ "type": "text",
348
+ "primaryKey": false,
349
+ "notNull": true,
350
+ "autoincrement": false
351
+ },
352
+ "created_at": {
353
+ "name": "created_at",
354
+ "type": "integer",
355
+ "primaryKey": false,
356
+ "notNull": true,
357
+ "autoincrement": false
358
+ }
359
+ },
360
+ "indexes": {
361
+ "sender_reports_domain_idx": {
362
+ "name": "sender_reports_domain_idx",
363
+ "columns": [
364
+ "domain",
365
+ "address"
366
+ ],
367
+ "isUnique": false
368
+ }
369
+ },
370
+ "foreignKeys": {
371
+ "sender_reports_mailbox_id_mailboxes_id_fk": {
372
+ "name": "sender_reports_mailbox_id_mailboxes_id_fk",
373
+ "tableFrom": "sender_reports",
374
+ "tableTo": "mailboxes",
375
+ "columnsFrom": [
376
+ "mailbox_id"
377
+ ],
378
+ "columnsTo": [
379
+ "id"
380
+ ],
381
+ "onDelete": "cascade",
382
+ "onUpdate": "no action"
383
+ }
384
+ },
385
+ "compositePrimaryKeys": {
386
+ "sender_reports_mailbox_id_email_id_pk": {
387
+ "columns": [
388
+ "mailbox_id",
389
+ "email_id"
390
+ ],
391
+ "name": "sender_reports_mailbox_id_email_id_pk"
392
+ }
393
+ },
394
+ "uniqueConstraints": {},
395
+ "checkConstraints": {}
396
+ }
397
+ },
398
+ "views": {},
399
+ "enums": {},
400
+ "_meta": {
401
+ "schemas": {},
402
+ "tables": {},
403
+ "columns": {}
404
+ },
405
+ "internal": {
406
+ "indexes": {}
407
+ }
408
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "sqlite",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "6",
8
+ "when": 1793490619333,
9
+ "tag": "0000_email",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
package/package.json CHANGED
@@ -1,6 +1,93 @@
1
1
  {
2
2
  "name": "@open-workspace/email",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Team mail for OpenWorkspace: personal and shared inboxes, mailing lists, previews, safety, and AI.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/iivankin/openworkspace.git",
10
+ "directory": "packages/email"
11
+ },
12
+ "homepage": "https://openworkspace.dev",
13
+ "exports": {
14
+ ".": "./index.ts",
15
+ "./alchemy": "./alchemy.ts",
16
+ "./*": "./*"
17
+ },
18
+ "scripts": {
19
+ "typecheck": "tsc -p tsconfig.json",
20
+ "db:generate": "drizzle-kit generate && drizzle-kit generate --config=drizzle.mailbox.config.ts && bun scripts/embed-mailbox-migrations.ts"
21
+ },
22
+ "peerDependencies": {
23
+ "@open-workspace/core": "0.1.0",
24
+ "alchemy": "2.0.0-beta.84",
25
+ "@alchemy.run/cloudflare": "2.0.0-beta.84",
26
+ "effect": "4.0.7"
27
+ },
28
+ "dependencies": {
29
+ "@hono/zod-validator": "0.9.1",
30
+ "@mmmike/web-push": "1.3.0",
31
+ "@modelcontextprotocol/server": "2.0.0",
32
+ "aws4fetch": "1.0.20",
33
+ "drizzle-orm": "0.45.2",
34
+ "hono": "4.13.5",
35
+ "html-to-text": "10.0.1",
36
+ "postal-mime": "3.0.0",
37
+ "tldts": "7.4.19",
38
+ "zod": "4.5.4"
39
+ },
40
+ "devDependencies": {
41
+ "@alchemy.run/cloudflare": "2.0.0-beta.84",
42
+ "@base-ui/react": "1.7.0",
43
+ "@effect/platform-bun": "4.0.7",
44
+ "@effect/platform-node": "4.0.7",
45
+ "@open-workspace/core": "0.1.0",
46
+ "@simplewebauthn/browser": "13.3.0",
47
+ "@tanstack/react-query": "5.102.8",
48
+ "@tiptap/extension-image": "3.30.5",
49
+ "@tiptap/extension-placeholder": "3.30.5",
50
+ "@tiptap/pm": "3.30.5",
51
+ "@tiptap/react": "3.30.5",
52
+ "@tiptap/starter-kit": "3.30.5",
53
+ "@types/html-to-text": "9.0.4",
54
+ "@types/node": "26.4.0",
55
+ "@types/react": "19.2.18",
56
+ "@types/react-dom": "19.2.5",
57
+ "alchemy": "2.0.0-beta.84",
58
+ "class-variance-authority": "0.7.1",
59
+ "clsx": "2.1.1",
60
+ "cmdk": "1.1.1",
61
+ "date-fns": "4.4.0",
62
+ "dompurify": "3.4.14",
63
+ "drizzle-kit": "0.31.10",
64
+ "effect": "4.0.7",
65
+ "lucide-react": "1.38.0",
66
+ "pdfjs-dist": "6.3.289",
67
+ "react": "19.2.8",
68
+ "react-dom": "19.2.8",
69
+ "react-router": "8.3.1",
70
+ "sonner": "2.0.8",
71
+ "tailwind-merge": "3.6.0",
72
+ "typescript": "7.0.2",
73
+ "vite": "8.2.2"
74
+ },
75
+ "files": [
76
+ "index.ts",
77
+ "alchemy.ts",
78
+ "worker",
79
+ "shared",
80
+ "migrations",
81
+ "containers",
82
+ "README.md",
83
+ "LICENSE"
84
+ ],
85
+ "keywords": [
86
+ "cloudflare",
87
+ "workers",
88
+ "email",
89
+ "jmap",
90
+ "self-hosted",
91
+ "alchemy"
92
+ ]
93
+ }
@@ -0,0 +1,67 @@
1
+ import { isComposerInlineImageContentType } from "./mail";
2
+ import { officePreviewExtension, type PdfPreviewStatus } from "./office-preview";
3
+
4
+ const TEXT_TYPES = new Set([
5
+ "text/plain",
6
+ "text/markdown",
7
+ "text/csv",
8
+ "text/tab-separated-values",
9
+ "text/xml",
10
+ "text/yaml",
11
+ "application/json",
12
+ "application/xml",
13
+ "application/yaml",
14
+ ]);
15
+
16
+ const EXTENSION_TYPES: Record<string, string> = {
17
+ pdf: "application/pdf",
18
+ png: "image/png",
19
+ jpg: "image/jpeg",
20
+ jpeg: "image/jpeg",
21
+ gif: "image/gif",
22
+ webp: "image/webp",
23
+ avif: "image/avif",
24
+ bmp: "image/bmp",
25
+ txt: "text/plain",
26
+ log: "text/plain",
27
+ md: "text/markdown",
28
+ csv: "text/csv",
29
+ tsv: "text/tab-separated-values",
30
+ json: "application/json",
31
+ xml: "application/xml",
32
+ yaml: "application/yaml",
33
+ yml: "application/yaml",
34
+ };
35
+
36
+ export function attachmentPreview(file: {
37
+ filename: string;
38
+ contentType: string;
39
+ pdfPreviewStatus?: PdfPreviewStatus | null;
40
+ }): {
41
+ kind: "pdf" | "image" | "text";
42
+ contentType: string;
43
+ } | null {
44
+ if (file.pdfPreviewStatus && officePreviewExtension(file.filename)) {
45
+ return { kind: "pdf", contentType: "application/pdf" };
46
+ }
47
+ let contentType = file.contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
48
+ if (!contentType || contentType === "application/octet-stream") {
49
+ const extension = file.filename.split(".").at(-1)?.toLowerCase() ?? "";
50
+ contentType = Object.hasOwn(EXTENSION_TYPES, extension) ? EXTENSION_TYPES[extension]! : "";
51
+ }
52
+ if (contentType === "application/pdf") return { kind: "pdf", contentType };
53
+ if (
54
+ isComposerInlineImageContentType(contentType)
55
+ || contentType === "image/avif"
56
+ || contentType === "image/bmp"
57
+ ) {
58
+ return { kind: "image", contentType };
59
+ }
60
+ if (TEXT_TYPES.has(contentType)) {
61
+ const charset = /;\s*charset\s*=\s*"?([a-zA-Z0-9_-]+)/iu.exec(file.contentType)?.[1]
62
+ ?? "utf-8";
63
+ // XML and other structured text must never execute as same-origin documents.
64
+ return { kind: "text", contentType: `text/plain; charset=${charset}` };
65
+ }
66
+ return null;
67
+ }