has no remaining items, the whole
// container is removed.
func removeLargeAttachment(snapshot *DraftSnapshot, token string) error {
token = strings.TrimSpace(token)
if token == "" {
return fmt.Errorf("remove_attachment: token is empty")
}
if err := removeTokenFromIDsHeader(snapshot, token); err != nil {
return err
}
if err := removeTokenFromHTMLBody(snapshot, token); err != nil {
return err
}
return nil
}
// removeTokenFromIDsHeader removes the given token from whichever large
// attachment header is present (CLI or server format). Returns an error
// if no header is found or the token is not listed. After removal, the
// header is re-encoded in CLI format (X-Lms-Large-Attachment-Ids) so
// the server can process the update on upload.
func removeTokenFromIDsHeader(snapshot *DraftSnapshot, token string) error {
headerIdx := -1
for i, h := range snapshot.Headers {
if IsLargeAttachmentHeader(h.Name) {
headerIdx = i
break
}
}
if headerIdx < 0 {
return fmt.Errorf("remove_attachment: draft has no large attachment header")
}
items, err := decodeLargeAttachmentHeader(snapshot.Headers[headerIdx].Value)
if err != nil {
return fmt.Errorf("remove_attachment: malformed large attachment header: %w", err)
}
filtered := make([]largeAttHeaderEntry, 0, len(items))
removed := false
for _, it := range items {
if it.token() == token {
removed = true
continue
}
filtered = append(filtered, it)
}
if !removed {
return fmt.Errorf("remove_attachment: token %q not found in large attachment header", token)
}
if len(filtered) == 0 {
snapshot.Headers = append(snapshot.Headers[:headerIdx], snapshot.Headers[headerIdx+1:]...)
return nil
}
cliItems := make([]struct {
ID string `json:"id"`
}, len(filtered))
for i, it := range filtered {
cliItems[i].ID = it.token()
}
encoded, err := json.Marshal(cliItems)
if err != nil {
return fmt.Errorf("remove_attachment: failed to re-encode large attachment header: %w", err)
}
snapshot.Headers[headerIdx].Name = LargeAttachmentIDsHeader
snapshot.Headers[headerIdx].Value = base64.StdEncoding.EncodeToString(encoded)
return nil
}
// removeTokenFromHTMLBody walks the HTML body, removes the single
// large-file-item whose anchor has data-mail-token == token, and if the
// enclosing container becomes empty (no more large-file-item children),
// removes the whole container.
//
// It is not an error if the HTML body or item is missing — the header
// removal is still considered the authoritative operation. This handles
// cases where the HTML was already edited out but the header wasn't.
func removeTokenFromHTMLBody(snapshot *DraftSnapshot, token string) error {
htmlPart := FindHTMLBodyPart(snapshot.Body)
if htmlPart == nil || len(htmlPart.Body) == 0 {
return nil
}
body := string(htmlPart.Body)
newBody, changed := RemoveLargeFileItemFromHTML(body, token)
if !changed {
return nil
}
htmlPart.Body = []byte(newBody)
htmlPart.Dirty = true
return nil
}
// RemoveLargeFileItemFromHTML parses the HTML, finds the large-file-item
// containing an
whose token matches (via data-mail-token attribute or
// href URL token= parameter), removes that item, and if the enclosing
// large-file-area container becomes empty, removes the container as well.
// Returns the updated HTML and a changed flag.
func RemoveLargeFileItemFromHTML(htmlBody, token string) (string, bool) {
doc, err := xhtml.Parse(strings.NewReader(htmlBody))
if err != nil {
return htmlBody, false
}
item := findLargeFileItemByToken(doc, token)
if item == nil {
return htmlBody, false
}
container := item.Parent
// Detach the item from its parent.
if container != nil {
container.RemoveChild(item)
}
// If the container is a large-file-area and has no remaining
// large-file-item children, remove the whole container.
if container != nil && isLargeFileAreaContainer(container) && !hasLargeFileItemChild(container) {
if grand := container.Parent; grand != nil {
grand.RemoveChild(container)
}
}
var buf bytes.Buffer
if err := xhtml.Render(&buf, doc); err != nil {
return htmlBody, false
}
return stripHTMLEnvelope(buf.String()), true
}
func findLargeFileItemByToken(n *xhtml.Node, token string) *xhtml.Node {
if n == nil {
return nil
}
if n.Type == xhtml.ElementNode && n.Data == "div" && attr(n, "id") == LargeFileItemID {
if itemContainsToken(n, token) {
return n
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
if found := findLargeFileItemByToken(c, token); found != nil {
return found
}
}
return nil
}
func itemContainsToken(item *xhtml.Node, token string) bool {
if item == nil {
return false
}
for c := item.FirstChild; c != nil; c = c.NextSibling {
if c.Type == xhtml.ElementNode && c.Data == "a" {
if attr(c, LargeAttachmentTokenAttr) == token {
return true
}
if hrefContainsToken(attr(c, "href"), token) {
return true
}
}
if itemContainsToken(c, token) {
return true
}
}
return false
}
func hrefContainsToken(href, token string) bool {
if href == "" || token == "" {
return false
}
u, err := url.Parse(href)
if err != nil {
return false
}
return u.Query().Get("token") == token
}
func isLargeFileAreaContainer(n *xhtml.Node) bool {
if n == nil || n.Type != xhtml.ElementNode || n.Data != "div" {
return false
}
return strings.HasPrefix(attr(n, "id"), LargeFileContainerIDPrefix)
}
func hasLargeFileItemChild(n *xhtml.Node) bool {
if n == nil {
return false
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type == xhtml.ElementNode && c.Data == "div" && attr(c, "id") == LargeFileItemID {
return true
}
if hasLargeFileItemChild(c) {
return true
}
}
return false
}
// stripHTMLEnvelope removes the ...
// wrapper that xhtml.Parse + xhtml.Render adds around HTML fragments.
func stripHTMLEnvelope(s string) string {
s = strings.TrimPrefix(s, "")
s = strings.TrimSuffix(s, "")
return s
}