diff options
author | Runxi Yu <me@runxiyu.org> | 2025-04-01 13:27:26 +0800 |
---|---|---|
committer | Runxi Yu <me@runxiyu.org> | 2025-04-01 13:29:14 +0800 |
commit | a8eee4110fe52e132411e4d171e3e08d22fb0079 (patch) | |
tree | e09781c31212628dcae8732cdc7087df7f435d54 | |
parent | Stub LMTP listener (diff) | |
download | forge-a8eee4110fe52e132411e4d171e3e08d22fb0079.tar.gz forge-a8eee4110fe52e132411e4d171e3e08d22fb0079.tar.zst forge-a8eee4110fe52e132411e4d171e3e08d22fb0079.zip |
Basic debugging LMTP handler
-rw-r--r-- | .golangci.yaml | 8 | ||||
-rw-r--r-- | config.go | 3 | ||||
-rw-r--r-- | forge.scfg | 3 | ||||
-rw-r--r-- | go.mod | 3 | ||||
-rw-r--r-- | go.sum | 7 | ||||
-rw-r--r-- | lmtp_server.go | 117 |
6 files changed, 138 insertions, 3 deletions
diff --git a/.golangci.yaml b/.golangci.yaml index 760bb07..00ba1ea 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -14,6 +14,8 @@ linters: - nakedret # patterns should be consistent - nonamedreturns # i like named returns - wrapcheck # wrapping all errors is just not necessary + - varnamelen # "from" and "to" are very valid + - stylecheck - maintidx # e - nestif # e - gocognit # e @@ -27,6 +29,12 @@ linters: - unused # e - exhaustruct # e +linters-settings: + revive: + rules: + - name: error-strings + disabled: true + issues: max-issues-per-linter: 0 max-same-issues: 0 @@ -32,7 +32,8 @@ var config struct { Execs string `scfg:"execs"` } `scfg:"hooks"` LMTP struct { - Socket string `scfg:"socket"` + Socket string `scfg:"socket"` + MaxSize int64 `scfg:"max_size"` } `scfg:"lmtp"` Git struct { RepoDir string `scfg:"repo_dir"` @@ -80,4 +80,7 @@ hooks { lmtp { # On which UNIX domain socket should we listen for LMTP on? socket /var/run/lindenii/forge/lmtp.sock + + # What's the maximum acceptable message size? + max_size 1000000 } @@ -29,6 +29,9 @@ require ( github.com/cloudflare/circl v1.6.0 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/emersion/go-message v0.18.2 // indirect + github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect + github.com/emersion/go-smtp v0.21.3 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect @@ -38,6 +38,12 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg= +github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-smtp v0.21.3 h1:7uVwagE8iPYE48WhNsng3RRpCUpFvNl39JGNSIyGVMY= +github.com/emersion/go-smtp v0.21.3/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= @@ -169,6 +175,7 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/lmtp_server.go b/lmtp_server.go index 57f319a..8e574e4 100644 --- a/lmtp_server.go +++ b/lmtp_server.go @@ -1,10 +1,123 @@ // SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> +// SPDX-FileCopyrightText: Copyright (c) 2024 Robin Jarry <robin@jarry.cc> package main -import "net" +import ( + "bytes" + "errors" + "io" + "log/slog" + "net" + "strings" -func serveLMTP(_ net.Listener) error { + "github.com/emersion/go-message" + "github.com/emersion/go-smtp" +) + +type lmtpHandler struct{} + +type lmtpSession struct { + from string + to []string +} + +func (session *lmtpSession) Reset() { + session.from = "" + session.to = nil +} + +func (session *lmtpSession) Logout() error { + return nil +} + +func (session *lmtpSession) AuthPlain(_, _ string) error { + return nil +} + +func (session *lmtpSession) Mail(from string, _ *smtp.MailOptions) error { + session.from = from + return nil +} + +func (session *lmtpSession) Rcpt(to string, _ *smtp.RcptOptions) error { + session.to = append(session.to, to) return nil } + +func (*lmtpHandler) NewSession(_ *smtp.Conn) (smtp.Session, error) { + // TODO + session := &lmtpSession{} + return session, nil +} + +func serveLMTP(listener net.Listener) error { + smtpServer := smtp.NewServer(&lmtpHandler{}) + return smtpServer.Serve(listener) +} + +func (session *lmtpSession) Data(r io.Reader) error { + var ( + email *message.Entity + from string + to []string + err error + buf bytes.Buffer + data []byte + n int64 + ) + + n, err = io.CopyN(&buf, r, config.LMTP.MaxSize) + switch { + case n == config.LMTP.MaxSize: + err = errors.New("Message too big.") + // drain whatever is left in the pipe + _, _ = io.Copy(io.Discard, r) + goto end + case errors.Is(err, io.EOF): + // message was smaller than max size + break + case err != nil: + goto end + } + + data = buf.Bytes() + + email, err = message.Read(bytes.NewReader(data)) + if err != nil && message.IsUnknownCharset(err) { + goto end + } + + switch strings.ToLower(email.Header.Get("Auto-Submitted")) { + case "auto-generated", "auto-replied": + // disregard automatic emails like OOO replies + slog.Info("ignoring automatic message", + "from", session.from, + "to", strings.Join(session.to, ","), + "message-id", email.Header.Get("Message-Id"), + "subject", email.Header.Get("Subject"), + ) + goto end + } + + slog.Info("message received", + "from", session.from, + "to", strings.Join(session.to, ","), + "message-id", email.Header.Get("Message-Id"), + "subject", email.Header.Get("Subject"), + ) + + // Make local copies of the values before to ensure the references will + // still be valid when the queued task function is evaluated. + from = session.from + to = session.to + + // TODO: Process the actual message contents + _, _ = from, to + +end: + session.to = nil + session.from = "" + return err +} |