项目文件夹

文件
wehub-resource-sync bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

115 行
3.3 KiB
Go

// 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 (0x00–0x1F, 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
}