قم بحظر رسائل البريد الإلكتروني التي يمكن التخلص منها في Next.js باستخدام ملف وسيط واحد
برنامج وسيط Next.js مكون من 40 سطرًا يستدعي واجهة برمجة تطبيقات botoi لرفض الاشتراكات من عناوين البريد الإلكتروني المؤقتة. نسخ، لصق، نشر.
يقوم المستخدم بالتسجيل باستخدام test92847@mailinator.com، يحترق خلال النسخة التجريبية المجانية، ويختفي.
يعودون غدا مع test92848@mailinator.com وتفعل ذلك مرة أخرى.
قائمة انتظار الدعم الخاصة بك تمتلئ بالحسابات الوهمية. تُظهر تحليلاتك أعدادًا كبيرة من المستخدمين لا تعني شيئًا.
يتم تفعيل اكتشاف إساءة الاستخدام بعد فوات الأوان لأن الحساب يستهلك الموارد بالفعل.
الحل: حظر رسائل البريد الإلكتروني التي يمكن التخلص منها عند الباب، قبل أن يصل طلب التسجيل إلى قاعدة البيانات الخاصة بك. يوضح هذا الدليل كيفية القيام بذلك في Next.js باستخدام ملف وسيط واحد وبدون أي تبعيات تتجاوز استدعاء الجلب.
ما سوف تبنيه
برنامج وسيط Next.js يعترض طلبات POST إلى نقطة نهاية التسجيل الخاصة بك، يستخرج البريد الإلكتروني من نص الطلب، ويتحقق منه مقابل botoi واجهة برمجة تطبيقات البريد الإلكتروني القابل للتصرف, ويعيد استجابة 422 إذا كان البريد الإلكتروني ينتمي إلى خدمة سريعة. الملف بأكمله أقل من 50 سطرًا.
الوسيطة
يخلق middleware.ts في جذر المشروع الخاص بك (أو src/middleware.ts إذا كنت تستخدم src دليل):
import { NextRequest, NextResponse } from 'next/server';
const BOTOI_URL = 'https://api.botoi.com/v1/disposable-email/check';
export async function middleware(req: NextRequest) {
// Only intercept POST requests to the signup route
if (req.method !== 'POST') {
return NextResponse.next();
}
let body: { email?: string };
try {
body = await req.json();
} catch {
return NextResponse.json(
{ error: 'Invalid request body' },
{ status: 400 }
);
}
const email = body.email?.trim().toLowerCase();
if (!email) {
return NextResponse.next();
}
try {
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const data = await res.json();
if (data.success && data.data.is_disposable) {
return NextResponse.json(
{ error: 'Disposable email addresses are not allowed. Please use a permanent email.' },
{ status: 422 }
);
}
} catch {
// API unreachable; fail open so real users aren't blocked
console.warn('botoi disposable-email check failed, allowing request through');
}
return NextResponse.next();
}
export const config = {
matcher: ['/api/auth/signup', '/api/register'],
};
كيف يعمل
مطابقة المسار
ال config.matcher يخبر المصفوفة Next.js بالمسارات التي تؤدي إلى تشغيل هذه البرامج الوسيطة.
قم بتغيير هذه المسارات لتتناسب مع نقاط نهاية التسجيل الخاصة بك. تعمل البرامج الوسيطة على الحافة قبل تنفيذ معالج المسار الخاص بك،
لذا فإن الطلبات المرفوضة لا تمس قاعدة البيانات أو مزود المصادقة الخاص بك أبدًا.
استخراج البريد الإلكتروني
يقرأ البرنامج الوسيط نص الطلب باستخدام req.json() ويسحب email مجال.
إذا فشل التحليل أو لم يكن هناك بريد إلكتروني، فسيتم تمرير الطلب دون تغيير.
يؤدي هذا إلى إبقاء البرامج الوسيطة غير مرئية للطرق غير الخاصة بالتسجيل.
استدعاء API
وظيفة واحدة ل https://api.botoi.com/v1/disposable-email/check مع البريد الإلكتروني في الجسم.
الرد يتضمن:
{
"success": true,
"data": {
"email": "throwaway@mailinator.com",
"domain": "mailinator.com",
"is_disposable": true,
"is_free": false,
"provider": "Mailinator"
}
}
ال is_disposable العلم هو البوابة. عندما يكون true، تقوم البرامج الوسيطة بإرجاع 422 برسالة واضحة.
عندما يكون false, NextResponse.next() يتيح للطلب الاستمرار في معالج التسجيل الخاص بك.
تصميم غير مفتوح
ال catch الحظر حول استدعاء الجلب يعني أن فشل الشبكة، أو انتهاء المهلات، أو توقف واجهة برمجة التطبيقات (API) لا يؤدي إلى انقطاع عمليات الاشتراك.
تسجل البرمجيات الوسيطة تحذيرًا وتسمح للطلب بالمرور. لا يرى المستخدمون أبدًا أي خطأ ناتج عن انقطاع الخدمة من جهة خارجية.
التعامل مع حالات الحافة
المهلات
تستجيب واجهة برمجة تطبيقات botoi في أقل من 50 مللي ثانية لمعظم الطلبات نظرًا لأنها تستخدم قائمة نطاقات في الذاكرة.
إذا كنت تريد مهلة صعبة، فقم بإحضار الجلب AbortSignal.timeout():
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000), // 3 second timeout
});
حدود المعدل
تسمح الطبقة المجانية بـ 5 طلبات في الدقيقة. إذا كان تطبيقك يعالج عمليات اشتراك أكثر من ذلك، الحصول على مفتاح API وقم بتمريرها كرمز مميز لحاملها:
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': \`Bearer \${process.env.BOTOI_API_KEY}\`,
},
body: JSON.stringify({ email }),
});
محل BOTOI_API_KEY في الخاص بك .env.local ملف. لا تلزمه أبدًا بالتحكم في الإصدار.
الشيكات المكررة
إذا وصل البريد الإلكتروني نفسه إلى نقطة نهاية الاشتراك مرتين متتابعتين سريعًا (النقر المزدوج، إعادة المحاولة المنطقية)، ستقوم بإجراء استدعاءين لواجهة برمجة التطبيقات (API) لنفس المجال. بالنسبة لمعظم التطبيقات، هذا أمر جيد. إذا كان الأمر مهمًا، أضف ذاكرة تخزين مؤقت قصيرة العمر (مغطاة أدناه).
تصلب الإنتاج
إضافة ذاكرة تخزين مؤقت في الذاكرة
قم بتخزين نتيجة الفحص القابل للتصرف لكل مجال لمدة 5 دقائق. يؤدي هذا إلى تقليل استدعاءات واجهة برمجة التطبيقات (API) وتسريع عمليات التحقق المتكررة لنفس النطاق:
const cache = new Map<string, { isDisposable: boolean; expires: number }>();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
async function isDisposableEmail(email: string): Promise<boolean> {
const domain = email.split('@')[1];
const cached = cache.get(domain);
if (cached && cached.expires > Date.now()) {
return cached.isDisposable;
}
try {
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
});
const data = await res.json();
const isDisposable = data.success && data.data.is_disposable;
cache.set(domain, {
isDisposable,
expires: Date.now() + CACHE_TTL,
});
return isDisposable;
} catch {
return false; // fail open
}
}
تعمل ذاكرة التخزين المؤقت المستندة إلى الخريطة في أوقات التشغيل بدون خادم وأوقات تشغيل الحافة. بالنسبة لعمليات النشر متعددة المثيلات، استبدلها بـ Redis أو Upstash:
import { Redis } from '@upstash/redis';
const redis = Redis.fromEnv();
async function isDisposableEmail(email: string): Promise<boolean> {
const domain = email.split('@')[1];
const cached = await redis.get<boolean>(\`disposable:\${domain}\`);
if (cached !== null) {
return cached;
}
try {
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
});
const data = await res.json();
const isDisposable = data.success && data.data.is_disposable;
await redis.set(\`disposable:\${domain}\`, isDisposable, { ex: 300 });
return isDisposable;
} catch {
return false;
}
}
إدراج نطاقات الشركة في القائمة المسموح بها
تستخدم بعض الشركات نطاقات مخصصة لا ترغب أبدًا في حظرها، حتى لو كانت تطابق نمطًا مشبوهًا. إضافة قائمة مسموح بها:
const ALLOWED_DOMAINS = new Set([
'yourcompany.com',
'partner-corp.io',
'bigclient.co',
]);
async function isDisposableEmail(email: string): Promise<boolean> {
const domain = email.split('@')[1];
if (ALLOWED_DOMAINS.has(domain)) {
return false;
}
// ... rest of the check logic
}
تسجيل المحاولات المحظورة
تتبع النطاقات التي تم رفضها حتى تتمكن من مراقبة أنماط إساءة الاستخدام وتعديل إستراتيجيتك:
if (data.success && data.data.is_disposable) {
console.log(
JSON.stringify({
event: 'disposable_email_blocked',
domain: data.data.domain,
provider: data.data.provider,
timestamp: new Date().toISOString(),
})
);
return NextResponse.json(
{ error: 'Disposable email addresses are not allowed.' },
{ status: 422 }
);
}
الوسيطة كاملة مع كل تصلب
إليك الملف الكامل الذي يتضمن التخزين المؤقت والقائمة المسموح بها والمهلة والتسجيل المنظم:
import { NextRequest, NextResponse } from 'next/server';
const BOTOI_URL = 'https://api.botoi.com/v1/disposable-email/check';
const CACHE_TTL = 5 * 60 * 1000;
const ALLOWED_DOMAINS = new Set([
// Add your corporate or partner domains here
]);
const cache = new Map<string, { isDisposable: boolean; expires: number }>();
async function checkDisposable(email: string): Promise<{
isDisposable: boolean;
domain: string;
provider: string | null;
}> {
const domain = email.split('@')[1];
if (ALLOWED_DOMAINS.has(domain)) {
return { isDisposable: false, domain, provider: null };
}
const cached = cache.get(domain);
if (cached && cached.expires > Date.now()) {
return { isDisposable: cached.isDisposable, domain, provider: null };
}
try {
const res = await fetch(BOTOI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(3000),
});
const data = await res.json();
const isDisposable = data.success && data.data.is_disposable;
const provider = data.data?.provider ?? null;
cache.set(domain, { isDisposable, expires: Date.now() + CACHE_TTL });
return { isDisposable, domain, provider };
} catch {
return { isDisposable: false, domain, provider: null };
}
}
export async function middleware(req: NextRequest) {
if (req.method !== 'POST') {
return NextResponse.next();
}
let body: { email?: string };
try {
body = await req.json();
} catch {
return NextResponse.json(
{ error: 'Invalid request body' },
{ status: 400 }
);
}
const email = body.email?.trim().toLowerCase();
if (!email || !email.includes('@')) {
return NextResponse.next();
}
const result = await checkDisposable(email);
if (result.isDisposable) {
console.log(
JSON.stringify({
event: 'disposable_email_blocked',
domain: result.domain,
provider: result.provider,
timestamp: new Date().toISOString(),
})
);
return NextResponse.json(
{ error: 'Disposable email addresses are not allowed. Please use a permanent email.' },
{ status: 422 }
);
}
return NextResponse.next();
}
export const config = {
matcher: ['/api/auth/signup', '/api/register'],
};
اختباره محليا
ابدأ تشغيل خادم Next.js dev الخاص بك وقم بإطلاق طلب باستخدام بريد إلكتروني معروف يمكن التخلص منه:
curl -X POST http://localhost:3000/api/auth/signup \\
-H "Content-Type: application/json" \\
-d '{"email": "test@mailinator.com", "password": "hunter2"}'
الرد المتوقع:
{
"error": "Disposable email addresses are not allowed. Please use a permanent email."
}
جرب بريدًا إلكترونيًا شرعيًا للتأكد من مروره عبر:
curl -X POST http://localhost:3000/api/auth/signup \\
-H "Content-Type: application/json" \\
-d '{"email": "dev@acme-corp.com", "password": "hunter2"}'
يصل هذا الطلب إلى معالج التسجيل الخاص بك كالمعتاد.
متى يجب الاطمئنان على العميل أيضًا
تلتقط البرامج الوسيطة رسائل البريد الإلكتروني التي يمكن التخلص منها على الخادم. ولكن يمكنك أيضًا الاتصال بنفس واجهة برمجة التطبيقات (API) من نموذج الاشتراك الخاص بك لإظهار خطأ مضمّن *قبل* إرسال المستخدم. فحص سريع من جانب العميل بعد أن يفقد حقل البريد الإلكتروني التركيز يوفر على المستخدم رحلة ذهابًا وإيابًا ويقدم تعليقات أسرع:
async function validateEmail(email: string): Promise<string | null> {
const res = await fetch('https://api.botoi.com/v1/disposable-email/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const data = await res.json();
if (data.success && data.data.is_disposable) {
return 'Please use a permanent email address.';
}
return null; // no error
}
لا تزال البرامج الوسيطة بمثابة البوابة الرسمية. يعد الفحص من جانب العميل بمثابة تحسين لتجربة المستخدم، وليس إجراءً أمنيًا.
FAQ
- هل يعمل هذا مع جهاز توجيه التطبيقات Next.js؟
- نعم. يتم تشغيل البرنامج الوسيط Next.js قبل أي معالج مسار، بغض النظر عما إذا كنت تستخدم جهاز توجيه التطبيقات أو جهاز توجيه الصفحات. يوجد ملف middleware.ts في جذر المشروع (أو داخل src/ إذا كنت تستخدم دليل src)، وهو يعترض الطلبات الموجهة إلى المسارات المطابقة قبل أن تصل إلى مسارات API أو إجراءات الخادم.
- هل أحتاج إلى مفتاح API لفحص البريد الإلكتروني القابل للتصرف من botoi؟
- لا. تسمح الطبقة المجانية بـ 5 طلبات في الدقيقة بدون مفتاح API. بالنسبة لتطبيقات الإنتاج التي تتعامل مع المزيد من عمليات الاشتراك، احصل على مفتاح من صفحة مستندات botoi API لفتح حدود المعدلات الأعلى.
- ماذا يحدث إذا كانت واجهة برمجة تطبيقات botoi معطلة؟
- تكتشف البرامج الوسيطة أخطاء الشبكة وتسمح للطلب بالمرور. ويعني أسلوب الفتح الفاشل هذا أن انقطاع واجهة برمجة التطبيقات المؤقت لا يمنع المستخدمين الشرعيين من التسجيل. يمكنك إضافة التسجيل لتتبع متى يبدأ السلوك الاحتياطي.
- هل يمكنني استخدام هذا مع أطر عمل أخرى مثل Remix أو SvelteKit؟
- يعمل استدعاء API نفسه من أي بيئة من جانب الخادم. نمط البرامج الوسيطة الموضح هنا خاص بـ Next.js، لكن المنطق الأساسي (POST إلى نقطة النهاية، تحقق من is_disposable في الاستجابة) يترجم مباشرة إلى أدوات تحميل Remix أو خطافات SvelteKit أو البرامج الوسيطة Express.
- ما مدى دقة اكتشاف البريد الإلكتروني القابل للتصرف؟
- تتحقق نقطة النهاية من قائمة تضم أكثر من 700 نطاق معروف يمكن التخلص منه وتستخدم مطابقة الأنماط لالتقاط الاختلافات. كما أنه يحدد أيضًا موفري خدمة البريد الإلكتروني المجانية (Gmail وOutlook وYahoo) بشكل منفصل عن موفري خدمة البريد الإلكتروني المتاحين، بحيث يمكنك التمييز بين عنوان Gmail الشخصي وعنوان Mailinator المهمل.
ابدأ البناء مع botoi
أكثر من 150 نقطة نهاية API للبحث ومعالجة النصوص وتوليد الصور وأدوات المطورين. باقة مجانية، بدون بطاقة ائتمان.