initial commit
This commit is contained in:
commit
f28bfd0ace
7 changed files with 358 additions and 0 deletions
4
.envrc
Normal file
4
.envrc
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
if ! has nix_direnv_version || ! nix_direnv_version 2.2.0; then
|
||||||
|
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.0/direnvrc" "sha256-5EwyKnkJNQeXrRkYbwwRBcXbibosCJqyIUuz9Xq+LRc="
|
||||||
|
fi
|
||||||
|
use flake
|
12
cmd/inbound/inbound.go
Normal file
12
cmd/inbound/inbound.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "flag"
|
||||||
|
|
||||||
|
var listenPort int
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.IntVar(&listenPort, "p", 2525, "port on which to listen for incoming emails")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
}
|
61
flake.lock
Normal file
61
flake.lock
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681202837,
|
||||||
|
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1683627095,
|
||||||
|
"narHash": "sha256-8u9SejRpL2TrMuHBdhYh4FKc1OGPDLyWTpIbNTtoHsA=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "a08e061a4ee8329747d54ddf1566d34c55c895eb",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-22.11",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
52
flake.nix
Normal file
52
flake.nix
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
description = "tkcli devshell";
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11";
|
||||||
|
#nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; # localstack is broken right now (2023-01-25) in unstable, due to a missing dependency
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils, ... }@inputs:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
|
||||||
|
gci = pkgs.buildGoModule rec {
|
||||||
|
name = "gci";
|
||||||
|
src = pkgs.fetchFromGitHub {
|
||||||
|
owner = "daixiang0";
|
||||||
|
repo = "gci";
|
||||||
|
rev = "v0.10.1";
|
||||||
|
sha256 = "sha256-/YR61lovuYw+GEeXIgvyPbesz2epmQVmSLWjWwKT4Ag=";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Switch to fake vendor sha for upgrades:
|
||||||
|
#vendorSha256 = pkgs.lib.fakeSha256;
|
||||||
|
vendorSha256 = "sha256-g7htGfU6C2rzfu8hAn6SGr0ZRwB8ZzSf9CgHYmdupE8=";
|
||||||
|
};
|
||||||
|
|
||||||
|
cclint = pkgs.writeScriptBin "lint" ''
|
||||||
|
#!/bin/sh
|
||||||
|
pushd $(git rev-parse --show-toplevel)/
|
||||||
|
${pkgs.go}/bin/go mod tidy
|
||||||
|
${pkgs.gofumpt}/bin/gofumpt -w *.go ./cmd/*
|
||||||
|
${gci}/bin/gci write --skip-generated -s standard -s default -s "Prefix(git.cycore.io/cycore)" .
|
||||||
|
${pkgs.golangci-lint}/bin/golangci-lint run ./...
|
||||||
|
${pkgs.go}/bin/go test -v ./...
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
bashInteractive
|
||||||
|
envsubst
|
||||||
|
gci
|
||||||
|
gofumpt
|
||||||
|
golangci-lint
|
||||||
|
go
|
||||||
|
go-tools
|
||||||
|
cclint
|
||||||
|
];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
17
go.mod
Normal file
17
go.mod
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
module git.cycore.io/cycore/mail
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/emersion/go-imap v1.2.1 // indirect
|
||||||
|
github.com/emersion/go-message v0.16.0 // indirect
|
||||||
|
github.com/emersion/go-milter v0.3.3 // indirect
|
||||||
|
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
|
||||||
|
github.com/emersion/go-smtp v0.16.0 // indirect
|
||||||
|
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
|
||||||
|
go.uber.org/atomic v1.7.0 // indirect
|
||||||
|
go.uber.org/multierr v1.6.0 // indirect
|
||||||
|
go.uber.org/zap v1.24.0 // indirect
|
||||||
|
golang.org/x/text v0.3.7 // indirect
|
||||||
|
golang.org/x/time v0.3.0 // indirect
|
||||||
|
)
|
35
go.sum
Normal file
35
go.sum
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
|
||||||
|
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
|
||||||
|
github.com/emersion/go-message v0.11.2/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY=
|
||||||
|
github.com/emersion/go-message v0.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY=
|
||||||
|
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
|
||||||
|
github.com/emersion/go-message v0.16.0 h1:uZLz8ClLv3V5fSFF/fFdW9jXjrZkXIpE1Fn8fKx7pO4=
|
||||||
|
github.com/emersion/go-message v0.16.0/go.mod h1:pDJDgf/xeUIF+eicT6B/hPX/ZbEorKkUMPOxrPVG2eQ=
|
||||||
|
github.com/emersion/go-milter v0.3.3 h1:DiP9Xmw2FqEuosNCd01XPDBb1K3OziNmt7BG2ddFlgs=
|
||||||
|
github.com/emersion/go-milter v0.3.3/go.mod h1:ablHK0pbLB83kMFBznp/Rj8aV+Kc3jw8cxzzmCNLIOY=
|
||||||
|
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.16.0 h1:eB9CY9527WdEZSs5sWisTmilDX7gG+Q/2IdRcmubpa8=
|
||||||
|
github.com/emersion/go-smtp v0.16.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||||
|
github.com/emersion/go-textwrapper v0.0.0-20160606182133-d0e65e56babe/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||||
|
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
|
||||||
|
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
|
||||||
|
github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||||
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||||
|
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||||
|
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||||
|
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||||
|
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
177
server/server.go
Normal file
177
server/server.go
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emersion/go-message/mail"
|
||||||
|
"github.com/emersion/go-milter"
|
||||||
|
"github.com/emersion/go-smtp"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ConnectionTimeout = 3 * time.Minute
|
||||||
|
|
||||||
|
// ErrRateLimit indicates that the message is rejected due to a rate limit.
|
||||||
|
var ErrRateLimit = &smtp.SMTPError{
|
||||||
|
Code: 450,
|
||||||
|
EnhancedCode: [3]int{4, 2, 0},
|
||||||
|
Message: "too many requests",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrBadGateway indicates a downstream system failure.
|
||||||
|
var ErrBadGateway = &smtp.SMTPError{
|
||||||
|
Code: 451,
|
||||||
|
EnhancedCode: [3]int{4, 4, 3},
|
||||||
|
Message: "bad gateway",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend provides a backend implementation of an SMTP server.
|
||||||
|
type Backend struct {
|
||||||
|
AllowedDomains []string
|
||||||
|
|
||||||
|
Log *zap.Logger
|
||||||
|
|
||||||
|
RootContext context.Context
|
||||||
|
|
||||||
|
RSpam *milter.Client
|
||||||
|
|
||||||
|
OverallLimiter *rate.Limiter
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSession implements smtp.Backend
|
||||||
|
func (b *Backend) NewSession(conn *smtp.Conn) (smtp.Session, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(b.RootContext, ConnectionTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := b.OverallLimiter.Wait(ctx); err != nil {
|
||||||
|
return nil, ErrRateLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
rs, err := b.RSpam.Session()
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrBadGateway
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Session{
|
||||||
|
b: b,
|
||||||
|
conn: conn,
|
||||||
|
rspam: rs,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session provides an implementation of an SMTP session.
|
||||||
|
type Session struct {
|
||||||
|
b *Backend
|
||||||
|
conn *smtp.Conn
|
||||||
|
rspam *milter.ClientSession
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discard currently processed message.
|
||||||
|
func (session *Session) Reset() {
|
||||||
|
panic("not implemented") // TODO: Implement
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free all resources associated with session.
|
||||||
|
func (session *Session) Logout() error {
|
||||||
|
panic("not implemented") // TODO: Implement
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate the user using SASL PLAIN.
|
||||||
|
func (session *Session) AuthPlain(username string, password string) error {
|
||||||
|
return smtp.ErrAuthUnsupported
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set return path for currently processed message.
|
||||||
|
func (session *Session) Mail(from string, opts *smtp.MailOptions) error {
|
||||||
|
action, err := session.rspam.Mail(from, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("filter failure: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseAction(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add recipient for currently processed message.
|
||||||
|
func (session *Session) Rcpt(to string) error {
|
||||||
|
if err := session.validateDomain(to); err != nil {
|
||||||
|
return &smtp.SMTPError{
|
||||||
|
Code: 550,
|
||||||
|
EnhancedCode: [3]int{5, 1, 2},
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
action, err := session.rspam.Rcpt(to, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("filter failure: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseAction(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set currently processed message contents and send it.
|
||||||
|
//
|
||||||
|
// r must be consumed before Data returns.
|
||||||
|
func (session *Session) Data(r io.Reader) error {
|
||||||
|
// TODO: handle the modifyAction below, whatever it is.
|
||||||
|
_, action, err := session.rspam.BodyReadFrom(r)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("filter failure: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseAction(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *Session) validateDomain(to string) error {
|
||||||
|
toAddr, err := mail.ParseAddress(to)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pieces := strings.Split(toAddr.Address, "@")
|
||||||
|
if len(pieces) != 2 {
|
||||||
|
return fmt.Errorf("invalid address")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range session.b.AllowedDomains {
|
||||||
|
if strings.Contains(pieces[2], d) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("invalid domain")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAction(action *milter.Action) error {
|
||||||
|
switch action.Code {
|
||||||
|
case milter.ActAccept:
|
||||||
|
return nil
|
||||||
|
case milter.ActContinue:
|
||||||
|
return nil
|
||||||
|
case milter.ActDiscard:
|
||||||
|
return milterReject(action)
|
||||||
|
case milter.ActReject:
|
||||||
|
return milterReject(action)
|
||||||
|
case milter.ActTempFail:
|
||||||
|
return milterReject(action)
|
||||||
|
case milter.ActReplyCode:
|
||||||
|
return milterReject(action)
|
||||||
|
case milter.ActSkip:
|
||||||
|
return milterReject(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("unhandled action code %q", action.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func milterReject(action *milter.Action) error {
|
||||||
|
return &smtp.SMTPError{
|
||||||
|
Code: action.SMTPCode,
|
||||||
|
EnhancedCode: [3]int{4, 2, 0},
|
||||||
|
Message: action.SMTPText,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue