كيف ترسل البريد من Cloudflare Workers

لا يستطيع Worker تشغيل خادم بريد، لكنه يستطيع التحدث إلى خادم بريد. إليك ما ينجح وما يفشل ولماذا، من تجربة بناء MakiSend، وهو API بريد يعمل بالكامل على Workers.

آخر تحديث · نسخة Markdown (بالإنجليزية)

ثلاث طرق للإرسال

أمام Worker ثلاثة منافذ للخروج. اختر بحسب الجهة التي تريد الاعتماد عليها، لا بحسب الشيفرة: فالشيفرة قصيرة في كل الحالات.

الطريقةكيف يرسل Workerما تحتاج إليهانتبه إلى
API بريد عبر HTTPfetch() إلى API المزوّدحساب ومفتاح API لدى ذلك المزوّدصيغة طلبات مزوّد واحد، وسعره لكل رسالة
SMTP عبر TCPnodemailer (مع nodejs_compat) على مقابس TCP من Cloudflareبيانات اعتماد SMTP من أي مزوّد أو صندوق بريدالمنافذ المسموح بها، ووضع TLS، ورحلة ذهاب وإياب لكل أمر SMTP
Cloudflare Email Serviceربط send_emailنطاق مرسِل مُضاف إلى Email Sendingالإرسال إلى أي مستلم يتطلب Workers Paid

SMTP هو الخيار الذي يُبقيك حرًّا في التنقل: الشيفرة نفسها ترسل عبر Google Workspace أو Zoho أو Microsoft 365 أو Amazon SES أو SendGrid أو Mailgun أو Postmark أو Brevo أو SMTP الخاص بـ Resend. وهو أيضًا الخيار الذي فيه فخاخ.

منافذ SMTP التي يستطيع Worker استخدامها

تحظر Cloudflare الاتصالات الصادرة على المنفذ 25، لذا لا يستطيع Worker التسليم مباشرة إلى خادم MX لدى المستلم. عليه أن يسلّم الرسالة إلى خادم إرسال، على أحد هذه المنافذ:

المنفذالأمانالمعنى
465TLS ضمني (tls)TLS من أول بايت
587STARTTLS (starttls)يبدأ بنص واضح ثم يرتقي إلى TLS قبل المصادقة
2465TLS ضمني (tls)بديل للمنفذ 465 يقدمه بعض المزوّدين
2525, 2587STARTTLS (starttls)بدائل للمنفذ 587 يقدمها بعض المزوّدين
25لا شيءمحظور على Workers

اقرن المنفذ بالوضع الصحيح. المنفذ 465 مع STARTTLS، أو 587 مع TLS ضمني، يفشل أثناء مصافحة TLS (wrong version number)، قبل إرسال أي بيانات اعتماد. يرفض MakiSend المنفذ 25 عند حفظ الإعداد ويستنتج الوضع من المنفذ.

تشغيل nodemailer على Workers

يعمل nodemailer على Workers بمجرد تفعيل علم التوافق nodejs_compat: إذ يُربط node:net وnode:tls بمقابس TCP من Cloudflare. ومع ذلك أوقعتنا ثلاثة أمور.

  1. يحلّ nodemailer اسم المضيف ثم يتصل بعنوان IP. ويرفض workerd الاتصال بعنوان IP مع TLS ضمني على المنفذ 465 (proxy request failed, cannot connect to the specified address)، بينما يُفتح الخادم نفسه باسمه دون مشكلة. ولا تقول رسالة الخطأ شيئًا عن السبب الحقيقي.
  2. الدالتان dns.resolve4 وdns.resolve6 غير مطبّقتين في التطوير المحلي، لذا قد يتوقف النقل الافتراضي تحت wrangler dev.
  3. من دون family: 4 يسجّل workerd تحذيرات شبكة عند كل إرسال.

حل المشكلتين الأوليين هو خيار getSocket في nodemailer: افتح المقبس بنفسك، باسم المضيف، ثم سلّمه إليه. يظل nodemailer يشغّل STARTTLS عند الحاجة ويتحقق من الشهادة مقابل اسم المضيف.

smtp-transport.tsts
import * as net from "node:net";
import * as tls from "node:tls";
import nodemailer from "nodemailer";

export function createTransport(env: { SMTP_USER: string; SMTP_PASSWORD: string }) {
  const host = "smtp.example.com";
  const port = 465; // 465 or 2465: implicit TLS. 587, 2525, 2587: STARTTLS.
  const implicitTls = port === 465 || port === 2465;

  return nodemailer.createTransport({
    host,
    port,
    secure: implicitTls,
    requireTLS: !implicitTls,
    family: 4,
    tls: { rejectUnauthorized: true },
    auth: { user: env.SMTP_USER, pass: env.SMTP_PASSWORD },
    // Open the socket by name: workerd refuses implicit TLS to a bare IP address.
    getSocket(_options, callback) {
      const socket = implicitTls
        ? tls.connect({ host, port, servername: host, rejectUnauthorized: true })
        : net.connect({ host, port });
      socket.once(implicitTls ? "secureConnect" : "connect", () =>
        callback(null, implicitTls ? { connection: socket, secured: true } : { connection: socket }),
      );
      socket.once("error", (error) => callback(error, false));
    },
  });
}

تضيف نسخة MakiSend مهلة زمنية، ويستخدمها كلٌّ من فحص الاتصال والإرسال التجريبي والإرسال الفعلي: smtp-connection.ts.

تتحدث Cloudflare Email Service بروتوكول SMTP أيضًا على smtp.mx.cloudflare.net، لكن Worker لا يستطيع فتح مقبس إلى ذلك المضيف. من داخل Worker، أرسل عبر ربط send_email بدلًا من ذلك.

لا ترسل داخل الطلب

ينتظر SMTP ردًّا على كل أمر: TCP، TLS، EHLO، AUTH، MAIL FROM، RCPT TO، DATA، QUIT. على Workers يستغرق الإرسال من 300 إلى 400 ms في أفضل الحالات ونحو ثانيتين في أسوئها، ويفتح Worker اتصالًا جديدًا لكل رسالة. لا ينبغي أن ينتظر المستخدم ذلك، ولا ينبغي أن يضيع إرسال فاشل.

  • أجب بـ 202 Accepted فور حفظ الرسالة، ثم ضع معرّفها في Cloudflare Queue.
  • أرسل من Cloudflare Workflow: كل خطوة دائمة، فتستأنف إعادة المحاولة من حيث توقف التشغيل.
  • لا تُعِد إلا ما يمكن أن ينجح لاحقًا. يقوم MakiSend بمحاولة واحدة وثلاث إعادات بفاصل 30 ثانية؛ وتتوقف أخطاء المصادقة وTLS وDNS والإعداد و5xx فورًا، وتذهب الرسالة التي استنفدت محاولاتها إلى طابور الرسائل الميتة.
  • اقبل ترويسة Idempotency-Key حتى لا يرسل العميل الذي يعيد طلبه الرسالة مرتين.

انتظار الشبكة لا يُحتسب ضمن حد المعالج (10 ms لكل استدعاء على Workers Free)، لذا يتسع الإرسال نفسه للخطة المجانية. تتبع صفحة البنية رسالة واحدة عبر هذه الخطوات.

عناوين IP الثابتة وصندوق الوارد

  • تتصل Workers من عناوين Cloudflare مشتركة ومتغيرة. المزوّد الذي لا يقبل إلا عناوين IP من قائمة سماح سيرفض الاتصال: أوقف قائمة السماح واعتمد على بيانات اعتماد SMTP، أو أرسل من خادم بعنوان IP ثابت.
  • وصول البريد إلى صندوق الوارد يعتمد على نطاق المرسِل (SPF وDKIM وDMARC) وسمعة المزوّد، لا على Worker. في اختباراتنا، تصل رسائل MakiSend إلى صندوق الوارد بعد ضبط هذه السجلات الثلاثة.
  • تضع Gmail وGoogle Workspace حدًّا لما يرسله صندوق بريد واحد يوميًا. للأحجام الكبيرة، استخدم مزوّدًا للبريد المعاملاتي.

أو دع MakiSend يتولى الأمر

MakiSend هو كل ما في هذه الصفحة، جاهزًا: POST /send-email موثّق، وطابور، وWorkflow دائم مع إعادة المحاولة، وnodemailer على مقابس تُفتح باسم المضيف، وربط Cloudflare Email Service، ولوحة تحكم فيها السجلات. وهو مفتوح المصدر ويُنشر على حساب Cloudflare الخاص بك، حتى لو كان حساب Workers Free.

أسئلة

هل يستطيع Cloudflare Worker إرسال البريد عبر SMTP؟

نعم. مع علم nodejs_compat يعمل nodemailer على مقابس TCP من Cloudflare ويتحدث إلى أي مزوّد SMTP على المنافذ 465 أو 587 أو 2465 أو 2525 أو 2587. المنفذ 25 محظور.

لماذا لا يعمل المنفذ 25 من Worker؟

تحظر Cloudflare الاتصالات الصادرة على المنفذ 25. استخدم منفذ الإرسال لدى مزوّدك بدلًا منه: 465 مع TLS ضمني أو 587 مع STARTTLS.

لماذا يفشل nodemailer برسالة "proxy request failed" على المنفذ 465؟

يتصل nodemailer بعنوان IP الذي حلّه، ويرفض workerd استخدام TLS ضمني مع عنوان IP مجرد. افتح المقبس باسم المضيف عبر خيار getSocket في nodemailer.

هل أستطيع استخدام Cloudflare Email Service على الخطة المجانية؟

ليس للإرسال إلى أي مستلم: فذلك يتطلب Workers Paid ($5 شهريًا). على Workers Free، أرسل عبر مزوّد SMTP آخر.