package entity import ( "net/mail" "strings" "unicode" "unicode/utf8" ) // MaxProviderLoginLength — предел длины логина у провайдера. // // Значение приходит заголовком, то есть целиком задаётся тем, кто шлёт запрос, и // без предела в колонку уехало бы столько, сколько влезет в заголовки. Число то // же, что у имени: длиннее имени логин не бывает, а два разных предела на // соседних колонках одной записи разошлись бы молча. const MaxProviderLoginLength = 255 // MaxDisplayNameLength — предел длины имени, пригодного к показу. const MaxDisplayNameLength = 255 // AcceptProviderLogin приводит пришедшее значение к годному логину либо // отвергает его. // // Отвергается пустое, состоящее из одних пробельных знаков, длиннее предела и // несущее управляющие знаки. Пустое значение — не крайний случай: обратный // прокси штатно шлёт заголовок пустым там, где никого не назвал, и без этой // проверки все неназванные собрались бы в одну учётную запись с общим архивом. // // Обрамляющие пробелы срезаются: заголовок с ведущим пробелом и без него // называет одного человека, а две записи о нём разошлись бы молча. Сравнение при // поиске остаётся точным, знак в знак: приведение регистра завело бы правило, // которого у провайдера нет. // // Живёт в домене, а не в транспорте и не в хранилище: правило одно на все // способы представиться, а уложенное куском в слой оно разошлось бы двумя // копиями при первом же втором способе. func AcceptProviderLogin(value string) (string, bool) { login := strings.TrimSpace(value) // Предел считается в **знаках**, а не в байтах: колонка считает знаки, и два // предела в разных единицах разошлись бы вдвое на любой кириллице. if login == "" || utf8.RuneCountInString(login) > MaxProviderLoginLength { return "", false } for _, r := range login { if unicode.IsControl(r) { return "", false } } return login, true } // AcceptDisplayName приводит имя к годному для колонки значению. // // Обрезается по пределу и чистится от управляющих знаков — тем же приёмом, каким // приём записи чистит имя файла отправителя. Пустое значение законно: имени у // человека может не быть вовсе, и негодное значение необязательного поля не // вправе отменять заведение учётной записи. func AcceptDisplayName(value string) string { name := strings.TrimSpace(stripControls(value)) runes := []rune(name) if len(runes) > MaxDisplayNameLength { return string(runes[:MaxDisplayNameLength]) } return name } // AcceptEmail отдаёт адрес почты, если он вообще похож на адрес. // // Негодный отбрасывается здесь, а не отказом сохранения: иначе опечатка в // заголовке кончалась бы либо отказом сервиса, либо — что хуже — ветвью «почта // занята», и владелец искал бы общий ящик там, где сломан контур. func AcceptEmail(value string) (string, bool) { email := strings.TrimSpace(value) if email == "" { return "", false } // Разбор адреса принимает и форму «Имя <адрес>»: такую строку заголовок // приносить не должен, и адресом она не считается. parsed, err := mail.ParseAddress(email) if err != nil || parsed.Name != "" || parsed.Address != email { return "", false } return email, true } // stripControls убирает управляющие знаки: они приезжают заголовком и в колонке // им делать нечего. func stripControls(value string) string { return strings.Map(func(r rune) rune { if unicode.IsControl(r) { return -1 } return r }, value) }