// 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 . // // 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 . 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, ";") }