Files
larksuite-cli/shortcuts/mail/address.go
梁硕 83dfb068ad feat: open-source lark-cli — the official CLI for Lark/Feishu
Change-Id: I113d9cdb5403cec347efe4595415e34a18b7decf
2026-03-28 10:36:25 +08:00

115 lines
3.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package mail
import (
"encoding/base64"
"strings"
)
// Mailbox is a parsed RFC 2822 address: an optional display name plus an
// email address. The zero value represents a bare address with no name.
type Mailbox struct {
Name string // display name; empty if not present
Email string
}
// ParseMailbox parses a single address in any of the following forms:
//
// alice@example.com
// Alice Smith <alice@example.com>
// "Alice Smith" <alice@example.com>
//
// The function is intentionally total (never returns an error): syntactic
// validation of the email address is left to the Lark API. Control
// characters are stripped as a defense against header injection.
func ParseMailbox(raw string) Mailbox {
raw = strings.TrimSpace(raw)
if lt := strings.LastIndex(raw, "<"); lt >= 0 {
if gt := strings.Index(raw[lt:], ">"); gt >= 0 {
email := sanitizeControlChars(strings.TrimSpace(raw[lt+1 : lt+gt]))
namePart := strings.TrimSpace(raw[:lt])
// Strip surrounding quotes: "Alice" → Alice
namePart = strings.TrimPrefix(namePart, `"`)
namePart = strings.TrimSuffix(namePart, `"`)
return Mailbox{Name: sanitizeControlChars(namePart), Email: email}
}
}
return Mailbox{Email: sanitizeControlChars(raw)}
}
// ParseMailboxList splits a comma-separated address list and parses each
// entry. Entries with an empty email address are silently dropped.
func ParseMailboxList(raw string) []Mailbox {
var out []Mailbox
for _, part := range splitAddressList(raw) {
m := ParseMailbox(part)
if m.Email != "" {
out = append(out, m)
}
}
return out
}
// String formats the mailbox for an RFC 2822 header value.
// Non-ASCII display names are encoded using RFC 2047.
func (m Mailbox) String() string {
if m.Name == "" {
return m.Email
}
return encodeHeader(m.Name) + " <" + m.Email + ">"
}
// sanitizeControlChars strips ASCII control characters (0x000x1F, 0x7F)
// from a string. This is applied at the address-parse boundary as a
// defence-in-depth measure against CRLF injection: an attacker who controls
// a display name or email value cannot smuggle extra header lines.
func sanitizeControlChars(s string) string {
var b strings.Builder
b.Grow(len(s))
for _, r := range s {
if r >= 0x20 && r != 0x7F {
b.WriteRune(r)
}
}
return b.String()
}
// encodeHeader encodes a header value that contains non-ASCII characters
// using RFC 2047 base64 ("B") encoding. ASCII-only values are returned
// unchanged.
func encodeHeader(val string) string {
for _, r := range val {
if r > 127 {
return "=?UTF-8?B?" + base64.StdEncoding.EncodeToString([]byte(val)) + "?="
}
}
return val
}
// splitAddressList splits a raw comma-separated address list while respecting
// quoted strings (so a display name like `"Doe, Jane" <j@x>` is not split on
// the comma inside the quotes).
func splitAddressList(raw string) []string {
var parts []string
var cur strings.Builder
inQuote := false
for _, r := range raw {
switch {
case r == '"':
inQuote = !inQuote
cur.WriteRune(r)
case r == ',' && !inQuote:
parts = append(parts, strings.TrimSpace(cur.String()))
cur.Reset()
default:
cur.WriteRune(r)
}
}
if s := strings.TrimSpace(cur.String()); s != "" {
parts = append(parts, s)
}
return parts
}