eb63d8cbc1
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>
127 lines
3.0 KiB
Go
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
|
|
}
|
|
|