aboutsummaryrefslogtreecommitdiff
path: root/src/smtp.lua
blob: 25d7f7436012228802da6095c8dec0b68287cae2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
-- make sure LuaSocket is loaded
if not LUASOCKET_LIBNAME then error('module requires LuaSocket') end
-- get LuaSocket namespace
local socket = _G[LUASOCKET_LIBNAME] 
if not socket then error('module requires LuaSocket') end
-- create smtp namespace inside LuaSocket namespace
local smtp = {}
socket.smtp = smtp
-- make all module globals fall into smtp namespace
setmetatable(smtp, { __index = _G })
setfenv(1, smtp)

-- default port
PORT = 25 
-- domain used in HELO command and default sendmail 
-- If we are under a CGI, try to get from environment
DOMAIN = os.getenv("SERVER_NAME") or "localhost"
-- default server used to send e-mails
SERVER = "localhost"

-- tries to get a pattern from the server and closes socket on error
local function try_receiving(connection, pattern)
    local data, message = connection:receive(pattern)
    if not data then connection:close() end
    print(data)
    return data, message
end

-- tries to send data to server and closes socket on error
local function try_sending(connection, data)
    local sent, message = connection:send(data)
    if not sent then connection:close() end
    io.write(data)
    return sent, message
end

-- gets server reply
local function get_reply(connection)
    local code, current, separator, _
    local line, message = try_receiving(connection)
    local reply = line
    if message then return nil, message end
    _, _, code, separator = string.find(line, "^(%d%d%d)(.?)")
    if not code then return nil, "invalid server reply" end
    if separator == "-" then -- reply is multiline
        repeat
            line, message = try_receiving(connection)
            if message then return nil, message end
            _,_, current, separator = string.find(line, "^(%d%d%d)(.)")
            if not current or not separator then 
                return nil, "invalid server reply" 
            end
            reply = reply .. "\n" .. line
        -- reply ends with same code
        until code == current and separator == " " 
    end
    return code, reply
end

-- metatable for server connection object
local metatable = { __index = {} }

-- switch handler for execute function
local switch = {}

-- execute the "check" instruction
function switch.check(connection, instruction)
    local code, reply = get_reply(connection)
    if not code then return nil, reply end
    if type(instruction.check) == "function" then
        return instruction.check(code, reply)
    else
        if string.find(code, instruction.check) then return code, reply
        else return nil, reply end
    end
end

-- stub for invalid instructions 
function switch.invalid(connection, instruction)
    return nil, "invalid instruction"
end

-- execute the "command" instruction
function switch.command(connection, instruction)
    local line
    if instruction.argument then
        line = instruction.command .. " " .. instruction.argument .. "\r\n"
    else line = instruction.command .. "\r\n" end
    return try_sending(connection, line)
end

function switch.raw(connection, instruction)
    if type(instruction.raw) == "function" then
        local f = instruction.raw
        while true do 
            local chunk, new_f = f()
            if not chunk then return nil, new_f end
            if chunk == "" then return true end
            f = new_f or f
            local code, message = try_sending(connection, chunk)
            if not code then return nil, message end
        end
    else return try_sending(connection, instruction.raw) end
end

-- finds out what instruction are we dealing with
local function instruction_type(instruction) 
    if type(instruction) ~= "table" then return "invalid" end
    if instruction.command then return "command" end
    if instruction.check then return "check" end
    if instruction.raw then return "raw" end
    return "invalid"
end

-- execute a list of instructions
function metatable.__index:execute(instructions)
    if type(instructions) ~= "table" then error("instruction expected", 1) end
    if not instructions[1] then instructions = { instructions } end
    local code, message
    for _, instruction in ipairs(instructions) do
        local type = instruction_type(instruction)
        code, message = switch[type](self.connection, instruction)
        if not code then break end
    end
    return code, message
end

-- closes the underlying connection
function metatable.__index:close()
    self.connection:close()
end

-- connect with server and return a smtp connection object
function connect(host)
    local connection, message = socket.connect(host, PORT)
    if not connection then return nil, message end
    return setmetatable({ connection = connection }, metatable)
end

-- simple test drive 

--[[
c, m = connect("localhost")
assert(c, m)
assert(c:execute {check = "2.." })
assert(c:execute {{command = "EHLO", argument = "localhost"}, {check = "2.."}})
assert(c:execute {command = "MAIL", argument = "FROM:<diego@princeton.edu>"})
assert(c:execute {check = "2.."})
assert(c:execute {command = "RCPT", argument = "TO:<diego@cs.princeton.edu>"})
assert(c:execute {check = function (code) return code == "250" end})
assert(c:execute {{command = "DATA"}, {check = "3.."}})
assert(c:execute {{raw = "This is the message\r\n.\r\n"}, {check = "2.."}})
assert(c:execute {{command = "QUIT"}, {check = "2.."}})
c:close()
]]

return smtp