diff --git a/apps/ai/package.json b/apps/ai/package.json
index f006dac273..fcc9cea1c9 100644
--- a/apps/ai/package.json
+++ b/apps/ai/package.json
@@ -1,6 +1,6 @@
{
"name": "@calcom/ai",
- "version": "1.2.0",
+ "version": "1.2.1",
"private": true,
"author": "Cal.com Inc.",
"dependencies": {
diff --git a/apps/ai/src/app/api/agent/route.ts b/apps/ai/src/app/api/agent/route.ts
index e39f7329a5..0ffbfa36b0 100644
--- a/apps/ai/src/app/api/agent/route.ts
+++ b/apps/ai/src/app/api/agent/route.ts
@@ -40,6 +40,13 @@ export const POST = async (request: NextRequest) => {
return new NextResponse("ok");
} catch (error) {
+ await sendEmail({
+ subject: `Re: ${subject}`,
+ text: "Thanks for using Cal.ai! We're experiencing high demand and can't currently process your request. Please try again later.",
+ to: user.email,
+ from: agentEmail,
+ });
+
return new NextResponse(
(error as Error).message || "Something went wrong. Please try again or reach out for help.",
{ status: 500 }
diff --git a/apps/ai/src/app/api/receive/route.ts b/apps/ai/src/app/api/receive/route.ts
index cf6ec9da71..63d6e5d0e4 100644
--- a/apps/ai/src/app/api/receive/route.ts
+++ b/apps/ai/src/app/api/receive/route.ts
@@ -3,6 +3,7 @@ import { simpleParser } from "mailparser";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
+import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import prisma from "@calcom/prisma";
import { env } from "../../../env.mjs";
@@ -31,18 +32,37 @@ export const POST = async (request: NextRequest) => {
const formData = await request.formData();
const body = Object.fromEntries(formData);
-
- // body.dkim looks like {@domain-com.22222222.gappssmtp.com : pass}
- const signature = (body.dkim as string).includes(" : pass");
-
const envelope = JSON.parse(body.envelope as string);
const aiEmail = envelope.to[0];
+ const subject = body.subject || "";
+
+ try {
+ await checkRateLimitAndThrowError({
+ identifier: `ai:email:${envelope.from}`,
+ rateLimitingType: "ai",
+ });
+ } catch (error) {
+ await sendEmail({
+ subject: `Re: ${subject}`,
+ text: "Thanks for using Cal.ai! You've reached your daily limit. Please try again tomorrow.",
+ to: envelope.from,
+ from: aiEmail,
+ });
+
+ return new NextResponse("Exceeded rate limit", { status: 200 }); // Don't return 429 to avoid triggering retry logic in SendGrid
+ }
// Parse email from mixed MIME type
const parsed: ParsedMail = await simpleParser(body.email as Source);
if (!parsed.text && !parsed.subject) {
+ await sendEmail({
+ subject: `Re: ${subject}`,
+ text: "Thanks for using Cal.ai! It looks like you forgot to include a message. Please try again.",
+ to: envelope.from,
+ from: aiEmail,
+ });
return new NextResponse("Email missing text and subject", { status: 400 });
}
@@ -62,11 +82,14 @@ export const POST = async (request: NextRequest) => {
where: { email: envelope.from },
});
+ // body.dkim looks like {@domain-com.22222222.gappssmtp.com : pass}
+ const signature = (body.dkim as string).includes(" : pass");
+
// User is not a cal.com user or is using an unverified email.
if (!signature || !user) {
await sendEmail({
html: `Thanks for your interest in Cal.ai! To get started, Make sure you have a cal.com account with this email address.`,
- subject: `Re: ${body.subject}`,
+ subject: `Re: ${subject}`,
text: `Thanks for your interest in Cal.ai! To get started, Make sure you have a cal.com account with this email address. You can sign up for an account at: https://cal.com/signup`,
to: envelope.from,
from: aiEmail,
@@ -83,7 +106,7 @@ export const POST = async (request: NextRequest) => {
await sendEmail({
html: `Thanks for using Cal.ai! To get started, the app must be installed. Click this link to install it.`,
- subject: `Re: ${body.subject}`,
+ subject: `Re: ${subject}`,
text: `Thanks for using Cal.ai! To get started, the app must be installed. Click this link to install the Cal.ai app: ${url}`,
to: envelope.from,
from: aiEmail,
@@ -110,7 +133,7 @@ export const POST = async (request: NextRequest) => {
if ("error" in availability) {
await sendEmail({
- subject: `Re: ${body.subject}`,
+ subject: `Re: ${subject}`,
text: "Sorry, there was an error fetching your availability. Please try again.",
to: user.email,
from: aiEmail,
@@ -121,7 +144,7 @@ export const POST = async (request: NextRequest) => {
if ("error" in eventTypes) {
await sendEmail({
- subject: `Re: ${body.subject}`,
+ subject: `Re: ${subject}`,
text: "Sorry, there was an error fetching your event types. Please try again.",
to: user.email,
from: aiEmail,
@@ -139,8 +162,8 @@ export const POST = async (request: NextRequest) => {
body: JSON.stringify({
apiKey,
userId: user.id,
- message: parsed.text,
- subject: parsed.subject,
+ message: parsed.text || "",
+ subject: parsed.subject || "",
replyTo: aiEmail,
user: {
email: user.email,
diff --git a/packages/lib/checkRateLimitAndThrowError.ts b/packages/lib/checkRateLimitAndThrowError.ts
index dd96494d37..88c43f0230 100644
--- a/packages/lib/checkRateLimitAndThrowError.ts
+++ b/packages/lib/checkRateLimitAndThrowError.ts
@@ -1,4 +1,4 @@
-import { TRPCError } from "@calcom/trpc";
+import { TRPCError } from "@calcom/trpc/server";
import type { RateLimitHelper } from "./rateLimit";
import { rateLimiter } from "./rateLimit";
diff --git a/packages/lib/rateLimit.ts b/packages/lib/rateLimit.ts
index 0608d47eea..876efb79aa 100644
--- a/packages/lib/rateLimit.ts
+++ b/packages/lib/rateLimit.ts
@@ -7,7 +7,7 @@ import logger from "./logger";
const log = logger.getChildLogger({ prefix: ["RateLimit"] });
export type RateLimitHelper = {
- rateLimitingType?: "core" | "forcedSlowMode" | "common" | "api";
+ rateLimitingType?: "core" | "forcedSlowMode" | "common" | "api" | "ai";
identifier: string;
};
@@ -75,6 +75,12 @@ export function rateLimiter() {
prefix: "ratelimit:api",
limiter: Ratelimit.fixedWindow(10, "60s"),
}),
+ ai: new Ratelimit({
+ redis,
+ analytics: true,
+ prefix: "ratelimit",
+ limiter: Ratelimit.fixedWindow(20, "1d"),
+ }),
};
async function rateLimit({ rateLimitingType = "core", identifier }: RateLimitHelper) {