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