Files
pop/internal/attachment/manager.go
Paperclip af25fd5575 FRE-682: Add folder/label management, search, and fix PGP build
- Add pop mail search CLI command with pagination support
- Create internal/labels package with types and API client
- Add folder list/create/update/delete CLI commands
- Add label list/create/update/delete/apply/remove CLI commands
- Register folder and label commands in root.go
- Fix gopenpgp v2 API mismatches in pgp.go (NewPlainMessage, Armor,
  KeyRing.Encrypt/Decrypt, SessionKey)
- Fix NewSessionManager error handling across cmd files
- Fix variable shadowing bug in mail/client.go
2026-04-28 06:37:47 -04:00

140 lines
3.4 KiB
Go

package attachment
import (
"io"
"os"
"path/filepath"
)
// ErrInvalidAttachmentID is returned when attachmentID contains unsafe characters
var ErrInvalidAttachmentID = os.ErrInvalid
type AttachmentManager struct {
attachmentsDir string
}
const maxUploadSize = 50 * 1024 * 1024 // 50MB
func NewAttachmentManager() *AttachmentManager {
return &AttachmentManager{
attachmentsDir: filepath.Join(os.Getenv("HOME"), ".config", "pop", "attachments"),
}
}
// isAttachmentIDSafe validates that attachmentID contains only safe characters
// to prevent path traversal attacks
func isAttachmentIDSafe(id string) bool {
if id == "" {
return false
}
// Only allow alphanumeric, hyphen, and underscore
for _, r := range id {
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9') || r == '-' || r == '_') {
return false
}
}
return true
}
// sanitizeAttachmentID ensures the attachmentID is safe and the resolved path
// is within the attachments directory
func sanitizeAttachmentID(id string) (string, error) {
if !isAttachmentIDSafe(id) {
return "", ErrInvalidAttachmentID
}
// Use filepath.Clean to resolve any .. or . components
cleanID := filepath.Clean(id)
if cleanID != id {
return "", ErrInvalidAttachmentID
}
return cleanID, nil
}
func (m *AttachmentManager) Download(attachmentID, name, destPath string) error {
// Sanitize attachmentID to prevent path traversal
if sanitizedID, err := sanitizeAttachmentID(attachmentID); err != nil {
return err
} else {
attachmentID = sanitizedID
}
if err := os.MkdirAll(m.attachmentsDir, 0700); err != nil {
return err
}
srcPath := filepath.Join(m.attachmentsDir, attachmentID)
dest := filepath.Join(destPath, name)
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
return err
}
data, err := os.ReadFile(srcPath)
if err != nil {
return err
}
return os.WriteFile(dest, data, 0600)
}
func (m *AttachmentManager) Upload(attachmentID, name string, reader io.Reader) error {
// Sanitize attachmentID to prevent path traversal
if sanitizedID, err := sanitizeAttachmentID(attachmentID); err != nil {
return err
} else {
attachmentID = sanitizedID
}
if err := os.MkdirAll(m.attachmentsDir, 0700); err != nil {
return err
}
// Limit reader to maxUploadSize to prevent DoS
limitedReader := io.LimitReader(reader, maxUploadSize)
data, err := io.ReadAll(limitedReader)
if err != nil {
return err
}
return os.WriteFile(filepath.Join(m.attachmentsDir, attachmentID), data, 0600)
}
func (m *AttachmentManager) Get(attachmentID string) ([]byte, error) {
// Sanitize attachmentID to prevent path traversal
if sanitizedID, err := sanitizeAttachmentID(attachmentID); err != nil {
return nil, err
} else {
attachmentID = sanitizedID
}
path := filepath.Join(m.attachmentsDir, attachmentID)
return os.ReadFile(path)
}
func (m *AttachmentManager) Delete(attachmentID string) error {
// Sanitize attachmentID to prevent path traversal
if sanitizedID, err := sanitizeAttachmentID(attachmentID); err != nil {
return err
} else {
attachmentID = sanitizedID
}
path := filepath.Join(m.attachmentsDir, attachmentID)
return os.Remove(path)
}
func (m *AttachmentManager) List() ([]string, error) {
entries, err := os.ReadDir(m.attachmentsDir)
if err != nil {
if os.IsNotExist(err) {
return []string{}, nil
}
return nil, err
}
ids := make([]string, len(entries))
for i, entry := range entries {
ids[i] = entry.Name()
}
return ids, nil
}