Skip to content

Commit

Permalink
revert: Don't use AMQP for emails (#482)
Browse files Browse the repository at this point in the history
* revert amql

* fix
  • Loading branch information
storm1729 authored Jan 3, 2024
1 parent 7848c7d commit 3a98a10
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 228 deletions.
1 change: 1 addition & 0 deletions src/app/[lang]/dashboard/verify/GetStartedSaaS.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export function GetStartedSaaS({
data: {
to_email: email,
},
retries: 1,
})
.then((r) => {
setResult(r);
Expand Down
168 changes: 168 additions & 0 deletions src/app/api/v0/check_email/backends.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { v4 } from "uuid";
import axios, { AxiosError } from "axios";
import { supabaseAdmin } from "@/supabase/supabaseAdmin";
import { CheckEmailInput, CheckEmailOutput } from "@reacherhq/api";
import { Tables } from "@/supabase/database.types";
import { convertPgError } from "@/util/helpers";

// Vercel functions time out after 60s.
const VERCEL_TIMEOUT = 90_000; // ms

interface ReacherBackend {
url: string;
name: string;
}

const RCH_BACKENDS = [
{
url: "http://backend1.reacher.dev",
name: "backend1-ovh",
},
{
url: "http://backend3.reacher.dev",
name: "backend3-do",
},
];

/**
* Forwards the Next.JS request to Reacher's backends, try them all in the
* order given by `RCH_BACKENDS`.
*/
export async function tryAllBackends(
emailInput: CheckEmailInput,
user: Tables<"users">
): Promise<Response> {
try {
// The final result to return.
let result: CheckEmailOutput;

let t: NodeJS.Timeout | undefined;

async function makeBackendCalls(): Promise<Response> {
// Create a unique UUID for each verification. The purpose of this
// verificationId is that we insert one row in the `calls` table per
// backend call. However, if we try the backends sequentially, we don't
// want to charge the user 2 credits for 1 email verification.
const verificationId = v4();

// Note that we don't loop the last element of reacherBackends. That
// one gets treated specially, as we'll always return its response.
for (let i = 0; i < RCH_BACKENDS.length - 1; i++) {
try {
result = await makeSingleBackendCall(
verificationId,
RCH_BACKENDS[i],
emailInput,
user
);

if (result.is_reachable !== "unknown") {
return Response.json(result);
}
} catch {
// Continue loop
}
}

// If we arrive here, it means all previous backend calls errored or
// returned "unknown". We make the last backend call, and always return
// its response.
result = await makeSingleBackendCall(
verificationId,
RCH_BACKENDS[RCH_BACKENDS.length - 1],
emailInput,
user
);

return Response.json(result);
}

const res = await Promise.race([
makeBackendCalls(),
new Promise<Response>((resolve) => {
t = setTimeout(() => {
return resolve(
Response.json(
{
error: `The email ${emailInput.to_email} can't be verified within 90s. This is because the email provider imposes obstacles to prevent real-time email verification, such as greylisting. Please try again later.`,
},
{ status: 504 }
)
);
}, VERCEL_TIMEOUT);
}),
]);

if (t) {
clearTimeout(t);
}

return res;
} catch (err) {
const statusCode = (err as AxiosError).response?.status;
if (!statusCode) {
throw err;
}

return Response.json(
{
error: (err as AxiosError).response?.data,
},
{
status: statusCode,
}
);
}
}

/**
* Make a single call to the backend, and log some metadata to the DB.
*/
async function makeSingleBackendCall(
verificationId: string,
reacherBackend: ReacherBackend,
emailInput: CheckEmailInput,
user: Tables<"users">
): Promise<CheckEmailOutput> {
const t0 = performance.now();
// Send an API request to Reacher backend, which handles email
// verifications, see https://github.com/reacherhq/backend.
const result = await axios.post<CheckEmailOutput>(
`${reacherBackend.url}/v0/check_email`,
emailInput,
{
headers: {
"x-reacher-secret": process.env.RCH_HEADER_SECRET || "",
},
}
);
const t1 = performance.now();
console.log(`[🐢] Call ${reacherBackend.name}: +${Math.round(t1 - t0)}ms`);

// Get the domain of the email, i.e. the part after '@'.

const parts = emailInput.to_email.split("@");
const domain = parts && parts[1];

// If successful, also log an API call entry in the database.
const { error } = await supabaseAdmin.from("calls").insert({
endpoint: "/v0/check_email",
user_id: user.id,
backend: reacherBackend.name,
backend_ip: result.request?.socket?.remoteAddress as string,
domain,
verification_id: verificationId,
duration:
(result?.data.debug?.duration.secs || 0) * 1000 +
Math.round((result.data.debug?.duration.nanos || 0) * 1e-6), // in ms
is_reachable: result.data.is_reachable,
verif_method: result.data.debug?.smtp?.verif_method?.type,
result: result.data,
});
if (error) {
throw convertPgError(error);
}
console.log(`[🐢] Log DB entry: +${Math.round(performance.now() - t1)}ms`);

return result.data;
}
Loading

0 comments on commit 3a98a10

Please sign in to comment.