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 }