Display IMAP server message when writing a command to it

I’m trying to hack out some codes to fetch emails from IMAP server.

I’m able to connect to an IMAP server and send a message to the server
I changed the user name and the password in the code below, because I do not wan to put my user name and password on a public forum.

julia> using Sockets

julia> imap = connect("imap.139.com", 143)
TCPSocket(RawFD(0x00000049) open, 0 bytes waiting)

julia> print(readline(imap))
* OK RICHINFO System IMap Server Ready (caixuns[3764603d6f0ab88-00000])

julia> write(imap, "01 AUTH user_foo password_bar")
30

So I am able to communicate by using write(). However, I do not get the nice message back from the IMAP server. Julia gave me the output of 30, which I think is the length of the message 01 BAD errno:1404, imap command error [1].

The question is, how do I receive the entire message from the server, instead of 30?

[1] I got the message by using telnet to test the IMAP server, not from within Julia.

1 Like

I believe 30 here is the number of bytes written. What I think you want to do after the write() is a readline(imap) when will read a single line back from the server.

EDIT: Thinking about it, since the interface seems to be text you probably want to do:

println(imap, "01 AUTH user_foo password_bar")

Instead of write so that a \n is sent to the server, it may be waiting for that to complete the line of text.

Thank you!

println() did not return any output. But it could be that println() was the better option.

I found this old post from 2019: Connect to secure server using secure(SSL) sockets(plan tcp client). Maybe @universe68 got the model right, that there needs to be a two threads, where one thread listens to the output from the server (in my case IMAP server)?

Two threads wouldn’t hurt, but technically you only need them if the server is sending you asynchronous events. If the server only sends you a message in response to you sending it a message then 1 thread is fine. (Also assuming you can tell how many lines the server is sending back in response.)

So you need to do a println() to send a request to the server and a readline() to get the server’s response. If the imap server sends back multiple lines in response then you would have to do multiple readlines to get the full response back.

With multiple threads you would send a response, then have to wait on your response thread for it to get the message back, which is basically what you are doing with println/readline pairs.

The challenge is that readline() does not give anything after println() or write(). Sorry I didn’t manage to convey this issue.

So after println() or write(), if I use readline(imap), I do not get any response.

Could you maybe try to run this piece of codes? The user name and password are made up, but they suffice for the purpose of testing, because the problem right now is not able getting any message back after first println() or write(). Of course the message will be an error message, if one manages to obtain it.

using Sockets
imap = connect("imap.139.com", 143)
readline(imap)
println(imap, "01 AUTH someuser someepassword")
readline(imap)

I ran the code and readline() is returning which means the server didn’t send a response back…let me play with it.

Okay the IMAP protocol is terminated with \r\n so you need to do:

write(imap, "01 AUTH someuser someepassword\r\n")
1 Like

Thank you! It worked!

Using \r\n did the trick!

By the way, I made a mistake in the IMAP command. I should have used login, instead of auth.

1 Like

By the way, I found it challenging to obtain multi-line response from an IMAP server.

For example, the previous code only returns one line of response. If I run readline(imap) twice, in the second time, readline() tries to read a line until the connection is broken.

I tried to use this while() loop, however, the loop would still hang until the connection is broken.

while (! eof(imap))
   print(readline(imap))
end

I just thought it would be a good opportunity to try to get your opinion on this here. But I can also open a new thread to ask this question!

Sorry. Gotta go to bed. To be continued tomorrow.

A quick look at the example of the protocol here Internet Message Access Protocol - Wikipedia
It seems like each command is terminated with a “[id] OK [something] completed” so what I would probably do is write a method like:

function readresponse(imap, id, cmd)
    full = Vector{String}()
    done = false

    while !done 
        line = readline(imap)
        push!(full, line)
        parsed = split(line)
        if parsed[2] == "BAD"
            done = true
        elseif length(parsed) == 4 && parsed[1] == id && 
               parsed[2] == "OK" && parsed[3] == uppercase(cmd) &&
               parsed[5] == "completed"
            done = true
        end
    end

    return full 
end

That should just read the response lines and stop if the response indicates an error or when it sees the completed line.

You might also want to update the if to detect the the “Ready” line you get when you connect to the server.

Another option, and I know this now goes threads and I tried to steer you away from that, but this is kind of clean, and I like clean…

struct Response
    id::String
    error::String
    data::Vector{String}
    Response(id, err::String) = new(id, err, Vector{String}())
    Response(id, data) = new(id, "", data)
end

using Sockets
imap = connect("imap.139.com", 143)
readline(imap)

responses = Channel() do ch
    current = Vector{String}()

    while !eof(imap) do
        line = readline(imap)
        parse = split(line)

        if length(parse) >= 2 && parse[2] == "BAD"
            put!(ch, Response(parse[1], line)
        elseif length(parse) > 2 && parse[2] == "OK" && parse[end] == "completed"
            put!(ch, Response(parse[1], current)
            current = Vector{String}()
        else
            push!(current, line)
        end            

    end
end

function imapwrite(id, cmd, data)
    write(imap, "$id $cmd $data\r\n");
    response = take!(responses)
    @assert response.id == id "Messages desynchronized, got $(response.id) expecting $id"
    @assert response.error == "" response.error
    return response.data
end

imapwrite("01", "auth", "someuser someepassword")
inbox = imapwrite("02", "select", "inbox")
dump(inbox)

I’m not 100% there are no errors in there, I just wrote it in discourse so I make no claims about correctness. It would also probably be better if the imapwrite() method generated the next ID but I’ll leave that to you.

1 Like

Thank you!

I never used threading on Julia. I’m studying the codes!

Your codes were quite good. I only needed to modify one place.

For example, here is the response from the IMAP server for a login command:

01 OK login completed

This means that the server only responded the action word of the command, in this case, login. And it is not in uppercase. It is not certain whether an other server would return LOGIN instead. Therefore, it’d be safer to use lowercase on both sides when comparing the strings.

Another implication is that parsed[4] needs to be compared with completed.

Because of that, the elseif part of your function needs to be written as

        elseif length(parsed) == 4 && parsed[1] == id && 
               parsed[2] == "OK" &&
               lowercase(parsed[3]) == lowercase(split(cmd)[1]) &&
               parsed[4] == "completed"
            done = true

I tested the function after modification and it runs well.

I haven’t extensively tested it yet.

I gotta go to bed, and will read the threading version and get back to you.

1 Like

It would be nice to start writing a julia imap client package based on this. I might be tempted to start one day if nobody beats me to it…