@novacraft-engineering/mailbox 0.4.17 → 0.4.18
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/app/mail/page.module.css +2 -0
- package/app/mail/page.tsx +5 -1
- package/lib/mailbox.ts +7 -2
- package/lib/schema.test.ts +50 -0
- package/package.json +1 -1
package/app/mail/page.module.css
CHANGED
|
@@ -1086,6 +1086,8 @@
|
|
|
1086
1086
|
|
|
1087
1087
|
.riskBanner strong { display: block; font-weight: 650; }
|
|
1088
1088
|
.riskBannerWhy { display: block; margin-top: 3px; opacity: 0.92; font-size: 12.5px; }
|
|
1089
|
+
.riskBannerReasons { margin: 4px 0 0; padding-left: 16px; opacity: 0.92; font-size: 12.5px; list-style: disc; }
|
|
1090
|
+
.riskBannerReasons li { margin-top: 2px; }
|
|
1089
1091
|
|
|
1090
1092
|
.riskNoteIcon,
|
|
1091
1093
|
.riskBannerIcon { display: inline-flex; flex-shrink: 0; }
|
package/app/mail/page.tsx
CHANGED
|
@@ -6312,7 +6312,11 @@ export default function DevMailPage() {
|
|
|
6312
6312
|
: 'This message may not be from who it claims'}
|
|
6313
6313
|
</strong>
|
|
6314
6314
|
{(inbound.riskReasons ?? []).length > 0 && (
|
|
6315
|
-
<
|
|
6315
|
+
<ul className={styles.riskBannerReasons}>
|
|
6316
|
+
{(inbound.riskReasons ?? []).map(reason => (
|
|
6317
|
+
<li key={reason}>{reason}</li>
|
|
6318
|
+
))}
|
|
6319
|
+
</ul>
|
|
6316
6320
|
)}
|
|
6317
6321
|
<span className={styles.riskBannerWhy}>
|
|
6318
6322
|
Treat links and attachments here with care, and do not enter passwords or payment details.
|
package/lib/mailbox.ts
CHANGED
|
@@ -1103,7 +1103,7 @@ export async function refreshThread(ownerRaw: string, threadId: string): Promise
|
|
|
1103
1103
|
const head = latest[0]
|
|
1104
1104
|
await sql`
|
|
1105
1105
|
INSERT INTO mail_threads (owner, thread_id, subject_key, subject, first_at, latest_at, latest_id, count,
|
|
1106
|
-
unread_count, starred_count, inbox_count, archived_count, trashed_count, spam_count, attach_count, senders, snippet, labels, addressed)
|
|
1106
|
+
unread_count, starred_count, inbox_count, archived_count, trashed_count, spam_count, attach_count, senders, snippet, labels, addressed, risk)
|
|
1107
1107
|
VALUES (${owner}, ${threadId}, ${subjectKey(String(head?.subject ?? ''))}, ${head?.subject ?? null},
|
|
1108
1108
|
${String(agg[0].first_at)}, ${String(agg[0].latest_at)}, ${head?.id ?? null}, ${total},
|
|
1109
1109
|
${Number(agg[0].unread ?? 0)}, ${Number(agg[0].starred ?? 0)}, ${Number(agg[0].inbox ?? 0)},
|
|
@@ -1361,7 +1361,12 @@ export async function rejudgeStored(options: { before?: string; limit?: number }
|
|
|
1361
1361
|
// they put it — back-fill may label it, never move it out from under them.
|
|
1362
1362
|
const untouched = !Number(row.read) && !Number(row.starred) && !Number(row.archived) && !Number(row.trashed)
|
|
1363
1363
|
const quarantine = verdict.quarantine && untouched
|
|
1364
|
-
if (String(row.risk ?? 'clean') === verdict.risk && Boolean(Number(row.spam)) === quarantine)
|
|
1364
|
+
if (String(row.risk ?? 'clean') === verdict.risk && Boolean(Number(row.spam)) === quarantine) {
|
|
1365
|
+
// The verdict is unchanged, but the conversation summary the list reads from may still
|
|
1366
|
+
// predate it — worth one refresh for the few that carry a warning.
|
|
1367
|
+
if (verdict.risk !== 'clean') await rethreadAfterChange(String(row.id))
|
|
1368
|
+
continue
|
|
1369
|
+
}
|
|
1365
1370
|
|
|
1366
1371
|
await sql`
|
|
1367
1372
|
UPDATE mail_inbox
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
const sourceFiles = (dir: string): string[] =>
|
|
6
|
+
readdirSync(dir).flatMap(entry => {
|
|
7
|
+
const path = join(dir, entry)
|
|
8
|
+
if (entry === 'node_modules' || entry === '.next') return []
|
|
9
|
+
if (statSync(path).isDirectory()) return sourceFiles(path)
|
|
10
|
+
return path.endsWith('.ts') || path.endsWith('.tsx') ? [path] : []
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const valueCount = (values: string): number => {
|
|
14
|
+
let depth = 0
|
|
15
|
+
let count = 1
|
|
16
|
+
for (const character of values) {
|
|
17
|
+
if ('([{'.includes(character)) depth += 1
|
|
18
|
+
else if (')]}'.includes(character)) depth -= 1
|
|
19
|
+
else if (character === ',' && depth === 0) count += 1
|
|
20
|
+
}
|
|
21
|
+
return count
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const closingIndex = (text: string): number => {
|
|
25
|
+
let depth = 1
|
|
26
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
27
|
+
if (text[index] === '(') depth += 1
|
|
28
|
+
else if (text[index] === ')') { depth -= 1; if (!depth) return index }
|
|
29
|
+
}
|
|
30
|
+
return -1
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let checked = 0
|
|
34
|
+
for (const path of [...sourceFiles('lib'), ...sourceFiles('app')]) {
|
|
35
|
+
const text = readFileSync(path, 'utf8')
|
|
36
|
+
const pattern = /INSERT\s+(?:OR\s+\w+\s+)?INTO\s+(\w+)\s*\(([^)]*)\)\s*\n?\s*VALUES\s*\(/gi
|
|
37
|
+
for (const match of text.matchAll(pattern)) {
|
|
38
|
+
const [table, columns] = [match[1], match[2]]
|
|
39
|
+
const tail = text.slice(match.index + match[0].length)
|
|
40
|
+
const end = closingIndex(tail)
|
|
41
|
+
if (end < 0) continue
|
|
42
|
+
const expected = columns.split(',').filter(column => column.trim()).length
|
|
43
|
+
const actual = valueCount(tail.slice(0, end))
|
|
44
|
+
assert.equal(actual, expected, `${path}: INSERT INTO ${table} lists ${expected} columns but ${actual} values`)
|
|
45
|
+
checked += 1
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
assert.ok(checked > 20, `expected to check many inserts, checked ${checked}`)
|
|
50
|
+
console.log(`ok - ${checked} inserts balance their column and value lists`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@novacraft-engineering/mailbox",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.18",
|
|
4
4
|
"description": "A shared webmail app: one codebase, one deployment per mailbox. Threads, a rich composer with signatures, attachments on S3-compatible storage, sharing links, full-text search, web push and PWA install.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"webmail",
|