zip.go raw
1 package bridge
2
3 import (
4 "archive/zip"
5 "bytes"
6 "fmt"
7
8 bridgesmtp "next.orly.dev/pkg/bridge/smtp"
9 )
10
11 const maxZipBytes = 25 * 1024 * 1024 // 25MB per spec
12
13 // ZipParts bundles non-plaintext MIME parts (HTML + attachments) into a
14 // flat zip archive. The text/plain body is excluded — it goes directly
15 // into the DM content.
16 func ZipParts(htmlBody string, attachments []bridgesmtp.Attachment) ([]byte, error) {
17 var buf bytes.Buffer
18 w := zip.NewWriter(&buf)
19
20 if htmlBody != "" {
21 f, err := w.Create("body.html")
22 if err != nil {
23 return nil, fmt.Errorf("create body.html: %w", err)
24 }
25 if _, err := f.Write([]byte(htmlBody)); err != nil {
26 return nil, fmt.Errorf("write body.html: %w", err)
27 }
28 }
29
30 for _, att := range attachments {
31 name := att.Filename
32 if name == "" {
33 name = "attachment"
34 }
35
36 f, err := w.Create(name)
37 if err != nil {
38 return nil, fmt.Errorf("create %s: %w", name, err)
39 }
40 if _, err := f.Write(att.Data); err != nil {
41 return nil, fmt.Errorf("write %s: %w", name, err)
42 }
43 }
44
45 if err := w.Close(); err != nil {
46 return nil, fmt.Errorf("close zip: %w", err)
47 }
48
49 if buf.Len() > maxZipBytes {
50 return nil, fmt.Errorf("zip exceeds %d byte limit (%d bytes)", maxZipBytes, buf.Len())
51 }
52
53 return buf.Bytes(), nil
54 }
55