blu*_*yke 7 .net email smtp node.js server
我见过很多关于设置 Node.js 以使用预先存在的 SMTP 服务器的博客和 Stack Overflow 问题,特别是通过像 nodemailer 等模块。我已经看到的一些内容:
https://www.zeolearn.com/magazine/sending-and-receiving-emails-using-nodejs
在 Node.js 中发送电子邮件?(不知道为什么关闭)
Nodemailer 无需 smtp 传输即可发送电子邮件——这与我想要的有点接近
如何在 Nodejs 中创建自定义 smtp 服务器来发送通知电子邮件?——这个问题如此接近却没有答案
SmtpJs API 不工作!有没有办法通过 JavaScript 或 JQuery 使用 SMTP 服务器发送电子邮件
/对nodejs中的smtp邮件服务器有什么建议吗?-- 这可能是唯一一个甚至试图回答这个问题的人,尽管从那里提到的服务(smtp-服务器)的文档中,我看不到 SMTP 服务器从头开始的实际构造在哪里,即我没有看到展示如何使用 Node.js 创建自己的 myemail@mydomain.com 的部分(假设服务器配置在某种 Linux VM 上,例如 Google 计算引擎)。
所有这些答案和博客仅解决通过其他电子邮件客户端发送电子邮件的问题。
我对任何其他电子邮件服务器不感兴趣。
我不相信 Gmail,也不相信任何其他第三方电子邮件提供商。我想从我自己的服务器托管我自己的服务器。
如何完全从头开始构建 SMTP 邮件服务器,仅利用 Node.js 中的“net”内置库,而不依赖于任何外部依赖项?假设我已经注册了自己的域并将其托管在具有 HTTPS 的虚拟机上,我的目标是使该服务器能够使用地址 myemail@mydomain.com 发送和接收电子邮件,而不涉及任何第三方服务器。
开展该项目的初步步骤是什么?是否有专门处理 SMTP 套接字协议的参考资料或教程?这些资源将为这一努力提供一个宝贵的起点。
我已经尝试开发一个 SMTP 客户端。虽然它当前的目标仅仅是向任何电子邮件提供商发送一封电子邮件,但我遇到了一个问题,尽管没有收到任何错误消息,但电子邮件无法显示,即使在垃圾邮件文件夹中也是如此。有趣的是,服务器文件确实成功接收了电子邮件。这里的问题主要在于客户端文件。
对于我的 DKIM 密钥,我使用这个基本脚本来生成它
/**
* B"H
* Generate DKIM key pairs for email usage
*/
const { generateKeyPairSync } = require('crypto');
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
modulusLength: 2048,
});
console.log('Private Key:', privateKey.export({
type: 'pkcs1',
format: 'pem',
}));
console.log('Public Key:', publicKey.export({
type: 'pkcs1',
format: 'pem',
}));
Run Code Online (Sandbox Code Playgroud)
并添加正确的记录
v=DKIM1; k=rsa; p=PUBLIC_KEY_without_---begin rsa or --end--rsa liens or new lines
Run Code Online (Sandbox Code Playgroud)
服务器(至少在基础级别上工作):
/**
* B"H
* @module AwtsMail
*/
const AwtsmoosClient = require("./awtsmoosEmailClient.js");
const net = require('net');
const CRLF = '\r\n';
module.exports = class AwtsMail {
constructor() {
console.log("Starting instance of email");
this.server = net.createServer(socket => {
console.log("Some connection happened!", Date.now());
socket.write('220 awtsmoos.one ESMTP Postfix' + CRLF);
let sender = '';
let recipients = [];
let data = '';
let receivingData = false;
let buffer = '';
socket.on('data', chunk => {
buffer += chunk.toString();
let index;
while ((index = buffer.indexOf(CRLF)) !== -1) {
const command = buffer.substring(0, index);
buffer = buffer.substring(index + CRLF.length);
console.log("Received command:", command);
console.log("Command length:", command.length);
if (receivingData) {
if (command === '.') {
receivingData = false;
console.log("Received email data:", data);
socket.write(`250 2.0.0 Ok: queued as 12345${CRLF}`);
// Simulate sending a reply back.
if (sender) {
console.log("The email has ended!")
/*
console.log(`Sending a reply back to ${sender}`);
const replyData = `Subject: Reply from Awtsmoos ${
Math.floor(Math.random() * 8)
}\r\n\r\nB"H\n\nHello from the Awtsmoos, the time is ${
Date.now()
}.`;
this.smtpClient.sendMail('reply@awtsmoos.one', sender, replyData);
*/
}
} else {
data += command + CRLF;
}
continue;
}
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
socket.write(`250-Hello${CRLF}`);
socket.write(`250 SMTPUTF8${CRLF}`);
} else if (command.startsWith('MAIL FROM')) {
sender = command.slice(10);
socket.write(`250 2.1.0 Ok${CRLF}`);
console.log("The SENDER is:", sender);
} else if (command.startsWith('RCPT TO')) {
recipients.push(command.slice(8));
socket.write(`250 2.1.5 Ok${CRLF}`);
} else if (command.startsWith('DATA')) {
receivingData = true;
socket.write(`354 End data with <CR><LF>.<CR><LF>${CRLF}`);
} else if (command.startsWith('QUIT')) {
socket.write(`221 2.0.0 Bye${CRLF}`);
socket.end();
} else {
console.log("Unknown command:", command);
socket.write('500 5.5.1 Error: unknown command' + CRLF);
}
}
});
socket.on("error", err => {
console.log("Socket error:", err);
});
socket.on("close", () => {
console.log("Connection closed");
});
});
//this.smtpClient = new AwtsmoosClient("awtsmoos.one");
this.server.on("error", err => {
console.log("Server error: ", err);
});
}
shoymayuh() {
this.server.listen(25, () => {
console.log("Awtsmoos mail listening to you, port 25");
}).on("error", err => {
console.log("Error starting server:", err);
});
}
}
Run Code Online (Sandbox Code Playgroud)
我有一个域 (awtsmoos.one),它配置了正确的 IP 地址 A 记录、MX 记录、SPF、DKIM 和 DMARC 记录。
此服务器代码确实成功接收电子邮件数据。问题在于客户端,无论如何它都没有向任何电子邮件提供商发送一条消息(甚至是测试提供商/10 分钟邮件/等)
/**
*B"H
* @module AwtsmoosEmailClient
* A client for sending emails.
* @requires crypto
* @requires net
* @requires tls
*/
const crypto = require('crypto');
const net = require('net');
const CRLF = '\r\n';
class AwtsmoosEmailClient {
constructor(smtpServer, port = 25, privateKey = null) {
this.smtpServer = smtpServer;
this.port = port;
this.privateKey = privateKey ? privateKey.replace(/\\n/g, '\n') : null;
this.multiLineResponse = '';
this.previousCommand = '';
}
/**
* Canonicalizes headers and body in relaxed mode.
* @param {string} headers - The headers of the email.
* @param {string} body - The body of the email.
* @returns {Object} - The canonicalized headers and body.
*/
canonicalizeRelaxed(headers, body) {
const canonicalizedHeaders = headers.split(CRLF)
.map(line => line.toLowerCase().split(/\s*:\s*/).join(':').trim())
.join(CRLF);
const canonicalizedBody = body.split(CRLF)
.map(line => line.split(/\s+/).join(' ').trimEnd())
.join(CRLF).trimEnd();
return { canonicalizedHeaders, canonicalizedBody };
}
/**
* Signs the email using DKIM.
* @param {string} domain - The sender's domain.
* @param {string} selector - The selector.
* @param {string} privateKey - The private key.
* @param {string} emailData - The email data.
* @returns {string} - The DKIM signature.
*/
signEmail(domain, selector, privateKey, emailData) {
const [headers, ...bodyParts] = emailData.split(CRLF + CRLF);
const body = bodyParts.join(CRLF + CRLF);
const { canonicalizedHeaders, canonicalizedBody } = this.canonicalizeRelaxed(headers, body);
const bodyHash = crypto.createHash('sha256').update(canonicalizedBody).digest('base64');
const dkimHeader = `v=1;a=rsa-sha256;c=relaxed/relaxed;d=${domain};s=${selector};bh=${bodyHash};h=from:to:subject:date;`;
const signature = crypto.createSign('SHA256').update(dkimHeader + canonicalizedHeaders).sign(privateKey, 'base64');
return `${dkimHeader}b=${signature}`;
}
/**
* Determines the next command to send to the server.
* @returns {string} - The next command.
*/
getNextCommand() {
const commandOrder = ['EHLO', 'MAIL FROM', 'RCPT TO', 'DATA', 'END OF DATA'];
const currentIndex = commandOrder.indexOf(this.previousCommand);
if (currentIndex === -1) {
throw new Error(`Unknown previous command: ${this.previousCommand}`);
}
if (currentIndex + 1 >= commandOrder.length) {
throw new Error('No more commands to send.');
}
return commandOrder[currentIndex + 1];
}
/**
* Handles the SMTP response from the server.
* @param {string} line - The response line from the server.
* @param {net.Socket} client - The socket connected to the server.
* @param {string} sender - The sender email address.
* @param {string} recipient - The recipient email address.
* @param {string} emailData - The email data.
*/
handleSMTPResponse(line, client, sender, recipient, emailData) {
console.log('Server Response:', line);
this.handleErrorCode(line);
if (line.endsWith('-')) {
console.log('Multi-line Response:', line);
return;
}
this.previousCommand = this.currentCommand;
const nextCommand = this.getNextCommand();
const commandHandlers = {
'EHLO': () => client.write(`MAIL FROM:<${sender}>${CRLF}`),
'MAIL FROM': () => client.write(`RCPT TO:<${recipient}>${CRLF}`),
'RCPT TO': () => client.write(`DATA${CRLF}`),
'DATA': () => client.write(`${emailData}${CRLF}.${CRLF}`),
'END OF DATA': () => client.end(),
};
const handler = commandHandlers[nextCommand];
if (!handler) {
throw new Error(`Unknown next command: ${nextCommand}`);
}
handler();
this.currentCommand = nextCommand;
}
/**
* Handles error codes in the server response.
* @param {string} line - The response line from the server.
*/
handleErrorCode(line) {
if (line.startsWith('4') || line.startsWith('5')) {
throw new Error(line);
}
}
/**
* Sends an email.
* @param {string} sender - The sender email address.
* @param {string} recipient - The recipient email address.
* @param {string} subject - The subject of the email.
* @param {string} body - The body of the email.
* @returns {Promise} - A promise that resolves when the email is sent.
*/
async sendMail(sender, recipient, subject, body) {
return new Promise((resolve, reject) => {
const client = net.createConnection(this.port, this.smtpServer);
client.setEncoding('utf-8');
let buffer = '';
const emailData = `From: ${sender}${CRLF}To: ${recipient}${CRLF}Subject: ${subject}${CRLF}${CRLF}${body}`;
const domain = 'awtsmoos.com';
const selector = 'selector';
const dkimSignature = this.signEmail(domain, selector, this.privateKey, emailData);
const signedEmailData = `DKIM-Signature: ${dkimSignature}${CRLF}${emailData}`;
client.on('connect', () => {
this.currentCommand = 'EHLO';
client.write(`EHLO ${this.smtpServer}${CRLF}`);
});
client.on('data', (data) => {
buffer += data;
let index;
while ((index = buffer.indexOf(CRLF)) !== -1) {
const line = buffer.substring(0, index).trim();
buffer = buffer.substring(index + CRLF.length);
if (line.endsWith('-')) {
this.multiLineResponse += line + CRLF;
continue;
}
const fullLine = this.multiLineResponse + line;
this.multiLineResponse = '';
try {
this.handleSMTPResponse(fullLine, client, sender, recipient, signedEmailData);
} catch (err) {
client.end();
reject(err);
return;
}
}
});
client.on('end', resolve);
client.on('error', reject);
client.on('close', () => {
if (this.previousCommand !== 'END OF DATA') {
reject(new Error('Connection closed prematurely'));
} else {
resolve();
}
});
});
}
}
const privateKey = process.env.BH_key;
const smtpClient = new AwtsmoosEmailClient('awtsmoos.one', 25, privateKey);
async function main() {
try {
await smtpClient.sendMail('me@awtsmoos.com', 'awtsmoos@gmail.com', 'B"H', 'This is a test email.');
console.log('Email sent successfully');
} catch (err) {
console.error('Failed to send email:', err);
}
}
main();
module.exports = AwtsmoosEmailClient;
Run Code Online (Sandbox Code Playgroud)
Wes*_*Wes 13
一些友好的建议——如果您只想在本地计算机上接收邮件,您可能需要使用现成的 MTA,例如 postfix、exim4 或 sendmail。
我这样说是因为我在职业生涯中花了很大一部分时间来实现 MTA,并且我觉得我应该警告您,这是一个已解决的问题,可以让您完全控制邮件流量,并且有一些非常棘手的问题需要解决编写一个可大规模处理大量邮件的 MTA。
也就是说,SMTP(注意拼写)是一个非常简单的协议,如果您对这些东西感兴趣,那么它也是一个很好的“第一个协议”。用 NodeJS 编写一个非常容易。
您感兴趣的第一个版本是在 1982 年左右发布的,称为 RFC-821,又名 IETF STD-10。然后多年来它更新为 RFC-2821 和一系列相关规范,但基本的 RFC-821 支持将为您提供与当今互联网上 99% 的主机交谈所需的功能。(当您需要 TLS 的 ESMTP 支持时,该数字将会下降 - 但这并不困难,也没有太大不同)。
您的守护进程需要侦听端口 25,并且需要处理如下命令:
YOU: 220 my.computer.com SMTP Service Ready
THEM: EHLO blah blah
YOU: 500 Syntax Error. Try again using SMTP.
THEM: HELO blah blah
YOU: 250 G'day mate
THEM: MAIL FROM: <billg@microsoft.com>
YOU: 250 Sender Okay
THEM: RCPT TO: <steve@apple.com>
YOU: 250 OK
THEM: DATA
YOU: 354 Enter mail, end with "." on a line by itself...
THEM: <BUNCH OF STUFF>
.
YOU: 250 Mail accepted
THEM: QUIT
YOU: 221 Goodbye
Run Code Online (Sandbox Code Playgroud)
显然这里还有更多关于错误处理等的内容——阅读规范——但这就是它的要点。这些数字是响应代码并具有特定含义。这些行由 \r\n 分隔,宽度应该小于 1024 字节。
<BUNCH OF STUFF> 是一封电子邮件,不会有一行,其中只有一个点。如果电子邮件有这样的点,另一端会发送一个额外的点。这是规范中的。
最后,将 <XXXX><BUNCH OF STUFF> 写入您的 $MAIL 文件(可能是 /var/mail/username 或 /var/spool/mail/username)并将您的 MUA 指向它。Pine、Alpine、Elm 或 mutt 都是很好的 MUA,可以用来整理这些东西。
<XXXX> 需要以 From(无冒号)开头并以 \n 结尾。这是 Berkeley mbox 文件格式。它应该反映 SMTP 事务中的 MAIL FROM 标头。
这种文件格式非常常见,大多数 POP3 和 IMAP4 服务器都支持。您或许还可以使用 Mozilla Thunderbird 来阅读它。我知道 Netscape Mail 当年就支持它。
经过多次尝试和错误后,我通过 DKIM 签名、反向 DNS 查找和 TLS 加密成功完成了这一任务。
\n客户:
\n/**\n * B"H\n * @module AwtsmoosEmailClient\n * A client for sending emails.\n * @requires crypto\n * @requires net\n * @requires tls\n * @optional privateKey environment variable for your DKIM private key\n * matching your public key, can gnerate with generateKeyPairs.js script\n * @optional BH_email_cert and BH_email_key environemnt variables for certbot\n * TLS cert and key\n * @overview:\n * \n * \n * @method handleSMTPResponse: This method handles the \n * SMTP server responses for each command sent. It builds the multi-line response, checks\n * for errors, and determines the next command to be sent based on the server\xe2\x80\x99s response.\n\n@method handleErrorCode: This helper method throws an\n error if the server responds with a 4xx or 5xx status code.\n\n@property commandHandlers: An object map where keys are SMTP \ncommands and values are functions that handle sending the next SMTP command.\n\n@method sendMail: This asynchronous method initiates the process \nof sending an email. It establishes a connection to the SMTP server, sends the SMTP \ncommands sequentially based on server responses, and handles the \nclosure and errors of the connection.\n\n@method emailData: The email content formatted with headers such as From, To, and Subject.\n\n@method dkimSignature: If a private key is provided, it computes the\n DKIM signature and appends it to the email data.\n\n@event client.on('connect'): Initiates the SMTP conversation by sending the EHLO command upon connection.\n\n@event client.on('data'): Listens for data from the server,\n parses the responses, and calls handleSMTPResponse to handle them.\n\n@event client.on('end'), client.on('error'), client.on('close'): These\n handlers resolve or reject the promise based on the connection status\n and the success of the email sending process.\n\nVariables and Constants:\n\n@const CRLF: Stands for Carriage Return Line Feed, which is not shown\n in the code but presumably represents the newline sequence "\\r\\n".\nthis.smtpServer, this.port, this.privateKey: Instance variables that\n store the SMTP server address, port, and private key for DKIM signing, respectively.\nthis.multiLineResponse, this.previousCommand, this.currentCommand: \nInstance variables used to store the state of the SMTP conversation.\n */\n\nconst crypto = require('crypto');\nconst tls = require("tls");\nconst fs = require("fs");\nconst net = require('net');\nconst dns = require('dns');\nconst CRLF = '\\r\\n';\n\n\n\nclass AwtsmoosEmailClient {\n socket = null;\n useTLS = false;\n cert = null;\n key = null;\n\n commandHandlers = {\n 'START': ({\n sender,\n recipient,\n emailData,\n client\n } = {}) => {\n this.currentCommand = 'EHLO';\n var command = `EHLO ${this.smtpServer}${CRLF}`;\n console.log("Sending to server: ", command)\n client.write(command);\n },\n 'EHLO': ({\n sender,\n recipient,\n emailData,\n client,\n lineOrMultiline\n } = {}) => {\n \n console.log("Handling EHLO");\n if (lineOrMultiline.includes('STARTTLS')) {\n var cmd = `STARTTLS${CRLF}`;\n console.log("Sending command: ", cmd);\n client.write(cmd);\n } else {\n var cmd = `MAIL FROM:<${sender}>${CRLF}`;\n console.log("Sending command: ", cmd);\n client.write(cmd);\n }\n },\n 'STARTTLS': ({\n sender,\n recipient,\n emailData,\n client,\n lineOrMultiline \n } = {}) => {\n // Read the response from the server\n \n console.log("Trying to start TLS");\n \n const options = {\n socket: client,\n servername: 'gmail-smtp-in.l.google.com',\n minVersion: 'TLSv1.2',\n ciphers: 'HIGH:!aNULL:!MD5',\n maxVersion: 'TLSv1.3',\n key:this.key,\n cert:this.cert\n };\n \n const secureSocket = tls.connect(options, () => {\n console.log('TLS handshake completed.');\n console.log("Waiting for secure connect handler");\n \n });\n \n \n \n secureSocket.on('error', (err) => {\n console.error('TLS Error:', err);\n console.error('Stack Trace:', err.stack);\n this.previousCommand = '';\n });\n \n secureSocket.on("secureConnect", () => {\n console.log("Secure connect!");\n this.socket = secureSocket;\n client.removeAllListeners();\n \n \n \n try {\n this.handleClientData({\n client: secureSocket,\n sender,\n recipient,\n dataToSend: emailData\n });\n } catch(e) {\n console.error(e)\n console.error("Stack", e)\n throw new Error(e)\n }\n\n console.log("Setting", this.previousCommand, "to: ")\n this.previousCommand = "STARTTLS";\n console.log(this.previousCommand, "<< set")\n // Once the secure connection is established, resend the EHLO command\n var command = `EHLO ${this.smtpServer}${CRLF}`;\n console.log("Resending EHLO command over secure connection:", command);\n secureSocket.write(command);\n\n\n \n });\n \n secureSocket.on("clientError", err => {\n console.error("A client error", err);\n console.log("Stack", err.stack);\n });\n \n secureSocket.on('close', () => {\n console.log('Connection closed');\n secureSocket.removeAllListeners();\n this.previousCommand = '';\n });\n \n \n \n // Send the STARTTLS command to the server\n // client.write('STARTTLS\\r\\n');\n },\n 'MAIL FROM': ({\n sender,\n recipient,\n emailData,\n client\n } = {}) => {\n \n var rc = `RCPT TO:<${recipient}>${CRLF}`;\n console.log("Sending RCPT:", rc)\n client.write(rc)\n },\n 'RCPT TO': ({\n sender,\n recipient,\n emailData,\n client\n } = {}) => {\n var c = `DATA${CRLF}`;\n console.log("Sending data (RCPT TO) info: ", c)\n client.write(c)\n },\n 'DATA': ({\n sender,\n recipient,\n emailData,\n client\n } = {}) => {\n var data = `${emailData}${CRLF}.${CRLF}`;\n console.log("Sending data to the server: ", data)\n client.write(data);\n this.previousCommand = 'END OF DATA'; \n // Set previousCommand to 'END OF DATA' \n //after sending the email content\n },\n };\n constructor({\n port = 25\n } = {}) {\n \n const privateKey = process.env.BH_key;\n if(privateKey) {\n this.privateKey = \n privateKey.replace(/\\\\n/g, '\\n');\n }\n\n this.port = port || 25;\n this.multiLineResponse = '';\n this.previousCommand = '';\n\n\n const certPath = process.env.BH_email_cert;\n const keyPath = process.env.BH_email_key;\n\n console.log("certPath at",certPath,"keyPath at", keyPath)\n if (certPath && keyPath) {\n try {\n this.cert = fs.readFileSync(certPath, 'utf-8');\n this.key = fs.readFileSync(keyPath, 'utf-8');\n // if both are successfully loaded, set useTLS to true\n this.useTLS = true;\n console.log("Loaded cert and key")\n } catch (err) {\n console.error("Error reading cert or key files: ", err);\n // handle error, perhaps set useTLS to false or throw an error\n }\n }\n }\n\n /**\n * @method getDNSRecords\n * @param {String (Email format)} email \n * @returns \n */\n async getDNSRecords(email) {\n return new Promise((r,j) => {\n if(typeof(email) != "string") {\n j("Email paramter not a string");\n return;\n }\n const domain = email.split('@')[1];\n if(!domain) return j("Not an email");\n // Perform MX Record Lookup\n dns.resolveMx(domain, (err, addresses) => {\n if (err) {\n console.error('Error resolving MX records:', err);\n j(err);\n return;\n }\n \n // Sort the MX records by priority\n addresses.sort((a, b) => a.priority - b.priority);\n r(addresses);\n return addresses\n });\n })\n \n }\n\n\n /**\n * Determines the next command to send to the server.\n * @returns {string} - The next command.\n */\n getNextCommand() {\n const commandOrder = [\n 'START',\n 'EHLO', \n 'STARTTLS', // Add STARTTLS to the command order\n 'EHLO',\n 'MAIL FROM', \n 'RCPT TO', \n 'DATA', \n 'END OF DATA'\n ];\n\n console.log("Current previousCommand:", this.previousCommand);\n\n\n const currentIndex = commandOrder.indexOf(this.previousCommand);\n \n if (currentIndex === -1) {\n return commandOrder[0]; \n }\n \n if (currentIndex + 1 >= commandOrder.length) {\n throw new Error('No more commands to send.');\n }\n \n // If the previous command was STARTTLS, return EHLO to be resent over the secure connection\n if (this.previousCommand === 'STARTTLS') {\n return 'EHLO';\n }\n\n\n var nextCommand = commandOrder[currentIndex + 1]\n console.log("Next command: ",nextCommand)\n return nextCommand ;\n }\n \n \n /**\n * Handles the SMTP response from the server.\n * @param {string} lineOrMultiline - The response line from the server.\n * @param {net.Socket} client - The socket connected to the server.\n * @param {string} sender - The sender email address.\n * @param {string} recipient - The recipient email address.\n * @param {string} emailData - The email data.\n */\n \n handleSMTPResponse({\n lineOrMultiline, \n client, \n sender, \n recipient, \n emailData\n } = {}) {\n console.log('Server Response:', lineOrMultiline);\n \n this.handleErrorCode(lineOrMultiline);\n \n var isMultiline = lineOrMultiline.charAt(3) === '-';\n var lastLine = lineOrMultiline;\n var lines;\n if(isMultiline) {\n lines = lineOrMultiline.split(CRLF)\n lastLine = lines[lines.length - 1]\n }\n \n console.log("Got full response: ", lines, lastLine.toString("utf-8"))\n this.multiLineResponse = ''; // Reset accumulated multiline response.\n \n try {\n let nextCommand = this.getNextCommand();\n \n if (lastLine.includes('250-STARTTLS')) {\n console.log('Ready to send STARTTLS...');\n } else if (lastLine.startsWith('220 ') && lastLine.includes('Ready to start TLS')) {\n console.log('Ready to initiate TLS...');\n // TLS handshake has been completed, send EHLO again.\n nextCommand = 'STARTTLS';\n } else if (this.previousCommand === 'STARTTLS' && lastLine.startsWith('250 ')) {\n console.log('Successfully received EHLO response after STARTTLS');\n // Proceed with the next command after validating EHLO response.\n // Additional checks here to validate the EHLO response if needed.\n this.previousCommand = 'EHLO'; // Update previousCommand here\n } else if (this.previousCommand === 'EHLO' && lastLine.startsWith('250 ')) {\n console.log('Successfully received EHLO response');\n nextCommand = 'MAIL FROM';\n }\n \n \n const handler = this.commandHandlers[nextCommand];\n if (!handler) {\n throw new Error(`Unknown next command: ${nextCommand}`);\n }\n \n handler({\n client,\n sender,\n recipient,\n emailData,\n lineOrMultiline\n });\n if (nextCommand !== 'DATA') this.previousCommand = nextCommand; // Update previousCommand here for commands other than 'DATA'\n } catch (e) {\n console.error(e.message);\n client.end();\n } \n }\n \n \n\n \n\n /**\n * Handles error codes in the server response.\n * @param {string} line - The response line from the server.\n */\n handleErrorCode(line) {\n if (line.startsWith('4') || line.startsWith('5')) {\n throw new Error(line);\n }\n }\n\n /**\n * Sends an email.\n * @param {string} sender - The sender email address.\n * @param {string} recipient - The recipient email address.\n * @param {string} subject - The subject of the email.\n * @param {string} body - The body of the email.\n * @returns {Promise} - A promise that resolves when the email is sent.\n */\n async sendMail(sender, recipient, subject, body) {\n return new Promise(async (resolve, reject) => {\n console.log("Getting DNS records..");\n var addresses = await this.getDNSRecords(recipient);\n console.log("Got addresses", addresses);\n var primary = addresses[0].exchange;\n \n\n console.log("Primary DNS of recepient: ", primary)\n this.smtpServer = primary;\n \n \n \n this.socket = net.createConnection(\n this.port, this.smtpServer\n );\n \n \n this.socket.setEncoding('utf-8');\n \n\n const emailData = `From: ${sender}${CRLF}To: ${recipient}${CRLF}Subject: ${subject}${CRLF}${CRLF}${body}`;\n const domain = 'awtsmoos.one';\n const selector = 'selector';\n var dataToSend=emailData\n if(this. privateKey) {\n const dkimSignature = this.signEmail(\n domain, selector, this.privateKey, emailData\n );\n const signedEmailData = `DKIM-Signature: ${dkimSignature}${CRLF}${emailData}`;\n dataToSend=signedEmailData;\n console.log("Just DKIM signed the email. Data: ", signedEmailData)\n }\n\n this.socket.on('connect', () => {\n console.log(\n "Connected, waiting for first server response (220)"\n )\n });\n\n\n try {\n this.handleClientData({\n client: this.socket,\n sender,\n recipient,\n dataToSend\n });\n } catch(e) {\n reject(e);\n }\n \n\n\n this.socket.on('end', () => {\n this.socket.removeAllListeners();\n this.previousCommand = ''\n resolve()\n });\n\n this.socket.on('error', (e)=>{\n this.socket.removeAllListeners();\n console.error("Client error: ",e)\n this.previousCommand = ''\n reject("Error: " + e)\n });\n\n this.socket.on('close', () => {\n this.socket.removeAllListeners();\n if (this.previousCommand !== 'END OF DATA') {\n reject(new Error('Connection closed prematurely'));\n } else {\n this.previousCommand = ''\n resolve();\n }\n });\n });\n }\n\n /**\n * \n * @param {Object} \n * @method handleClientData\n * @description binds the data event\n * to the client socket, useful for switching\n * between net and tls sockets.\n * \n * @param {NET or TLS socket} clientSocket \n * @param {String <email>} sender \n * @param {String <email>} recipient \n * @param {String <email body>} dataToSend \n * \n * \n */\n handleClientData({\n client,\n sender,\n recipient,\n dataToSend\n } = {}) {\n var firstData = false;\n\n let buffer = '';\n let multiLineBuffer = ''; // Buffer for accumulating multi-line response\n let isMultiLine = false; // Flag for tracking multi-line status\n let currentStatusCode = ''; // Store the current status code for multi-line responses\n\n client.on('data', (data) => {\n buffer += data;\n let index;\n\n while ((index = buffer.indexOf(CRLF)) !== -1) {\n const line = buffer.substring(0, index).trim();\n buffer = buffer.substring(index + CRLF.length);\n\n if (!firstData) {\n firstData = true;\n console.log("First time connected, should wait for 220");\n }\n\n const potentialStatusCode = line.substring(0, 3); // Extract the first three characters\n const fourthChar = line.charAt(3); // Get the 4th character\n\n // If the line's 4th character is a '-', it's a part of a multi-line response\n if (fourthChar === '-') {\n isMultiLine = true;\n currentStatusCode = potentialStatusCode;\n multiLineBuffer += line + CRLF; // Remove the status code and '-' and add to buffer\n \n continue; // Continue to the next iteration to keep collecting multi-line response\n }\n\n // If this line has the same status code as a previous line but no '-', then it is the end of a multi-line response\n if (isMultiLine && currentStatusCode === potentialStatusCode && fourthChar === ' ') {\n const fullLine = multiLineBuffer + line; // Remove the status code and space\n multiLineBuffer = ''; // Reset the buffer\n isMultiLine = false; // Reset the multi-line flag\n currentStatusCode = ''; // Reset the status code\n\n try {\n console.log("Handling complete multi-line response:", fullLine);\n this.handleSMTPResponse({\n lineOrMultiline: fullLine, \n client, \n sender, \n recipient, \n emailData: dataToSend,\n multiline:true\n })
| 归档时间: |
|
| 查看次数: |
1124 次 |
| 最近记录: |