JUNOHUB-test/src/lib/gmail/gmailAutoReplyGuards.ts
2026-04-12 00:40:25 +05:00

94 lines
2.9 KiB
TypeScript

import { Prisma } from "@prisma/client"
import { prisma } from "@/lib/prisma"
const GMAIL_AUTO_REPLY_TRIGGERS = [
"GMAIL_WEBHOOK",
"GMAIL_POLL",
"AI_L1_REPLY",
] as const
export function parseEnvInt(name: string, defaultVal: number): number {
const v = process.env[name]
if (v == null || v === "") return defaultVal
const n = Number.parseInt(v, 10)
return Number.isFinite(n) && n >= 0 ? n : defaultVal
}
export function parseEnvFloat(name: string, defaultVal: number): number {
const v = process.env[name]
if (v == null || v === "") return defaultVal
const n = Number.parseFloat(v)
return Number.isFinite(n) ? n : defaultVal
}
/**
* Per-sender caps on automated Gmail replies (loop / abuse mitigation).
* Set env to 0 to disable that check.
*/
export async function checkGmailAutoReplyRateLimits(
storeId: string,
senderEmail: string
): Promise<{ ok: true } | { ok: false; reason: string }> {
const maxPerHour = parseEnvInt("GMAIL_AUTO_REPLY_MAX_PER_SENDER_PER_HOUR", 15)
const burstMax = parseEnvInt("GMAIL_AUTO_REPLY_BURST_MAX_PER_10MIN", 6)
if (maxPerHour <= 0 && burstMax <= 0) return { ok: true }
const now = Date.now()
const oneHourAgo = new Date(now - 60 * 60 * 1000)
const tenMinAgo = new Date(now - 10 * 60 * 1000)
const baseWhere = {
storeId,
to: senderEmail,
isAutomated: true,
status: "SENT" as const,
trigger: { in: [...GMAIL_AUTO_REPLY_TRIGGERS] },
}
const [hourlyCount, burstCount] = await Promise.all([
maxPerHour > 0
? prisma.emailLog.count({
where: { ...baseWhere, sentAt: { gte: oneHourAgo } },
})
: Promise.resolve(0),
burstMax > 0
? prisma.emailLog.count({
where: { ...baseWhere, sentAt: { gte: tenMinAgo } },
})
: Promise.resolve(0),
])
if (burstMax > 0 && burstCount >= burstMax) {
return { ok: false, reason: "burst_auto_reply_cap" }
}
if (maxPerHour > 0 && hourlyCount >= maxPerHour) {
return { ok: false, reason: "hourly_auto_reply_cap" }
}
return { ok: true }
}
/** Returns false if this inbound Gmail message was already processed (idempotency). */
export async function tryAcquireGmailInboundLease(
userId: string,
storeId: string,
gmailMessageId: string
): Promise<boolean> {
try {
await prisma.gmailInboundAutoReply.create({
data: { userId, storeId, gmailMessageId },
})
return true
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2002") {
return false
}
throw e
}
}
export async function releaseGmailInboundLease(userId: string, gmailMessageId: string): Promise<void> {
await prisma.gmailInboundAutoReply.deleteMany({
where: { userId, gmailMessageId },
})
}