Inspect IMAP Traffic Using a Node.js Proxy

- -

At Handle we talk to a lot of email servers, mostly via IMAP. Something most engineers don’t realize is how crazy IMAP has become, in large part due to fragmentation and providers who add functionality on top of the protocol. Talking to Yahoo? No IMAP IDLE. Talking to Gmail? You may or may not be blessed with an “All Mail” folder. It might also be localized to the user’s language (e.g. “Alle Nachrichten” for German users). There’s also COMPRESS, MODESEQ, Gmail MSGID & THRID – the list goes on. How does a programmer begin to learn the nuances?

Open source clients like Thunderbird or Zimbra are a good place to start. But sometimes it’s faster to watch an exchange than trace code. What about a TCP dump? Sometimes the server only responds to SSL and it’s not possible to sniff the handshake. Or even if you record a session, the IMAP server might have compressed parts of it. Annoyances such as these are what prompted me to write an IMAP proxy, making it easy to inspect any IMAP conversation.

To run the script you’ll need node.js and coffee-script (via npm). To start the script, just run coffee imap_proxy.coffee. Then configure your email client to connect to your machine on port 3737. Disable SSL. Save your settings and watch the conversation unfold!

(imap_proxy.coffee) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/usr/bin/env coffee

tls = require("tls")
net = require("net")
Socket = require("net").Socket
state = {}

IMAP_SERVER = "imap.gmail.com"
GREEN_TEXT_CODE = '\x1b[0;32m'
RED_TEXT_CODE = '\x1b[0;31m'

clientListener = (connectionToClient) ->
  # This callback is run when the server gets a connection from a client.
  isConnected = true
  console.log "Connected to mail client"

  connectionToClient.on "data", (data) ->
    console.log "#{RED_TEXT_CODE} Mail client: ", data.toString()
    connectionToImapServer.write(data)
  connectionToClient.on "end", ->
    isConnected = false
    console.log "Disconnected from #{IMAP_SERVER}"

  # Now that we have a client on the line, make a connection to the IMAP server.
  state.conn = new Socket()
  connectionToImapServer = tls.connect(
    socket: state.conn,
    # Skip TLS certificate verification. Don't use this script on sensitive data!
    rejectUnauthorized: false
  , ->
    console.log "Client connected"
    state.conn = connectionToImapServer
  )
  connectionToImapServer.on "data", (data) ->
    console.log "#{GREEN_TEXT_CODE} #{IMAP_SERVER}: ", data.toString()
    return if !isConnected

    # Do something sneaky during the CAPABILITY exchange. Remove "DEFLATE" from
    # the list (if present) so this proxy doesn't have to decompress messages.
    if data.toString().match(/CAPABILITY.*COMPRESS=DEFLATE/)
      minusCompress = data.toString().replace "COMPRESS=DEFLATE ", ""
      console.log "Proxy substitution: ", minusCompress
      connectionToClient.write(minusCompress)
    else
      connectionToClient.write(data)

  state.conn.connect 993, IMAP_SERVER

server = net.createServer(clientListener)
server.listen 3737, ->
  console.log "IMAP proxy is listening on port 3737"

I hope this helps on your next IMAP adventure. And if you’re passionate about IMAP, Handle is hiring!