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 "errors"23 "strings"24)2526// Split splits a email address (as defined by RFC 5321 as a forward-path27// token) into local part (mailbox) and domain.28//29// Note that definition of the forward-path token includes the special30// 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 the35// output.36func Split(addr string) (mailbox, domain string, err error) {37 if strings.EqualFold(addr, "postmaster") {38 return addr, "", nil39 }4041 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 return54}5556// UnquoteMbox undoes escaping and quoting of the local-part. That is, for57// local-part `"test\" @ test"` it will return `test" @test`.58func UnquoteMbox(mbox string) (string, error) {59 var (60 quoted bool61 escaped bool62 terminatedQuote bool63 mailboxB strings.Builder64 )65 for _, ch := range mbox {66 if terminatedQuote {67 return "", errors.New("address: closing quote should be right before at-sign")68 }6970 switch ch {71 case '"':72 if !escaped {73 quoted = !quoted74 if !quoted {75 terminatedQuote = true76 }77 continue78 }79 case '\\':80 if !escaped {81 if !quoted {82 return "", errors.New("address: escapes are allowed only in quoted strings")83 }84 escaped = true85 continue86 }87 case '@':88 if !quoted {89 return "", errors.New("address: extra at-sign in non-quoted local-part")90 }91 }9293 escaped = false9495 mailboxB.WriteRune(ch)96 }9798 if mailboxB.Len() == 0 {99 return "", errors.New("address: empty local part")100 }101102 return mailboxB.String(), nil103}104105// "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}112113func QuoteMbox(mbox string) string {114 var mailboxEsc strings.Builder115 mailboxEsc.Grow(len(mbox))116 quoted := false117 for _, ch := range mbox {118 if _, ok := mboxSpecial[ch]; ok {119 if ch == '\\' || ch == '"' {120 mailboxEsc.WriteRune('\\')121 }122 mailboxEsc.WriteRune(ch)123 quoted = true124 } else {125 mailboxEsc.WriteRune(ch)126 }127 }128 if quoted {129 return `"` + mailboxEsc.String() + `"`130 }131 return mbox132}