1 --- Simple IMAP client library.
3 -- Copyright 2009 by David Maus <maus.david@gmail.com>
5 -- This program is free software: you can redistribute it and/or modify
6 -- it under the terms of the GNU General Public License as published by
7 -- the Free Software Foundation, either version 3 of the License, or
8 -- (at your option) any later version.
10 -- This program is distributed in the hope that it will be useful,
11 -- but WITHOUT ANY WARRANTY; without even the implied warranty of
12 -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 -- GNU General Public License for more details.
15 -- You should have received a copy of the GNU General Public License
16 -- along with this program. If not, see <http://www.gnu.org/licenses/>.
18 -- @release $Revision$
22 local socket = require("socket")
23 local ssl = require("ssl")
28 local setmetatable = setmetatable
42 --- Create and return new imap client object.
43 -- @param server Hostname or ip address of remote server
44 -- @param port Port to connect to (Default: 993)
45 -- @param ssl_proto SSL/TLS protocol to use (Default: "sslv3")
46 -- @param mailbox Mailbox to check (Default: Inbox)
47 -- @param timeout Timeout value for TCP connection (Default: 5s)
48 -- @return A shiny new imap client control object
49 function new(server, port, ssl_proto, mailbox, timeout)
52 setmetatable(_imap, imap.__index)
54 _imap.port = port or PORT
55 _imap.mailbox = mailbox or MAILBOX
56 if ssl_proto == "none" then
59 _imap.ssl = ssl_proto or SSL
61 _imap.timeout = timeout or TIMEOUT
63 _imap.logged_in = false
69 --- Connect to remote server.
70 -- @return True in case of success, nil followed by the errormessage in case of failure.
71 function imap:connect()
73 local res, msg = socket.connect(self.server, self.port)
74 if not res then return nil, msg end
78 res, msg = ssl.wrap(self.socket, { mode = "client", protocol = self.ssl })
79 if not res then return nil, msg end
81 res, msg = self.socket:dohandshake()
82 if not res then return nil, msg end
86 self.socket:settimeout(self.timeout)
92 --- Login using username and password.
93 -- @param user Username
94 -- @param pass Password
95 -- @return True in case of success, nil followed by the errormessage in case of failure.
96 function imap:login(user, pass)
97 local res, msg = self:request("LOGIN " .. user .. " \"" .. pass .. "\"")
98 if not res then return nil, msg end
101 local res, msg = self:request("EXAMINE " .. self.mailbox)
102 if not res then return nil, msg end
104 self.logged_in = true
110 -- @return True in case of success, nil followed by the errormessage in case of failure.
111 function imap:logout()
112 local res, msg = self:request("LOGOUT")
113 if not res then return nil, msg end
115 self.logged_in = false
120 --- Send command to server and return answer.
121 -- @param command Client command
122 -- @param unprefixed If true, don't prefix command and don't increase command counter
123 -- @return True in case of success, nil followed by the errormessage in case of failure.
124 function imap:request(command, unprefixed)
126 -- check if we the socket exists, return error if not
127 if not self.socket then return nil, "Not connected" end
130 if not unprefixed then
131 self.cmd_count = self.cmd_count + 1
132 prefix = "0x0" .. self.cmd_count .. " "
135 local res, msg = self.socket:send(prefix .. command .. "\r\n")
136 if not res then return nil, msg end
140 local res, msg = self.socket:receive("*l")
141 if not res then return nil, msg end
142 while not res:match(prefix) do
143 table.insert(answer, res)
144 res, msg = self.socket:receive("*l")
145 if not res then return nil, msg end
148 if not res:match(prefix .. "OK ") then
155 --- Return number of new messages in mailbox.
156 -- @return Number of new messages in case of success, nil followed by the errormessage in case of failure.
157 function imap:recent()
159 local res, msg = self:request("EXAMINE " .. self.mailbox)
160 if not res then return nil, msg end
164 for k,v in pairs(msg) do
165 if v:match("^* %d+ RECENT") then n = v:match("^* (%d+) RECENT") end
172 --- Return total number of message in mailbox.
173 -- @return Total number of messages in case of success, nil followed by the errormessage in case of failure.
174 function imap:total()
176 local res, msg = self:request("EXAMINE " .. self.mailbox)
177 if not res then return nil, msg end
181 for k,v in pairs(msg) do
182 if v:match("^* %d+ EXISTS") then n = v:match("^* (%d+) EXISTS") end
189 --- Return number of unread message in mailbox.
190 -- Determining the number of unread messages requires to perform a
192 -- @return Number of unread messages in case of success, nil followed by the errormessage in case of failure.
193 function imap:unread()
195 -- perform an EXAMINE to select the mailbox
196 local res, msg = self:request("SEARCH (UNSEEN)")
197 if not res then return nil, msg end
201 for k,v in pairs(msg) do
202 if v:match("^* SEARCH %d+") then
203 while v:find("%d+") do
204 local s, e = v:find("%d+")
217 --- Check for total number, number of unread and new messages.
218 -- @return Table with number of total, new and unread messages in case
219 -- of success, nil followed by the errormessage in case of failure.
220 function imap:check()
222 local messages = { total = 0, unread = 0, recent = 0 }
224 local res, msg = self:total()
225 if not res then return nil, msg end
228 local res, msg = self:unread()
229 if not res then return nil, msg end
230 messages.unread = res
232 local res, msg = self:recent()
233 if not res then return nil, msg end
234 messages.recent = res
240 --- Return information about messages in mailbox.
241 -- @param recent If true, return information an recent messages (Default: true)
242 -- @param unread If true, return information on unread messages (Default: false)
243 -- @param total If true, return information on all messages (Default: false)
244 -- @return Table with information on all messages that matched the criteria.
245 function imap:fetch(recent, unread, total)
247 if recent == nil then recent = true end
248 if unread == nil then unread = false end
249 if total == nil then total = false end
251 -- build a table with all search queries that we have to issue
253 if recent then table.insert(query, "RECENT") end
254 if unread then table.insert(query, "UNSEEN") end
255 if total then query = { "ALL" } end
259 for _, q in pairs(query) do
260 print ("Perform search for: " .. q)
261 local res, msg = self:request("SEARCH (" .. q .. ")")
262 if not res then return nil, msg end
264 for k,v in pairs(msg) do
265 if v:match("^* SEARCH %d+") then
266 while v:find("%d+") do
267 local s, e = v:find("%d+")
268 local uid = v:sub(s, e)
272 local r,m = self:request("FETCH " .. uid .. " (FLAGS RFC822.SIZE BODY[HEADER.FIELDS (FROM SUBJECT)])")
273 if not r then return nil, m end
275 for l,w in pairs(m) do
276 if w:match("RFC822.SIZE %d+") then messages[uid].size = w:match("RFC822.SIZE (%d+)") end
277 if w:match("^From:") then messages[uid].from = w:match("^From:%s+(.*)") end
278 if w:match("^Subject:") then messages[uid].subject = w:match("^Subject:%s+(.*)") end
279 if w:match("FLAGS") then
280 if w:match("\Recent") then messages[uid].recent = true end
281 if not w:match("\Seen") then messages[uid].unread = true end
292 return true, messages