IlmHamroh
JavaScript Full-stack/29-qism. Backend integratsiyalar: fayl, to'lov, bot, realtime13/69-dars24 daqiqa
Mundarija (32)

Email yuborish Node'da: SMTP protokoli va Nodemailer

Qisqacha: Ilova xatni o'zi yetkazmaydi — u xatni SMTP protokoli bilan pochta serveriga (provayderga) topshiradi, qolganini server qiladi. Node'da buni Nodemailer bajaradi: createTransport({ host, port, secure, auth }) bilan ulanish sozlanadi, sendMail() xatni yuboradi. Port 587 da secure: false + requireTLS: true, port 465 da secure: true yoziladi — aralashtirsangiz ulanish ishlamaydi. Ilovani ishga tushirishda verify() bilan sozlamani tekshiring, lokal sinov uchun esa xatni hech qayerga yubormaydigan Mailpit ishlating.

Bu darsda

  • Xat ilovadan pochta qutisigacha qaysi yo'ldan o'tishini va SMTP dialogini o'qiy olasiz.
  • Nodemailer transporter'ini 587 va 465 portlar uchun to'g'ri sozlaysiz.
  • Kanon olti xil xatoni qanday ushlaganini va har biri nimani anglatishini bilasiz.
  • Ilova (attachment) xatni nega uchdan biriga og'irlashtirishini hisoblaysiz.
  • Kanon Pochta modulining yuborish qismini yozasiz va lokal SMTP serverda sinaysiz.

Oldin bilishingiz kerak: Domen va DNS, HTTPS va TLS, net, dns va tls, Buffer va binar ma'lumot (base64), Sirlarni boshqarish.

Oldingi darsda QR kod yasab, natijani foydalanuvchiga fayl sifatida berdik. Endi yangi blok — bildirishnomalar: email, SMS, push va ularni birlashtiradigan tizim. Birinchisi — email.

1. Nega bu kerak?

vazifalar ilovasida email kerak bo'ladigan joylar ko'p:

  • ro'yxatdan o'tganda emailni tasdiqlash havolasi (28-qismda Email tasdiqlash va parol tiklash);
  • parolni tiklash;
  • "Non olish" vazifasining muddati ertaga — eslatma;
  • haftalik hisobot PDF ilova bilan.

Bularning hammasi tranzaksion xatlar (transactional email): foydalanuvchining harakatiga javoban bittadan ketadi. Reklama tarqatmasi (marketing) — boshqa narsa, uning qoidalari Email yetkazib berish darsida.

O'xshatish: siz xatni pochta bo'limiga topshirasiz. Uni qabul qiluvchining shahriga olib borish, eshigiga yetkazish — pochtaning ishi. Ilova uchun "pochta bo'limi" — SMTP server. Ilova unga xatni topshiradi va "qabul qildim" javobini oladi. Xat egasining qutisiga tushganmi — bu boshqa savol. Bu darsda topshirishni o'rganamiz.

2. Xat qaysi yo'ldan o'tadi

2.1 To'rt bekat

sequenceDiagram
  participant I as Ilova (Node)
  participant P as Provayder SMTP
  participant D as DNS
  participant Q as Qabul serveri
  participant A as Aziz (Gmail)
  I->>P: SMTP, port 587, login
  P-->>I: 250 qabul qildim
  P->>D: example.uz ning MX yozuvi?
  D-->>P: mx.example.uz
  P->>Q: SMTP, port 25
  Q-->>P: 250
  A->>Q: ilova yoki IMAP bilan o'qiydi

Diagrammaga qarang: ilova faqat birinchi strelkada qatnashadi. U xatni o'z provayderiga (Resend, Amazon SES, Postmark, Brevo yoki kompaniyaning pochta serveri) port 587 orqali, login va parol bilan topshiradi. Provayder qabul qiluvchi domenning MX yozuvini (mail exchanger — "bu domen xatlarini qaysi server qabul qiladi") DNS'dan so'raydi va xatni o'sha serverga port 25 orqali uzatadi. Aziz esa xatni o'z ilovasi bilan o'qiydi.

Nega ilova to'g'ridan port 25 ga yubormaydi? Uyda yoki bulutdagi serverdan port 25 ga chiqish ko'pincha yopiq. Ochiq bo'lsa ham, notanish IP'dan kelgan xatni Gmail spam deb belgilaydi. Obro'li provayder orqali yuborish — amaldagi yagona yo'l. Buning sabablari (SPF, DKIM, obro') Email yetkazib berish darsida.

2.2 SMTP dialogi

SMTP — matnli protokol: mijoz buyruq yozadi, server uch xonali kod bilan javob beradi. HTTP'ga o'xshaydi, lekin bitta so'rov-javob emas, butun suhbat. Biz Nodemailer'ning ichki jurnalini yoqib, lokal SMTP serverga bitta xat yubordik (C: — mijoz, S: — server). Server qatorlaridagi uch xonali kodga va STARTTLS dan oldingi va keyingi qismga qarang:

text
S: 220 smtp.lokal ESMTP
C: EHLO ilova.lokal
S: 250-smtp.lokal Nice to meet you, [127.0.0.1]
S: 250-PIPELINING
S: 250-8BITMIME
S: 250-SMTPUTF8
S: 250-AUTH LOGIN PLAIN
S: 250 STARTTLS
C: STARTTLS
S: 220 Ready to start TLS
C: EHLO ilova.lokal
S: 250-smtp.lokal Nice to meet you, [127.0.0.1]
S: 250-PIPELINING
S: 250-8BITMIME
S: 250-SMTPUTF8
S: 250 AUTH LOGIN PLAIN
C: AUTH PLAIN AGlsb3ZhAC8qIHNlY3JldCAqLw==
S: 235 Authentication successful
C: MAIL FROM:<noreply@vazifalar.example.uz>
C: RCPT TO:<aziz@example.uz>
C: DATA
S: 250 Accepted
S: 250 Accepted
S: 354 End data with <CR><LF>.<CR><LF>
S: 250 OK: message queued

Qatorma-qator:

  1. 220 — server salomlashdi.
  2. EHLO — mijoz o'zini tanishtirdi. Server imkoniyatlarini sanadi: STARTTLS (shifrlashni yoqish mumkin), AUTH (login usullari), PIPELINING (bir nechta buyruqni javob kutmasdan yuborish).
  3. STARTTLS — shu ulanishning o'zi TLS ga o'tadi (HTTPS va TLS darsidagi qo'l siqish). Shundan keyin hamma narsa shifrlangan.
  4. Ikkinchi EHLO — shifrlangan kanalda qaytadan tanishish. E'tibor bering: endi STARTTLS ro'yxatda yo'q, u bajarib bo'lindi.
  5. AUTH PLAIN — login va parol.
  6. MAIL FROM va RCPT TO — konvert (envelope): kimdan va kimga. Bu xat ichidagi From: va To: sarlavhalari emas — pochta konvertidagi manzil kabi alohida.
  7. DATA — xatning o'zi. 354 — "yuboring", 250 — qabul qilindi.

MAIL FROM, RCPT TO va DATA javob kutmasdan ketma-ket ketdi — bu PIPELINING. Uchta javob keyin keldi.

2.3 Base64 — shifr emas

AUTH PLAIN qatoridagi satr shifrlanganga o'xshaydi. Uni ochib ko'ramiz:

js
const auth = "AGlsb3ZhAC8qIHNlY3JldCAqLw==";
console.log(JSON.stringify(atob(auth)));

Konsolda:

text
"\u0000ilova\u0000/* secret */"

Bu base64 — kodlash, shifr emas (Buffer va binar ma'lumot). Ichida nol bayt, login ilova, nol bayt va parol. Bu yerda parol o'rnida /* secret */ turibdi: Nodemailer jurnalga parolni yozmasdan, uni shu so'z bilan almashtiradi. Haqiqiy tarmoqda esa haqiqiy parol ketadi. Uni ko'rgan har kim atob bilan ochadi. Shuning uchun AUTH faqat STARTTLS dan keyin bo'lishi shart. Buni Nodemailer'ga qanday majburlash — «Port 587 yoki 465» bo'limida.

2.4 Javob kodlari

Javob kodining birinchi raqami asosiy ma'noni beradi — HTTP status kodlariga o'xshash (HTTP metodlari va status kodlari):

js
function javobTuri(kod) {
  if (kod >= 200 && kod < 300) return "muvaffaqiyat";
  if (kod >= 300 && kod < 400) return "davom eting";
  if (kod >= 400 && kod < 500) return "vaqtinchalik — keyinroq qayta";
  if (kod >= 500 && kod < 600) return "doimiy — qayta urinmang";
  return "noma'lum";
}

for (const kod of [250, 354, 421, 451, 535, 550]) {
  console.log(kod, javobTuri(kod));
}

Konsolda:

text
250 muvaffaqiyat
354 davom eting
421 vaqtinchalik — keyinroq qayta
451 vaqtinchalik — keyinroq qayta
535 doimiy — qayta urinmang
550 doimiy — qayta urinmang

Bu farq amalda muhim. 421 (server band) yoki 451 (vaqtinchalik xato) — xatni bir necha daqiqadan keyin qayta yuborish kerak. 535 (login xato) yoki 550 (bunday quti yo'q) — qayta urinish foydasiz, faqat provayder oldida obro'ingizni tushiradi. Qayta urinishni BullMQ asoslari darsida navbat bilan qilamiz.

Tekshirib ko'ring: RCPT TO dagi manzil va xat ichidagi To: sarlavhasi har doim bir xilmi?

Javob

Yo'q. RCPT TO — konvert: server xatni aynan shu manzilga yetkazadi. To: — xat ichidagi matn, Aziz uni ekranda ko'radi. Bcc (yashirin nusxa) aynan shu farqqa tayanadi: yashirin qabul qiluvchi konvertda (RCPT TO) bor, xat sarlavhalarida esa yo'q. Nodemailer konvertni from, to, cc, bcc maydonlaridan o'zi yasaydi.

3. Nodemailer: transporter va birinchi xat

3.1 O'rnatish

Nodemailer — Node uchun eng ko'p ishlatiladigan email kutubxonasi. Kanon 10.1.7 versiyasini ishlatdi (Node 24.21.0):

bash
npm i nodemailer@10.1.7

Unda ikki asosiy tushuncha bor. Transporter — xatni qanday va qayerga yuborish sozlamasi (SMTP manzili, port, login). U bir marta yaratiladi va butun ilova davomida ishlatiladi. Xat (message) — sendMail() ga beriladigan obyekt: kimdan, kimga, mavzu, matn, HTML va ilovalar.

js
import nodemailer from "nodemailer";

const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST,
  port: Number(process.env.SMTP_PORT),
  secure: false, // 587: STARTTLS
  requireTLS: true,
  auth: {
    user: process.env.SMTP_USER,
    pass: process.env.SMTP_PASS,
  },
});

const natija = await transporter.sendMail({
  from: "Vazifalar <noreply@vazifalar.example.uz>",
  to: "aziz@example.uz",
  subject: "Eslatma: Non olish",
  text: "Vazifa muddati yaqin: Non olish — 28.10 18:00.",
  html: "<p>Vazifa muddati yaqin: <b>Non olish</b></p>",
});
console.log(natija.response);

Login va parol — faqat muhit o'zgaruvchilaridan (Sirlarni boshqarish). .env.example uslubida namuna:

bash
SMTP_HOST=smtp.provayder.example
SMTP_PORT=587
SMTP_USER=ilova
SMTP_PASS=haqiqiy-parolni-bu-yerga-yozmang

Xatda doim ikki versiya bering: text va html. Matn versiyasini ba'zi pochta ilovalari, ekran o'quvchilari va spam filtrlari o'qiydi. Nodemailer ularni bitta xatga multipart/alternative sifatida joylaydi — qabul qiluvchi ilova qaysi birini ko'rsatishni o'zi tanlaydi.

3.2 Port 587 yoki 465

Provayderlar odatda ikkala portni beradi. Farq — shifrlash qachon boshlanadi:

Port Nodemailer Shifrlash
587 secure: false, requireTLS: true ochiq ulanish, keyin STARTTLS
465 secure: true birinchi baytdan TLS (implicit TLS)
25 — serverlar orasida; ilova uchun emas

Kanon ikkala portda lokal SMTP serverni o'z-o'zini imzolagan sertifikat bilan ishga tushirdi va bittadan xat yubordi. Qatorlar uzun — tls: true ga va ulanish so'zi necha marta takrorlanishiga qarang:

text
587 STARTTLS: verify OK, javob "250 OK: message queued", server logi: ulanish 127.0.0.1 | AUTH PLAIN user=ilova | ulanish 127.0.0.1 | AUTH PLAIN user=ilova | MAIL FROM noreply@vazifalar.example.uz (tls: true) | RCPT TO aziz@example.uz
465 TLS: verify OK, javob "250 OK: message queued", server logi: ulanish 127.0.0.1 | AUTH PLAIN user=ilova | ulanish 127.0.0.1 | AUTH PLAIN user=ilova | MAIL FROM noreply@vazifalar.example.uz (tls: true) | RCPT TO aziz@example.uz

Ikkalasida ham tls: true — xat shifrlangan kanalda ketdi. Yana bir narsa ko'rinadi: har birida ikki ulanish va ikki AUTH. Birinchisi — verify(), ikkinchisi — sendMail(). Demak verify() xat yubormaydi, lekin to'liq ulanib, login qilib ko'radi.

3.3 verify() — ishga tushishda bir marta

verify() ni ilova ishga tushganda bir marta chaqiring. Parol xato yoki host noto'g'ri bo'lsa, buni birinchi foydalanuvchi parolini tiklay olmaganda emas, deploy paytida bilasiz. Lekin uni har xatdan oldin chaqirmang: kanon logi ko'rsatdi — bu har safar qo'shimcha ulanish va login.

4. Xatolar: kanon olti holatni sinadi

4.1 Xato xabarlari

Kanon ataylab noto'g'ri sozlamalar bilan verify() va sendMail() chaqirdi. Natijalar aynan:

text
noto'g'ri parol → Error [EAUTH]: Invalid login: 535 Invalid username or password
465 ga secure:false → Error [ETIMEDOUT]: Greeting never received
587 ga secure:true → Error [ESOCKET]: E0530000:error:0A00010B:SSL routines:tls_validate_record_header:wrong version number:openssl\ssl\record\methods\tlsany_meth.c:78:
sertifikatga ishonmaslik (ca yo'q) → Error [ESOCKET]: self-signed certificate; if the root CA is installed locally, try running Node.js with --use-system-ca
bitta qabul qiluvchi 550: {"accepted":["bor@example.uz"],"rejected":["kimdir@yoq.uz"]}
hammasi rad: Error [EENVELOPE]: Can't send mail - all recipients were rejected: 550 Mailbox unavailable responseCode 550
STARTTLS taklif qilinmaydi + requireTLS → Error [ETLS]: Error upgrading connection with STARTTLS: 500 Error: command not recognized
STARTTLS yo'q, requireTLS yo'q (parol ochiq matnda ketardi) → OK

Har birini tarjima qilamiz:

Kod Xabar (tarjima) Sabab
EAUTH "Login noto'g'ri: 535" parol yoki login xato
ETIMEDOUT "Salom kelmadi" 465 ga TLS'siz ulandik: server TLS kutyapti, mijoz SMTP salomini
ESOCKET "noto'g'ri versiya raqami" 587 ga TLS bilan: server oddiy matn yubordi, mijoz uni TLS deb o'qidi
ESOCKET "o'z-o'zini imzolagan sertifikat" serverning sertifikatiga ishonch yo'q
EENVELOPE "hamma qabul qiluvchi rad etildi" birorta ham RCPT TO o'tmadi
ETLS "STARTTLS ga o'tishda xato: 500" server STARTTLS'ni bilmaydi, biz esa talab qildik

Ikkinchi va uchinchi qator — portni noto'g'ri secure bilan aralashtirish. Ikkala xato ham "parol xato" yoki "server ishlamayapti" ga o'xshamaydi, shuning uchun ularni tanib olish kerak. Greeting never received 3 soniyadan keyin keldi — kanonda connectionTimeout: 3000 qo'yilgan edi. Sukutdagi kutish ancha uzoq, shuning uchun timeout'larni o'zingiz bering.

4.2 Qisman rad — xato emas

Beshinchi qatorga qayta qarang. Ikki qabul qiluvchidan biri 550 bilan rad etildi, lekin sendMail() xato tashlamadi. Natijada accepted va rejected ro'yxatlari bor. Faqat hammasi rad etilganda EENVELOPE keladi.

Demak await sendMail() muvaffaqiyatli tugadi — bu "hammaga yetdi" degani emas. rejected ni tekshirish sizning ishingiz. Kanon modulida yubor() aynan shuning uchun qabul va rad ro'yxatlarini qaytaradi.

4.3 Eng xavfli qator — oxirgisi

STARTTLS yo'q, requireTLS yo'q → OK. Server shifrlashni taklif qilmadi, Nodemailer esa ochiq kanalda login qilib, xatni yubordi. Parol tarmoqda base64 holida ketdi — «Base64 — shifr emas» bo'limida uni bir qatorda ochdik.

Nodemailer port 587 da sukutda STARTTLS ni imkon bo'lsa ishlatadi. Taklif bo'lmasa — jim davom etadi. requireTLS: true buni "shifrlash bo'lmasa, umuman yubormang" ga aylantiradi (oldingi qatordagi ETLS). Bu qoidaning ahamiyati «Hujumchi nigohi» bo'limida.

5. Ilovalar va o'zbekcha mavzu

5.1 Base64 xatni og'irlashtiradi

SMTP tarixan faqat matnni tashiydi. PDF yoki rasm kabi ikkilik fayl xatga base64 bo'lib kiradi: har 3 bayt — 4 belgi, har 76 belgidan keyin qator oxiri. Kanon 300 KB lik ilova bilan xat yubordi:

text
ilova 300 KB → xat 411.5 KB (+37.2 %)

Hisobini o'zimiz tekshiramiz:

js
const ilova = 300 * 1024; // 300 KB PDF
const base64 = Math.ceil(ilova / 3) * 4;
const qatorlar = Math.ceil(base64 / 76); // har qator ≤ 76 belgi
const jami = base64 + qatorlar * 2; // har qator oxirida \r\n
console.log(base64, qatorlar, jami);
console.log((jami / 1024).toFixed(1), "KB");
console.log(btoa("Sal"), btoa("Salom"));

Konsolda:

text
409600 5390 420380
410.5 KB
U2Fs U2Fsb20=

Ilovaning o'zi 410.5 KB ga aylandi. Qolgan 1 KB — sarlavhalar, matn va HTML qismlar. btoa qatori qoidani ko'rsatadi: 3 harf — 4 belgi, 5 harf — 8 belgi (oxirida = to'ldirish).

Amaliy xulosa: provayderlar xat hajmini cheklaydi (ko'pi 10–25 MB, aniq qiymat — provayder hujjatida). 20 MB fayl base64 bilan limitdan oshib ketadi. Katta fayllarni ilova qilmang — Presigned URL'lar darsidagidek vaqtinchalik havola yuboring.

5.2 Mavzudagi — va o'zbek harflari

Xat sarlavhalari ham tarixan faqat ASCII. Kanon mavzusi Hisobot — oktabr edi, xom xatda esa:

text
Subject: =?UTF-8?Q?Hisobot_=E2=80=94_oktabr?=

Bu encoded-word (RFC 2047): =?charset?Q?…?=. Q usulida _ — bo'sh joy, =E2=80=94 — UTF-8 baytlari o'n oltilik shaklda. Uni o'zimiz ochamiz:

js
function mavzuniOch(sarlavha) {
  const m = sarlavha.match(/^=\?UTF-8\?Q\?(.*)\?=$/i);
  if (!m) return sarlavha;
  const baytlar = [];
  const s = m[1].replaceAll("_", " ");
  for (let i = 0; i < s.length; i++) {
    if (s[i] === "=") {
      baytlar.push(parseInt(s.slice(i + 1, i + 3), 16));
      i += 2;
    } else {
      baytlar.push(s.charCodeAt(i));
    }
  }
  return new TextDecoder().decode(new Uint8Array(baytlar));
}

console.log(mavzuniOch("=?UTF-8?Q?Hisobot_=E2=80=94_oktabr?="));
console.log(mavzuniOch("Sinov"));

Konsolda:

text
Hisobot — oktabr
Sinov

— belgisi UTF-8 da uch bayt: E2 80 94 (Matn kodlash: ASCII, Unicode va UTF-8). Nodemailer kodlashni o'zi qiladi — siz oddiy satr yozasiz. Bu bo'lim xom xatni o'qiy olishingiz uchun: spam filtri yoki qabul qiluvchi server muammo qilsa, xatni shu ko'rinishda ko'rasiz. Kanondagi ilova nomi hisobot oʻzbek.pdf ham to'g'ri qabul qilindi: multipart/mixed ichida text, html va application/pdf qismlar.

Tekshirib ko'ring: 15 MB lik PDF'ni ilova qilmoqchisiz, provayder limiti 20 MB. Xat o'tadimi?

Javob

Taxminan yo'q. Base64 hajmni ~37 % oshiradi: 15 × 1.37 ≈ 20.6 MB — limitdan katta. Yuborishdan oldin base64 hajmini hisoblang yoki faylni havola orqali bering.

6. Ko'p xat: ulanishlar hovuzi

6.1 Har xatga yangi ulanish

Sukutda Nodemailer har sendMail() uchun yangi ulanish ochadi: TCP, salom, EHLO, STARTTLS va TLS qo'l siqishi, AUTH — keyin xat va uzish. Bitta xat uchun bu sezilmaydi. Haftalik hisobotni 200 foydalanuvchiga yuborganda esa sezildi.

6.2 pool: true

Hovuz (pool) — bir nechta ochiq ulanishni saqlab, xatlarni ular orqali navbatma-navbat yuborish (Connection pooling g'oyasi). Kanon lokal Mailpit'ga 200 ta xatni parallel yubordi:

text
pool=false: 200 xat 1192 ms
pool=true: 200 xat 477 ms
200 xat yuborish vaqti (lokal Mailpit)
  • har xatga yangi ulanish1 192 ms
  • pool, 5 ulanish477 ms

Manba: IlmHamroh kanon tajribasi, 2026-10-27: nodemailer 10.1.7, Mailpit v1.31.0, Node 24.21.0, i5-12500H, Windows 11; bitta o'lchov

Ikki yarim barobar tez. Mailpit lokal va TLS'siz — haqiqiy provayderda har ulanishga TLS va tarmoq kechikishi qo'shiladi, farq yanada katta bo'ladi. Hovuzda maxConnections (sukutda 5) va maxMessages (bitta ulanishdan nechta xat) bor. Provayderning limitlariga qarab tanlang: ko'pchilik bir vaqtdagi ulanishlar va soniyadagi xatlar sonini cheklaydi.

Maslahat: Hovuz — tezlik uchun. Ishonchlilik uchun esa xatlar navbat orqali yuboriladi: jarayon qayta ishga tushsa, xat yo'qolmaydi, vaqtinchalik xatoda qayta urinadi. Buni Bildirishnoma tizimi arxitekturasi va BullMQ asoslari darslarida ulaymiz.

7. Lokal sinov: Mailpit

Ishlab chiqishda haqiqiy xat yuborish xavfli: test ma'lumotlaridagi manzil kimningdir haqiqiy pochtasi bo'lishi mumkin. Yechim — SMTP catcher: o'zini SMTP server deb ko'rsatadigan, lekin xatni hech qayerga yubormaydigan dastur. Kanon Mailpit v1.31.0 ishlatdi: SMTP 1025-portda qabul qiladi, xatlarni veb-sahifada ko'rsatadi va HTTP API orqali beradi. Docker bilan ishga tushiriladi (Konteyner nima va Docker — 26-qism):

bash
docker run -d --name mailpit \
  -p 127.0.0.1:1025:1025 -p 127.0.0.1:8025:8025 \
  axllent/mailpit:v1.31.0

Transporter'da host: "127.0.0.1", port: 1025, secure: false, login yo'q. Brauzerda http://127.0.0.1:8025 — kelgan xatlar, HTML ko'rinishi va xom sarlavhalar. Kanon testlari Mailpit API'si (/api/v1/search, /api/v1/message/<ID>) bilan xatni o'qib, mavzu, matn va sarlavhalarni tekshiradi.

Nodemailer hujjatlarida Ethereal (nodemailer.createTestAccount()) ham tavsiya etiladi. Lekin u tashqi xizmatda haqiqiy hisob yaratadi — kanon uni ataylab ishlatmadi.

8. Hujumchi nigohi

Hujum Natijasi Himoya
STARTTLS'ni o'chirib qo'yish (STARTTLS stripping) tarmoqdagi hujumchi 250 STARTTLS qatorini o'chiradi, mijoz parolni ochiq yuboradi requireTLS: true yoki port 465
sarlavhaga \r\n qo'shish Subject: Salom\r\nBcc: hujumchi@… — yangi sarlavha Nodemailer \r\n ni tozalaydi; baribir foydalanuvchi matnini sarlavhaga cheklab qo'ying
SMTP parolining sizishi spamchi sizning domeningizdan yuboradi parol .env da, git'da emas; provayderda alohida, cheklangan kalit
"Parolni tiklash" tugmasini 1000 marta bosish sizning hisobingizdan 1000 xat, provayder bloklaydi so'rov limiti (Rate limiting) va bildirishnoma limiti

Birinchi qatorni kanon tajribasi tasdiqladi: STARTTLS taklif qilinmaganda requireTLS siz Nodemailer parolni jim ochiq yubordi. Hujumchi buning uchun serverni buzishi shart emas — u faqat tarmoqda (umumiy Wi-Fi, provayder) o'rtada turadi va bitta qatorni o'chiradi.

Ikkinchi qatorni biz tekshirdik: mavzuga Salom\r\nBcc: hujumchi@yomon.uz berib, xom xatni ko'rdik (nodemailer 10.1.7, Node 24.21.0):

text
[ 'Subject: Salom Bcc: hujumchi@yomon.uz' ]
konvert: { from: 'noreply@vazifalar.example.uz', to: [ 'aziz@example.uz' ] }

\r\n bo'shliqqa aylandi, Bcc sarlavha bo'lmadi va konvertga tushmadi. Bu kutubxonaning himoyasi. Lekin o'z SMTP mijozingizni yozsangiz yoki sarlavhani qo'lda yig'sangiz, bu teshik ochiladi.

9. Ko'p uchraydigan xatolar

Xato Belgisi To'g'risi
587 + secure: true wrong version number 587 — secure: false + requireTLS: true
465 + secure: false Greeting never received 465 — secure: true
requireTLS yo'q xavfli, lekin ishlaydi 587 da doim requireTLS: true
rejected ni tekshirmaslik bir qism qabul qiluvchiga xat bormaydi accepted/rejected ni log va holatga yozish
har xatdan oldin verify() ikki barobar ulanish ishga tushishda bir marta
sertifikat xatosida rejectUnauthorized: false hujumchi o'z sertifikati bilan o'rtada turadi to'g'ri CA (tls.ca) yoki tizim sertifikatlari
katta faylni ilova qilish +37 %, provayder limiti havola (presigned URL)
parol kodda git tarixida qoladi .env, maxfiy menejer

10. Mashqlar

Mashqlar kurs/mashqlar/29/13-smtp/ da.

1-mashq (oson): bo'shliqlarni to'ldiring

  • Ilova xatni provayderga topshiradigan port (STARTTLS bilan) —
  • Shu portda Nodemailer'da secure qiymati —
  • Shifrlash bo'lmasa yubormaslik uchun sozlama —
  • Qabul qiluvchi domenning pochta serverini ko'rsatadigan DNS yozuvi —
  • Bitta qabul qiluvchi 550 bilan rad etilsa, u natijaning qaysi ro'yxatida bo'ladi —
Yechim
  • 587 — submission porti; 465 da TLS birinchi baytdan.
  • false — ulanish ochiq boshlanadi, keyin STARTTLS.
  • requireTLS — usiz STARTTLS bo'lmasa parol ochiq ketadi (kanon: OK).
  • MX — mail exchanger.
  • rejected — sendMail xato tashlamaydi; hammasi rad etilsa EENVELOPE.

2-mashq (o'rta): xatoni tasniflash

Xatlarni fonda yuboradigan jarayon — worker (ishchi; navbatni Nega navbat kerak darsida batafsil ko'ramiz) — xatoni ko'rib, xatni qayta yuborish kerakmi, hal qilishi kerak. qaytaUrinish(xato) funksiyasini yozing: true — vaqtinchalik (qayta urinish), false — doimiy. Qoidalar: responseCode bo'lsa — 4xx true, 5xx false; responseCode bo'lmasa — code bo'yicha: ETIMEDOUT, ECONNECTION, ESOCKET true, EAUTH, EENVELOPE, ETLS va boshqalar false. Ishora: Set va has (Set); ?? bilan responseCode yo'qligini tekshiring.

Yechim
js
const VAQTINCHALIK = new Set(["ETIMEDOUT", "ECONNECTION", "ESOCKET"]);

function qaytaUrinish(xato) {
  const kod = xato.responseCode ?? null;
  if (kod !== null) return kod >= 400 && kod < 500;
  return VAQTINCHALIK.has(xato.code);
}

const xatolar = [
  { code: "EAUTH", responseCode: 535 },
  { code: "EENVELOPE", responseCode: 550 },
  { code: "EMESSAGE", responseCode: 421 },
  { code: "ETIMEDOUT" },
  { code: "ETLS" },
  { code: "ESOCKET" },
];
for (const x of xatolar) {
  console.log(x.code, x.responseCode ?? "-", qaytaUrinish(x));
}

Konsolda:

text
EAUTH 535 false
EENVELOPE 550 false
EMESSAGE 421 true
ETIMEDOUT - true
ETLS - false
ESOCKET - true

Tartib muhim: avval server kodi, keyin tarmoq kodi. Uchinchi qatordagi EMESSAGE nomi hech narsa demaydi. Uning 421 kodi esa "server band, keyinroq" degani — shuning uchun true. Ifodadagi ?? null faqat undefined va null ni almashtiradi. Endi ETLS ni ataylab doimiy deb oldik: server STARTTLS'ni bilmasa, qayta urinish yordam bermaydi — sozlamani tuzatish kerak. Bitta tuzoq: ESOCKET ham tarmoq uzilishi, ham wrong version number (port xato) bo'lishi mumkin. Ikkinchisida qayta urinish foydasiz, shuning uchun urinishlar soniga albatta chegara qo'yiladi.

3-mashq (qiyin): ulanishlarni sanash

Tajriba papkasida (npm i nodemailer@10.1.7 smtp-server@3.20.2) lokal SMTP qopqon (catcher) yozing: smtp-server paketi, login ixtiyoriy (authOptional: true), STARTTLS o'chiq, har ulanishni (onConnect) va har xatni (onData) sanasin. Server listen(0) da ochilsin. Keyin 20 ta xatni Promise.all bilan ikki marta yuboring: pool: false va pool: true, maxConnections: 3. Har safar pool=… : N xat, M ulanish ni chiqaring.

Yechim
js
import { SMTPServer } from "smtp-server";
import nodemailer from "nodemailer";
import { once } from "node:events";

let ulanishlar = 0;
let xatlar = 0;
const server = new SMTPServer({
  authOptional: true,
  disabledCommands: ["STARTTLS"],
  logger: false,
  onConnect(_sessiya, cb) {
    ulanishlar++;
    cb();
  },
  async onData(stream, _sessiya, cb) {
    for await (const _bolak of stream);
    xatlar++;
    cb();
  },
});
server.on("error", (e) => console.error("server:", e.message));
server.listen(0, "127.0.0.1");
await once(server.server, "listening");
const port = server.server.address().port;

async function olch(pool) {
  ulanishlar = 0;
  xatlar = 0;
  const tr = nodemailer.createTransport({
    host: "127.0.0.1", port, secure: false, pool, maxConnections: 3,
  });
  await Promise.all(
    Array.from({ length: 20 }, (_x, i) =>
      tr.sendMail({
        from: "noreply@vazifalar.example.uz",
        to: `u${i}@example.uz`,
        subject: `Eslatma ${i}`,
        text: "Vazifa muddati yaqin.",
      }),
    ),
  );
  tr.close();
  console.log(`pool=${pool}: ${xatlar} xat, ${ulanishlar} ulanish`);
}

await olch(false);
await olch(true);
server.close();

Konsolda (biz ishga tushirdik: Node 24.21.0, nodemailer 10.1.7, smtp-server 3.20.2):

text
pool=false: 20 xat, 20 ulanish
pool=true: 20 xat, 3 ulanish

Har xat o'z ulanishi bilan — 20 ta. Hovuzda esa maxConnections ga teng 3 ta. Ishlovchi onData dagi for await tanani oxirigacha o'qiydi: o'qimasangiz, server 250 javobini bermaydi va mijoz kutib qoladi. Qator server.on("error", …) majburiy: kanon tajribasida ishlovchisiz smtp-server xatosi butun jarayonni yiqitgan.

11. Loyiha qadami: Pochta moduli — yuborish qismi

Kanon emailni vazifalar-api dan mustaqil modulga yig'di: modullar/src/email/pochta.ts. Bu darsda uning transporter va yuborish qismini yozamiz, shablonlar qismi — keyingi darsda. Telefon uchun uzun qatorlar va izohlarni bo'ldik, kod ma'nosi kanondagidek (nodemailer 10.1.7, TypeScript 6.0.3):

ts
// pochta.ts — tranzaksion email: Nodemailer transporter (SMTP),
// Handlebars shablon (HTML + matn versiya), List-Unsubscribe, DKIM.
// Lokal: Mailpit (SMTP catcher) — xat hech qayerga ketmaydi.
// Prodda — provayder SMTP'si (Resend, SES, Postmark, Brevo …).
import nodemailer, { type Transporter } from "nodemailer";

export type PochtaSozlama = {
  host: string;
  port: number;
  /** true — 465 (implicit TLS); false — 587 (STARTTLS,
   * `requireTLS` bilan) yoki lokal 1025 */
  secure: boolean;
  requireTLS?: boolean;
  user?: string;
  pass?: string;
  from: string;
  dkim?: {
    domainName: string;
    keySelector: string;
    privateKey: string;
  };
};

export type Shablon = { mavzu: string; html: string; matn: string };
type Ilova = {
  filename: string;
  content: Buffer;
  contentType: string;
};
type Manzil = string | { address: string };

export class Pochta {
  readonly t: Transporter;
  readonly from: string;
  constructor(s: PochtaSozlama) {
    this.from = s.from;
    this.t = nodemailer.createTransport({
      host: s.host,
      port: s.port,
      secure: s.secure,
      ...(s.requireTLS === undefined
        ? {}
        : { requireTLS: s.requireTLS }),
      ...(s.user && s.pass
        ? { auth: { user: s.user, pass: s.pass } }
        : {}),
      connectionTimeout: 10_000,
      greetingTimeout: 10_000,
      socketTimeout: 20_000,
      ...(s.dkim ? { dkim: s.dkim } : {}),
    });
  }

  /** Ishga tushishda: ulanish va auth to'g'rimi (xat yubormaydi). */
  async tekshir(): Promise<void> {
    await this.t.verify();
  }

  /**
   * Yuboradi. `obunaBekor` berilsa (marketing/eslatma) — RFC 2369
   * `List-Unsubscribe` va RFC 8058 one-click
   * (`List-Unsubscribe-Post`). `idempotentlikKaliti` → `Message-ID`
   * (qayta urinishda bir xil ID — qabul qiluvchi takrorni aniqlaydi).
   */
  async yubor(x: {
    kimga: string;
    xat: Shablon;
    obunaBekor?: string;
    idempotentlikKaliti?: string;
    ilovalar?: Ilova[];
  }): Promise<{ messageId: string; qabul: string[]; rad: string[] }> {
    const domen = this.from.replace(/^.*@/, "").replace(/>.*$/, "");
    const r = (await this.t.sendMail({
      from: this.from,
      to: x.kimga,
      subject: x.xat.mavzu,
      html: x.xat.html,
      text: x.xat.matn,
      ...(x.idempotentlikKaliti
        ? { messageId: `<${x.idempotentlikKaliti}@${domen}>` }
        : {}),
      ...(x.obunaBekor
        ? {
            list: {
              unsubscribe: {
                url: x.obunaBekor,
                comment: "Obunani bekor qilish",
              },
            },
            headers: {
              "List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
            },
          }
        : {}),
      ...(x.ilovalar ? { attachments: x.ilovalar } : {}),
    })) as {
      messageId: string;
      accepted: Manzil[];
      rejected: Manzil[];
    };
    const manzil = (a: Manzil) =>
      typeof a === "string" ? a : a.address;
    return {
      messageId: r.messageId,
      qabul: r.accepted.map(manzil),
      rad: r.rejected.map(manzil),
    };
  }

  yop(): void {
    this.t.close();
  }
}

Darsdagi qoidalar modulda qanday aks etgan:

  • Timeout'lar aniq: ulanish va salom 10 s, jim soket 20 s. Sukutdagi uzoq kutish worker'ni band qilib qo'ymaydi.
  • requireTLS sozlamadan keladi: prodda 587 bilan true, lokal Mailpit'da berilmaydi.
  • qabul va rad — qisman rad etishni chaqiruvchi ko'radi.
  • idempotentlikKaliti → Message-ID: xat qayta urinishda bir xil ID bilan ketadi. Takrorlangan xatni aniqlash shu ID ga tayanadi (Idempotency-Key g'oyasi).
  • dkim va obunaBekor — Email yetkazib berish darsida.

Modulni lokal SMTP serverga (smtp-server, port 1025, Mailpit o'rnida) ulab sinadik. yubor() ikki marta chaqirildi: oddiy eslatma va ikki qabul qiluvchili xat, ulardan biri 550 bilan rad etiladi:

text
tekshir(): OK
{
  messageId: '<eslatma-42-2026-10-28@vazifalar.example.uz>',
  qabul: [ 'aziz@example.uz' ],
  rad: []
}
{ qabul: [ 'aziz@example.uz' ], rad: [ 'kimdir@yoq.uz' ] }

messageId — biz bergan kalitdan. Ikkinchi chaqiruv xato tashlamadi, rad etilgan manzil rad da.

Kanon testlari — modullar/test/email.test.ts, to'rtta. Uchtasi Mailpit API'si orqali xatni o'qib tekshiradi: verify() va tasdiqlash xati, List-Unsubscribe va ilova, noto'g'ri port → ECONNREFUSED. Biz Docker'siz ishga tushirdik — Mailpit yo'q, shuning uchun bu to'plam o'tkazildi (﹣), shablon testi esa o'tdi (Node 24.21.0):

bash
node --test test/email.test.ts
text
✔ shablon: {{}} ekranlaydi, matn versiyasi ekranlamaydi, yo'q o'zgaruvchi — xato (13.986ms)
﹣ Mailpit orqali yuborish (0.1852ms) # MAILPIT_API yo'q
ℹ tests 1
ℹ suites 1
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 839.6057

skipped 0 deyilgani bilan aldanmang: o'tkazilgan to'plam (describe) bu hisobga kirmaydi — ﹣ belgisiga qarang. Kanon Mailpit bilan butun modullar paketini npm run check dan o'tkazgan: tsc 0, eslint 0, testlar 82/82, o'tkazilgani 0.

Modul ikki darsda yoziladi, shuning uchun hali commit qilmang — shablon qismini keyingi darsda qo'shib, bitta commit qilamiz.

vazifalar-api ga ulash 28-qism (auth) tugagach bo'ladi: emailni tasdiqlash va parolni tiklash xatlari aynan shu modul orqali ketadi.

12. Real ishda

  • Provayderlar: Resend, Amazon SES, Postmark, Brevo, Mailgun, SendGrid — hammasi SMTP ham, o'z HTTP API'sini ham beradi. SMTP + Nodemailer — provayderni almashtirsangiz kod o'zgarmaydigan yo'l. HTTP API — tezroq va qo'shimcha imkoniyatli (teglar, statistika). Kanon ularning hech birini sinamagan.
  • NestJS da @nestjs-modules/mailer ichida ham Nodemailer ishlaydi.
  • Kod review'da — requireTLS yo'qligi, sirlarning kodda turgani va rejected ning e'tiborsiz qolishi.
  • Intervyuda: "587 va 465 farqi?", "STARTTLS nima?", "Email yuborishni qanday test qilasiz?", "Xat yuborilmasa nima qilasiz?"

Xulosa

  • Ilova xatni SMTP bilan provayderga topshiradi; provayder MX orqali qabul qiluvchiga yetkazadi.
  • SMTP — matnli dialog: EHLO, STARTTLS, AUTH, MAIL FROM/RCPT TO (konvert), DATA; 4xx — qayta urinish, 5xx — yo'q.
  • 587: secure: false + requireTLS: true; 465: secure: true; aralashtirish — wrong version number yoki Greeting never received.
  • requireTLS siz STARTTLS bo'lmasa, Nodemailer parolni jim ochiq yuboradi.
  • Qisman rad etish xato emas — rejected ni tekshiring; base64 ilovani +37 % og'irlashtiradi.
  • pool: true 200 xatni 1192 ms dan 477 ms ga tushirdi; lokal sinov — Mailpit, Ethereal emas.

Keyingi dars: Email shablonlarini serverda render qilish — xat matnini Handlebars va React Email bilan serverda yig'amiz, foydalanuvchi ma'lumotini ekranlaymiz va matn versiyasini avtomatik olamiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!