Closed titanism closed 2 months ago
// Check if the command can be run in current state
+ const states = handler.state && [].concat(handler.state || []);
+ if (states && !states.includes(this.connection.state)) {
- if (handler.state && [].concat(handler.state || []).indexOf(this.connection.state) < 0) {
let err = new Error(parsed.command.toUpperCase() + ' not allowed now');
err.responseCode = 500;
err.code = 'InvalidState';
+ // <https://github.com/nodemailer/wildduck/issues/726>
+ if (states.includes('Authenticated')) {
+ this.connection.clearNotificationListener();
+ this.connection.send('* BYE Logout requested');
+ setImmediate(() => this.connection.close());
+ }
return callback(err);
}
How does this even happen? If Wildduck is restarted then the TCP connection gets closed as well. Thunderbird initiates a new TCP connection but thinks its still the previous one?
I'm thinking there's an edge case I don't know of, and an obvious edge case is if something like out of memory error occurs and the process gets killed, then the state of the client will not match server.
But regardless I think we should dummy-proof this for clients somehow, the client clearly thinks it's logged in when it's not.
I think it is better to figure out why the client thinks they are still connected. For example, they re-use a TLS session and think they are still on the same session even if TCP was reconnected. Or there is a proxy in between that fails to handle the lost connection or whatever.
Users are continuously reporting this issue in Thunderbird with our service. I haven't pinpointed the underlying issue yet though.
I've managed to turn on some rudimentary debug logging and can see this happening in real-time for dozens of commands from Thunderbird mail clients.
For example:
SELECT not allowed now
session.clientId
has value { name: 'Thunderbird', version: '128.1.0' }
session.commandCounters
has value { CAPABILITY: 2, 'AUTHENTICATE PLAIN': 1, ID: 1, SELECT: 1 }
connection.state
is "Not Authenticated"
parsed
value is { tag: '73', command: 'select', attributes: [ { type: 'STRING', value: 'INBOX' } ] }
I think that sometimes connections don't close, as mentioned ENOMEM could be such a case. In any regard, I think we should dummy-proof this and instruct the client to logout somehow.
@andris9 see https://github.com/nodemailer/wildduck/pull/727
Having AUTHENTICATE PLAIN
in command counters should indicate that the client did authenticate. So maybe the issue really is on wildduck side somehow 🤔
@andris9 on all of these errors the this.connection.session.user
property is not set. Occurs on Thunderbird and also a quite a few other clients such as Android:
5|imap-993 | clientId: {
5|imap-993 | name: 'com.google.android.gm',
5|imap-993 | os: 'android',
5|imap-993 | 'os-version': '14; UP1A.231005.007',
5|imap-993 | vendor: 'Xiaomi'
5|imap-993 | },
We're using PM2, not using setImmediate
before invoking our command callbacks, and not setting custom UV_THREADPOOL_SIZE, but I don't think any of that makes a difference since these are long-lived socket connections.
Do you log IMAP commands? Can you check what was the result of AUTHENTICATE PLAIN in the same session before SELECT?
I don't log IMAP commands, but we return early, e.g. callback(err)
if auth failed with an error such as:
const err = new Error('Auth failed');
err.responseCode = 535;
err.imapResponse = 'AUTHENTICATIONFAILED';
However we also set err.response
equal to err.message
, so that's probably the culprit:
let payload = imapHandler.compiler(this.parsed, false, true);
errors.notifyConnection(this.connection, err, {
payload: payload ? (payload.length < 256 ? payload : payload.toString().substr(0, 150) + '...') : false
});
this.connection.send(this.tag + ' ' + (err.response || 'BAD') + ' ' + err.message);
You can see it's going to send the connection a super long message that contains err.response
twice basically.
Not sure if this is going to fix it:
As you can see we don't set error.response
anymore. Do you think this was the issue @andris9?
I reproduced the same issue locally, trying to figure this out now.
We simply were responding incorrectly to IMAP protocol command. The err.response
other than "BAD"
or "NO"
will make the client think that authentication succeeded (e.g. err.response = "AUTHENTICATIONFAILED"
is incorrect).
@andris9 Clients such as Thunderbird that have inconsistent state with the server (e.g. if a WildDuck server is restarted and LOGOUT is not invoked quickly enough to the client - or the client doesn't receive the command) – then the client itself will not be in a correct state.
If a command such as
NAMESPACE
requires'Authenticated'
state, then perhaps we should write the LOGOUT command to the client?