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>
This commit is contained in:
+126
@@ -0,0 +1,126 @@
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user