maddy

Fork https://github.com/foxcpp/maddy

git clone git://git.lin.moe/go/maddy.git

  1/*
  2Maddy Mail Server - Composable all-in-one email server.
  3Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors
  4
  5This program is free software: you can redistribute it and/or modify
  6it under the terms of the GNU General Public License as published by
  7the Free Software Foundation, either version 3 of the License, or
  8(at your option) any later version.
  9
 10This program is distributed in the hope that it will be useful,
 11but WITHOUT ANY WARRANTY; without even the implied warranty of
 12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13GNU General Public License for more details.
 14
 15You should have received a copy of the GNU General Public License
 16along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17*/
 18
 19package address
 20
 21import (
 22	"fmt"
 23	"strings"
 24	"unicode/utf8"
 25
 26	"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)
 31
 32// ForLookup transforms the local-part of the address into a canonical form
 33// 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 "", nil
 41	}
 42
 43	mbox, domain, err := Split(addr)
 44	if err != nil {
 45		return strings.ToLower(addr), err
 46	}
 47
 48	if domain != "" {
 49		domain, err = dns.ForLookup(domain)
 50		if err != nil {
 51			return strings.ToLower(addr), err
 52		}
 53	}
 54
 55	mbox = strings.ToLower(norm.NFC.String(mbox))
 56
 57	if domain == "" {
 58		return mbox, nil
 59	}
 60
 61	return mbox + "@" + domain, nil
 62}
 63
 64// 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-path
 72		return "", nil
 73	}
 74
 75	mbox, domain, err := Split(addr)
 76	if err != nil {
 77		return addr, err
 78	}
 79
 80	uDomain, err := idna.ToUnicode(domain)
 81	if err != nil {
 82		return addr, err
 83	}
 84	uDomain = strings.ToLower(norm.NFC.String(uDomain))
 85
 86	if domain == "" {
 87		return mbox, nil
 88	}
 89
 90	return mbox + "@" + uDomain, nil
 91}
 92
 93// Equal reports whether addr1 and addr2 are considered to be
 94// case-insensitively equivalent.
 95//
 96// The equivalence is defined to be the conjunction of IDN label equivalence
 97// for the domain part and canonical equivalence* of the local-part converted
 98// 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-string
104// comparison with case-folding applied.
105func Equal(addr1, addr2 string) bool {
106	// Short circuit. If they are bit-equivalent, then they are also canonically
107	// equivalent.
108	if addr1 == addr2 {
109		return true
110	}
111
112	uAddr1, _ := ForLookup(addr1)
113	uAddr2, _ := ForLookup(addr2)
114	return uAddr1 == uAddr2
115}
116
117func IsASCII(s string) bool {
118	for _, ch := range s {
119		if ch > utf8.RuneSelf {
120			return false
121		}
122	}
123	return true
124}
125
126func FQDNDomain(addr string) string {
127	if strings.HasSuffix(addr, ".") {
128		return addr
129	}
130	return addr + "."
131}
132
133// PRECISFold applies UsernameCaseMapped to the local part and dns.ForLookup
134// to domain part of the address.
135func PRECISFold(addr string) (string, error) {
136	return precisEmail(addr, precis.UsernameCaseMapped)
137}
138
139// PRECIS applies UsernameCasePreserved to the local part and dns.ForLookup
140// to domain part of the address.
141func PRECIS(addr string) (string, error) {
142	return precisEmail(addr, precis.UsernameCasePreserved)
143}
144
145func 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	}
150
151	// PRECISFold is not included in the regular address.ForLookup since it reduces
152	// 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 all
154	// email addresses.
155
156	// Side note: For used profiles, there is no practical difference between
157	// CompareKey and String.
158	mbox, err = profile.CompareKey(mbox)
159	if err != nil {
160		return "", fmt.Errorf("address: precis: %w", err)
161	}
162
163	domain, err = dns.ForLookup(domain)
164	if err != nil {
165		return "", fmt.Errorf("address: precis: %w", err)
166	}
167
168	return mbox + "@" + domain, nil
169}