1 -----------------------------------------------------------------------------
2 -- SMTP client support for the Lua language.
5 -----------------------------------------------------------------------------
7 -----------------------------------------------------------------------------
8 -- Declare module and import dependencies
9 -----------------------------------------------------------------------------
11 local coroutine
= require("coroutine")
12 local string = require("string")
13 local math
= require("math")
14 local os
= require("os")
15 local socket
= require("socket")
16 local tp
= require("socket.tp")
17 local ltn12
= require("ltn12")
18 local headers
= require("socket.headers")
19 local mime
= require("mime")
22 -----------------------------------------------------------------------------
24 -----------------------------------------------------------------------------
25 -- timeout for connection
27 -- default server used to send e-mails
31 -- domain used in HELO command and default sendmail
32 -- If we are under a CGI, try to get from environment
33 DOMAIN
= os
.getenv("SERVER_NAME") or "localhost"
34 -- default time zone (means we don't know)
37 ---------------------------------------------------------------------------
39 -----------------------------------------------------------------------------
40 local metat
= { __index
= {} }
42 function metat
.__index
:greet(domain
)
43 self
.try(self
.tp
:check("2.."))
44 self
.try(self
.tp
:command("EHLO", domain
or DOMAIN
))
45 return socket
.skip(1, self
.try(self
.tp
:check("2..")))
48 function metat
.__index
:mail(from
)
49 self
.try(self
.tp
:command("MAIL", "FROM:" .. from
))
50 return self
.try(self
.tp
:check("2.."))
53 function metat
.__index
:rcpt(to
)
54 self
.try(self
.tp
:command("RCPT", "TO:" .. to
))
55 return self
.try(self
.tp
:check("2.."))
58 function metat
.__index
:data(src
, step
)
59 self
.try(self
.tp
:command("DATA"))
60 self
.try(self
.tp
:check("3.."))
61 self
.try(self
.tp
:source(src
, step
))
62 self
.try(self
.tp
:send("\r\n.\r\n"))
63 return self
.try(self
.tp
:check("2.."))
66 function metat
.__index
:quit()
67 self
.try(self
.tp
:command("QUIT"))
68 return self
.try(self
.tp
:check("2.."))
71 function metat
.__index
:close()
72 return self
.tp
:close()
75 function metat
.__index
:login(user
, password
)
76 self
.try(self
.tp
:command("AUTH", "LOGIN"))
77 self
.try(self
.tp
:check("3.."))
78 self
.try(self
.tp
:send(mime
.b64(user
) .. "\r\n"))
79 self
.try(self
.tp
:check("3.."))
80 self
.try(self
.tp
:send(mime
.b64(password
) .. "\r\n"))
81 return self
.try(self
.tp
:check("2.."))
84 function metat
.__index
:plain(user
, password
)
85 local auth
= "PLAIN " .. mime
.b64("\0" .. user
.. "\0" .. password
)
86 self
.try(self
.tp
:command("AUTH", auth
))
87 return self
.try(self
.tp
:check("2.."))
90 function metat
.__index
:auth(user
, password
, ext
)
91 if not user
or not password
then return 1 end
92 if string.find(ext
, "AUTH[^\n]+LOGIN") then
93 return self
:login(user
, password
)
94 elseif string.find(ext
, "AUTH[^\n]+PLAIN") then
95 return self
:plain(user
, password
)
97 self
.try(nil, "authentication not supported")
101 -- send message or throw an exception
102 function metat
.__index
:send(mailt
)
103 self
:mail(mailt
.from
)
104 if base
.type(mailt
.rcpt
) == "table" then
105 for i
,v
in base
.ipairs(mailt
.rcpt
) do
109 self
:rcpt(mailt
.rcpt
)
111 self
:data(ltn12
.source
.chain(mailt
.source
, mime
.stuff()), mailt
.step
)
114 function open(server
, port
, create
)
115 local tp
= socket
.try(tp
.connect(server
or SERVER
, port
or PORT
,
117 local s
= base
.setmetatable({tp
= tp
}, metat
)
118 -- make sure tp is closed if we get an exception
119 s
.try
= socket
.newtry(function()
125 -- convert headers to lowercase
126 local function lower_headers(headers
)
128 for i
,v
in base
.pairs(headers
or lower
) do
129 lower
[string.lower(i
)] = v
134 ---------------------------------------------------------------------------
135 -- Multipart message source
136 -----------------------------------------------------------------------------
137 -- returns a hopefully unique mime boundary
139 local function newboundary()
141 return string.format('%s%05d==%05u', os
.date('%d%m%Y%H%M%S'),
142 math
.random(0, 99999), seqno
)
145 -- send_message forward declaration
148 -- yield the headers all at once, it's faster
149 local function send_headers(tosend
)
150 local canonic
= headers
.canonic
152 for f
,v
in base
.pairs(tosend
) do
153 h
= (canonic
[f
] or f
) .. ': ' .. v
.. "\r\n" .. h
158 -- yield multipart message body from a multipart message table
159 local function send_multipart(mesgt
)
160 -- make sure we have our boundary and send headers
161 local bd
= newboundary()
162 local headers
= lower_headers(mesgt
.headers
or {})
163 headers
['content-type'] = headers
['content-type'] or 'multipart/mixed'
164 headers
['content-type'] = headers
['content-type'] ..
165 '; boundary="' .. bd
.. '"'
166 send_headers(headers
)
168 if mesgt
.body
.preamble
then
169 coroutine
.yield(mesgt
.body
.preamble
)
170 coroutine
.yield("\r\n")
172 -- send each part separated by a boundary
173 for i
, m
in base
.ipairs(mesgt
.body
) do
174 coroutine
.yield("\r\n--" .. bd
.. "\r\n")
177 -- send last boundary
178 coroutine
.yield("\r\n--" .. bd
.. "--\r\n\r\n")
180 if mesgt
.body
.epilogue
then
181 coroutine
.yield(mesgt
.body
.epilogue
)
182 coroutine
.yield("\r\n")
186 -- yield message body from a source
187 local function send_source(mesgt
)
188 -- make sure we have a content-type
189 local headers
= lower_headers(mesgt
.headers
or {})
190 headers
['content-type'] = headers
['content-type'] or
191 'text/plain; charset="iso-8859-1"'
192 send_headers(headers
)
193 -- send body from source
195 local chunk
, err
= mesgt
.body()
196 if err
then coroutine
.yield(nil, err
)
197 elseif chunk
then coroutine
.yield(chunk
)
202 -- yield message body from a string
203 local function send_string(mesgt
)
204 -- make sure we have a content-type
205 local headers
= lower_headers(mesgt
.headers
or {})
206 headers
['content-type'] = headers
['content-type'] or
207 'text/plain; charset="iso-8859-1"'
208 send_headers(headers
)
209 -- send body from string
210 coroutine
.yield(mesgt
.body
)
214 function send_message(mesgt
)
215 if base
.type(mesgt
.body
) == "table" then send_multipart(mesgt
)
216 elseif base
.type(mesgt
.body
) == "function" then send_source(mesgt
)
217 else send_string(mesgt
) end
220 -- set defaul headers
221 local function adjust_headers(mesgt
)
222 local lower
= lower_headers(mesgt
.headers
)
223 lower
["date"] = lower
["date"] or
224 os
.date("!%a, %d %b %Y %H:%M:%S ") .. (mesgt
.zone
or ZONE
)
225 lower
["x-mailer"] = lower
["x-mailer"] or socket
._VERSION
226 -- this can't be overriden
227 lower
["mime-version"] = "1.0"
231 function message(mesgt
)
232 mesgt
.headers
= adjust_headers(mesgt
)
233 -- create and return message source
234 local co
= coroutine
.create(function() send_message(mesgt
) end)
236 local ret
, a
, b
= coroutine
.resume(co
)
237 if ret
then return a
, b
238 else return nil, a
end
242 ---------------------------------------------------------------------------
243 -- High level SMTP API
244 -----------------------------------------------------------------------------
245 send
= socket
.protect(function(mailt
)
246 local s
= open(mailt
.server
, mailt
.port
, mailt
.create
)
247 local ext
= s
:greet(mailt
.domain
)
248 s
:auth(mailt
.user
, mailt
.password
, ext
)