aboutsummaryrefslogtreecommitdiff
path: root/src/smtp.lua
diff options
context:
space:
mode:
Diffstat (limited to 'src/smtp.lua')
-rw-r--r--src/smtp.lua138
1 files changed, 90 insertions, 48 deletions
diff --git a/src/smtp.lua b/src/smtp.lua
index 0bebce3..d256388 100644
--- a/src/smtp.lua
+++ b/src/smtp.lua
@@ -10,23 +10,27 @@ socket.smtp = smtp
10setmetatable(smtp, { __index = _G }) 10setmetatable(smtp, { __index = _G })
11setfenv(1, smtp) 11setfenv(1, smtp)
12 12
13-- default server used to send e-mails
14SERVER = "localhost"
13-- default port 15-- default port
14PORT = 25 16PORT = 25
15-- domain used in HELO command and default sendmail 17-- domain used in HELO command and default sendmail
16-- If we are under a CGI, try to get from environment 18-- If we are under a CGI, try to get from environment
17DOMAIN = os.getenv("SERVER_NAME") or "localhost" 19DOMAIN = os.getenv("SERVER_NAME") or "localhost"
18-- default server used to send e-mails 20-- default time zone (means we don't know)
19SERVER = "localhost" 21ZONE = "-0000"
20 22
21function stuff() 23function stuff()
22 return ltn12.filter.cycle(dot, 2) 24 return ltn12.filter.cycle(dot, 2)
23end 25end
24 26
25local function skip(a, b, c) 27local function shift(a, b, c)
26 return b, c 28 return b, c
27end 29end
28 30
31-- send message or throw an exception
29function psend(control, mailt) 32function psend(control, mailt)
33 socket.try(control:check("2.."))
30 socket.try(control:command("EHLO", mailt.domain or DOMAIN)) 34 socket.try(control:command("EHLO", mailt.domain or DOMAIN))
31 socket.try(control:check("2..")) 35 socket.try(control:check("2.."))
32 socket.try(control:command("MAIL", "FROM:" .. mailt.from)) 36 socket.try(control:command("MAIL", "FROM:" .. mailt.from))
@@ -34,11 +38,12 @@ function psend(control, mailt)
34 if type(mailt.rcpt) == "table" then 38 if type(mailt.rcpt) == "table" then
35 for i,v in ipairs(mailt.rcpt) do 39 for i,v in ipairs(mailt.rcpt) do
36 socket.try(control:command("RCPT", "TO:" .. v)) 40 socket.try(control:command("RCPT", "TO:" .. v))
41 socket.try(control:check("2.."))
37 end 42 end
38 else 43 else
39 socket.try(control:command("RCPT", "TO:" .. mailt.rcpt)) 44 socket.try(control:command("RCPT", "TO:" .. mailt.rcpt))
45 socket.try(control:check("2.."))
40 end 46 end
41 socket.try(control:check("2.."))
42 socket.try(control:command("DATA")) 47 socket.try(control:command("DATA"))
43 socket.try(control:check("3..")) 48 socket.try(control:check("3.."))
44 socket.try(control:source(ltn12.source.chain(mailt.source, stuff()))) 49 socket.try(control:source(ltn12.source.chain(mailt.source, stuff())))
@@ -48,6 +53,7 @@ function psend(control, mailt)
48 socket.try(control:check("2..")) 53 socket.try(control:check("2.."))
49end 54end
50 55
56-- returns a hopefully unique mime boundary
51local seqno = 0 57local seqno = 0
52local function newboundary() 58local function newboundary()
53 seqno = seqno + 1 59 seqno = seqno + 1
@@ -55,62 +61,98 @@ local function newboundary()
55 math.random(0, 99999), seqno) 61 math.random(0, 99999), seqno)
56end 62end
57 63
58local function sendmessage(mesgt) 64-- sendmessage forward declaration
59 -- send headers 65local sendmessage
66
67-- yield multipart message body from a multipart message table
68local function sendmultipart(mesgt)
69 local bd = newboundary()
70 -- define boundary and finish headers
71 coroutine.yield('content-type: multipart/mixed; boundary="' ..
72 bd .. '"\r\n\r\n')
73 -- send preamble
74 if mesgt.body.preamble then coroutine.yield(mesgt.body.preamble) end
75 -- send each part separated by a boundary
76 for i, m in ipairs(mesgt.body) do
77 coroutine.yield("\r\n--" .. bd .. "\r\n")
78 sendmessage(m)
79 end
80 -- send last boundary
81 coroutine.yield("\r\n--" .. bd .. "--\r\n\r\n")
82 -- send epilogue
83 if mesgt.body.epilogue then coroutine.yield(mesgt.body.epilogue) end
84end
85
86-- yield message body from a source
87local function sendsource(mesgt)
88 -- set content-type if user didn't override
89 if not mesgt.headers or not mesgt.headers["content-type"] then
90 coroutine.yield('content-type: text/plain; charset="iso-88591"\r\n')
91 end
92 -- finish headers
93 coroutine.yield("\r\n")
94 -- send body from source
95 while true do
96 local chunk, err = mesgt.body()
97 if err then coroutine.yield(nil, err)
98 elseif chunk then coroutine.yield(chunk)
99 else break end
100 end
101end
102
103-- yield message body from a string
104local function sendstring(mesgt)
105 -- set content-type if user didn't override
106 if not mesgt.headers or not mesgt.headers["content-type"] then
107 coroutine.yield('content-type: text/plain; charset="iso-88591"\r\n')
108 end
109 -- finish headers
110 coroutine.yield("\r\n")
111 -- send body from string
112 coroutine.yield(mesgt.body)
113
114end
115
116-- yield the headers one by one
117local function sendheaders(mesgt)
60 if mesgt.headers then 118 if mesgt.headers then
61 for i,v in pairs(mesgt.headers) do 119 for i,v in pairs(mesgt.headers) do
62 coroutine.yield(i .. ':' .. v .. "\r\n") 120 coroutine.yield(i .. ':' .. v .. "\r\n")
63 end 121 end
64 end 122 end
65 -- deal with multipart 123end
66 if type(mesgt.body) == "table" then 124
67 local bd = newboundary() 125-- message source
68 -- define boundary and finish headers 126function sendmessage(mesgt)
69 coroutine.yield('mime-version: 1.0\r\n') 127 sendheaders(mesgt)
70 coroutine.yield('content-type: multipart/mixed; boundary="' .. 128 if type(mesgt.body) == "table" then sendmultipart(mesgt)
71 bd .. '"\r\n\r\n') 129 elseif type(mesgt.body) == "function" then sendsource(mesgt)
72 -- send preamble 130 else sendstring(mesgt) end
73 if mesgt.body.preamble then coroutine.yield(mesgt.body.preamble) end 131end
74 -- send each part separated by a boundary 132
75 for i, m in ipairs(mesgt.body) do 133-- set defaul headers
76 coroutine.yield("\r\n--" .. bd .. "\r\n") 134local function adjustheaders(mesgt)
77 sendmessage(m) 135 mesgt.headers = mesgt.headers or {}
78 end 136 mesgt.headers["mime-version"] = "1.0"
79 -- send last boundary 137 mesgt.headers["date"] = mesgt.headers["date"] or
80 coroutine.yield("\r\n--" .. bd .. "--\r\n\r\n") 138 os.date("%a, %d %b %Y %H:%M:%S") .. (mesgt.zone or ZONE)
81 -- send epilogue 139 mesgt.headers["x-mailer"] = mesgt.headers["x-mailer"] or socket.version
82 if mesgt.body.epilogue then coroutine.yield(mesgt.body.epilogue) end
83 -- deal with a source
84 elseif type(mesgt.body) == "function" then
85 -- finish headers
86 coroutine.yield("\r\n")
87 while true do
88 local chunk, err = mesgt.body()
89 if err then return nil, err
90 elseif chunk then coroutine.yield(chunk)
91 else break end
92 end
93 -- deal with a simple string
94 else
95 -- finish headers
96 coroutine.yield("\r\n")
97 coroutine.yield(mesgt.body)
98 end
99end 140end
100 141
101function message(mesgt) 142function message(mesgt)
143 adjustheaders(mesgt)
144 -- create and return message source
102 local co = coroutine.create(function() sendmessage(mesgt) end) 145 local co = coroutine.create(function() sendmessage(mesgt) end)
103 return function() return skip(coroutine.resume(co)) end 146 return function() return shift(coroutine.resume(co)) end
104end 147end
105 148
106function send(mailt) 149function send(mailt)
107 local control, err = socket.tp.connect(mailt.server or SERVER, 150 local c, e = socket.tp.connect(mailt.server or SERVER, mailt.port or PORT)
108 mailt.port or PORT) 151 if not c then return nil, e end
109 if not control then return nil, err end 152 local s, e = pcall(psend, c, mailt)
110 local status, err = pcall(psend, control, mailt) 153 c:close()
111 control:close() 154 if s then return true
112 if status then return true 155 else return nil, e end
113 else return nil, err end
114end 156end
115 157
116return smtp 158return smtp