170 lines
5.6 KiB
Go
170 lines
5.6 KiB
Go
// This file is part of the happyDomain (R) project.
|
|
// Copyright (c) 2020-2024 happyDomain
|
|
// Authors: Pierre-Olivier Mercier, et al.
|
|
//
|
|
// This program is offered under a commercial and under the AGPL license.
|
|
// For commercial licensing, contact us at <contact@happydomain.org>.
|
|
//
|
|
// For AGPL licensing:
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package svcs
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.happydns.org/happyDomain/services/common"
|
|
)
|
|
|
|
type DMARC struct {
|
|
Version uint `json:"version" happydomain:"label=Version,placeholder=1,required,description=The version of DMARC to use.,default=1,hidden"`
|
|
Request string `json:"p" happydomain:"label=Requested Mail Receiver policy,choices=none;quarantine;reject,description=Indicates the policy to be enacted by the Receiver,required"`
|
|
SRequest string `json:"sp" happydomain:"label=Requested Mail Receiver policy for all subdomains,choices=;none;quarantaine;reject,description=Indicates the policy to be enacted by the Receiver when it receives mail for a subdomain"`
|
|
AURI []string `json:"rua" happydomain:"label=RUA,description=Addresses for aggregate feedback,placeholder=mailto:name@example.com"`
|
|
FURI []string `json:"ruf" happydomain:"label=RUF,description=Addresses for message-specific failure information,placeholder=mailto:name@example.com"`
|
|
ADKIM bool `json:"adkim" happydomain:"label=Strict DKIM Alignment"`
|
|
ASPF bool `json:"aspf" happydomain:"label=Strict SPF Alignment"`
|
|
AInterval common.Duration `json:"ri" happydomain:"label=Interval between aggregate reports"`
|
|
FailureOptions []string `json:"fo" happydomain:"label=Failure reporting options,choices=0;1;d;s"`
|
|
RegisteredFormats []string `json:"rf" happydomain:"label=Format of the failure reports,choices=;afrf"`
|
|
Percent uint8 `json:"pct" happydomain:"label=Policy applies on,description=Percentage of messages to which the DMARC policy is to be applied.,unit=%"`
|
|
}
|
|
|
|
func analyseFields(txt string) map[string]string {
|
|
ret := map[string]string{}
|
|
|
|
for _, f := range strings.Split(txt, ";") {
|
|
f = strings.TrimSpace(f)
|
|
|
|
kv := strings.SplitN(f, "=", 2)
|
|
if len(kv) == 1 {
|
|
ret[strings.TrimSpace(kv[0])] = ""
|
|
} else {
|
|
ret[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
|
|
}
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func (t *DMARC) Analyze(txt string) error {
|
|
fields := analyseFields(txt)
|
|
|
|
if v, ok := fields["v"]; ok {
|
|
if !strings.HasPrefix(v, "DMARC") {
|
|
return fmt.Errorf("not a valid DMARC record: should begin with v=DMARCv1, seen v=%q", v)
|
|
}
|
|
|
|
version, err := strconv.ParseUint(v[5:], 10, 32)
|
|
if err != nil {
|
|
return fmt.Errorf("not a valid DMARC record: bad version number: %w", err)
|
|
}
|
|
t.Version = uint(version)
|
|
} else {
|
|
return fmt.Errorf("not a valid DMARC record: version not found")
|
|
}
|
|
|
|
if p, ok := fields["p"]; ok {
|
|
t.Request = p
|
|
}
|
|
if sp, ok := fields["sp"]; ok {
|
|
t.SRequest = sp
|
|
}
|
|
if rua, ok := fields["rua"]; ok {
|
|
t.AURI = strings.Split(rua, ",")
|
|
}
|
|
if ruf, ok := fields["ruf"]; ok {
|
|
t.FURI = strings.Split(ruf, ",")
|
|
}
|
|
if adkim, ok := fields["adkim"]; ok && adkim == "s" {
|
|
t.ADKIM = true
|
|
}
|
|
if aspf, ok := fields["aspf"]; ok && aspf == "s" {
|
|
t.ASPF = true
|
|
}
|
|
if ri, ok := fields["ri"]; ok {
|
|
v, err := strconv.ParseUint(ri, 10, 32)
|
|
if err != nil {
|
|
return fmt.Errorf("not a valid DMARC record: bad interval value (ri): %w", err)
|
|
}
|
|
|
|
t.AInterval = common.Duration(v)
|
|
} else {
|
|
t.AInterval = 86400
|
|
}
|
|
if fo, ok := fields["fo"]; ok {
|
|
t.FailureOptions = strings.Split(fo, ":")
|
|
}
|
|
if rf, ok := fields["rf"]; ok {
|
|
t.RegisteredFormats = strings.Split(rf, ":")
|
|
}
|
|
if pct, ok := fields["pct"]; ok {
|
|
v, err := strconv.ParseUint(pct, 10, 8)
|
|
if err != nil {
|
|
return fmt.Errorf("not a valid DMARC record: bad percent value (prc): %w", err)
|
|
}
|
|
|
|
t.Percent = uint8(v)
|
|
} else {
|
|
t.Percent = 100
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *DMARC) String() string {
|
|
fields := []string{
|
|
fmt.Sprintf("v=DMARC%d", t.Version),
|
|
}
|
|
|
|
if t.Request != "" {
|
|
fields = append(fields, fmt.Sprintf("p=%s", t.Request))
|
|
}
|
|
if t.SRequest != "" {
|
|
fields = append(fields, fmt.Sprintf("sp=%s", t.SRequest))
|
|
}
|
|
if len(t.AURI) > 0 {
|
|
fields = append(fields, fmt.Sprintf("rua=%s", strings.Join(t.AURI, ",")))
|
|
}
|
|
if len(t.FURI) > 0 {
|
|
fields = append(fields, fmt.Sprintf("ruf=%s", strings.Join(t.FURI, ",")))
|
|
}
|
|
if t.ADKIM {
|
|
fields = append(fields, "adkim=s")
|
|
} else {
|
|
fields = append(fields, "adkim=r")
|
|
}
|
|
if t.ASPF {
|
|
fields = append(fields, "aspf=s")
|
|
} else {
|
|
fields = append(fields, "aspf=r")
|
|
}
|
|
if t.AInterval != 86400 && t.AInterval != 0 {
|
|
fields = append(fields, fmt.Sprintf("ri=%d", t.AInterval))
|
|
}
|
|
if len(t.FailureOptions) > 0 {
|
|
fields = append(fields, fmt.Sprintf("fo=%s", strings.Join(t.FailureOptions, ":")))
|
|
}
|
|
if len(t.RegisteredFormats) > 0 {
|
|
fields = append(fields, fmt.Sprintf("rf=%s", strings.Join(t.RegisteredFormats, ":")))
|
|
}
|
|
if t.Percent != 100 {
|
|
fields = append(fields, fmt.Sprintf("pct=%d", t.Percent))
|
|
}
|
|
|
|
return strings.Join(fields, ";")
|
|
}
|