Skip to content

Unhandled 'error' event on ImapFlow client crashes the whole process on socket timeout #57

Description

@darkm00nch

Description

Environment

  • @codefuturist/email-mcp: 0.2.3
  • imapflow (transitive dep): ^1.2.9
  • Node.js: v24.18.0
  • Deployed behind sparfenyuk/mcp-proxy (streamable HTTP, --no-stateless)

What happened

Under a burst of ~20+ concurrent tool calls (mark_email/move_email) against
a single IMAP account, one underlying IMAP connection was left idle after a
STATUS command. After ImapFlow's default 5-minute socketTimeout elapsed, the
server closed it and ImapFlow emitted an 'error' event on the client. Since
no 'error' listener appears to be attached, Node's default behavior for an
unhandled 'error' event on an EventEmitter took over: it threw and crashed
the entire process.

Server-side (Dovecot) log confirming the ~300s idle window before close:

imap(user@example.com)<sessionid>: Disconnected: Connection closed
(STATUS finished 300.114 secs ago) ...

Crash trace from our systemd journal:

node:events:487
      throw er; // Unhandled 'error' event
      ^
Error: Socket timeout
    at TLSSocket.<anonymous> (.../imapflow/lib/imap-flow.js:966:29)
    at TLSSocket.emit (node:events:509:28)
    at Socket._onTimeout (node:net:611:8)
    ...
Emitted 'error' event on ImapFlow instance at:
    at ImapFlow.emitError (.../imapflow/lib/imap-flow.js:468:14)
    ...
{
  code: 'ETIMEOUT',
  _connId: '...'
}

Impact

Because we run email-mcp as a single long-lived backend behind mcp-proxy
(--no-stateless, one shared process per session), this single unhandled
error brought down the entire MCP tool surface for every client using that
session — not just the one in-flight call — until the parent process
noticed and respawned the child.

Suspected root cause

Somewhere in the codebase an ImapFlow client instance is created without
a .on('error', ...) handler attached. ImapFlow's own docs recommend
attaching an error listener on long-lived clients specifically because
transient conditions (like a socket timeout on an idle connection) surface
as an 'error' event rather than a rejected promise — and Node treats an
unlistened 'error' event as fatal.

This is the same bug class reported in another ImapFlow consumer:
twentyhq/twenty#20509

Suggested fix

  • Attach an .on('error', ...) handler (log + close gracefully) to every
    ImapFlow client the server creates, rather than letting it propagate as
    an unhandled EventEmitter error.
  • Consider whether connections opened for a single command (e.g. STATUS)
    are being closed/logged-out explicitly after use, or left dangling until
    ImapFlow's own idle timeout fires — reusing/pooling connections would
    also reduce how many concurrent IMAP sessions get opened under bursts of
    parallel tool calls.

Happy to provide more logs or test a patch if useful.

Steps to reproduce

see details above

Expected behavior

see details above

Actual behavior

see details above

Version

0.2.3

Email provider

Other / Self-hosted

MCP client

Claude Desktop

Node.js version

24.18.0

Operating system

debian 13

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions