Skip to main content

IMAP Library

If you're new to the IMAP protocol, take a look at IMAP Concepts first!

The IMAP Library is used to interact with the email server using a pure Lua implementation of the IMAP protocol.

IMAPclient connects to the email server via TCP and authenticates

The IMAPclient uses Meta tables to make a simple IMAP adapter with discrete methods for different operations.

IMAPclient is Using a table as a function argument to pass all details required to authenticate with the email server.

To begin the TCP conversation, the IMAPclient initializes the TCP connection, gets the connection response and calls IMAPcommand to login and authenticate with the server.

if statements and the error() function are used to implement error handling for missing authentication details or a failed login attempt.

If the login is successful, the resultant meta table containing the starting Id, TCP connection and the IMAP methods is returned.

require ’IMAP.IMAPcommand’   

local MetaTable = {}

MetaTable.__index = {}
MetaTable.__index.selectInbox = require ’IMAP.IMAPselectInbox’
MetaTable.__index.fetch = require ’IMAP.IMAPfetch’
MetaTable.__index.close = require ’IMAP.IMAPclose’
MetaTable.__index.fetchSummary = require ’IMAP.IMAPfetchSummary’
MetaTable.__index.fetchBatch = require ’IMAP.IMAPfetchBatch’

function IMAPclient(T)
local Result = {}
if T.email == "" or T.password == "" then
error("Missing login details", 2)
end
setmetatable(Result, MetaTable)
Result.Id = 0;
Result.Conn = net.tcp.connect{host=T.host, port=993, secure=true}
local Response, Success = IMAPreadResponse(Result.Conn, ’’)
local Command = "LOGIN "..T.email.." "..T.password:gsub(" ", "")
local Success = IMAPcommand(Result, Command)
trace(Success)
if (Success ~= ’OK’) then
error("Failed login", 2)
end
return Result
end

IMAPcommand sends the commands and reads the response

IMAPcommand is passed the connection and IMAP command as arguments. The command is sent with an incremented ID to manage the client-server interactions. The ID and command are formated by Concatenating strings together.

function IMAPcommand(C, Command)   
C.Id = C.Id + 1
local ID = "a0"..C.Id
C.Conn:send(ID.." "..Command.."\r\n")
local Response, Ended = IMAPreadResponse(C.Conn, ID)
return Ended, ID, Response
end

The response is received and read by the IMAPreadResponse function. An Infinite loop is used to continually receive responses until IMAPended returns an IMAP response code that does not equal nil.

function IMAPreadResponse(Conn, ID)   
local Response = ’’
while (true) do
Response = Response..Conn:recv()
local Ended = IMAPended(Response, ID)
if Ended ~= nil then
return Response,Ended
end
end
end

The IMAPended function is called by IMAPreadResponse to String:find() and return the IMAP response code - OK, NO, or BAD.

function IMAPended(Response, ID)   
if (Response:find(ID..’ OK’)) then
return "OK"
elseif Response:find(ID..’ NO’) then
return "NO"
elseif Response:find(ID..’ BAD’) then
return "BAD"
end
return nil
end

IMAPselectInbox contains the command to select the mail inbox and find the total number of emails

IMAPselectInbox using the SELECT command to choose the INBOX to query. The IMAPhighestId function takes the response of the SELECT command, and uses String:split() to split the response into a Lua table as list. Using a for loop and the # Operator on Lua Tables, each line of the response is checked to find the position of “EXISTS“ and return the extracted number value representing the number of emails in the Inbox.

local function IMAPhighestId(T)   
local Lines = T:split("\n")
for i=1, #Lines do
if Lines[i]:find(’EXISTS’) then
return tonumber(Lines[i]:split(" ")[2])
end
end
end

local function IMAPselectInbox(C)
local R, ID, Response = IMAPcommand(C, "SELECT INBOX")
return IMAPhighestId(Response)
end

return IMAPselectInbox

IMAPfetch gets the email body

IMAPfetch gets the email body. The connection and command arguments are passed to IMAPcommand to send the command. To get the email body we are using FETCH with MailId and the “BODY.PEEK[]“ command.

String:find() is used on the IMAP response, to find the positions of the first and last line that wrap the email body, which we want to capture.

String:sub() extracts and returns the body content between the first and last line positions.

local function IMAPfetch(C, MailId)   
local Success, ID, R = IMAPcommand(C, "FETCH "..MailId.." BODY.PEEK[]")
local FirstLine = R:find("\r\n")+2
local LastLine = R:find(ID)
return R:sub(FirstLine, LastLine–4)
end

return IMAPfetch

IMAPfetchSummary gets the email header

IMAPfetchSummary gets the email header. The connection and command arguments are passed to IMAPcommand to send the command. It uses the same pattern matching strategy as described above in IMAPfetch, however this time is capturing and returning the email header information with the “BODY.PEEK[HEADER]“ command.

local function IMAPfetchSummary(C, MailId)   
local Success, ID, R = IMAPcommand(C, "FETCH "..MailId.." BODY.PEEK[HEADER]")
local FirstLine = R:find("\r\n")+2
local LastLine = R:find(ID)
return R:sub(FirstLine, LastLine–4)
end

return IMAPfetchSummary

IMAPfetchBatch gets a batch of emails

IMAPfetchBatch can be used to get a batch of emails - this can help with improving processing performance when you have a large volume of data. This function operates similarly to the other fetch modules, however is using the first and last MailId to be processed and the “BODY.PEEK[]” command to get the headers and bodies from the specified batch of emails.

local function IMAPfetchBatch(C, FirstMailId, LastMailId)   
local Success, ID, R = IMAPcommand(C, "FETCH "..FirstMailId..":"..LastMailId.." BODY.PEEK[]")
local FirstLine = R:find("\r\n")+2
local LastLine = R:find(ID)
return R:sub(FirstLine, LastLine–4)
end

return IMAPfetchBatch

IMAPclose is used to close the TCP connection

Simple, but important function using io.close() to close the TCP connection.

local function IMAPclose(C)   
C.Conn:close()
end

return IMAPclose

By separating out the IMAP operations, the IMAP Library becomes very extensible!