├── README.md ├── example.lua ├── imap4-scm-0.rockspec └── imap4.lua /README.md: -------------------------------------------------------------------------------- 1 | # imap4.lua 2 | 3 | Simple IMAP4 protocol wrapper, based on [RFC3501](https://tools.ietf.org/html/rfc3501). 4 | 5 | Most of it is untested, so don't expect it to work. 6 | 7 | ## Documentation 8 | 9 | TODO 10 | 11 | ## License 12 | 13 | Copyright (c) 2012 Matthias Richter 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy of 16 | this software and associated documentation files (the "Software"), to deal in 17 | the Software without restriction, including without limitation the rights to 18 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 19 | of the Software, and to permit persons to whom the Software is furnished to do 20 | so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | Except as contained in this notice, the name(s) of the above copyright holders 26 | shall not be used in advertising or otherwise to promote the sale, use or 27 | other dealings in this Software without prior written authorization. 28 | 29 | If you find yourself in a situation where you can safe the author's life 30 | without risking your own safety, you are obliged to do so. 31 | 32 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 33 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 34 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 35 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 36 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 37 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 38 | SOFTWARE. 39 | 40 | -------------------------------------------------------------------------------- /example.lua: -------------------------------------------------------------------------------- 1 | require 'luarocks.require' 2 | 3 | local imap4 = require 'imap4' 4 | 5 | -- If in doubt, see RFC 3501: 6 | -- https://tools.ietf.org/html/rfc3501#section-6 7 | 8 | -- Create new imap4 connection. 9 | -- Port is optional and defaults to 143. 10 | local connection = imap4('localhost', 143) 11 | 12 | -- If you are connecting to gmail, yahoo or any other server that needs a SSL 13 | -- connection before accepting commands, uncomment this line: 14 | -- 15 | -- connection:enabletls{protocol = 'sslv3'} 16 | -- 17 | -- You can skip this step by creating the connection using 18 | -- 19 | -- local connection = imap4('imap.gmail.com', 993, {protocol = 'sslv3'}) 20 | 21 | -- Print the servers capabilities. 22 | print(table.concat(connection:capability(), ', ')) 23 | 24 | -- Make sure we can do what we came for. 25 | assert(connection:isCapable('IMAP4rev1')) 26 | 27 | -- Login. Warning: The credentials are sent in plaintext unless you 28 | -- tunnel the connection over ssh, or use SSL (either via the method shown 29 | -- above or calling connection:starttls(params) before logging in). 30 | connection:login(user, pass) 31 | 32 | -- connection:lsub() lists all subscribed mailboxes. 33 | for mb, info in pairs(connection:lsub()) do 34 | -- connection:status(mailbox, items) queries status of a mailbox. 35 | -- Note: The mailbox name may contain unescaped whitespace. You are 36 | -- responsible to escape it properly - try ("%q"):format(mb). 37 | local stat = connection:status(mb, {'MESSAGES', 'RECENT', 'UNSEEN'}) 38 | print(mb, stat.MESSAGES, stat.RECENT, stat.UNSEEN) 39 | end 40 | 41 | -- Select INBOX with read only permissions. 42 | local info = connection:examine('INBOX') 43 | print(info.exist, info.recent) 44 | 45 | -- List info on the 4 most recent mails. 46 | -- See https://tools.ietf.org/html/rfc3501#section-6.4.5 47 | for _,v in pairs(connection:fetch('UID BODY.PEEK[HEADER.FIELDS (From Date Subject)]', (info.exist-4)..':*')) do 48 | -- `v' contains the response as mixed (possibly nested) table. 49 | -- Keys are stored in the list part. In this example: 50 | -- 51 | -- v[1] = "UID", v[2] = BODY 52 | -- 53 | -- `v[key]' holds the value of that part, e.g. 54 | -- 55 | -- v.UID = 10 56 | -- 57 | -- `v.BODY' is the only exception and returns a table of the format 58 | -- 59 | -- {parts = part-table, value = response} 60 | -- 61 | -- For example: 62 | -- 63 | -- v.BODY = { 64 | -- parts = {"HEADER.FIELDS", {"From", "Date", "Subject"}}, 65 | -- value = "From: Foo \r\nDate:..." 66 | -- } 67 | print(v.id, v.UID, v.BODY.value) 68 | end 69 | 70 | -- close connection 71 | connection:logout() 72 | -------------------------------------------------------------------------------- /imap4-scm-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "imap4" 2 | version = "scm-0" 3 | description = { 4 | summary = "Simple IMAP4 protocol wrapper.", 5 | detailed = [[ 6 | imap4.lua is a thin abstraction layer above RFC 3501 (IMAP4rev1), upon 7 | which more complex systems can be built. 8 | ]], 9 | license = "modified MIT", 10 | homepage = "https://github.com/vrld/imap4.lua", 11 | } 12 | 13 | dependencies = { 14 | "lua >= 5.1", 15 | "luasocket>=2.0.2", 16 | "luasec>=0.4", 17 | } 18 | 19 | source = { 20 | url = "git://github.com/vrld/imap4.lua.git", 21 | branch = "master", 22 | } 23 | 24 | build = { 25 | type = "builtin", 26 | modules = { 27 | imap4 = "imap4.lua", 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /imap4.lua: -------------------------------------------------------------------------------- 1 | --Copyright (c) 2012 Matthias Richter 2 | -- 3 | --Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | --this software and associated documentation files (the "Software"), to deal in 5 | --the Software without restriction, including without limitation the rights to 6 | --use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | --of the Software, and to permit persons to whom the Software is furnished to do 8 | --so, subject to the following conditions: 9 | -- 10 | --The above copyright notice and this permission notice shall be included in all 11 | --copies or substantial portions of the Software. 12 | -- 13 | --Except as contained in this notice, the name(s) of the above copyright holders 14 | --shall not be used in advertising or otherwise to promote the sale, use or 15 | --other dealings in this Software without prior written authorization. 16 | -- 17 | --If you find yourself in a situation where you can safe the author's life 18 | --without risking your own safety, you are obliged to do so. 19 | -- 20 | --THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | --IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | --FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | --AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | --LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | --OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | --SOFTWARE. 27 | 28 | local socket = require 'socket' 29 | 30 | -- helper 31 | local function Set(t) 32 | local s = {} 33 | for _,v in ipairs(t) do s[v] = true end 34 | return s 35 | end 36 | 37 | -- argument checkers. 38 | -- e.g.: assert_arg(1, foo).type('string', 'number') 39 | -- assert_arg(2, bar).any('one', 2, true) 40 | local function assert_arg(n,v) 41 | return { 42 | type = function(...) 43 | local s = type(v) 44 | for t in pairs(Set{...}) do if s == t then return end end 45 | local t = table.concat({...}, "' or `") 46 | error(("Error in argument %s: Expected `%s', got `%s'"):format(n, t, s), 2) 47 | end, 48 | any = function(...) 49 | for u in pairs(Set{...}) do if u == v then return end end 50 | local u = table.concat({...}, "', `") 51 | error(("Error in argument %s: Expected to be one of (`%s'), got `%s'"):format(n, u, tostring(v)), 2) 52 | end 53 | } 54 | end 55 | 56 | -- generates tokens for IMAP conversations 57 | local function token_generator() 58 | local prefix = math.random() 59 | local n = 0 60 | return function() 61 | n = n + 1 62 | return prefix .. n 63 | end 64 | end 65 | 66 | -- (nested) table to IMAP lists. table may not contain loops 67 | local function to_list(tbl) 68 | if type(tbl) == 'table' then 69 | local s = {} 70 | for k,v in ipairs(tbl) do s[k] = to_list(v) end 71 | return '(' .. table.concat(s, ' ') .. ')' 72 | end 73 | return tbl 74 | end 75 | 76 | -- make a table out of an IMAP list 77 | local function to_table(s) 78 | local stack = {i = 0} 79 | local function push(v) 80 | local cur = stack[stack.i] 81 | cur[#cur+1] = v 82 | end 83 | 84 | local i = 1 85 | while i <= #s do 86 | local c = s:sub(i,i) 87 | if c == '(' then -- open list 88 | stack.i = stack.i + 1 89 | stack[stack.i] = {} 90 | elseif c == ')' then -- close list 91 | if stack.i == 1 then return stack[stack.i] end 92 | stack.i = stack.i - 1 93 | push(stack[stack.i+1]) 94 | elseif c == '[' then -- quoted list 95 | local k = i 96 | i = assert(s:find(']', i+1), "Expected token `]', got EOS") 97 | push(s:sub(k+1, i-1)) 98 | elseif c == '"' then -- quoted string 99 | local k = i 100 | repeat 101 | i = assert(s:find('"', i+1), "Expected token `\"', got EOS") 102 | until s:sub(i-1,i) ~= '\\"' 103 | push(s:sub(k+1,i-1)) 104 | elseif c == '{' then -- literal 105 | local k = assert(s:find('}', i+1), "Expected token `}', got EOS") 106 | local n = tonumber(s:sub(i+1,k-1)) 107 | local sep = s:sub(k+1,k+2) 108 | assert(sep == '\r\n', ("Invalid literal: Expected 0x%02x 0x%02x, got 0x%02x 0x%02x"):format(('\r'):byte(1), ('\n'):byte(1), sep:byte(1,-1))) 109 | k, i = k+3, k+3+n+1 110 | assert(i <= #s, "Invalid literal: Requested more bytes than available") 111 | push(s:sub(k,i)) 112 | elseif c:match('%S') then 113 | local k = i 114 | i = assert(s:find('[%s%)%[]',i+1), "Expected token , `)' or `[', got EOS") - 1 115 | push(s:sub(k,i)) 116 | end 117 | i = i + 1 118 | end 119 | error("Expected token `)', got EOS:\n"..s) 120 | end 121 | 122 | -- imap4 connection 123 | local IMAP = {} 124 | IMAP.__index = IMAP 125 | 126 | -- constructor 127 | function IMAP.new(host, port, tls_params) 128 | assert_arg(1, host).type('string') 129 | assert_arg(2, port).type('number', 'nil') 130 | 131 | port = port or 143 132 | local s = assert(socket.connect(host, port), ("Cannot connect to %s:%u"):format(host, port)) 133 | s:settimeout(5) 134 | 135 | local imap = setmetatable({ 136 | host = host, 137 | port = port, 138 | socket = s, 139 | next_token = token_generator(), 140 | }, IMAP) 141 | 142 | -- check the server greeting before executing the first command 143 | imap._do_cmd = function(self, ...) 144 | self._do_cmd = IMAP._do_cmd 145 | local greeting = imap:_receive():match("^%*%s+(.*)") 146 | if not greeting then 147 | self.socket:close() 148 | assert(nil, ("Did not receive greeting from %s:%u"):format(host, port)) 149 | end 150 | return self:_do_cmd(...) 151 | end 152 | 153 | -- shortcut to enabling a secure connection 154 | if tls_params then 155 | imap:enabletls(tls_params) 156 | end 157 | 158 | return imap 159 | end 160 | 161 | -- Enable ssl connection. Some servers *cough*gmail*cough* dont honor the 162 | -- standard and close the connection before letting you send any command, 163 | -- including STARTTLS. 164 | function IMAP:enabletls(tls_params) 165 | assert_arg(1, tls_params).type('table', 'nil') 166 | tls_params = tls_params or {protocol = 'sslv3'} 167 | tls_params.mode = tls_params.mode or 'client' 168 | 169 | local ssl = require 'ssl' 170 | self.socket = assert(ssl.wrap(self.socket, tls_params)) 171 | return self.socket:dohandshake() 172 | end 173 | 174 | -- gets a full line from the socket. may block 175 | function IMAP:_receive(mode) 176 | local r = {} 177 | repeat 178 | local result, errstate, partial = self.socket:receive(mode or '*l') 179 | if not result then 180 | assert(errstate ~= 'closed', ('Connection to %s:%u closed unexpectedly'):format(self.host, self.port)) 181 | assert(#partial > 0, ('Connection to %s:%u timed out'):format(self.host, self.port)) 182 | r[#r+1] = partial 183 | end 184 | r[#r+1] = result -- does nothing if result is nil 185 | until result 186 | return table.concat(r) 187 | end 188 | 189 | -- invokes a tagged command and returns response blocks 190 | function IMAP:_do_cmd(cmd, ...) 191 | --assert(self.socket, 'Connection closed') 192 | local token = self:next_token() 193 | 194 | -- send request 195 | local data = token .. ' ' .. cmd:format(...) .. '\r\n' 196 | local len = assert(self.socket:send(data)) 197 | assert(len == #data, 'Broken connection: Could not send all required data') 198 | 199 | -- receive answer line by line and pack into blocks 200 | local blocks = {} 201 | local literal_bytes = 0 202 | while true do 203 | -- return if there was a tagged response 204 | if literal_bytes > 0 then 205 | blocks[#blocks] = blocks[#blocks] .. '\r\n' .. self:_receive(literal_bytes) 206 | literal_bytes = 0 207 | end 208 | 209 | local line = self:_receive() 210 | local status, msg = line:match('^'..token..' ([A-Z]+) (.*)$') 211 | if status == 'OK' then 212 | break 213 | elseif status == 'NO' or status == 'BAD' then 214 | error(("Command `%s' failed: %s"):format(cmd:format(...), msg), 3) 215 | end 216 | 217 | local firstchar = line:sub(1,1) 218 | if firstchar == '*' then 219 | blocks[#blocks+1] = line:sub(3) 220 | literal_bytes = tonumber(line:match('{(%d+)}$')) or 0 221 | elseif firstchar ~= '+' and #line > 0 then 222 | blocks[#blocks] = blocks[#blocks] .. '\r\n' .. line 223 | end 224 | end 225 | 226 | -- transform blocks into response table: 227 | -- { TOKEN1 = {arg1.1, arg1.2, ...}, TOKEN2 = {arg2.1, arg2.2, ...} } 228 | local res = setmetatable({}, {__index = function(t,k) local s = {}; rawset(t,k,s); return s; end}) 229 | for i = 1,#blocks do 230 | local token, args = blocks[i]:match('^(%S+) (.*)$') 231 | if tonumber(token) ~= nil then 232 | local n = token 233 | token, args = args:match('^(%S+)%s*(.*)$') 234 | args = n .. ' ' .. args 235 | end 236 | if not token then token = blocks[i] end 237 | local t = res[token] 238 | t[#t+1] = args 239 | end 240 | return res 241 | end 242 | 243 | -- any state 244 | 245 | -- returns table with server capabilities 246 | function IMAP:capability() 247 | local cap = {} 248 | local res = self:_do_cmd('CAPABILITY') 249 | for w in table.concat(res.CAPABILITY, ' '):gmatch('%S+') do 250 | cap[#cap+1] = w 251 | cap[w] = true 252 | end 253 | return cap, res 254 | end 255 | 256 | -- test if server is capable of *all* listed arguments 257 | function IMAP:isCapable(...) 258 | local cap = self:capability() 259 | for _,v in ipairs{...} do 260 | if not cap[v] then return false end 261 | end 262 | return true 263 | end 264 | 265 | -- does nothing, but may receive updated state 266 | function IMAP:noop() 267 | return self:_do_cmd('NOOP') 268 | end 269 | 270 | function IMAP:logout() 271 | local res = self:_do_cmd('LOGOUT') 272 | self.socket:close() 273 | return res 274 | end 275 | 276 | -- start TLS connection. requires luasec. see luasec documentation for 277 | -- infos on what tls_params should be. 278 | function IMAP:starttls(tls_params) 279 | assert(self:isCapable('STARTTLS')) 280 | local res = self:_do_cmd('STARTTLS') 281 | self:enabletls(tls_params) 282 | return res 283 | end 284 | 285 | function IMAP:authenticate() 286 | error('Not implemented') 287 | end 288 | 289 | -- plain text login. do not use unless connection is secure (i.e. TLS or SSH tunnel) 290 | function IMAP:login(user, pass) 291 | local res = self:_do_cmd('LOGIN %s %s', user, pass) 292 | return res 293 | end 294 | 295 | -- authenticated state 296 | -- select and examine get the same results 297 | local function parse_select_examine(res) 298 | return { 299 | flags = to_table(res.FLAGS[1] or "()"), 300 | exist = tonumber(res.EXISTS[1]), 301 | recent = tonumber(res.RECENT[1]) 302 | } 303 | end 304 | 305 | -- select a mailbox so that messages in the mailbox can be accessed 306 | -- returns a table of the following format: 307 | -- { flags = {string...}, exist = number, recent = number} 308 | function IMAP:select(mailbox) 309 | -- if this fails we go back to authenticated state 310 | local res = self:_do_cmd('SELECT %s', mailbox) 311 | return parse_select_examine(res), res 312 | end 313 | 314 | -- same as IMAP:select, except that the mailbox is set to read-only 315 | function IMAP:examine(mailbox) 316 | local res = self:_do_cmd('SELECT %s', mailbox) 317 | return parse_select_examine(res), res 318 | end 319 | 320 | -- create a new mailbox 321 | function IMAP:create(mailbox) 322 | return self:_do_cmd('CREATE %s', mailbox) 323 | end 324 | 325 | -- delete an existing mailbox 326 | function IMAP:delete(mailbox) 327 | return self:_do_cmd('DELETE %s', mailbox) 328 | end 329 | 330 | -- renames a mailbox 331 | function IMAP:rename(from, to) 332 | return self:_do_cmd('RENAME %s %s', from, to) 333 | end 334 | 335 | -- marks mailbox as subscribed 336 | -- subscribed mailboxes will be listed with the lsub command 337 | function IMAP:subscribe(mailbox) 338 | return self:_do_cmd('SUBSCRIBE %s', mailbox) 339 | end 340 | 341 | -- unsubscribe a mailbox 342 | function IMAP:unsubscribe(mailbox) 343 | return self:_do_cmd('UNSUBSCRIBE %s', mailbox) 344 | end 345 | 346 | -- parse response from IMAP:list() and IMAP:lsub() 347 | local function parse_list_lsub(res, token) 348 | local mailboxes = {} 349 | for _,r in ipairs(res[token]) do 350 | local flags, delim, name = r:match('^(%b()) (%b"") (.+)$') 351 | flags = to_table(flags) 352 | for _,f in ipairs(flags) do 353 | flags[f:sub(2)] = true 354 | end 355 | 356 | if name:sub(1,1) == '"' and name:sub(-1) == '"' then 357 | name = name:sub(2,-2) 358 | end 359 | mailboxes[name] = {delim = delim:sub(2,-2), flags = flags} 360 | end 361 | return mailboxes 362 | end 363 | 364 | -- list mailboxes, where `mailbox' is a mailbox name with possible 365 | -- wildcards and `ref' is a reference name. Default parameters are: 366 | -- mailbox = '*' (match all) and ref = '""' (no reference name) 367 | -- See RFC3501 Sec 6.3.8 for details. 368 | function IMAP:list(mailbox, ref) 369 | mailbox = mailbox or '*' 370 | ref = ref or '""' 371 | local res = self:_do_cmd('LIST %s %s', ref, mailbox) 372 | return parse_list_lsub(res, 'LIST'), res 373 | end 374 | 375 | -- same as IMAP:list(), but lists only subscribed or active mailboxes. 376 | function IMAP:lsub(mailbox, ref) 377 | mailbox = mailbox or "*" 378 | ref = ref or '""' 379 | local res = self:_do_cmd('LSUB %s %s', ref, mailbox) 380 | return parse_list_lsub(res, 'LSUB'), res 381 | end 382 | 383 | -- get mailbox information. `status' may be a string or a table of strings 384 | -- as defined by RFC3501 Sec 6.3.10: 385 | -- MESSAGES, RECENT, UIDNEXT, UIDVALIDITY and UNSEEN 386 | function IMAP:status(mailbox, names) 387 | assert_arg(1, mailbox).type('string') 388 | assert_arg(2, names).type('string', 'table', 'nil') 389 | 390 | names = to_list(names or '(MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN)') 391 | local res = self:_do_cmd('STATUS %s %s', mailbox, names) 392 | 393 | local list = to_table(assert(res.STATUS[1]:match('(%b())%s*$'), 'Invalid response')) 394 | assert(#list % 2 == 0, "Invalid response size") 395 | 396 | local status = {} 397 | for i = 1,#list,2 do 398 | status[list[i]] = tonumber(list[i+1]) 399 | end 400 | return status, res 401 | end 402 | 403 | -- append a message to a mailbox 404 | function IMAP:append(mailbox, message, flags, date) 405 | assert_arg(1, mailbox).type('string') 406 | assert_arg(2, message).type('string') 407 | assert_arg(3, flags).type('table', 'string', 'nil') 408 | assert_arg(4, date).type('string', 'nil') 409 | 410 | message = ('{%d}\r\n%s'):format(#message, message) -- message literal 411 | flags = flags and ' ' .. to_list(flags) or '' 412 | date = date and ' ' .. date or '' 413 | 414 | return self:_do_cmd('APPEND %s%s%s %s', mailbox, flags, date, message) 415 | end 416 | 417 | -- requests a checkpoint of the currently selected mailbox 418 | function IMAP:check() 419 | return self:_do_cmd('CHECK') 420 | end 421 | 422 | -- permanently removes all messages with \Deleted flag from currently 423 | -- selected mailbox without giving responses. return to 424 | -- 'authenticated' state. 425 | function IMAP:close() 426 | local res = self:_do_cmd('CLOSE') 427 | return res 428 | end 429 | 430 | -- permanently removes all messages with \Deleted flag from currently 431 | -- selected mailbox. returns a table of deleted message numbers/ids. 432 | function IMAP:expunge() 433 | local res = self:_do_cmd('EXPUNGE') 434 | return res.EXPUNGE, res 435 | end 436 | 437 | -- searches the mailbox for messages that match the given searching criteria 438 | -- See RFC3501 Sec 6.4.4 for details 439 | function IMAP:search(criteria, charset, uid) 440 | assert_arg(1, criteria).type('string', 'table') 441 | assert_arg(2, charset).type('string', 'nil') 442 | 443 | charset = charset and 'CHARSET ' .. charset or '' 444 | criteria = to_list(criteria) 445 | uid = uid and 'UID ' or '' 446 | 447 | local res = self:_do_cmd('%sSEARCH %s %s', uid, charset, criteria) 448 | local ids = {} 449 | for id in res.SEARCH[1]:gmatch('%S+') do 450 | ids[#ids+1] = tonumber(id) 451 | end 452 | return ids, res 453 | end 454 | 455 | -- parses response to fetch() and store() commands 456 | local function parse_fetch(res) 457 | local messages = {} 458 | for _, m in ipairs(res.FETCH) do 459 | local id, list = m:match("^(%d+) (.*)$") 460 | list = to_table(list) 461 | local msg = {id = id} 462 | local i = 1 463 | while i < #list do 464 | local key = list[i] 465 | local value = list[i+1] 466 | if key == 'BODY' then 467 | value = { 468 | parts = (type(value) == 'string') and to_table('('..value..')') or value, 469 | value = list[i+2] 470 | } 471 | i = i + 1 472 | end 473 | msg[key] = value 474 | msg[#msg+1] = key 475 | i = i + 2 476 | end 477 | messages[#messages+1] = msg 478 | end 479 | return messages 480 | end 481 | 482 | function IMAP:fetch(what, sequence, uid) 483 | assert_arg(1, what).type('string', 'table', 'nil') 484 | assert_arg(2, sequence).type('string', 'nil') 485 | 486 | what = to_list(what or '(UID BODY[HEADER.FIELDS (DATE FROM SUBJECT)])') 487 | sequence = sequence and tostring(sequence) or '1:*' 488 | uid = uid and 'UID ' or '' 489 | 490 | local res = self:_do_cmd('%sFETCH %s %s', uid, sequence, what) 491 | return parse_fetch(res), res 492 | end 493 | 494 | function IMAP:store(mode, flags, sequence, silent, uid) 495 | assert_arg(1, mode).any('set', '+', '-') 496 | assert_arg(2, flags).type('string', 'table') 497 | assert_arg(3, sequence).type('string', 'number') 498 | 499 | mode = mode == 'set' and '' or mode 500 | flags = to_list(flags) 501 | sequence = tostring(sequence) 502 | silent = silent and '.SILENT' or '' 503 | uid = uid and 'UID ' or '' 504 | 505 | local res = self:_do_cmd('%sSTORE %s %sFLAGS%s %s', uid, sequence, mode, silent, flags) 506 | return parse_fetch(res), res 507 | end 508 | 509 | function IMAP:copy(sequence, mailbox, uid) 510 | assert_arg(1, sequence).type('string', 'number') 511 | assert_arg(2, mailbox).type('string') 512 | 513 | sequence = tostring(sequence) 514 | uid = uid and 'UID ' or '' 515 | return self:_do_cmd('%sCOPY %s %s', uid, sequence, mailbox) 516 | end 517 | 518 | -- utility library 519 | IMAP.util = {} 520 | 521 | -- transforms t = {k1, v1, k2, v2, ...} to r = {[k1] = v1, [k2] = v2, ...} 522 | function IMAP.util.collapse_list(t) 523 | if t == 'NIL' then return {} end 524 | local r = {} 525 | for i = 1,#t,2 do 526 | r[t[i]] = t[i+1] 527 | end 528 | return r 529 | end 530 | 531 | -- transforms bodystructure response in a more usable format 532 | function IMAP.util.get_bodystructure(t, part) 533 | local function tnil(s) return s == 'NIL' and nil or s end 534 | 535 | local r = {part = part} 536 | 537 | if type(t[1]) == 'table' then 538 | local i = 1 539 | while type(t[i]) == 'table' do 540 | r[i] = IMAP.util.get_bodystructure(t[i], (part and part .. '.' or '') .. i) 541 | i = i + 1 542 | end 543 | r.type = t[i] 544 | r.params = IMAP.util.collapse_list(t[i+1]) 545 | r.disposition = tnil(t[i+2]) 546 | r.language = tnil(t[i+3]) 547 | r.location = tnil(t[i+4]) 548 | return r 549 | end 550 | 551 | r.type = t[1] 552 | r.subtype = t[2] 553 | r.params = IMAP.util.collapse_list(t[3]) 554 | r.id = tnil(t[4]) 555 | r.description = tnil(t[5]) 556 | r.encoding = tnil(t[6]) 557 | r.size = tonumber(t[7]) 558 | 559 | local line_field = 8 560 | if r.type:lower() == 'message' and r.subtype:lower() == 'rfc822' then 561 | r.envelope = tnil(t[8]) 562 | r.body = tnil(t[9]) 563 | line_field = 10 564 | end 565 | 566 | r.lines = tonumber(t[line_field]) 567 | r.md5 = tnil(t[line_field + 1]) 568 | r.disposition = tnil(t[line_field + 2]) 569 | r.language = tnil(t[line_field + 3]) 570 | r.location = tnil(t[line_field + 4]) 571 | 572 | return r 573 | end 574 | 575 | return setmetatable(IMAP, {__call = function(_, ...) return IMAP.new(...) end}) 576 | --------------------------------------------------------------------------------