2023-12-24 10:18:08 +00:00
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2024 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
2020-09-23 19:57:51 +00:00
//
2023-12-24 10:18:08 +00:00
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
2020-09-23 19:57:51 +00:00
//
2023-12-24 10:18:08 +00:00
// 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.
2020-09-23 19:57:51 +00:00
//
2023-12-24 10:18:08 +00:00
// 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.
2020-09-23 19:57:51 +00:00
//
2023-12-24 10:18:08 +00:00
// 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/>.
2020-09-23 19:57:51 +00:00
package svcs
import (
2023-12-06 03:50:30 +00:00
"fmt"
"strconv"
2020-09-23 19:57:51 +00:00
"strings"
2023-12-06 03:50:30 +00:00
"git.happydns.org/happyDomain/services/common"
2020-09-23 19:57:51 +00:00
)
2020-10-10 16:44:17 +00:00
type DMARC struct {
2023-12-06 03:50:30 +00:00
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
2020-09-23 19:57:51 +00:00
}
2020-10-10 16:44:17 +00:00
func ( t * DMARC ) String ( ) string {
2023-12-06 03:50:30 +00:00
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 ) )
2021-01-22 14:52:29 +00:00
}
2023-12-06 03:50:30 +00:00
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 ) )
}
2021-01-22 14:52:29 +00:00
return strings . Join ( fields , ";" )
2020-09-23 19:57:51 +00:00
}