Files
steve eb63d8cbc1 Rewrite from Python to Go for single-binary cross-platform builds
Replaces imapdown.py with a multi-file Go implementation using
github.com/emersion/go-imap/v2. All features preserved: SSL/STARTTLS,
incremental UID-based downloads, attachment extraction to zip,
modified UTF-7 folder name decoding, and full-mode safety checks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 17:54:41 +00:00

127 lines
3.0 KiB
Go

package main
import (
"encoding/base64"
"encoding/binary"
"path/filepath"
"regexp"
"strings"
"unicode/utf16"
)
// sanitizeFilenameRegex matches invalid filesystem characters
var sanitizeFilenameRegex = regexp.MustCompile(`[<>:"/\\|?*\x00-\x1f]`)
// SanitizeFilename removes invalid filesystem characters and truncates to maxLength
func SanitizeFilename(name string, maxLength int) string {
if name == "" {
return "untitled"
}
// Replace invalid characters with underscore
name = sanitizeFilenameRegex.ReplaceAllString(name, "_")
// Trim leading/trailing dots and spaces
name = strings.Trim(name, ". ")
// Truncate to max length
if len(name) > maxLength {
name = name[:maxLength]
}
// Trim again after truncation
name = strings.Trim(name, ". ")
if name == "" {
return "untitled"
}
return name
}
// SanitizeFolderPath converts IMAP folder paths to filesystem paths
func SanitizeFolderPath(folderName string) string {
// Replace both / and . with OS path separator
normalized := strings.ReplaceAll(folderName, "/", string(filepath.Separator))
normalized = strings.ReplaceAll(normalized, ".", string(filepath.Separator))
// Split and sanitize each part
parts := strings.Split(normalized, string(filepath.Separator))
sanitized := make([]string, 0, len(parts))
for _, part := range parts {
if part != "" {
sanitized = append(sanitized, SanitizeFilename(part, 100))
}
}
if len(sanitized) == 0 {
return "INBOX"
}
return filepath.Join(sanitized...)
}
// DecodeModifiedUTF7 decodes IMAP modified UTF-7 folder names
// Modified UTF-7 uses & as escape character, &- for literal &,
// and uses , instead of / in base64 encoding
func DecodeModifiedUTF7(s string) (string, error) {
var result strings.Builder
i := 0
for i < len(s) {
if s[i] == '&' {
// Check for &- (literal ampersand)
if i+1 < len(s) && s[i+1] == '-' {
result.WriteByte('&')
i += 2
continue
}
// Find the closing -
end := strings.IndexByte(s[i+1:], '-')
if end == -1 {
// No closing -, just append rest of string
result.WriteString(s[i:])
break
}
end += i + 1 // Adjust to absolute position
encoded := s[i+1 : end]
if encoded != "" {
// Replace , with / for standard base64
encoded = strings.ReplaceAll(encoded, ",", "/")
// Add padding to make length divisible by 4
padding := (4 - len(encoded)%4) % 4
encoded += strings.Repeat("=", padding)
// Decode base64
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
// On error, just append the original string
result.WriteString(s[i : end+1])
i = end + 1
continue
}
// Convert UTF-16BE bytes to UTF-16 runes, then to string
utf16Runes := make([]uint16, len(decoded)/2)
for j := 0; j < len(decoded); j += 2 {
utf16Runes[j/2] = binary.BigEndian.Uint16(decoded[j : j+2])
}
result.WriteString(string(utf16.Decode(utf16Runes)))
}
i = end + 1
} else {
result.WriteByte(s[i])
i++
}
}
return result.String(), nil
}