Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Rate Limit per remote #155

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
smtprelay
22 changes: 22 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM golang:1.22-alpine AS build
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please respect #50 #54


WORKDIR /app

COPY go.mod ./
COPY go.sum ./

RUN go mod download

COPY *.go ./

RUN go build -o smtprelay

FROM golang:1.22-alpine

WORKDIR /app

COPY --from=build /app/smtprelay ./

EXPOSE 25

ENTRYPOINT ["./smtprelay"]
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
Simple Golang based SMTP relay/proxy server that accepts mail via SMTP
and forwards it directly to another SMTP server.


## Why another SMTP server?

Outgoing mails are usually send via SMTP to an MTA (Mail Transfer Agent)
Expand All @@ -19,7 +18,6 @@ cron via msmtp/sSMTP/dma, mails from various services and network printers
via a remote SMTP server without giving away my mail credentials to each
device which produces mail.


## Main features

* Simple configuration with ini file .env file or environment variables
Expand All @@ -30,3 +28,7 @@ device which produces mail.
* Forwards all mail to a smarthost (any SMTP server)
* Small codebase
* IPv6 support

## Rate Limit

* Add rate limit option in remote (?rate=1/1h) using <https://github.com/sethvargo/go-limiter>
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ require (
)

require (
github.com/Hepri/rate_limiter v0.0.0-20170628005159-cf154f46b333 // indirect
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm where is that dependency coming from?

github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sethvargo/go-limiter v1.0.0 // indirect
golang.org/x/sys v0.19.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

go 1.20
go 1.22

toolchain go1.22.2
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/Hepri/rate_limiter v0.0.0-20170628005159-cf154f46b333 h1:JlFmeHM9nEHCfGlMYlZBHSqb1T7JtwdSMtQKVl829IU=
github.com/Hepri/rate_limiter v0.0.0-20170628005159-cf154f46b333/go.mod h1:SSFHoCFNk9cmyeMoZcuhDiInWAF/JdyFEG7kHfu9rp4=
github.com/chrj/smtpd v0.3.1 h1:kogHFkbFdKaoH3bgZkqNC9uVtKYOFfM3uV3rroBdooE=
github.com/chrj/smtpd v0.3.1/go.mod h1:JtABvV/LzvLmEIzy0NyDnrfMGOMd8wy5frAokwf6J9Q=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
Expand All @@ -20,6 +22,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sethvargo/go-limiter v1.0.0 h1:JqW13eWEMn0VFv86OKn8wiYJY/m250WoXdrjRV0kLe4=
github.com/sethvargo/go-limiter v1.0.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ func mailHandler(peer smtpd.Peer, env smtpd.Envelope) error {
environ = append(environ, fmt.Sprintf("%s=%s", "SMTPRELAY_PEER", peerIP))

cmd := exec.Cmd{
Env: environ,
Env: environ,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whitespace change?

Path: *command,
}

Expand Down
39 changes: 32 additions & 7 deletions remotes.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,23 @@ import (
"fmt"
"net/smtp"
"net/url"
"strconv"
"strings"
"time"

"github.com/sethvargo/go-limiter"
"github.com/sethvargo/go-limiter/memorystore"
)

type Remote struct {
SkipVerify bool
Auth smtp.Auth
Scheme string
Hostname string
Port string
Addr string
Sender string
SkipVerify bool
Auth smtp.Auth
Scheme string
Hostname string
Port string
Addr string
Sender string
RateLimiter *limiter.Store
}

// ParseRemote creates a remote from a given url in the following format:
Expand All @@ -25,6 +32,7 @@ type Remote struct {
// Supported Params:
// - skipVerify: can be "true" or empty to prevent ssl verification of remote server's certificate.
// - auth: can be "login" to trigger "LOGIN" auth instead of "PLAIN" auth
// - rate: an optional rate limiter in the format <msgs>/<time unit> like 100/1h
func ParseRemote(remoteURL string) (*Remote, error) {
u, err := url.Parse(remoteURL)
if err != nil {
Expand Down Expand Up @@ -79,5 +87,22 @@ func ParseRemote(remoteURL string) (*Remote, error) {
r.Sender = u.Path[1:]
}

if hasVal, rate := q.Has("rate"), q.Get("rate"); hasVal && strings.Contains(rate, "/") {
i, err := strconv.ParseInt(strings.Split(rate, "/")[0], 10, 32)
if err == nil {
t, err := time.ParseDuration(strings.Split(rate, "/")[1])
log.Infof("Configuring rate limiter %v/%v", i, t)
if err == nil {
store, err := memorystore.New(&memorystore.Config{
Tokens: uint64(i),
Interval: t,
})
if err == nil {
r.RateLimiter = &store
}
}
}
}

return r, nil
}
10 changes: 10 additions & 0 deletions smtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package main

import (
"context"
"crypto/tls"
"encoding/base64"
"errors"
Expand All @@ -27,6 +28,8 @@ import (
"net/smtp"
"net/textproto"
"strings"

"github.com/chrj/smtpd"
)

// A Client represents a client connection to an SMTP server.
Expand Down Expand Up @@ -321,6 +324,13 @@ var testHookStartTLS func(*tls.Config) // nil, except for tests
// functionality. Higher-level packages exist outside of the standard
// library.
func SendMail(r *Remote, from string, to []string, msg []byte) error {
if r.RateLimiter != nil {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a copy from upstream Golang net/smtp and touching it should be avoided if possible.

tokens, remaining, _, ok, err := (*r.RateLimiter).Take(context.Background(), "")
if err != nil || !ok {
return smtpd.Error{Code: 452, Message: "Rate limit reached"}
}
log.Debugf("Remaining %v tokens of %v", remaining, tokens)
}
if r.Sender != "" {
from = r.Sender
}
Expand Down