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	"errors"
 23	"strings"
 24)
 25
 26// Split splits a email address (as defined by RFC 5321 as a forward-path
 27// token) into local part (mailbox) and domain.
 28//
 29// Note that definition of the forward-path token includes the special
 30// postmaster address without the domain part. Split will return domain == ""
 31// in this case.
 32//
 33// Split does almost no sanity checks on the input and is intentionally naive.
 34// If this is a concern, ValidMailbox and ValidDomain should be used on the
 35// output.
 36func Split(addr string) (mailbox, domain string, err error) {
 37	if strings.EqualFold(addr, "postmaster") {
 38		return addr, "", nil
 39	}
 40
 41	indx := strings.LastIndexByte(addr, '@')
 42	if indx == -1 {
 43		return "", "", errors.New("address: missing at-sign")
 44	}
 45	mailbox = addr[:indx]
 46	domain = addr[indx+1:]
 47	if mailbox == "" {
 48		return "", "", errors.New("address: empty local-part")
 49	}
 50	if domain == "" {
 51		return "", "", errors.New("address: empty domain")
 52	}
 53	return
 54}
 55
 56// UnquoteMbox undoes escaping and quoting of the local-part.  That is, for
 57// local-part `"test\" @ test"` it will return `test" @test`.
 58func UnquoteMbox(mbox string) (string, error) {
 59	var (
 60		quoted          bool
 61		escaped         bool
 62		terminatedQuote bool
 63		mailboxB        strings.Builder
 64	)
 65	for _, ch := range mbox {
 66		if terminatedQuote {
 67			return "", errors.New("address: closing quote should be right before at-sign")
 68		}
 69
 70		switch ch {
 71		case '"':
 72			if !escaped {
 73				quoted = !quoted
 74				if !quoted {
 75					terminatedQuote = true
 76				}
 77				continue
 78			}
 79		case '\\':
 80			if !escaped {
 81				if !quoted {
 82					return "", errors.New("address: escapes are allowed only in quoted strings")
 83				}
 84				escaped = true
 85				continue
 86			}
 87		case '@':
 88			if !quoted {
 89				return "", errors.New("address: extra at-sign in non-quoted local-part")
 90			}
 91		}
 92
 93		escaped = false
 94
 95		mailboxB.WriteRune(ch)
 96	}
 97
 98	if mailboxB.Len() == 0 {
 99		return "", errors.New("address: empty local part")
100	}
101
102	return mailboxB.String(), nil
103}
104
105// "specials" from RFC5322 grammar with dot removed (it is defined in grammar separately, for some reason)
106var mboxSpecial = map[rune]struct{}{
107	'(': {}, ')': {}, '<': {}, '>': {},
108	'[': {}, ']': {}, ':': {}, ';': {},
109	'@': {}, '\\': {}, ',': {},
110	'"': {}, ' ': {},
111}
112
113func QuoteMbox(mbox string) string {
114	var mailboxEsc strings.Builder
115	mailboxEsc.Grow(len(mbox))
116	quoted := false
117	for _, ch := range mbox {
118		if _, ok := mboxSpecial[ch]; ok {
119			if ch == '\\' || ch == '"' {
120				mailboxEsc.WriteRune('\\')
121			}
122			mailboxEsc.WriteRune(ch)
123			quoted = true
124		} else {
125			mailboxEsc.WriteRune(ch)
126		}
127	}
128	if quoted {
129		return `"` + mailboxEsc.String() + `"`
130	}
131	return mbox
132}