Skip to content
This repository has been archived by the owner on Dec 23, 2023. It is now read-only.

Commit

Permalink
feat: implement server-side recipient blacklist (resolve #33)
Browse files Browse the repository at this point in the history
  • Loading branch information
muety committed Aug 7, 2022
1 parent 9521e67 commit 028e27f
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 26 deletions.
37 changes: 19 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,24 +163,25 @@ $ curl -XPOST \
## 🔧 Configuration Options
You can specify configuration options either via a config file (`config.yml`) or via environment variables. Here is an overview of all options.

| YAML Key | Environment Variable | Default | Description |
|---------------------------|------------------------------|---------------------------|-----------------------------------------------------------------------------------|
| `env` | `MW_ENV` | `dev` | Whether to use development- or production settings |
| `mail.domain` | `MW_MAIL_DOMAIN` | - | Default domain for sending mails |
| `web.listen_addr` | `MW_WEB_LISTEN_ADDR` | `127.0.0.1:3000` | IP and port for the web server to listen on (can be IPv4 or IPv6) |
| `web.cors_origin` | - | [`http://localhost:5000`] | List of URLs which to accept CORS requests for |
| `web.public_url` | `MW_PUBLIC_URL` | `http://localhost:3000` | The URL under which your MailWhale server is available from the public internet |
| `smtp.host` | `MW_SMTP_HOST` | - | SMTP relay host name or IP |
| `smtp.port` | `MW_SMTP_PORT` | - | SMTP relay port |
| `smtp.username` | `MW_SMTP_USER` | - | SMTP relay authentication user name |
| `smtp.password` | `MW_SMTP_PASS` | - | SMTP relay authentication password |
| `smtp.tls` | `MW_SMTP_TLS` | `false` | Whether to require full TLS (not to be confused with STARTTLS) for the SMTP relay |
| `smtp.skip_verify_tls` | `MW_SMTP_SKIP_VERIFY_TLS` | `false` | Whether to skip certificate verification (e.g. trust self-signed certs) |
| `store.path` | `MW_STORE_PATH` | `./data.json.db` | Target location of the database file |
| `security.pepper` | `MW_SECURITY_PEPPER` | - | Pepper to use for hashing user passwords |
| `security.allow_signup` | `MW_SECURITY_ALLOW_SIGNUP` | `true` | Whether to allow the registration of new users |
| `security.verify_users` | `MW_SECURITY_VERIFY_USERS` | `true` | Whether to require new users to activate their account using a confirmation mail |
| `security.verify_senders` | `MW_SECURITY_VERIFY_SENDERS` | `true` | Whether to validate sender addresses and their domains' SPF records |
| YAML Key | Environment Variable | Default | Description |
|---------------------------|------------------------------|---------------------------|------------------------------------------------------------------------------------|
| `env` | `MW_ENV` | `dev` | Whether to use development- or production settings |
| `mail.domain` | `MW_MAIL_DOMAIN` | - | Default domain for sending mails |
| `web.listen_addr` | `MW_WEB_LISTEN_ADDR` | `127.0.0.1:3000` | IP and port for the web server to listen on (can be IPv4 or IPv6) |
| `web.cors_origin` | - | [`http://localhost:5000`] | List of URLs which to accept CORS requests for |
| `web.public_url` | `MW_PUBLIC_URL` | `http://localhost:3000` | The URL under which your MailWhale server is available from the public internet |
| `smtp.host` | `MW_SMTP_HOST` | - | SMTP relay host name or IP |
| `smtp.port` | `MW_SMTP_PORT` | - | SMTP relay port |
| `smtp.username` | `MW_SMTP_USER` | - | SMTP relay authentication user name |
| `smtp.password` | `MW_SMTP_PASS` | - | SMTP relay authentication password |
| `smtp.tls` | `MW_SMTP_TLS` | `false` | Whether to require full TLS (not to be confused with STARTTLS) for the SMTP relay |
| `smtp.skip_verify_tls` | `MW_SMTP_SKIP_VERIFY_TLS` | `false` | Whether to skip certificate verification (e.g. trust self-signed certs) |
| `store.path` | `MW_STORE_PATH` | `./data.json.db` | Target location of the database file |
| `security.pepper` | `MW_SECURITY_PEPPER` | - | Pepper to use for hashing user passwords |
| `security.allow_signup` | `MW_SECURITY_ALLOW_SIGNUP` | `true` | Whether to allow the registration of new users |
| `security.verify_users` | `MW_SECURITY_VERIFY_USERS` | `true` | Whether to require new users to activate their account using a confirmation mail |
| `security.verify_senders` | `MW_SECURITY_VERIFY_SENDERS` | `true` | Whether to validate sender addresses and their domains' SPF records |
| `security.block_list` | `MW_SECURITY_BLOCK_LIST` | `[]` | List of [regexes](https://regex101.com/) used to block certain recipient addresses |

### Sender verification & SPF Check
By default, mails are sent using a randomly generated address in the `From` header, which belongs to the domain configured via `mail.domain` (i.e. `user+abcdefgh@wakapi.dev`). Optionally, custom sender addresses can be configured on a per-API-client basis. However, it is recommended to properly configure [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework) on that custom domain and instruct MailWhale to verify that configuration.
Expand Down
5 changes: 4 additions & 1 deletion config.default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,7 @@ security:
pepper: 'sshhh' # Change this!
allow_signup: true
verify_users: true # Whether to send verification mail when registering a new user
verify_senders: true # Whether to send verification mail when adding new sender addresses
verify_senders: true # Whether to send verification mail when adding new sender addresses
block_list: # List of regexes to validate e-mail recipients against
- 'evilcompany\.org'
- 'drevil@gmail\.com'
46 changes: 42 additions & 4 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package config

import (
"errors"
"flag"
"fmt"
"github.com/emvi/logbuch"
"github.com/jinzhu/configor"
"github.com/muety/mailwhale/types"
"io/ioutil"
"os"
"regexp"
"strings"
)

Expand Down Expand Up @@ -47,12 +49,16 @@ type storeConfig struct {
}

type securityConfig struct {
Pepper string `env:"MW_SECURITY_PEPPER"`
AllowSignup bool `yaml:"allow_signup" env:"MW_SECURITY_ALLOW_SIGNUP" default:"true"`
VerifySenders bool `yaml:"verify_senders" default:"true" env:"MW_SECURITY_VERIFY_SENDERS"`
VerifyUsers bool `yaml:"verify_users" default:"true" env:"MW_SECURITY_VERIFY_USERS"`
Pepper string `env:"MW_SECURITY_PEPPER"`
AllowSignup bool `yaml:"allow_signup" env:"MW_SECURITY_ALLOW_SIGNUP" default:"true"`
VerifySenders bool `yaml:"verify_senders" default:"true" env:"MW_SECURITY_VERIFY_SENDERS"`
VerifyUsers bool `yaml:"verify_users" default:"true" env:"MW_SECURITY_VERIFY_USERS"`
BlockList []string `yaml:"block_list" env:"MW_SECURITY_BLOCK_LIST"`
blockListParsed BlockList
}

type BlockList []*regexp.Regexp

type Config struct {
Env string `default:"dev" env:"MW_ENV"`
Version string
Expand Down Expand Up @@ -101,6 +107,7 @@ func Load() *Config {
logbuch.Info("User registration enabled: %v", config.Security.AllowSignup)
logbuch.Info("Account activation required: %v", config.Security.VerifyUsers)
logbuch.Info("Sender address verification required: %v", config.Security.VerifySenders)
logbuch.Info("Blocked recipient patterns: %d", len(config.Security.BlockListPatterns()))
logbuch.Info("---")

Set(config)
Expand All @@ -119,10 +126,41 @@ func (c *mailConfig) SystemSender() types.MailAddress {
return types.MailAddress(strings.Replace(c.SystemSenderTpl, "{0}", c.Domain, -1))
}

func (c *securityConfig) BlockListPatterns() BlockList {
if len(c.BlockList) != len(c.blockListParsed) {
for _, r := range c.BlockList {
if p, err := regexp.Compile(r); err == nil {
c.blockListParsed = append(c.blockListParsed, p)
} else {
logbuch.Error("failed to parse block list pattern '%s': %v", err)
}
}
}
return c.blockListParsed
}

func (c *Config) IsDev() bool {
return c.Env == "dev" || c.Env == "development"
}

func (l BlockList) Check(email string) error {
for _, p := range l {
if p.MatchString(email) {
return errors.New(fmt.Sprintf("recipient '%s' blocked by the server", email))
}
}
return nil
}

func (l BlockList) CheckBatch(emails []string) error {
for _, e := range emails {
if err := l.Check(e); err != nil {
return err
}
}
return nil
}

func readVersion() string {
file, err := os.Open("version.txt")
if err != nil {
Expand Down
8 changes: 7 additions & 1 deletion service/send.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ func NewSendService() *SendService {
}

func (s *SendService) Send(mail *types.Mail) error {
recipients := mail.To.RawStrings()

if err := s.config.Security.BlockListPatterns().CheckBatch(recipients); err != nil {
return err
}

return sendMail(
s.config.Smtp.ConnStr(),
s.config.Smtp.TLS,
Expand All @@ -36,7 +42,7 @@ func (s *SendService) Send(mail *types.Mail) error {
},
s.auth,
mail.From.Raw(),
mail.To.RawStrings(),
recipients,
mail.Reader(),
)
}
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.12.0
0.13.0
7 changes: 6 additions & 1 deletion web/routes/api/mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/muety/mailwhale/util"
"github.com/muety/mailwhale/web/handlers"
"net/http"
"strings"
)

const routeMail = "/api/mail"
Expand Down Expand Up @@ -88,7 +89,11 @@ func (h *MailHandler) post(w http.ResponseWriter, r *http.Request) {
}

if err := h.sendService.Send(mail); err != nil {
util.RespondError(w, r, http.StatusInternalServerError, err)
status := http.StatusInternalServerError
if strings.Contains(err.Error(), "blocked") {
status = http.StatusBadRequest
}
util.RespondError(w, r, status, err)
return
}

Expand Down

0 comments on commit 028e27f

Please sign in to comment.