util-soc-imp-smtp.lua /size: 7018 b    last modification: 2020-07-01 14:35
1-- original file : smtp.lua
2-- for more into : see util-soc.lua
3
4local type, setmetatable, next = type, setmetatable, next
5local find, lower, format = string.find, string.lower, string.format
6local osdate, osgetenv = os.date, os.getenv
7local random = math.random
8
9local socket           = socket         or require("socket")
10local headers          = socket.headers or require("socket.headers")
11local ltn12            = ltn12          or require("ltn12")
12local tp               = socket.tp      or require("socket.tp")
13local mime             = mime           or require("mime")
14
15local mimeb64          = mime.b64
16local mimestuff        = mime.stuff
17
18local skipsocket       = socket.skip
19local trysocket        = socket.try
20local newtrysocket     = socket.newtry
21local protectsocket    = socket.protect
22
23local normalizeheaders = headers.normalize
24local lowerheaders     = headers.lower
25
26local createcoroutine  = coroutine.create
27local resumecoroutine  = coroutine.resume
28local yieldcoroutine   = coroutine.resume
29
30local smtp = {
31    TIMEOUT = 60,
32    SERVER  = "localhost",
33    PORT    = 25,
34    DOMAIN  = osgetenv("SERVER_NAME") or "localhost",
35    ZONE    = "-0000",
36}
37
38socket.smtp = smtp
39
40local methods = { }
41local mt      = { __index = methods }
42
43function methods.greet(self, domain)
44    local try = self.try
45    local tp  = self.tp
46    try(tp:check("2.."))
47    try(tp:command("EHLO", domain or _M.DOMAIN))
48    return skipsocket(1, try(tp:check("2..")))
49end
50
51function methods.mail(self, from)
52    local try = self.try
53    local tp  = self.tp
54    try(tp:command("MAIL", "FROM:" .. from))
55    return try(tp:check("2.."))
56end
57
58function methods.rcpt(self, to)
59    local try = self.try
60    local tp  = self.tp
61    try(tp:command("RCPT", "TO:" .. to))
62    return try(tp:check("2.."))
63end
64
65function methods.data(self, src, step)
66    local try = self.try
67    local tp  = self.tp
68    try(tp:command("DATA"))
69    try(tp:check("3.."))
70    try(tp:source(src, step))
71    try(tp:send("\r\n.\r\n"))
72    return try(tp:check("2.."))
73end
74
75function methods.quit(self)
76    local try = self.try
77    local tp  = self.tp
78    try(tp:command("QUIT"))
79    return try(tp:check("2.."))
80end
81
82function methods.close(self)
83    return self.tp:close()
84end
85
86function methods.login(self, user, password)
87    local try = self.try
88    local tp  = self.tp
89    try(tp:command("AUTH", "LOGIN"))
90    try(tp:check("3.."))
91    try(tp:send(mimeb64(user) .. "\r\n"))
92    try(tp:check("3.."))
93    try(tp:send(mimeb64(password) .. "\r\n"))
94    return try(tp:check("2.."))
95end
96
97function methods.plain(self, user, password)
98    local try  = self.try
99    local tp   = self.tp
100    local auth = "PLAIN " .. mimeb64("\0" .. user .. "\0" .. password)
101    try(tp:command("AUTH", auth))
102    return try(tp:check("2.."))
103end
104
105function methods.auth(self, user, password, ext)
106    if not user or not password then
107        return 1
108    end
109    local try = self.try
110    if find(ext, "AUTH[^\n]+LOGIN") then
111        return self:login(user,password)
112    elseif find(ext, "AUTH[^\n]+PLAIN") then
113        return self:plain(user,password)
114    else
115        try(nil, "authentication not supported")
116    end
117end
118
119function methods.send(self, mail)
120    self:mail(mail.from)
121    local receipt = mail.rcpt
122    if type(receipt) == "table" then
123        for i=1,#receipt do
124            self:rcpt(receipt[i])
125        end
126    elseif receipt then
127        self:rcpt(receipt)
128    end
129    self:data(ltn12.source.chain(mail.source, mimestuff()), mail.step)
130end
131
132local function opensmtp(self, server, port, create)
133    if not server or server == "" then
134        server = smtp.SERVER
135    end
136    if not port or port == "" then
137        port = smtp.PORT
138    end
139    local s = {
140        tp  = trysocket(tp.connect(server, port, smtp.TIMEOUT, create)),
141        try = newtrysocket(function()
142            s:close()
143        end),
144    }
145    setmetatable(s, mt)
146    return s
147end
148
149smtp.open = opensmtp
150
151local nofboundaries = 0
152
153local function newboundary()
154    nofboundaries = nofboundaries + 1
155    return format('%s%05d==%05u', osdate('%d%m%Y%H%M%S'), random(0,99999), nofboundaries)
156end
157
158local send_message
159
160local function send_headers(headers)
161    yieldcoroutine(normalizeheaders(headers))
162end
163
164local function send_multipart(message)
165    local boundary = newboundary()
166    local headers  = lowerheaders(message.headers)
167    local body     = message.body
168    local preamble = body.preamble
169    local epilogue = body.epilogue
170    local content  = headers['content-type'] or 'multipart/mixed'
171    headers['content-type'] = content .. '; boundary="' ..  boundary .. '"'
172    send_headers(headers)
173    if preamble then
174        yieldcoroutine(preamble)
175        yieldcoroutine("\r\n")
176    end
177    for i=1,#body do
178        yieldcoroutine("\r\n--" .. boundary .. "\r\n")
179        send_message(body[i])
180    end
181    yieldcoroutine("\r\n--" .. boundary .. "--\r\n\r\n")
182    if epilogue then
183        yieldcoroutine(epilogue)
184        yieldcoroutine("\r\n")
185    end
186end
187
188local default_content_type = 'text/plain; charset="UTF-8"'
189
190local function send_source(message)
191    local headers = lowerheaders(message.headers)
192    if not headers['content-type'] then
193        headers['content-type'] = default_content_type
194    end
195    send_headers(headers)
196    local getchunk = message.body
197    while true do
198        local chunk, err = getchunk()
199        if err then
200            yieldcoroutine(nil, err)
201        elseif chunk then
202            yieldcoroutine(chunk)
203        else
204            break
205        end
206    end
207end
208
209local function send_string(message)
210    local headers = lowerheaders(message.headers)
211    if not headers['content-type'] then
212        headers['content-type'] = default_content_type
213    end
214    send_headers(headers)
215    yieldcoroutine(message.body)
216end
217
218function send_message(message)
219    local body = message.body
220    if type(body) == "table" then
221        send_multipart(message)
222    elseif type(body) == "function" then
223        send_source(message)
224    else
225        send_string(message)
226    end
227end
228
229local function adjust_headers(message)
230    local headers = lowerheaders(message.headers)
231    if not headers["date"] then
232        headers["date"] = osdate("!%a, %d %b %Y %H:%M:%S ") .. (message.zone or smtp.ZONE)
233    end
234    if not headers["x-mailer"] then
235        headers["x-mailer"] = socket._VERSION
236    end
237    headers["mime-version"] = "1.0"
238    return headers
239end
240
241function smtp.message(message)
242    message.headers = adjust_headers(message)
243    local action = createcoroutine(function()
244        send_message(message)
245    end)
246    return function()
247        local ret, a, b = resumecoroutine(action)
248        if ret then
249            return a, b
250        else
251            return nil, a
252        end
253    end
254end
255
256smtp.send = protectsocket(function(mail)
257    local snd = opensmtp(smtp,mail.server, mail.port, mail.create)
258    local ext = snd:greet(mail.domain)
259    snd:auth(mail.user, mail.password, ext)
260    snd:send(mail)
261    snd:quit()
262    return snd:close()
263end)
264
265package.loaded["socket.smtp"] = smtp
266
267return smtp
268