mirror of
https://github.com/jeremyd/ergo.git
synced 2026-07-27 01:18:10 -07:00
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2020 Shivaram Lingamneni
|
||||
// released under the MIT license
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"errors"
|
||||
dkim "github.com/toorop/go-dkim"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingFields = errors.New("DKIM config is missing fields")
|
||||
)
|
||||
|
||||
type DKIMConfig struct {
|
||||
Domain string
|
||||
Selector string
|
||||
KeyFile string `yaml:"key-file"`
|
||||
keyBytes []byte
|
||||
}
|
||||
|
||||
func (dkim *DKIMConfig) Postprocess() (err error) {
|
||||
if dkim.Domain != "" {
|
||||
if dkim.Selector == "" || dkim.KeyFile == "" {
|
||||
return ErrMissingFields
|
||||
}
|
||||
dkim.keyBytes, err = ioutil.ReadFile(dkim.KeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var defaultOptions = dkim.SigOptions{
|
||||
Version: 1,
|
||||
Canonicalization: "relaxed/relaxed",
|
||||
Algo: "rsa-sha256",
|
||||
Headers: []string{"from", "to", "subject", "message-id"},
|
||||
BodyLength: 0,
|
||||
QueryMethods: []string{"dns/txt"},
|
||||
AddSignatureTimestamp: true,
|
||||
SignatureExpireIn: 0,
|
||||
}
|
||||
|
||||
func DKIMSign(message []byte, dkimConfig DKIMConfig) (result []byte, err error) {
|
||||
options := defaultOptions
|
||||
options.PrivateKey = dkimConfig.keyBytes
|
||||
options.Domain = dkimConfig.Domain
|
||||
options.Selector = dkimConfig.Selector
|
||||
err = dkim.Sign(&message, options)
|
||||
return message, err
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) 2020 Shivaram Lingamneni
|
||||
// released under the MIT license
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/oragono/oragono/irc/smtp"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBlacklistedAddress = errors.New("Email address is blacklisted")
|
||||
ErrInvalidAddress = errors.New("Email address is blacklisted")
|
||||
ErrNoMXRecord = errors.New("Couldn't resolve MX record")
|
||||
)
|
||||
|
||||
type MTAConfig struct {
|
||||
Server string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
type MailtoConfig struct {
|
||||
// legacy config format assumed the use of an MTA/smarthost,
|
||||
// so server, port, etc. appear directly at top level
|
||||
// XXX: see https://github.com/go-yaml/yaml/issues/63
|
||||
MTAConfig `yaml:",inline"`
|
||||
Sender string
|
||||
HeloDomain string `yaml:"helo-domain"`
|
||||
RequireTLS bool `yaml:"require-tls"`
|
||||
VerifyMessageSubject string `yaml:"verify-message-subject"`
|
||||
DKIM DKIMConfig
|
||||
MTAReal MTAConfig `yaml:"mta"`
|
||||
BlacklistRegexes []string `yaml:"blacklist-regexes"`
|
||||
blacklistRegexes []*regexp.Regexp
|
||||
}
|
||||
|
||||
func (config *MailtoConfig) Postprocess(heloDomain string) (err error) {
|
||||
if config.Sender == "" {
|
||||
return errors.New("Invalid mailto sender address")
|
||||
}
|
||||
|
||||
// check for MTA config fields at top level,
|
||||
// copy to MTAReal if present
|
||||
if config.Server != "" && config.MTAReal.Server == "" {
|
||||
config.MTAReal = config.MTAConfig
|
||||
}
|
||||
|
||||
if config.HeloDomain == "" {
|
||||
config.HeloDomain = heloDomain
|
||||
}
|
||||
|
||||
for _, reg := range config.BlacklistRegexes {
|
||||
compiled, err := regexp.Compile(fmt.Sprintf("^%s$", reg))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.blacklistRegexes = append(config.blacklistRegexes, compiled)
|
||||
}
|
||||
|
||||
if config.MTAConfig.Server != "" {
|
||||
// smarthost, nothing more to validate
|
||||
return nil
|
||||
}
|
||||
|
||||
return config.DKIM.Postprocess()
|
||||
}
|
||||
|
||||
// get the preferred MX record hostname, "" on error
|
||||
func lookupMX(domain string) (server string) {
|
||||
var minPref uint16
|
||||
results, err := net.LookupMX(domain)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, result := range results {
|
||||
if minPref == 0 || result.Pref < minPref {
|
||||
server, minPref = result.Host, result.Pref
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func SendMail(config MailtoConfig, recipient string, msg []byte) (err error) {
|
||||
for _, reg := range config.blacklistRegexes {
|
||||
if reg.MatchString(recipient) {
|
||||
return ErrBlacklistedAddress
|
||||
}
|
||||
}
|
||||
|
||||
if config.DKIM.Domain != "" {
|
||||
msg, err = DKIMSign(msg, config.DKIM)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var addr string
|
||||
var auth smtp.Auth
|
||||
if config.MTAReal.Server != "" {
|
||||
addr = fmt.Sprintf("%s:%d", config.MTAReal.Server, config.MTAReal.Port)
|
||||
if config.MTAReal.Username != "" && config.MTAReal.Password != "" {
|
||||
auth = smtp.PlainAuth("", config.MTAReal.Username, config.MTAReal.Password, config.MTAReal.Server)
|
||||
}
|
||||
} else {
|
||||
idx := strings.IndexByte(recipient, '@')
|
||||
if idx == -1 {
|
||||
return ErrInvalidAddress
|
||||
}
|
||||
mx := lookupMX(recipient[idx+1:])
|
||||
if mx == "" {
|
||||
return ErrNoMXRecord
|
||||
}
|
||||
addr = fmt.Sprintf("%s:smtp", mx)
|
||||
}
|
||||
|
||||
return smtp.SendMail(addr, auth, config.HeloDomain, config.Sender, []string{recipient}, msg, config.RequireTLS)
|
||||
}
|
||||
Reference in New Issue
Block a user