mirror of
https://github.com/axllent/mailpit.git
synced 2026-03-03 03:57:01 +00:00
222 lines
5.6 KiB
Go
222 lines
5.6 KiB
Go
package smtpd
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net/smtp"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/axllent/mailpit/config"
|
|
"github.com/axllent/mailpit/internal/logger"
|
|
"github.com/axllent/mailpit/internal/tools"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// Wrapper to auto relay messages if configured
|
|
func autoRelayMessage(from string, to []string, data *[]byte) error {
|
|
if config.SMTPRelayConfig.BlockedRecipientsRegexp != nil {
|
|
filteredTo := []string{}
|
|
for _, address := range to {
|
|
if config.SMTPRelayConfig.BlockedRecipientsRegexp.MatchString(address) {
|
|
logger.Log().Debugf("[relay] ignoring auto-relay to %s: found in blocklist", address)
|
|
continue
|
|
}
|
|
|
|
filteredTo = append(filteredTo, address)
|
|
}
|
|
to = filteredTo
|
|
}
|
|
|
|
if len(to) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if config.SMTPRelayAll {
|
|
if err := Relay(from, to, *data); err != nil {
|
|
return errors.WithMessage(err, "[relay] error")
|
|
}
|
|
|
|
logger.Log().Debugf(
|
|
"[relay] sent message to %s from %s via %s:%d",
|
|
strings.Join(to, ", "), from, config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port,
|
|
)
|
|
} else if config.SMTPRelayMatchingRegexp != nil {
|
|
filtered := []string{}
|
|
for _, t := range to {
|
|
if config.SMTPRelayMatchingRegexp.MatchString(t) {
|
|
filtered = append(filtered, t)
|
|
}
|
|
}
|
|
|
|
if len(filtered) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if err := Relay(from, filtered, *data); err != nil {
|
|
return errors.WithMessage(err, "[relay] error")
|
|
}
|
|
|
|
logger.Log().Debugf(
|
|
"[relay] auto-relay message to %s from %s via %s:%d",
|
|
strings.Join(filtered, ", "), from, config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func createRelaySMTPClient(config config.SMTPRelayConfigStruct, addr string) (*smtp.Client, error) {
|
|
if config.TLS {
|
|
tlsConf := &tls.Config{ServerName: config.Host} // #nosec
|
|
tlsConf.InsecureSkipVerify = config.AllowInsecure
|
|
|
|
conn, err := tls.Dial("tcp", addr, tlsConf)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("TLS dial error: %v", err)
|
|
}
|
|
|
|
client, err := smtp.NewClient(conn, tlsConf.ServerName)
|
|
if err != nil {
|
|
_ = conn.Close()
|
|
return nil, fmt.Errorf("SMTP client error: %v", err)
|
|
}
|
|
|
|
// Note: The caller is responsible for closing the client
|
|
return client, nil
|
|
}
|
|
|
|
client, err := smtp.Dial(addr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error connecting to %s: %v", addr, err)
|
|
}
|
|
|
|
// Set the hostname for HELO/EHLO
|
|
if hostname, err := os.Hostname(); err == nil {
|
|
if err := client.Hello(hostname); err != nil {
|
|
return nil, fmt.Errorf("error saying HELO/EHLO to %s: %v", addr, err)
|
|
}
|
|
}
|
|
|
|
if config.STARTTLS {
|
|
tlsConf := &tls.Config{ServerName: config.Host} // #nosec
|
|
tlsConf.InsecureSkipVerify = config.AllowInsecure
|
|
|
|
if err = client.StartTLS(tlsConf); err != nil {
|
|
_ = client.Close()
|
|
return nil, fmt.Errorf("error creating StartTLS config: %v", err)
|
|
}
|
|
}
|
|
|
|
// Note: The caller is responsible for closing the client
|
|
return client, nil
|
|
}
|
|
|
|
// Relay will connect to a pre-configured SMTP server and send a message to one or more recipients.
|
|
func Relay(from string, to []string, msg []byte) error {
|
|
addr := fmt.Sprintf("%s:%d", config.SMTPRelayConfig.Host, config.SMTPRelayConfig.Port)
|
|
|
|
c, err := createRelaySMTPClient(config.SMTPRelayConfig, addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = c.Close() }()
|
|
|
|
auth := relayAuthFromConfig()
|
|
|
|
if auth != nil {
|
|
if err = c.Auth(auth); err != nil {
|
|
return fmt.Errorf("error response to AUTH command: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
if config.SMTPRelayConfig.OverrideFrom != "" {
|
|
msg, err = tools.OverrideFromHeader(msg, config.SMTPRelayConfig.OverrideFrom)
|
|
if err != nil {
|
|
return fmt.Errorf("error overriding From header: %s", err.Error())
|
|
}
|
|
|
|
from = config.SMTPRelayConfig.OverrideFrom
|
|
}
|
|
|
|
if err = c.Mail(from); err != nil {
|
|
return errors.WithMessage(err, "error sending MAIL command")
|
|
}
|
|
|
|
for _, addr := range to {
|
|
if err = c.Rcpt(addr); err != nil {
|
|
logger.Log().Warnf("error response to RCPT command for %s: %s", addr, err.Error())
|
|
if config.SMTPRelayConfig.ForwardSMTPErrors {
|
|
return errors.WithMessagef(err, "error response to RCPT command for %s", addr)
|
|
}
|
|
}
|
|
}
|
|
|
|
w, err := c.Data()
|
|
if err != nil {
|
|
return errors.WithMessage(err, "error response to DATA command")
|
|
}
|
|
|
|
if _, err := w.Write(msg); err != nil {
|
|
return errors.WithMessage(err, "error sending message")
|
|
}
|
|
|
|
if err := w.Close(); err != nil {
|
|
return errors.WithMessage(err, "error closing connection")
|
|
}
|
|
|
|
return c.Quit()
|
|
}
|
|
|
|
// Return the SMTP relay authentication based on config
|
|
func relayAuthFromConfig() smtp.Auth {
|
|
var a smtp.Auth
|
|
|
|
if config.SMTPRelayConfig.Auth == "plain" {
|
|
a = smtp.PlainAuth("", config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Password, config.SMTPRelayConfig.Host)
|
|
}
|
|
|
|
if config.SMTPRelayConfig.Auth == "login" {
|
|
a = LoginAuth(config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Password)
|
|
}
|
|
|
|
if config.SMTPRelayConfig.Auth == "cram-md5" {
|
|
a = smtp.CRAMMD5Auth(config.SMTPRelayConfig.Username, config.SMTPRelayConfig.Secret)
|
|
}
|
|
|
|
return a
|
|
}
|
|
|
|
// Custom implementation of LOGIN SMTP authentication
|
|
// @see https://gist.github.com/andelf/5118732
|
|
type loginAuth struct {
|
|
username, password string
|
|
}
|
|
|
|
// LoginAuth authentication
|
|
func LoginAuth(username, password string) smtp.Auth {
|
|
return &loginAuth{
|
|
username,
|
|
password,
|
|
}
|
|
}
|
|
|
|
func (a *loginAuth) Start(_ *smtp.ServerInfo) (string, []byte, error) {
|
|
return "LOGIN", []byte{}, nil
|
|
}
|
|
|
|
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
|
if more {
|
|
switch string(fromServer) {
|
|
case "Username:":
|
|
return []byte(a.username), nil
|
|
case "Password:":
|
|
return []byte(a.password), nil
|
|
default:
|
|
return nil, errors.New("unknown fromServer")
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|