]>
Commit | Line | Data |
---|---|---|
c70b624e ZU |
1 | --- Simple IMAP client library. |
2 | -- | |
3 | -- Copyright 2009 by David Maus <maus.david@gmail.com> | |
4 | -- | |
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. | |
9 | -- | |
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. | |
14 | -- | |
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/>. | |
17 | -- | |
18 | -- @release $Revision$ | |
19 | -- | |
20 | -- | |
21 | ||
22 | local socket = require("socket") | |
23 | local ssl = require("ssl") | |
24 | ||
25 | local os = os | |
26 | local pairs = pairs | |
27 | local print = print | |
28 | local setmetatable = setmetatable | |
29 | local table = table | |
30 | ||
31 | module("imap") | |
32 | ||
33 | imap = {} | |
34 | imap.__index = imap | |
35 | ||
36 | --- Default values. | |
37 | PORT = 993 | |
38 | SSL = "sslv3" | |
39 | MAILBOX = "Inbox" | |
40 | TIMEOUT = 5 | |
41 | ||
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) | |
50 | ||
51 | _imap = {} | |
52 | setmetatable(_imap, imap.__index) | |
53 | _imap.server = server | |
54 | _imap.port = port or PORT | |
55 | _imap.mailbox = mailbox or MAILBOX | |
56 | if ssl_proto == "none" then | |
57 | _imap.ssl = nil | |
58 | else | |
59 | _imap.ssl = ssl_proto or SSL | |
60 | end | |
61 | _imap.timeout = timeout or TIMEOUT | |
62 | _imap.cmd_count = 0 | |
63 | _imap.logged_in = false | |
64 | ||
65 | return _imap | |
66 | ||
67 | end | |
68 | ||
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() | |
72 | ||
73 | local res, msg = socket.connect(self.server, self.port) | |
74 | if not res then return nil, msg end | |
75 | self.socket = res | |
76 | ||
77 | if self.ssl then | |
78 | res, msg = ssl.wrap(self.socket, { mode = "client", protocol = self.ssl }) | |
79 | if not res then return nil, msg end | |
80 | self.socket = res | |
81 | res, msg = self.socket:dohandshake() | |
82 | if not res then return nil, msg end | |
83 | end | |
84 | ||
85 | -- set socket timeout | |
86 | self.socket:settimeout(self.timeout) | |
87 | ||
88 | return true | |
89 | ||
90 | end | |
91 | ||
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 | |
99 | ||
100 | -- select mailbox | |
101 | local res, msg = self:request("EXAMINE " .. self.mailbox) | |
102 | if not res then return nil, msg end | |
103 | ||
104 | self.logged_in = true | |
105 | ||
106 | return true | |
107 | end | |
108 | ||
109 | --- Logout. | |
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 | |
114 | ||
115 | self.logged_in = false | |
116 | ||
117 | return true | |
118 | end | |
119 | ||
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) | |
125 | ||
126 | -- check if we the socket exists, return error if not | |
127 | if not self.socket then return nil, "Not connected" end | |
128 | ||
129 | local prefix = "" | |
130 | if not unprefixed then | |
131 | self.cmd_count = self.cmd_count + 1 | |
132 | prefix = "0x0" .. self.cmd_count .. " " | |
133 | end | |
134 | ||
135 | local res, msg = self.socket:send(prefix .. command .. "\r\n") | |
136 | if not res then return nil, msg end | |
137 | ||
138 | local answer = {} | |
139 | ||
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 | |
146 | end | |
147 | ||
148 | if not res:match(prefix .. "OK ") then | |
149 | return nil, res | |
150 | end | |
151 | ||
152 | return true, answer | |
153 | end | |
154 | ||
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() | |
158 | ||
159 | local res, msg = self:request("EXAMINE " .. self.mailbox) | |
160 | if not res then return nil, msg end | |
161 | ||
162 | local n = 0 | |
163 | local k,v | |
164 | for k,v in pairs(msg) do | |
165 | if v:match("^* %d+ RECENT") then n = v:match("^* (%d+) RECENT") end | |
166 | end | |
167 | ||
168 | return n | |
169 | ||
170 | end | |
171 | ||
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() | |
175 | ||
176 | local res, msg = self:request("EXAMINE " .. self.mailbox) | |
177 | if not res then return nil, msg end | |
178 | ||
179 | local n = 0 | |
180 | local k,v | |
181 | for k,v in pairs(msg) do | |
182 | if v:match("^* %d+ EXISTS") then n = v:match("^* (%d+) EXISTS") end | |
183 | end | |
184 | ||
185 | return n | |
186 | ||
187 | end | |
188 | ||
189 | --- Return number of unread message in mailbox. | |
190 | -- Determining the number of unread messages requires to perform a | |
191 | -- SEARCH query. | |
192 | -- @return Number of unread messages in case of success, nil followed by the errormessage in case of failure. | |
193 | function imap:unread() | |
194 | ||
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 | |
198 | ||
199 | local n = 0 | |
200 | local k,v | |
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+") | |
205 | ||
206 | n = n + 1 | |
207 | v = v:sub(e + 1, #v) | |
208 | ||
209 | end | |
210 | end | |
211 | end | |
212 | ||
213 | return n | |
214 | ||
215 | end | |
216 | ||
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() | |
221 | ||
222 | local messages = { total = 0, unread = 0, recent = 0 } | |
223 | ||
224 | local res, msg = self:total() | |
225 | if not res then return nil, msg end | |
226 | messages.total = res | |
227 | ||
228 | local res, msg = self:unread() | |
229 | if not res then return nil, msg end | |
230 | messages.unread = res | |
231 | ||
232 | local res, msg = self:recent() | |
233 | if not res then return nil, msg end | |
234 | messages.recent = res | |
235 | ||
236 | return messages | |
237 | ||
238 | end | |
239 | ||
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) | |
246 | ||
247 | if recent == nil then recent = true end | |
248 | if unread == nil then unread = false end | |
249 | if total == nil then total = false end | |
250 | ||
251 | -- build a table with all search queries that we have to issue | |
252 | local query = {} | |
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 | |
256 | ||
257 | local messages = {} | |
258 | local _, q | |
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 | |
263 | local k,v | |
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) | |
269 | ||
270 | messages[uid] = {} | |
271 | ||
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 | |
274 | local l,w | |
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 | |
282 | end | |
283 | end | |
284 | ||
285 | v = v:sub(e + 1, #v) | |
286 | end | |
287 | end | |
288 | end | |
289 | end | |
290 | ||
291 | ||
292 | return true, messages | |
293 | ||
294 | end |