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

338 lines
8.1 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package draft
import (
"bytes"
"encoding/base64"
"fmt"
"math/rand"
"mime"
"mime/quotedprintable"
"strings"
)
func Serialize(snapshot *DraftSnapshot) (string, error) {
if snapshot == nil || snapshot.Body == nil {
return "", fmt.Errorf("draft snapshot is empty")
}
var buf bytes.Buffer
mimeVersionValue := "1.0"
wroteMimeVersion := false
for _, header := range snapshot.Headers {
if strings.EqualFold(header.Name, "MIME-Version") {
mimeVersionValue = header.Value
writeHeader(&buf, header.Name, header.Value)
wroteMimeVersion = true
continue
}
if isBodyHeader(header.Name) {
continue
}
writeHeader(&buf, header.Name, header.Value)
}
if !wroteMimeVersion {
writeHeader(&buf, "MIME-Version", mimeVersionValue)
}
if err := writeTopLevelBody(&buf, snapshot.Body); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(buf.Bytes()), nil
}
func writeTopLevelBody(buf *bytes.Buffer, root *Part) error {
if canReuseRawEntity(root) {
buf.Write(root.RawEntity)
if len(root.RawEntity) == 0 || root.RawEntity[len(root.RawEntity)-1] != '\n' {
buf.WriteByte('\n')
}
return nil
}
if root.IsMultipart() {
for _, header := range orderedPartHeaders(root, false) {
writeHeader(buf, header.Name, header.Value)
}
buf.WriteByte('\n')
return writeMultipartBody(buf, root)
}
for _, header := range orderedPartHeaders(root, true) {
writeHeader(buf, header.Name, header.Value)
}
buf.WriteByte('\n')
return writeLeafBody(buf, root)
}
func writeMultipartBody(buf *bytes.Buffer, part *Part) error {
boundary := part.MediaParams["boundary"]
if boundary == "" {
boundary = newBoundary()
part.MediaParams["boundary"] = boundary
}
if len(part.Preamble) > 0 {
buf.Write(part.Preamble)
if part.Preamble[len(part.Preamble)-1] != '\n' {
buf.WriteByte('\n')
}
}
for _, child := range part.Children {
if child == nil {
continue
}
fmt.Fprintf(buf, "--%s\n", boundary)
if canReuseRawEntity(child) {
buf.Write(child.RawEntity)
if n := len(child.RawEntity); n == 0 || child.RawEntity[n-1] != '\n' {
buf.WriteByte('\n')
}
continue
}
if child.IsMultipart() {
for _, header := range orderedPartHeaders(child, false) {
writeHeader(buf, header.Name, header.Value)
}
buf.WriteByte('\n')
if err := writeMultipartBody(buf, child); err != nil {
return err
}
continue
}
for _, header := range orderedPartHeaders(child, true) {
writeHeader(buf, header.Name, header.Value)
}
buf.WriteByte('\n')
if err := writeLeafBody(buf, child); err != nil {
return err
}
}
fmt.Fprintf(buf, "--%s--\n", boundary)
if len(part.Epilogue) > 0 {
buf.Write(part.Epilogue)
if part.Epilogue[len(part.Epilogue)-1] != '\n' {
buf.WriteByte('\n')
}
}
return nil
}
func orderedPartHeaders(part *Part, includeCTE bool) []Header {
contentTypeValue := existingHeaderValue(part.Headers, "Content-Type")
if contentTypeValue == "" {
contentTypeValue = mime.FormatMediaType(part.MediaType, cloneStringMap(part.MediaParams))
}
headers := make([]Header, 0, len(part.Headers)+4)
replacements := map[string]Header{
"content-type": {
Name: "Content-Type",
Value: contentTypeValue,
},
}
if includeCTE {
if cte := chooseTransferEncoding(part); cte != "" {
value := cte
if existing := existingHeaderValue(part.Headers, "Content-Transfer-Encoding"); strings.EqualFold(existing, cte) {
value = existing
}
replacements["content-transfer-encoding"] = Header{
Name: "Content-Transfer-Encoding",
Value: value,
}
}
}
if part.ContentDisposition != "" {
value := existingHeaderValue(part.Headers, "Content-Disposition")
if value == "" {
value = mime.FormatMediaType(part.ContentDisposition, cloneStringMap(part.ContentDispositionArg))
}
replacements["content-disposition"] = Header{
Name: "Content-Disposition",
Value: value,
}
}
if part.ContentID != "" {
value := existingHeaderValue(part.Headers, "Content-ID")
if value == "" {
value = "<" + part.ContentID + ">"
}
replacements["content-id"] = Header{Name: "Content-ID", Value: value}
}
written := make(map[string]bool, len(replacements))
for _, header := range part.Headers {
name := strings.ToLower(header.Name)
switch name {
case "mime-version":
continue
case "content-type", "content-transfer-encoding", "content-disposition", "content-id":
if replacement, ok := replacements[name]; ok {
replacement.Name = header.Name
headers = append(headers, replacement)
written[name] = true
}
default:
headers = append(headers, header)
}
}
for _, key := range []string{"content-type", "content-transfer-encoding", "content-disposition", "content-id"} {
if written[key] {
continue
}
if replacement, ok := replacements[key]; ok {
headers = append(headers, replacement)
}
}
return headers
}
func chooseTransferEncoding(part *Part) string {
if part.IsMultipart() {
return ""
}
switch {
case part.ContentDisposition == "attachment":
return "base64"
case strings.HasPrefix(part.MediaType, "text/"):
switch strings.ToLower(strings.TrimSpace(part.TransferEncoding)) {
case "quoted-printable":
return "quoted-printable"
case "base64":
if hasNonASCII(part.Body) {
return "base64"
}
}
if hasNonASCII(part.Body) {
return "quoted-printable"
}
return "7bit"
default:
return "base64"
}
}
func writeLeafBody(buf *bytes.Buffer, part *Part) error {
body, err := encodedLeafBody(part)
if err != nil {
return err
}
cte := chooseTransferEncoding(part)
switch cte {
case "base64":
writeFoldedBody(buf, base64.StdEncoding.EncodeToString(body), 76)
case "quoted-printable":
writer := quotedprintable.NewWriter(buf)
if _, err := writer.Write(body); err != nil {
_ = writer.Close()
return err
}
if err := writer.Close(); err != nil {
return err
}
if buf.Len() == 0 || buf.Bytes()[buf.Len()-1] != '\n' {
buf.WriteByte('\n')
}
default:
if len(body) > 0 {
buf.Write(body)
if body[len(body)-1] != '\n' {
buf.WriteByte('\n')
}
} else {
buf.WriteByte('\n')
}
}
return nil
}
func writeFoldedBody(buf *bytes.Buffer, encoded string, width int) {
if width <= 0 {
width = 76
}
for len(encoded) > width {
buf.WriteString(encoded[:width])
buf.WriteByte('\n')
encoded = encoded[width:]
}
if encoded != "" {
buf.WriteString(encoded)
buf.WriteByte('\n')
}
}
func writeHeader(buf *bytes.Buffer, name, value string) {
// Strip CR and LF as a last-resort defense against header injection.
// Callers (applyOp, Validate) already reject CR/LF explicitly; this
// sanitisation covers any path that bypasses those checks.
name = strings.NewReplacer("\r", "", "\n", "").Replace(name)
value = strings.NewReplacer("\r", "", "\n", "").Replace(value)
buf.WriteString(name)
buf.WriteString(": ")
buf.WriteString(value)
buf.WriteByte('\n')
}
func existingHeaderValue(headers []Header, name string) string {
for _, header := range headers {
if strings.EqualFold(header.Name, name) {
return header.Value
}
}
return ""
}
func canReuseRawEntity(part *Part) bool {
if part == nil || len(part.RawEntity) == 0 {
return false
}
return !partHasDirty(part)
}
func partHasDirty(part *Part) bool {
if part == nil {
return false
}
if part.Dirty {
return true
}
for _, child := range part.Children {
if partHasDirty(child) {
return true
}
}
return false
}
func hasNonASCII(body []byte) bool {
for _, b := range body {
if b > 127 {
return true
}
}
return false
}
func encodedLeafBody(part *Part) ([]byte, error) {
if part == nil {
return nil, nil
}
if !isTextualMediaType(part.MediaType) {
return part.Body, nil
}
charsetLabel := normalizeCharsetLabel(part.MediaParams["charset"])
if charsetLabel == "" {
charsetLabel = "UTF-8"
part.MediaParams["charset"] = charsetLabel
}
encoded, err := encodeTextCharset(part.Body, charsetLabel)
if err == nil {
return encoded, nil
}
part.MediaParams["charset"] = "UTF-8"
syncStructuredPartHeaders(part)
return part.Body, nil
}
func newBoundary() string {
return fmt.Sprintf("lark-draft-%d-%d", rand.Int63(), rand.Int63())
}