1/*2Maddy Mail Server - Composable all-in-one email server.3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors45This program is free software: you can redistribute it and/or modify6it under the terms of the GNU General Public License as published by7the Free Software Foundation, either version 3 of the License, or8(at your option) any later version.910This program is distributed in the hope that it will be useful,11but WITHOUT ANY WARRANTY; without even the implied warranty of12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the13GNU General Public License for more details.1415You should have received a copy of the GNU General Public License16along with this program. If not, see <https://www.gnu.org/licenses/>.17*/1819package address2021import (22 "fmt"23 "strings"24 "unicode/utf8"2526 "github.com/foxcpp/maddy/framework/dns"27 "golang.org/x/net/idna"28 "golang.org/x/text/secure/precis"29 "golang.org/x/text/unicode/norm"30)3132// ForLookup transforms the local-part of the address into a canonical form33// usable for map lookups or direct comparisons.34//35// If Equal(addr1, addr2) == true, then ForLookup(addr1) == ForLookup(addr2).36//37// On error, case-folded addr is also returned.38func ForLookup(addr string) (string, error) {39 if addr == "" { // Null return-path case.40 return "", nil41 }4243 mbox, domain, err := Split(addr)44 if err != nil {45 return strings.ToLower(addr), err46 }4748 if domain != "" {49 domain, err = dns.ForLookup(domain)50 if err != nil {51 return strings.ToLower(addr), err52 }53 }5455 mbox = strings.ToLower(norm.NFC.String(mbox))5657 if domain == "" {58 return mbox, nil59 }6061 return mbox + "@" + domain, nil62}6364// CleanDomain returns the address with the domain part converted into its canonical form.65//66// More specifically, converts the domain part of the address to U-labels,67// normalizes it to NFC and then case-folds it.68//69// Original value is also returned on the error.70func CleanDomain(addr string) (string, error) {71 if addr == "" { // Null return-path72 return "", nil73 }7475 mbox, domain, err := Split(addr)76 if err != nil {77 return addr, err78 }7980 uDomain, err := idna.ToUnicode(domain)81 if err != nil {82 return addr, err83 }84 uDomain = strings.ToLower(norm.NFC.String(uDomain))8586 if domain == "" {87 return mbox, nil88 }8990 return mbox + "@" + uDomain, nil91}9293// Equal reports whether addr1 and addr2 are considered to be94// case-insensitively equivalent.95//96// The equivalence is defined to be the conjunction of IDN label equivalence97// for the domain part and canonical equivalence* of the local-part converted98// to lower case.99//100// * IDN label equivalence is defined by RFC 5890 Section 2.3.2.4.101// ** Canonical equivalence is defined by UAX #15.102//103// Equivalence for malformed addresses is defined using regular byte-string104// comparison with case-folding applied.105func Equal(addr1, addr2 string) bool {106 // Short circuit. If they are bit-equivalent, then they are also canonically107 // equivalent.108 if addr1 == addr2 {109 return true110 }111112 uAddr1, _ := ForLookup(addr1)113 uAddr2, _ := ForLookup(addr2)114 return uAddr1 == uAddr2115}116117func IsASCII(s string) bool {118 for _, ch := range s {119 if ch > utf8.RuneSelf {120 return false121 }122 }123 return true124}125126func FQDNDomain(addr string) string {127 if strings.HasSuffix(addr, ".") {128 return addr129 }130 return addr + "."131}132133// PRECISFold applies UsernameCaseMapped to the local part and dns.ForLookup134// to domain part of the address.135func PRECISFold(addr string) (string, error) {136 return precisEmail(addr, precis.UsernameCaseMapped)137}138139// PRECIS applies UsernameCasePreserved to the local part and dns.ForLookup140// to domain part of the address.141func PRECIS(addr string) (string, error) {142 return precisEmail(addr, precis.UsernameCasePreserved)143}144145func precisEmail(addr string, profile *precis.Profile) (string, error) {146 mbox, domain, err := Split(addr)147 if err != nil {148 return "", fmt.Errorf("address: precis: %w", err)149 }150151 // PRECISFold is not included in the regular address.ForLookup since it reduces152 // the range of valid addresses to a subset of actually valid values.153 // PRECISFold is a matter of our own local policy, not a general rule for all154 // email addresses.155156 // Side note: For used profiles, there is no practical difference between157 // CompareKey and String.158 mbox, err = profile.CompareKey(mbox)159 if err != nil {160 return "", fmt.Errorf("address: precis: %w", err)161 }162163 domain, err = dns.ForLookup(domain)164 if err != nil {165 return "", fmt.Errorf("address: precis: %w", err)166 }167168 return mbox + "@" + domain, nil169}