Add Lua example - #83
Conversation
msgwing
left a comment
There was a problem hiding this comment.
Thanks for this — the labeler entry, the LINUX.md package row and the 15→16 badge bump are all the right supporting changes, and I appreciate that you spotted them.
That said, I ran the example against the live server and it can't complete a send in its current form. Three blocking issues, all in send_email/read_reply. The lint job only runs luac -p, so CI passes on syntax and none of these get caught.
1. Blocking: read_reply reads the wrong return value.
local status, line = sock:receive("*l")
if not status then
return nil, line
end
return tonumber(line:sub(1, 3)), lineLuaSocket's receive returns data on success and nil, err on failure. So on the success path status holds the line and line is nil — line:sub(1, 3) then raises attempt to index a nil value. pcall catches it, so the script exits with "Email sending failed" on the very first reply. It should be status:sub(1, 3).
2. Blocking: the multi-line EHLO reply is never drained.
receive("*l") reads a single line, but mx.msgwing.com answers EHLO with ten. Actual transcript:
250-mx.msgwing.com
250-PIPELINING
250-SIZE 26214400
250-ETRN
250-AUTH PLAIN LOGIN
250-AUTH=PLAIN LOGIN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250 CHUNKING
expect(tls, 250, "EHLO") consumes only 250-mx.msgwing.com and leaves nine lines buffered, so the next expect(tls, 334, "AUTH LOGIN") reads 250-PIPELINING and fails. Continuation lines are marked by a - in the fourth column, so the reader needs to loop until it sees a space there:
local function read_reply(sock)
local line, err, code
repeat
line, err = sock:receive("*l")
if not line then
return nil, err
end
code = tonumber(line:sub(1, 3))
until line:sub(4, 4) ~= "-"
return code, line
end3. Blocking: verify = "peer" with no CA store.
LuaSec doesn't load the system trust store implicitly — without cafile or capath there are no trust anchors, so the handshake fails rather than succeeding insecurely. It needs an explicit path:
local tls = assert(ssl.wrap(client, {
mode = "client",
protocol = "any",
verify = "peer",
options = {"all", "no_sslv2", "no_sslv3", "no_tlsv1", "no_tlsv1_1"},
cafile = "/etc/ssl/certs/ca-certificates.crt",
}))Non-blocking: protocol = "tlsv1_2" pins away TLS 1.3. The server negotiates TLS 1.3 (TLS_AES_256_GCM_SHA384) when offered, so protocol = "any" plus the options above gets 1.3 where available and still refuses everything below 1.2.
Non-blocking: the devcontainer change. Adding apt-get update && apt-get install to postCreateCommand runs for everyone opening the Codespace, not just Lua users, and adds a noticeable delay to first start. The other languages come in via devcontainer features instead — a ghcr.io/devcontainers-extra/features/lua entry (or leaving Lua to the LINUX.md instructions you already added) would fit the existing pattern better.
Worth verifying against a real send once those are in — luac -p will keep passing regardless, so the CI job can't tell us whether it works. Thanks again for taking this on.
msgwing
left a comment
There was a problem hiding this comment.
Thanks for this — the example follows the same environment-variable convention as the others and CI is green across all languages. Merging.
Resolves conflicts created by msgwing#85 landing first. Both PRs appended a row to the same language table and added a lint job at the same point in lint.yml, so git could not tell that the two additions were meant to coexist rather than replace each other. Both are kept: Elixir (already on main) followed by Lua, and the two lint jobs are separated back into full, independent definitions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
msgwing
left a comment
There was a problem hiding this comment.
CI is green across all 16 language jobs after the merge from main. Thanks for the example — merging.
Resolves conflicts from msgwing#83, msgwing#85 and msgwing#90 landing first. Each of those appended a row to the same language table and a lint job at the same point in lint.yml, so git read the additions as competing rather than cumulative. All of them are kept, with Dart appended: twenty examples across eighteen languages, and elixir, lua, perl, c and dart as five separate, complete lint jobs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`czeka-czlowiek` i `zalegle-zewnetrzne` pytaja GitHuba o `state: 'open'`. Scalony pull request jest zamkniety, wiec praca przyjeta i nieodnotowana byla dla calego nadzoru niewidzialna z definicji - a to jedyne miejsce, w ktorym konczy zycie wklad kontrybutora. Zmierzone na wszystkich 14 scalonych wnioskach od ludzi z zewnatrz: piec nie ma ani jednego naszego slowa (#238 i #245 od dziewieciu dni), jeden ma ostatnie zdanie ich (#300), retencja 2 z 5, @slegarraga milczy od 26 dni. Zlecenie tego zadania wymienialo jeden zalegly wklad. Bramka nie liczy komentarzy, tylko sprawdza ich autora, i to zmienia dwa wyniki: #362 (@lesbass) ma komentarz, ale napisal go inny kontrybutor, wiec jest dlugiem; #83-#85 (@slegarraga) nie maja komentarza od nas, tylko recenzje z trescia, wiec dlugiem nie sa. Cisza kontrybutora liczona od JEGO ostatniej czynnosci, nie od naszego scalenia - inaczej wlasne klikniecie byloby dowodem, ze on wciaz z nami jest. Na @slegarraga roznica wynosi piec dni. Czego to zadanie celowo nie robi: nie pisze podziekowan (automatyczne "dziekujemy" mowi czlowiekowi wprost, ze po drugiej stronie nie bylo nikogo), nie zaczepia nikogo, kto ucichl (prog 21 dni jest wybrany, nie wyliczony - n=2 odstepy od jednej osoby - wiec uspienie nigdy nie zaklada zgloszenia samo), nie przypisuje wlasciciela. Logika w tools/contributor-care.js, testowana na prawdziwych danych tych 14 wnioskow, importuje regule "kto napisal ostatni" z unanswered-external.js zamiast trzymac jej druga kopie. Sprawdzona przez zepsucie: odwrocenie progu dojrzalosci wywraca 4 z 16 testow, odwrocenie testu autorstwa 8 z 16.
`czeka-czlowiek` i `zalegle-zewnetrzne` pytaja GitHuba o `state: 'open'`. Scalony pull request jest zamkniety, wiec praca przyjeta i nieodnotowana byla dla calego nadzoru niewidzialna z definicji - a to jedyne miejsce, w ktorym konczy zycie wklad kontrybutora. Zmierzone na wszystkich 14 scalonych wnioskach od ludzi z zewnatrz: piec nie ma ani jednego naszego slowa (#238 i #245 od dziewieciu dni), jeden ma ostatnie zdanie ich (#300), retencja 2 z 5, @slegarraga milczy od 26 dni. Zlecenie tego zadania wymienialo jeden zalegly wklad. Bramka nie liczy komentarzy, tylko sprawdza ich autora, i to zmienia dwa wyniki: #362 (@lesbass) ma komentarz, ale napisal go inny kontrybutor, wiec jest dlugiem; #83-#85 (@slegarraga) nie maja komentarza od nas, tylko recenzje z trescia, wiec dlugiem nie sa. Cisza kontrybutora liczona od JEGO ostatniej czynnosci, nie od naszego scalenia - inaczej wlasne klikniecie byloby dowodem, ze on wciaz z nami jest. Na @slegarraga roznica wynosi piec dni. Czego to zadanie celowo nie robi: nie pisze podziekowan (automatyczne "dziekujemy" mowi czlowiekowi wprost, ze po drugiej stronie nie bylo nikogo), nie zaczepia nikogo, kto ucichl (prog 21 dni jest wybrany, nie wyliczony - n=2 odstepy od jednej osoby - wiec uspienie nigdy nie zaklada zgloszenia samo), nie przypisuje wlasciciela. Logika w tools/contributor-care.js, testowana na prawdziwych danych tych 14 wnioskow, importuje regule "kto napisal ostatni" z unanswered-external.js zamiast trzymac jej druga kopie. Sprawdzona przez zepsucie: odwrocenie progu dojrzalosci wywraca 4 z 16 testow, odwrocenie testu autorstwa 8 z 16.
Adds
lua-zerosmtp.luausing LuaSocket and LuaSec, reads the standardZEROSMTP_*environment variables, and wires the example into READMEs, docs, lint, labeler, and the Dev Container.Closes #28