2023-03-10 05:42:14 +02:00
|
|
|
// Package config handles the application configuration
|
2022-07-29 13:23:08 +02:00
|
|
|
package config
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
2022-08-05 14:09:20 +02:00
|
|
|
"fmt"
|
|
|
|
"os"
|
2022-10-31 11:22:02 +02:00
|
|
|
"path"
|
2022-08-30 12:42:43 +02:00
|
|
|
"path/filepath"
|
2022-07-29 13:23:08 +02:00
|
|
|
"regexp"
|
2022-10-31 11:13:41 +02:00
|
|
|
"strings"
|
2022-08-04 07:18:07 +02:00
|
|
|
|
2023-04-21 02:10:13 +02:00
|
|
|
"github.com/axllent/mailpit/utils/logger"
|
2022-11-13 05:45:54 +02:00
|
|
|
"github.com/mattn/go-shellwords"
|
2022-08-04 07:18:07 +02:00
|
|
|
"github.com/tg123/go-htpasswd"
|
2023-04-21 02:10:13 +02:00
|
|
|
"gopkg.in/yaml.v3"
|
2022-07-29 13:23:08 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
// SMTPListen to listen on <interface>:<port>
|
2023-04-12 06:03:36 +02:00
|
|
|
SMTPListen = "[::]:1025"
|
2022-07-29 13:23:08 +02:00
|
|
|
|
|
|
|
// HTTPListen to listen on <interface>:<port>
|
2023-04-12 06:03:36 +02:00
|
|
|
HTTPListen = "[::]:8025"
|
2022-07-29 13:23:08 +02:00
|
|
|
|
2022-08-30 12:42:43 +02:00
|
|
|
// DataFile for mail (optional)
|
|
|
|
DataFile string
|
2022-07-29 13:23:08 +02:00
|
|
|
|
|
|
|
// MaxMessages is the maximum number of messages a mailbox can have (auto-pruned every minute)
|
|
|
|
MaxMessages = 500
|
|
|
|
|
2023-03-10 05:42:14 +02:00
|
|
|
// UseMessageDates sets the Created date using the message date, not the delivered date
|
|
|
|
UseMessageDates bool
|
|
|
|
|
2023-03-12 00:31:15 +02:00
|
|
|
// UITLSCert file
|
|
|
|
UITLSCert string
|
2022-08-05 14:09:20 +02:00
|
|
|
|
2023-03-12 00:31:15 +02:00
|
|
|
// UITLSKey file
|
|
|
|
UITLSKey string
|
2022-08-04 07:18:07 +02:00
|
|
|
|
2022-08-06 10:00:05 +02:00
|
|
|
// UIAuthFile for basic authentication
|
|
|
|
UIAuthFile string
|
2022-08-04 07:18:07 +02:00
|
|
|
|
2022-08-06 10:00:05 +02:00
|
|
|
// UIAuth used for euthentication
|
|
|
|
UIAuth *htpasswd.File
|
|
|
|
|
2022-10-31 11:13:41 +02:00
|
|
|
// Webroot to define the base path for the UI and API
|
|
|
|
Webroot = "/"
|
|
|
|
|
2023-03-12 00:31:15 +02:00
|
|
|
// SMTPTLSCert file
|
|
|
|
SMTPTLSCert string
|
2022-08-06 10:00:05 +02:00
|
|
|
|
2023-03-12 00:31:15 +02:00
|
|
|
// SMTPTLSKey file
|
|
|
|
SMTPTLSKey string
|
2022-08-06 10:00:05 +02:00
|
|
|
|
|
|
|
// SMTPAuthFile for SMTP authentication
|
|
|
|
SMTPAuthFile string
|
|
|
|
|
2023-04-21 02:10:13 +02:00
|
|
|
// SMTPAuthConfig used for authentication auto-generated from SMTPAuthFile
|
|
|
|
SMTPAuthConfig *htpasswd.File
|
2022-10-07 08:46:39 +02:00
|
|
|
|
2023-03-11 23:51:49 +02:00
|
|
|
// SMTPAuthAllowInsecure allows PLAIN & LOGIN unencrypted authentication
|
|
|
|
SMTPAuthAllowInsecure bool
|
|
|
|
|
|
|
|
// SMTPAuthAcceptAny accepts any username/password including none
|
|
|
|
SMTPAuthAcceptAny bool
|
|
|
|
|
2022-11-13 05:45:54 +02:00
|
|
|
// SMTPCLITags is used to map the CLI args
|
|
|
|
SMTPCLITags string
|
|
|
|
|
|
|
|
// TagRegexp is the allowed tag characters
|
|
|
|
TagRegexp = regexp.MustCompile(`^([a-zA-Z0-9\-\ \_]){3,}$`)
|
|
|
|
|
|
|
|
// SMTPTags are expressions to apply tags to new mail
|
2023-04-21 02:10:13 +02:00
|
|
|
SMTPTags []AutoTag
|
|
|
|
|
|
|
|
// SMTPRelayConfigFile to parse a yaml file and store config of relay SMTP server
|
|
|
|
SMTPRelayConfigFile string
|
|
|
|
|
|
|
|
// SMTPRelayConfig to parse a yaml file and store config of relay SMTP server
|
|
|
|
SMTPRelayConfig smtpRelayConfigStruct
|
|
|
|
|
|
|
|
// ReleaseEnabled is whether message releases are enabled, requires a valid SMTPRelayConfigFile
|
|
|
|
ReleaseEnabled = false
|
|
|
|
|
|
|
|
// SMTPRelayAllIncoming is whether to relay all incoming messages via preconfgured SMTP server.
|
|
|
|
// Use with extreme caution!
|
|
|
|
SMTPRelayAllIncoming = false
|
2022-11-13 05:45:54 +02:00
|
|
|
|
2022-10-07 08:46:39 +02:00
|
|
|
// ContentSecurityPolicy for HTTP server
|
|
|
|
ContentSecurityPolicy = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self'; img-src * data: blob:; font-src 'self' data:; media-src 'self'; connect-src 'self' ws: wss:; object-src 'none'; base-uri 'self';"
|
2022-10-08 12:23:30 +02:00
|
|
|
|
|
|
|
// Version is the default application version, updated on release
|
|
|
|
Version = "dev"
|
|
|
|
|
|
|
|
// Repo on Github for updater
|
|
|
|
Repo = "axllent/mailpit"
|
|
|
|
|
|
|
|
// RepoBinaryName on Github for updater
|
|
|
|
RepoBinaryName = "mailpit"
|
2022-07-29 13:23:08 +02:00
|
|
|
)
|
|
|
|
|
2023-04-21 02:10:13 +02:00
|
|
|
// AutoTag struct for auto-tagging
|
|
|
|
type AutoTag struct {
|
2022-11-13 05:45:54 +02:00
|
|
|
Tag string
|
|
|
|
Match string
|
|
|
|
}
|
|
|
|
|
2023-04-21 02:10:13 +02:00
|
|
|
// SMTPRelayConfigStruct struct for parsing yaml & storing variables
|
|
|
|
type smtpRelayConfigStruct struct {
|
|
|
|
Host string `yaml:"host"`
|
|
|
|
Port int `yaml:"port"`
|
|
|
|
STARTTLS bool `yaml:"starttls"`
|
|
|
|
AllowInsecure bool `yaml:"allow-insecure"`
|
|
|
|
Auth string `yaml:"auth"` // none, plain, cram-md5
|
|
|
|
Username string `yaml:"username"` // plain & cram-md5
|
|
|
|
Password string `yaml:"password"` // plain
|
|
|
|
Secret string `yaml:"secret"` // cram-md5
|
|
|
|
ReturnPath string `yaml:"return-path"` // allows overriding the boune address
|
|
|
|
}
|
|
|
|
|
2022-07-29 13:23:08 +02:00
|
|
|
// VerifyConfig wil do some basic checking
|
|
|
|
func VerifyConfig() error {
|
2022-08-30 12:42:43 +02:00
|
|
|
if DataFile != "" && isDir(DataFile) {
|
|
|
|
DataFile = filepath.Join(DataFile, "mailpit.db")
|
|
|
|
}
|
|
|
|
|
2023-04-12 06:03:36 +02:00
|
|
|
re := regexp.MustCompile(`^((\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|(\[([\da-fA-F:])+\])):\d+$`)
|
2022-07-29 13:23:08 +02:00
|
|
|
if !re.MatchString(SMTPListen) {
|
|
|
|
return errors.New("SMTP bind should be in the format of <ip>:<port>")
|
|
|
|
}
|
|
|
|
if !re.MatchString(HTTPListen) {
|
|
|
|
return errors.New("HTTP bind should be in the format of <ip>:<port>")
|
|
|
|
}
|
|
|
|
|
2022-08-06 10:00:05 +02:00
|
|
|
if UIAuthFile != "" {
|
|
|
|
if !isFile(UIAuthFile) {
|
|
|
|
return fmt.Errorf("HTTP password file not found: %s", UIAuthFile)
|
2022-08-05 14:09:20 +02:00
|
|
|
}
|
|
|
|
|
2022-08-06 10:00:05 +02:00
|
|
|
a, err := htpasswd.New(UIAuthFile, htpasswd.DefaultSystems, nil)
|
2022-08-04 07:18:07 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-08-06 10:00:05 +02:00
|
|
|
UIAuth = a
|
|
|
|
}
|
|
|
|
|
2023-03-12 00:31:15 +02:00
|
|
|
if UITLSCert != "" && UITLSKey == "" || UITLSCert == "" && UITLSKey != "" {
|
|
|
|
return errors.New("You must provide both a UI TLS certificate and a key")
|
2022-08-06 10:00:05 +02:00
|
|
|
}
|
|
|
|
|
2023-03-12 00:31:15 +02:00
|
|
|
if UITLSCert != "" {
|
|
|
|
if !isFile(UITLSCert) {
|
|
|
|
return fmt.Errorf("TLS certificate not found: %s", UITLSCert)
|
2022-08-06 10:00:05 +02:00
|
|
|
}
|
|
|
|
|
2023-03-12 00:31:15 +02:00
|
|
|
if !isFile(UITLSKey) {
|
|
|
|
return fmt.Errorf("TLS key not found: %s", UITLSKey)
|
2022-08-06 10:00:05 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-12 00:31:15 +02:00
|
|
|
if SMTPTLSCert != "" && SMTPTLSKey == "" || SMTPTLSCert == "" && SMTPTLSKey != "" {
|
|
|
|
return errors.New("You must provide both an SMTP TLS certificate and a key")
|
2022-08-04 07:18:07 +02:00
|
|
|
}
|
|
|
|
|
2023-03-12 00:31:15 +02:00
|
|
|
if SMTPTLSCert != "" {
|
|
|
|
if !isFile(SMTPTLSCert) {
|
|
|
|
return fmt.Errorf("SMTP TLS certificate not found: %s", SMTPTLSCert)
|
2022-08-06 10:00:05 +02:00
|
|
|
}
|
|
|
|
|
2023-03-12 00:31:15 +02:00
|
|
|
if !isFile(SMTPTLSKey) {
|
|
|
|
return fmt.Errorf("SMTP TLS key not found: %s", SMTPTLSKey)
|
2022-08-06 10:00:05 +02:00
|
|
|
}
|
2022-08-05 14:09:20 +02:00
|
|
|
}
|
|
|
|
|
2022-08-06 10:00:05 +02:00
|
|
|
if SMTPAuthFile != "" {
|
|
|
|
if !isFile(SMTPAuthFile) {
|
|
|
|
return fmt.Errorf("SMTP password file not found: %s", SMTPAuthFile)
|
|
|
|
}
|
|
|
|
|
2023-03-11 23:51:49 +02:00
|
|
|
if SMTPAuthAcceptAny {
|
|
|
|
return errors.New("SMTP authentication can either use --smtp-auth-file or --smtp-auth-accept-any")
|
2022-08-05 14:09:20 +02:00
|
|
|
}
|
|
|
|
|
2022-08-06 10:00:05 +02:00
|
|
|
a, err := htpasswd.New(SMTPAuthFile, htpasswd.DefaultSystems, nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2022-08-05 14:09:20 +02:00
|
|
|
}
|
2023-04-21 02:10:13 +02:00
|
|
|
SMTPAuthConfig = a
|
2022-08-05 14:09:20 +02:00
|
|
|
}
|
|
|
|
|
2023-03-12 00:31:15 +02:00
|
|
|
if SMTPTLSCert == "" && (SMTPAuthFile != "" || SMTPAuthAcceptAny) && !SMTPAuthAllowInsecure {
|
|
|
|
return errors.New("SMTP authentication requires TLS encryption, run with `--smtp-auth-allow-insecure` to allow insecure authentication")
|
2023-03-11 23:51:49 +02:00
|
|
|
}
|
|
|
|
|
2023-03-10 03:48:28 +02:00
|
|
|
validWebrootRe := regexp.MustCompile(`[^0-9a-zA-Z\/\-\_\.]`)
|
2022-12-07 22:54:03 +02:00
|
|
|
if validWebrootRe.MatchString(Webroot) {
|
2023-03-10 03:48:28 +02:00
|
|
|
return fmt.Errorf("Invalid characters in Webroot (%s). Valid chars include: [a-z A-Z 0-9 _ . - /]", Webroot)
|
2022-10-31 11:13:41 +02:00
|
|
|
}
|
|
|
|
|
2022-12-07 22:54:03 +02:00
|
|
|
s := strings.TrimRight(path.Join("/", Webroot, "/"), "/") + "/"
|
2022-10-31 11:13:41 +02:00
|
|
|
Webroot = s
|
|
|
|
|
2023-04-21 02:10:13 +02:00
|
|
|
SMTPTags = []AutoTag{}
|
2022-11-13 05:45:54 +02:00
|
|
|
|
|
|
|
p := shellwords.NewParser()
|
|
|
|
|
|
|
|
if SMTPCLITags != "" {
|
|
|
|
args, err := p.Parse(SMTPCLITags)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Error parsing tags (%s)", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, a := range args {
|
|
|
|
t := strings.Split(a, "=")
|
|
|
|
if len(t) > 1 {
|
|
|
|
tag := strings.TrimSpace(t[0])
|
|
|
|
if !TagRegexp.MatchString(tag) || len(tag) == 0 {
|
|
|
|
return fmt.Errorf("Invalid tag (%s) - can only contain spaces, letters, numbers, - & _", tag)
|
|
|
|
}
|
|
|
|
match := strings.TrimSpace(strings.ToLower(strings.Join(t[1:], "=")))
|
|
|
|
if len(match) == 0 {
|
|
|
|
return fmt.Errorf("Invalid tag match (%s) - no search detected", tag)
|
|
|
|
}
|
2023-04-21 02:10:13 +02:00
|
|
|
SMTPTags = append(SMTPTags, AutoTag{Tag: tag, Match: match})
|
2022-11-13 05:45:54 +02:00
|
|
|
} else {
|
|
|
|
return fmt.Errorf("Error parsing tags (%s)", a)
|
|
|
|
}
|
|
|
|
}
|
2023-04-21 02:10:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := parseRelayConfig(SMTPRelayConfigFile); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-11-13 05:45:54 +02:00
|
|
|
|
2023-04-21 02:10:13 +02:00
|
|
|
if !ReleaseEnabled && SMTPRelayAllIncoming {
|
|
|
|
return errors.New("SMTP relay config must be set to relay all messages")
|
2022-11-13 05:45:54 +02:00
|
|
|
}
|
|
|
|
|
2023-04-21 02:10:13 +02:00
|
|
|
if SMTPRelayAllIncoming {
|
|
|
|
// this deserves a warning
|
|
|
|
logger.Log().Warnf("[smtp] enabling automatic relay of all new messages via %s:%d", SMTPRelayConfig.Host, SMTPRelayConfig.Port)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse & validate the SMTPRelayConfigFile (if set)
|
|
|
|
func parseRelayConfig(c string) error {
|
|
|
|
if c == "" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if !isFile(c) {
|
|
|
|
return fmt.Errorf("SMTP relay configuration not found: %s", SMTPRelayConfigFile)
|
|
|
|
}
|
|
|
|
|
|
|
|
data, err := os.ReadFile(c)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := yaml.Unmarshal(data, &SMTPRelayConfig); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if SMTPRelayConfig.Host == "" {
|
|
|
|
return errors.New("SMTP relay host not set")
|
|
|
|
}
|
|
|
|
|
|
|
|
if SMTPRelayConfig.Port == 0 {
|
|
|
|
SMTPRelayConfig.Port = 25 // default
|
|
|
|
}
|
|
|
|
|
|
|
|
SMTPRelayConfig.Auth = strings.ToLower(SMTPRelayConfig.Auth)
|
|
|
|
|
|
|
|
if SMTPRelayConfig.Auth == "" || SMTPRelayConfig.Auth == "none" || SMTPRelayConfig.Auth == "false" {
|
|
|
|
SMTPRelayConfig.Auth = "none"
|
|
|
|
} else if SMTPRelayConfig.Auth == "plain" {
|
|
|
|
if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Password == "" {
|
|
|
|
return fmt.Errorf("SMTP relay host username or password not set for PLAIN authentication (%s)", c)
|
|
|
|
}
|
|
|
|
} else if strings.HasPrefix(SMTPRelayConfig.Auth, "cram") {
|
|
|
|
SMTPRelayConfig.Auth = "cram-md5"
|
|
|
|
if SMTPRelayConfig.Username == "" || SMTPRelayConfig.Secret == "" {
|
|
|
|
return fmt.Errorf("SMTP relay host username or secret not set for CRAM-MD5 authentication (%s)", c)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return fmt.Errorf("SMTP relay authentication method not supported: %s", SMTPRelayConfig.Auth)
|
|
|
|
}
|
|
|
|
|
|
|
|
ReleaseEnabled = true
|
|
|
|
|
|
|
|
logger.Log().Infof("[smtp] enabling message relaying via %s:%d", SMTPRelayConfig.Host, SMTPRelayConfig.Port)
|
|
|
|
|
2022-07-29 13:23:08 +02:00
|
|
|
return nil
|
|
|
|
}
|
2022-08-05 14:09:20 +02:00
|
|
|
|
|
|
|
// IsFile returns if a path is a file
|
|
|
|
func isFile(path string) bool {
|
|
|
|
info, err := os.Stat(path)
|
|
|
|
if os.IsNotExist(err) || !info.Mode().IsRegular() {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
2022-08-30 12:42:43 +02:00
|
|
|
|
|
|
|
// IsDir returns whether a path is a directory
|
|
|
|
func isDir(path string) bool {
|
|
|
|
info, err := os.Stat(path)
|
|
|
|
if os.IsNotExist(err) || !info.IsDir() {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|