# signature { license=0, seed=457995617, key="d38c6a4c5f492d51a009d8b0f865036c" }
-- 
-- POP3 support for the Lua language.
-- 
-- Author: Guilherme Martins, Oliver Kroth
-- Conforming to: RFC 1939
-- 

local format, sub, match = string.format, string.sub, string.match
local concat, insert = table.concat, table.insert

local pop3 = 
{
	TIMEOUT = 60;	-- seconds before give up on a connection.
	PORT = 110;		-- default port for pop3 service.
	_VERSION = "pop3 1.3.2"
}

-- 
-- sends a command to the pop3 server
-- return the response of this command
-- 
local function cmd( conn, command, ...)
	-- send the command
	local ok, errMsg = conn:send( format( command, ...))
	if ok then ok, errMsg = conn:receive() end
	
	-- in case of error close the connection and returns nil plus error
	if not ok then 
		conn:close();
		return nil, errMsg
	end

	-- returns the response
	if sub( ok, 1, 3) == "+OK" then return ok end
	return nil, ok
end

--
-- fetch data, line by line
-- stop at a line that is "." (full stop)
-- call function f() with each line
--
local function get_data( conn, f )
	while true do
		local line = conn:receive()
		if line == "." then break end
		-- unstuff the line
		if sub(line, 1, 1) == "." then line = sub(line, 2) end
		f( line )
	end
end

--
-- fetch data, one table entry per line and
-- then concat it
--
local function get_text( conn )
	local text = {}
	get_data( conn, function( line) insert( text, line) end )
	insert( text, "")
	return concat( text, "\r\n")
end

--
-- mailbox template
--
local mailbox = {}

function mailbox:stat( )
	local ok, err = cmd( self.conn, "STAT\r\n")
	if not ok then return nil, err end

	local n, size = match( ok, "+OK (%d+) (%d+)")
	return tonumber( n), tonumber( size)	
end

function mailbox:dele( msg ) return cmd( self.conn, "DELE %d\r\n", msg) end
function mailbox:rset( )	 return cmd( self.conn, "RSET\r\n") end
function mailbox:noop( )	 return cmd( self.conn, "NOOP\r\n") end
function mailbox:quit( )	 return cmd( self.conn, "QUIT\r\n") end

function mailbox:retr( msg )
	local ok, err = cmd( self.conn, "RETR %d\r\n", msg)
	if not ok then return nil, err end
	return get_text( self.conn )
end

function mailbox:list( msg )
	msg = tonumber(msg)
	if msg then
		local resp, err = cmd( self.conn, "LIST %d\r\n", msg)
		return match( resp, "+OK (%d+) (%d+)")
	else
		local resp, err = cmd( self.conn, "LIST\r\n")
		if not resp then return nil, err end
		local data = {}
		get_data(self.conn, function ( line ) 
									   local k, v = match(line, "(%d+) (%d+)")
									   data[ tonumber(k) ] = tonumber(v)
								   end)
		return data								  
	end
end

function mailbox:uidl( msg )
	msg = tonumber(msg)
	if msg then
		local resp, err = cmd( self.conn, "UIDL %d\r\n", msg)
		return match( resp, "+OK (%d+) (%S+)")
	else
		local resp, err = cmd( self.conn, "UIDL\r\n")
		if not resp then return nil, err end
		local data = {}
		return get_data(self.conn, function ( line ) 
									   local k, v = match(line, "(%d+) (%S+)")
									   data[ tonumber(k) ] = v
								   end)
	end
end

function mailbox:top( msg, lines )
	local ok, err = cmd( self.conn, "TOP %d %d\r\n", msg, lines)
	if not ok then return nil, err end

	return get_text(self.conn)
end

function mailbox:messages( lines )
	local i=0
	local n = self:stat()
	local msg
	
	return function ()
			i=i+1
			if i > n then return nil end
			if lines then
				msg = self:top(i, lines)
			else
				msg = self:retr(i)
			end
			return i, msg 
		end
end

function mailbox:close()
	return self.conn:close()
end

local MT = { __index = mailbox }

-- 
-- Starts a pop3 server connection and log on 
-- Returns a mbox instance
-- 
function pop3.new( conf )
	local resp, err
	local socket = require("socket")
	local create = conf.create or socket.tcp

	local mbox = {}
	setmetatable( mbox, MT )

	mbox.conn = create()
	mbox.conn:settimeout( conf.timeout or pop3.TIMEOUT )

	resp, err = mbox.conn:connect( conf.host, conf.port or pop3.PORT )
	if not resp then 
		return nil, err end

	local banner = mbox.conn:receive() or "" -- save greeting line for APOP command
	local timestamp = match( banner, "(<%S+>)")

	-- check if I can do SSL
	local trySSL, ssl = pcall( require, "ssl")
  if tostring(mbox.conn):find("^SSL") then trySSL = false end -- is already SSL/TLS
	if trySSL then
		local resp, err = cmd( mbox.conn, "CAPA\r\n")
		trySSL = false;
		if resp then
			get_data( mbox.conn, function ( line ) trySSL = trySSL or line:find("^STLS") end )
		end
		if trySSL then
			-- switch to a SSL/TLS secured session
			cmd( mbox.conn, "STLS\r\n" )
			mbox.conn = ssl.wrap( mbox.conn, 
				{
					protocol = "tlsv1",
					options  = "all",
					verify   = "none",
					mode     = "client",
				}
			);
			repeat 
				ok, msg = mbox.conn:dohandshake()
			until ok or msg~="wantread" and msg~="wantwrite"
		end
	end
	
	if conf.md5 and timestamp then -- try APOP 
		local digest = conf.md5(timestamp .. conf.pass)
		local resp, err = cmd(mbox.conn, "APOP %s %s\r\n", conf.user, digest)
		if resp then return mbox end  -- authentication succeded
	end

	cmd( mbox.conn, "USER %s\r\n", conf.user)
	resp, err = cmd( mbox.conn, "PASS %s\r\n", conf.pass)
	if resp then return mbox end	

	return nil, err 
end

-- points pop3 call to conn
setmetatable( pop3, {__call = pop3.new})		
		
return pop3

