Closed niftylettuce closed 5 years ago
I updated the test above.
I'm looking at using messageSplitter.bodySize
right now.
https://github.com/niftylettuce/forward-email/blob/master/message-splitter.js#L22
If you have any thoughts let me know, thank you as always.
I feel like there has to be a better way to end / send a response over SMTP if it exceeds max byte length. It can't just be if (!exceeded) chunks.push
?
@andris9 here's a test specific against wildduck.email:
const path = require('path');
const os = require('os');
const fs = require('fs');
const bytes = require('bytes');
const nodemailer = require('nodemailer');
const uuid = require('uuid');
const Client = require('nodemailer/lib/smtp-connection');
const transporter = nodemailer.createTransport({
streamTransport: true
});
const size = bytes('1gb');
const tls = { rejectUnauthorized: false };
(async () => {
try {
const connection = new Client({
port: 25,
host: 'mail.wildduck.email',
tls,
debug: true
});
const filePath = path.join(os.tmpdir(), uuid());
const fh = fs.openSync(filePath, 'w');
fs.writeSync(fh, 'ok', size);
const info = await transporter.sendMail({
from: 'foo@forwardemail.net',
to: 'yipyip123123@wildduck.email',
subject: 'test',
text: 'test text',
html: '<strong>test text</strong>',
attachments: [{ path: filePath }]
});
console.log('info', info);
connection.once('end', () => {
console.log('connection ended');
});
connection.once('error', err => {
console.error('connection error', err);
});
connection.connect(() => {
connection.login(
{ credentials: { user: 'yipyip123123', pass: 'yipyip123123' } },
() => {
console.log('logged in');
connection.send(info.envelope, info.message, err => {
console.log('err', err);
fs.unlinkSync(filePath);
connection.close();
});
}
);
});
console.log('info', info);
} catch (err) {
console.error(err);
}
})();
On a related note, I saw that you wrapped this with a try/catch - but there is never an error thrown by that function (and all the functions it recursively calls). https://github.com/niftylettuce/forward-email/blob/master/message-splitter.js#L127-L131
In any case, if an error were to be thrown, it doesn't get sent to the callback, it actually causes an uncaught exception.
To elaborate on my previous comment:
If you change this code from:
try {
headersFound = this._checkHeaders(chunk);
} catch (err) {
return callback(err);
}
to
try {
headersFound = this._checkHeaders(chunk);
} catch (err) {
return callback(err);
}
// <--- add a random callback error here
return callback(new Error('UH OH! OOPS!'));
The line added causes an uncaught exception.
I extracted this standalone smtp-server + mailsplitter to test here. I can't reproduce these errorrs:
552 Error: message exceeds fixed maximum message size 10 MB
450 UH OH! OOPS!
on line 199 of your example at https://gist.github.com/andris9/179f3c685ddb161856a5b2ebc090fc14, you wrote stream.sizeExceeded
. Throughout that entire readable event listener function, and throughout the while loop in particular, the value of stream.sizeExceeded
is undefined.
what I am suggesting is that the moment the file limit is reached, the SMTP connection stays connected and continues to send along the rest of the file. it is not until the file is completely transferred that an error response is returned
my hope is that we can find a solution where the moment the file limit is reached in bytes, then an error occurs and the smtp connection disconnects
Oh, you're right, stream.sizeExceeded
is not set until input stream is ended. This is why your app ran out of memory. I updated the example to count the size at place, see lines 200 to 210.
You can close the socket once max size has reached but it is not advisable. Per SMTP until you get a 5xx response any transaction problem is a soft bounce, which means that if you do not respond with 5xx the sender keeps retrying to send the message and that may go on for several days. Sending 5xx immediatelly before closing the connection does not work (at least not for all the clients) as per SMTP the server can send a response after the client has sent message terminating \r\n.\r\n
sequence.
Just for an example, here's how Gmail treats large messages. I tried to send a message with 100MB attachment which is way beyond Gmails limit, and it waited for the terminating dot before replying with the error:
C: ...100MB attachment streamed...
C: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
C: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
C: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
C: AG9r
C: ----_NmP-c77a5c1c94feceb4-Part_1--
C: .
<143490292 bytes encoded mime message (source size 141650649 bytes)>
S: 552-5.2.3 Your message exceeded Google's message size limits. Please visit
S: 552-5.2.3 https://support.google.com/mail/?p=MaxSizeError to view our size
S: 552 5.2.3 guidelines. z11si4008837lji.68 - gsmtp
The Node process is consumed by the file upload though, and until the stream is finished the thread is not freed up for another SMTP request. Imagine you have a 4 CPU server, 4 threads running with pm2
, and you get 4x1 TB file attachment uploads at the same time. Your server would basically be frozen and unresponsive to other valid clients trying to connect. What can be done here?
What makes you think that? Thats the entire point of Node that the thread is not occupied due to using event loop. That TB attachment is not sent as a single large entity but as a series of small TCP chunks. For the uploader it takes a while to upload the chunk, for Node process it takes almost no time to process it (as nothing is done with the chunk).
And don’t test the TB uploads locally where you do not have netwotk latency, it does not give you realistic results
Not sure if you saw this, but if I try to connect 500+ connections at once to SMTP server (with default options), the server has an uncaught exception.
I narrowed the uncaught exception down to this line (based off my current version of Node and the output).
https://github.com/nodejs/node/blob/v10.15.3/lib/net.js#L1097
I figured out why it was throwing uncaught exception, it's because I did not have a connection.on('error', err => ...);
listener function. Full error object:
{ Error: connect ECONNRESET 127.0.0.1:52638
at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1097:14)
errno: 'ECONNRESET',
code: 'ESOCKET',
syscall: 'connect',
address: '127.0.0.1',
port: 52638,
command: 'CONN' }
Was it a handled exception that can be catched by smtpserverinstance.on(‘error’) or was it unhandled that kills your process?
At that count you might be running out of file descriptors etc. If you can catch the error then I wouldn’t worry too much.
smtp-server also has maxClients option to limit active connections. Also If you have 4cpus then I would use nodejs cluster module with 8 workers, each running its own smtp server instance. In this case if one instance explodes then it limits the damage to other open connections.
Any reason why two workers per CPU in particular? Just for efficiency 50/50 split?
Usually I just do 1 thread per CPU using PM2 which handles clustering
Yeah dont’ worry about these ECONNRESET errors under load. Thats most probably not legit traffic anyway.
Having more workers than threads is not so much about efficiency but damage control. You start as many workers as the system can reasonably handle and if one dies then it does not affect other connections
If you start having resource issues during normal run then you should consider upgrading hardware anyway. Having more workers than threads is analogous to web hosting overselling - normally it should not affect anything. If it does then upgrade
Thank you as always.
This simple test crashes the server (note I'm sending a 1gb file attachment):