Mundarija (32)
- Bu darsda
- 1. Nega bu kerak?
- 2. Xat qaysi yo'ldan o'tadi
- 2.1 To'rt bekat
- 2.2 SMTP dialogi
- 2.3 Base64 — shifr emas
- 2.4 Javob kodlari
- 3. Nodemailer: transporter va birinchi xat
- 3.1 O'rnatish
- 3.2 Port 587 yoki 465
- 3.3 verify() — ishga tushishda bir marta
- 4. Xatolar: kanon olti holatni sinadi
- 4.1 Xato xabarlari
- 4.2 Qisman rad — xato emas
- 4.3 Eng xavfli qator — oxirgisi
- 5. Ilovalar va o'zbekcha mavzu
- 5.1 Base64 xatni og'irlashtiradi
- 5.2 Mavzudagi — va o'zbek harflari
- 6. Ko'p xat: ulanishlar hovuzi
- 6.1 Har xatga yangi ulanish
- 6.2 pool: true
- 7. Lokal sinov: Mailpit
- 8. Hujumchi nigohi
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): xatoni tasniflash
- 3-mashq (qiyin): ulanishlarni sanash
- 11. Loyiha qadami: Pochta moduli — yuborish qismi
- 12. Real ishda
- Xulosa
- Manbalar
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 dasecure: false+requireTLS: true, port 465 dasecure: trueyoziladi — aralashtirsangiz ulanish ishlamaydi. Ilovani ishga tushirishdaverify()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
Pochtamodulining 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'qiydiDiagrammaga 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:
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 queuedQatorma-qator:
220— server salomlashdi.EHLO— mijoz o'zini tanishtirdi. Server imkoniyatlarini sanadi:STARTTLS(shifrlashni yoqish mumkin),AUTH(login usullari),PIPELINING(bir nechta buyruqni javob kutmasdan yuborish).STARTTLS— shu ulanishning o'zi TLS ga o'tadi (HTTPS va TLS darsidagi qo'l siqish). Shundan keyin hamma narsa shifrlangan.- Ikkinchi
EHLO— shifrlangan kanalda qaytadan tanishish. E'tibor bering: endiSTARTTLSro'yxatda yo'q, u bajarib bo'lindi. AUTH PLAIN— login va parol.MAIL FROMvaRCPT TO— konvert (envelope): kimdan va kimga. Bu xat ichidagiFrom:vaTo:sarlavhalari emas — pochta konvertidagi manzil kabi alohida.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:
const auth = "AGlsb3ZhAC8qIHNlY3JldCAqLw==";
console.log(JSON.stringify(atob(auth)));Konsolda:
"\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):
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:
250 muvaffaqiyat
354 davom eting
421 vaqtinchalik — keyinroq qayta
451 vaqtinchalik — keyinroq qayta
535 doimiy — qayta urinmang
550 doimiy — qayta urinmangBu 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 TOdagi manzil va xat ichidagiTo: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):
npm i nodemailer@10.1.7Unda 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.
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:
SMTP_HOST=smtp.provayder.example
SMTP_PORT=587
SMTP_USER=ilova
SMTP_PASS=haqiqiy-parolni-bu-yerga-yozmangXatda 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:
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.uzIkkalasida 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:
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) → OKHar 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:
ilova 300 KB → xat 411.5 KB (+37.2 %)Hisobini o'zimiz tekshiramiz:
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:
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:
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:
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:
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:
pool=false: 200 xat 1192 ms
pool=true: 200 xat 477 ms- 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):
docker run -d --name mailpit \
-p 127.0.0.1:1025:1025 -p 127.0.0.1:8025:8025 \
axllent/mailpit:v1.31.0Transporter'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):
[ '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
secureqiymati — - 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 —
sendMailxato tashlamaydi; hammasi rad etilsaEENVELOPE.
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
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:
EAUTH 535 false
EENVELOPE 550 false
EMESSAGE 421 true
ETIMEDOUT - true
ETLS - false
ESOCKET - trueTartib 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
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):
pool=false: 20 xat, 20 ulanish
pool=true: 20 xat, 3 ulanishHar 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):
// 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.
requireTLSsozlamadan keladi: prodda 587 bilantrue, lokal Mailpit'da berilmaydi.qabulvarad— 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).dkimvaobunaBekor— 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:
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):
node --test test/email.test.ts✔ 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.6057skipped 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/mailerichida ham Nodemailer ishlaydi. - Kod review'da —
requireTLSyo'qligi, sirlarning kodda turgani varejectedning 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 numberyokiGreeting never received. requireTLSsiz STARTTLS bo'lmasa, Nodemailer parolni jim ochiq yuboradi.- Qisman rad etish xato emas —
rejectedni tekshiring; base64 ilovani +37 % og'irlashtiradi. pool: true200 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
- Nodemailer (SMTP transport,
secure,requireTLS, pool, testing): https://nodemailer.com/smtp/ - RFC 5321 — SMTP: https://www.rfc-editor.org/rfc/rfc5321
- RFC 3207 — SMTP STARTTLS: https://www.rfc-editor.org/rfc/rfc3207
- RFC 8314 — email uchun implicit TLS (465): https://www.rfc-editor.org/rfc/rfc8314
- RFC 2047 — sarlavhalarda encoded-word: https://www.rfc-editor.org/rfc/rfc2047
- Mailpit: https://mailpit.axllent.org/
- Kanon tajribasi (IlmHamroh, 2026-10-27): nodemailer 10.1.7, smtp-server 3.20.2, mailparser 3.10.0, Mailpit v1.31.0, Node 24.21.0, Windows 11. SMTP dialogi, CRLF sinovi, modul va mashq yechimlari — biz ishga tushirdik (2026-10-28, o'sha versiyalar).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!