aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.golangci.yaml8
-rw-r--r--config.go3
-rw-r--r--forge.scfg3
-rw-r--r--go.mod3
-rw-r--r--go.sum7
-rw-r--r--lmtp_server.go117
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
diff --git a/config.go b/config.go
index 97e2588..7870a63 100644
--- a/config.go
+++ b/config.go
@@ -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"`
diff --git a/forge.scfg b/forge.scfg
index abf4f80..b2a34cd 100644
--- a/forge.scfg
+++ b/forge.scfg
@@ -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
}
diff --git a/go.mod b/go.mod
index 9e20462..6555e3b 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index c6c498d..70aa113 100644
--- a/go.sum
+++ b/go.sum
@@ -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
+}