nodemailer / smtp-server

Create custom SMTP servers on the fly
Other
846 stars 145 forks source link

Port changes prevent any connections #166

Closed nathandtrone closed 3 years ago

nathandtrone commented 3 years ago

I was attempting to create a simple Server <=> Client test to see how this module works, and experiment a bit. I noticed something breaking it. If you run the server.js without SU(super user) permissions, you cannot bind the port(465). That's normal, I know that. When I run it as SU, it is able to bind the port, and I can run client.js. Things execute properly, cool(I've got that down then.)

I don't like working in SU space when I'm testing or developing. So I changed my var PORT to 9465, on both the server.js and the client.js files. Ran the same thing again and it doesn't connect, the client clearly reaches the servers socket, but after a few seconds it times out and fails. The server reports unexpected socket close, and the client claims "Greeting never received". I've tried getting more debug info out of this, but I can't figure out how to do that. Here's the real kicker, I then thought ok, lets try running it as SU again(keeping the ports at 9465). When doing that, again, the server saw the socket close unexpectedly, and the client complained exactly the same.

Is this my problem or possibly a bug? I've replicated the project on another machine and it fails there too(attached at end).

Env Versions:

Node Versions: v10.24.0 through v14.16.1

Source Code

Here's the server.js code:

const smtp_server = require("smtp-server");
const mailparser = require("mailparser");
const crypto = require("crypto");
const fs = require("fs");

var PORT = 465; //ANYTHING BUT THIS VALUE CAUSES ISSUES

function hash(data, algorithm = 'md5') {
  // Algorithm depends on availability of OpenSSL on platform
  // Another algorithms: 'sha1', 'md5', 'sha256', 'sha512' ...
  let shasum = crypto.createHash(algorithm);
  try {
    shasum.update(data);
    return shasum.digest('hex');
  } catch (error) {
    return 'calc fail';
  }
}

/*OAuth2 authentication*/
function onOAuth(auth, session, callback) {
  if(auth.method !== "XOAUTH2") {
    // should never occur in this case as only XOAUTH2 is allowed
    return callback(new Error("Expecting XOAUTH2"));
  }
  if(auth.username !== "abc" || auth.accessToken !== "def") {
    return callback(null, {
      data: {
        status: "401",
        schemes: "bearer mac",
        scope: "my_smtp_access_scope_name"
      }
    });
  }
  callback(null, { user: 123 }); // where 123 is the user id or similar property
}

function sanitizeHtml(input) {
  return input.replace(/[\<]/g,"[").replace(/[\>]/g,"]");
}

const server = new smtp_server.SMTPServer({
  logger: console,
  name: "localhost",
  banner: "welcome?",
  size: 1024*8, // allow messages up to 1kb*#
  secure: true,
  key: fs.readFileSync("server.key"),
  cert: fs.readFileSync("server.crt"),
  onConnect: function( session, callback ) {
    console.error("onConnect", session, callback);
    if(session.remoteAddress != "127.0.0.1") {
      console.error("Error connection not from localhost");
      return callback(new Error("No connections from localhost allowed"));
    }
    return callback(); // Accept the connection
  },
  //authMethods: ["PLAIN", "LOGIN", "XOAUTH2"], //defaults to [‘PLAIN’, ‘LOGIN’].
  //authOptional: true,
  onAuth(auth, session, callback) {
    console.log("onAuth: User...", auth, session );
    if (auth.username !== "abc" || auth.password !== "def") {
      return callback(new Error("Invalid username or password"));
    }
    callback(null, { user: 123 }); // where 123 is the user id or similar property
  },
  onData(stream, session, callback) {
    console.log("onData: Receiving Mail...", session );
    mailparser.simpleParser(stream).then((parsed)=>{
      let mail = {
        to: parsed.to.text,
        from: parsed.from.text,
        subject: parsed.subject,
        received: parsed.date,
        body: parsed.text,
        htmlBody: ( parsed.html ? parsed.text : "" )
      };
      if(parsed.html) {
        try {
          mail.htmlBody = sanitizeHtml(parsed.html);
        } catch (error) {
          mail.htmlBody = parsed.text;
        }
      } else {
        mail.htmlBody = "";
      }
      console.log("email",mail);
    });
  },
  onRcptTo(address, session, callback) {
    console.log("onRcptTo", address, session);
    // do not accept messages larger than 100 bytes to specific recipients
    let expectedSize = Number(session.envelope.mailFrom.args.SIZE) || 0;
    if (address.address === "almost-full@example.com" && expectedSize > 100) {
      err = new Error("Insufficient channel storage: " + address.address);
      err.responseCode = 452;
      return callback(err);
    }
    callback();
  },
  onMailFrom(address, session, callback) {
    console.log("onMailFrom: Received Mail from", address, session );
    if(address.address !== "allowed@example.com") {
      return callback( new Error("Only allowed@example.com is allowed to send mail") );
    }
    return callback(); // Accept the address
  },
  onClose(address, session, callback) {
    console.log("onClose", address, session, callback);
  }
});
server.on("error", ( err )=>{
  console.error("Error", err.message);
});
server.on("close", ( session )=>{
  console.error("close", session);
  console.log("Client Disconnect", session);
});

server.on("connect", ( session )=>{
  console.error("connect", session);
});
server.on("auth", ( auth, session, callback )=>{
  console.error("auth", auth, session, callback);
});
server.on("mailfrom", ( address, session, callback )=>{
  console.log("mailfrom", address, session, callback );
});
server.on("rcptto", ( address, session, callback )=>{
  console.log("rcptto", address, session, callback );
});
server.on("data", ( stream, session, callback )=>{
  console.log("data", stream, session, callback );
});

console.log("Listen", PORT);
server.listen(PORT,"127.0.0.1", function() {
  console.log("Listening...", arguments);
});

This is the client.js code, sending an email to the server.js:

const nodemailer = require('nodemailer');

var PORT = 465; //ANYTHING BUT THIS VALUE CAUSES ISSUES

let transport = nodemailer.createTransport({
  host: '127.0.0.1',
  port: PORT,
  auth: {
     user: 'abc',
     pass: 'def'
  },
  tls: {
    rejectUnauthorized: false
  }
});
const message = {
  from: 'allowed@example.com', // Sender address
  to: 'allowed@example.com',         // List of recipients
  subject: 'a test email', // Subject line
  text: 'Text in an email body.' // Plain text body
};
transport.sendMail(message, function(err, info) {
  if(err) {
    console.error("Error:",err)
  } else {
    console.log("Sent message");
    console.log(info);
  }
});

Server Log Output(when running as SU and client connects to send)[AKA WORKING]:

===SERVER START====
Listen 465
{
  component: 'smtp-server',
  tnx: 'listen',
  host: '::',
  port: 465,
  secure: true,
  protocol: 'SMTP'
} %s%s Server listening on %s:%s Secure  SMTP [::] 465

====CLIENT SEND====

connect {
  id: '1234567898765432',
  secure: true,
  localAddress: '127.0.0.1',
  localPort: 465,
  remoteAddress: '127.0.0.1',
  remotePort: 63696,
  clientHostname: '[127.0.0.1]',
  openingCommand: false,
  hostNameAppearsAs: false,
  xClient: Map(0) {},
  xForward: Map(0) {},
  transmissionType: 'SMTPS',
  tlsOptions: {
    name: 'TLS_AES_256_GCM_SHA384',
    standardName: 'TLS_AES_256_GCM_SHA384',
    version: 'TLSv1.3'
  },
  envelope: { mailFrom: false, rcptTo: [] },
  transaction: 1
} [Function (anonymous)]
{
  component: 'smtp-server',
  tnx: 'connection',
  cid: '1234567898765432',
  host: '127.0.0.1',
  hostname: '[127.0.0.1]'
} Connection from %s [127.0.0.1]
connect {
  id: '1234567898765432',
  localAddress: '127.0.0.1',
  localPort: 465,
  remoteAddress: '127.0.0.1',
  remotePort: 63696,
  hostNameAppearsAs: false,
  clientHostname: '[127.0.0.1]'
}
{
  component: 'smtp-server',
  tnx: 'send',
  cid: '1234567898765432',
  user: undefined
} S: 220 localhost ESMTP welcome?
{
  component: 'smtp-server',
  tnx: 'command',
  cid: '1234567898765432',
  command: 'EHLO',
  user: undefined
} C: EHLO [127.0.0.1]
{
  component: 'smtp-server',
  tnx: 'send',
  cid: '1234567898765432',
  user: undefined
} S: 250-localhost Nice to meet you, [127.0.0.1]
250-PIPELINING
250-8BITMIME
250-SMTPUTF8
250-AUTH LOGIN PLAIN
250 SIZE 8192
{
  component: 'smtp-server',
  tnx: 'command',
  cid: '1234567898765432',
  command: 'AUTH',
  user: undefined
} C: AUTH PLAIN AGFiYwBkZWY=
onAuth: User... { method: 'PLAIN', username: 'abc', password: 'def' } {
  id: '1234567898765432',
  secure: true,
  localAddress: '127.0.0.1',
  localPort: 465,
  remoteAddress: '127.0.0.1',
  remotePort: 63696,
  clientHostname: '[127.0.0.1]',
  openingCommand: 'EHLO',
  hostNameAppearsAs: '[127.0.0.1]',
  xClient: Map(0) {},
  xForward: Map(0) {},
  transmissionType: 'ESMTPS',
  tlsOptions: {
    name: 'TLS_AES_256_GCM_SHA384',
    standardName: 'TLS_AES_256_GCM_SHA384',
    version: 'TLSv1.3'
  },
  envelope: { mailFrom: false, rcptTo: [] },
  transaction: 1
}
{
  component: 'smtp-server',
  tnx: 'auth',
  cid: '1234567898765432',
  method: 'PLAIN',
  user: 'abc'
} %s authenticated using %s abc PLAIN
{
  component: 'smtp-server',
  tnx: 'send',
  cid: '1234567898765432',
  user: 123
} S: 235 Authentication successful
{
  component: 'smtp-server',
  tnx: 'command',
  cid: '1234567898765432',
  command: 'MAIL',
  user: 123
} C: MAIL FROM:<allowed@example.com>
onMailFrom: Received Mail from { address: 'allowed@example.com', args: false } {
  id: '1234567898765432',
  secure: true,
  localAddress: '127.0.0.1',
  localPort: 465,
  remoteAddress: '127.0.0.1',
  remotePort: 63696,
  clientHostname: '[127.0.0.1]',
  openingCommand: 'EHLO',
  hostNameAppearsAs: '[127.0.0.1]',
  xClient: Map(0) {},
  xForward: Map(0) {},
  transmissionType: 'ESMTPSA',
  tlsOptions: {
    name: 'TLS_AES_256_GCM_SHA384',
    standardName: 'TLS_AES_256_GCM_SHA384',
    version: 'TLSv1.3'
  },
  envelope: { mailFrom: false, rcptTo: [] },
  transaction: 1,
  user: 123
}
{
  component: 'smtp-server',
  tnx: 'send',
  cid: '1234567898765432',
  user: 123
} S: 250 Accepted
{
  component: 'smtp-server',
  tnx: 'command',
  cid: '1234567898765432',
  command: 'RCPT',
  user: 123
} C: RCPT TO:<allowed@example.com>
onRcptTo { address: 'allowed@example.com', args: false } {
  id: '1234567898765432',
  secure: true,
  localAddress: '127.0.0.1',
  localPort: 465,
  remoteAddress: '127.0.0.1',
  remotePort: 63696,
  clientHostname: '[127.0.0.1]',
  openingCommand: 'EHLO',
  hostNameAppearsAs: '[127.0.0.1]',
  xClient: Map(0) {},
  xForward: Map(0) {},
  transmissionType: 'ESMTPSA',
  tlsOptions: {
    name: 'TLS_AES_256_GCM_SHA384',
    standardName: 'TLS_AES_256_GCM_SHA384',
    version: 'TLSv1.3'
  },
  envelope: {
    mailFrom: { address: 'allowed@example.com', args: false },
    rcptTo: []
  },
  transaction: 1,
  user: 123
}
{
  component: 'smtp-server',
  tnx: 'send',
  cid: '1234567898765432',
  user: 123
} S: 250 Accepted
{
  component: 'smtp-server',
  tnx: 'command',
  cid: '1234567898765432',
  command: 'DATA',
  user: 123
} C: DATA
onData: Receiving Mail... {
  id: '1234567898765432',
  secure: true,
  localAddress: '127.0.0.1',
  localPort: 465,
  remoteAddress: '127.0.0.1',
  remotePort: 63696,
  clientHostname: '[127.0.0.1]',
  openingCommand: 'EHLO',
  hostNameAppearsAs: '[127.0.0.1]',
  xClient: Map(0) {},
  xForward: Map(0) {},
  transmissionType: 'ESMTPSA',
  tlsOptions: {
    name: 'TLS_AES_256_GCM_SHA384',
    standardName: 'TLS_AES_256_GCM_SHA384',
    version: 'TLSv1.3'
  },
  envelope: {
    mailFrom: { address: 'allowed@example.com', args: false },
    rcptTo: [ [Object] ]
  },
  transaction: 1,
  user: 123
}
{
  component: 'smtp-server',
  tnx: 'send',
  cid: '1234567898765432',
  user: 123
} S: 354 End data with <CR><LF>.<CR><LF>
email {
  to: 'allowed@example.com',
  from: 'allowed@example.com',
  subject: 'a test email',
  received: 2021-06-04T18:43:31.000Z,
  body: 'Text in an email body.\n',
  htmlBody: ''
}

ERRORONOUS RUN

When I run the server with port 9465, 1465, or any other port not in restricted space. Here's the result in the log:

===SERVER START====
Listen 9465
{
  component: 'smtp-server',
  tnx: 'listen',
  host: '127.0.0.1',
  port: 9465,
  secure: true,
  protocol: 'SMTP'
} %s%s Server listening on %s:%s Secure  SMTP 127.0.0.1 9465
Listening... [Arguments] {}

====CLIENT SEND==== //Nothing is happening...
Error Socket closed unexpectedly

Client Log:

Error: Greeting never received
    at SMTPConnection._formatError (/node_modules/nodemailer/lib/smtp-connection/index.js:774:19)
    at SMTPConnection._onError (/node_modules/nodemailer/lib/smtp-connection/index.js:760:20)
    at Timeout.<anonymous> (/node_modules/nodemailer/lib/smtp-connection/index.js:694:22)
    at listOnTimeout (internal/timers.js:554:17)
    at processTimers (internal/timers.js:497:7) {
  code: 'ETIMEDOUT',
  command: 'CONN'
}

2nd Machine Test:

[debuguser@server ~]$ node server.js &
[1] 8460
[debuguser@server ~]$ Listen 9465
{ component: 'smtp-server',
  tnx: 'listen',
  host: '127.0.0.1',
  port: 9465,
  secure: true,
  protocol: 'SMTP' } '%s%s Server listening on %s:%s' 'Secure ' 'SMTP' '127.0.0.1' 9465
Listening... [Arguments] {}

[debuguser@server ~]$ 
[debuguser@server ~]$ 
[debuguser@server ~]$ node client.js
Error: { Error: Greeting never received
    at SMTPConnection._formatError (/home/debuguser/node_modules/nodemailer/lib/smtp-connection/index.js:774:19)
    at SMTPConnection._onError (/home/debuguser/node_modules/nodemailer/lib/smtp-connection/index.js:760:20)
    at Timeout._greetingTimeout.setTimeout (/home/debuguser/node_modules/nodemailer/lib/smtp-connection/index.js:694:22)
    at ontimeout (timers.js:436:11)
    at tryOnTimeout (timers.js:300:5)
    at listOnTimeout (timers.js:263:5)
    at Timer.processTimers (timers.js:223:10) code: 'ETIMEDOUT', command: 'CONN' }
Error Socket closed unexpectedly
[debuguser@server ~]$ 
andris9 commented 3 years ago

Add secure:true to nodemailer’s conf. When using port 465 then it’s the default, otherwise it is not.