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!