@@ -13,10 +13,15 @@ type Mailbox struct {
1313 // Path is the absolute path to the .mbox or .imapmbox directory.
1414 Path string
1515
16+ // MsgDir is the absolute path to the Messages/ directory
17+ // containing .emlx files. In legacy layouts this is Path/Messages;
18+ // in modern V10 layouts it is Path/<GUID>/Data/Messages.
19+ MsgDir string
20+
1621 // Label is the derived label for messages in this mailbox.
1722 Label string
1823
19- // Files contains sorted .emlx filenames within Messages/ .
24+ // Files contains sorted .emlx filenames within MsgDir .
2025 Files []string
2126}
2227
@@ -44,14 +49,15 @@ func DiscoverMailboxes(rootDir string) ([]Mailbox, error) {
4449
4550 // Auto-detect: if the path itself is a mailbox, import just that one.
4651 if isMailboxDir (abs ) {
47- files , err := listEmlxFiles (abs )
52+ msgDir , files , err := listEmlxFiles (abs )
4853 if err != nil {
4954 return nil , err
5055 }
5156 if len (files ) > 0 {
5257 label := LabelFromPath (filepath .Dir (abs ), abs )
5358 return []Mailbox {{
54- Path : abs , Label : label , Files : files ,
59+ Path : abs , MsgDir : msgDir ,
60+ Label : label , Files : files ,
5561 }}, nil
5662 }
5763 }
@@ -75,14 +81,15 @@ func DiscoverMailboxes(rootDir string) ([]Mailbox, error) {
7581 return nil
7682 }
7783
78- files , listErr := listEmlxFiles (path )
84+ msgDir , files , listErr := listEmlxFiles (path )
7985 if listErr != nil || len (files ) == 0 {
8086 return nil
8187 }
8288
8389 label := LabelFromPath (abs , path )
8490 mailboxes = append (mailboxes , Mailbox {
85- Path : path , Label : label , Files : files ,
91+ Path : path , MsgDir : msgDir ,
92+ Label : label , Files : files ,
8693 })
8794
8895 return nil
@@ -124,6 +131,10 @@ func LabelFromPath(rootDir, mailboxPath string) string {
124131 if strings .HasPrefix (p , "IMAP-" ) || strings .HasPrefix (p , "POP-" ) {
125132 continue
126133 }
134+ // V10 account GUID directories (e.g. 13C9A646-...-E07FFBDDEED3).
135+ if isUUID (p ) {
136+ continue
137+ }
127138 filtered = append (filtered , p )
128139 }
129140
@@ -147,10 +158,95 @@ func isMailboxDir(path string) bool {
147158 return false
148159 }
149160
150- // Must have a Messages/ subdirectory.
151- msgDir := filepath .Join (path , "Messages" )
152- info , err := os .Stat (msgDir )
153- return err == nil && info .IsDir ()
161+ return findMessagesDir (path ) != ""
162+ }
163+
164+ // findMessagesDir locates the Messages/ directory within a .mbox.
165+ // Returns "" if none found. Checks both legacy (Messages/) and
166+ // modern V10 (<GUID>/Data/Messages/) layouts. When both exist,
167+ // prefers whichever contains .emlx files.
168+ func findMessagesDir (mailboxPath string ) string {
169+ var candidates []string
170+
171+ // Legacy: direct Messages/ subdirectory.
172+ legacy := filepath .Join (mailboxPath , "Messages" )
173+ if info , err := os .Stat (legacy ); err == nil && info .IsDir () {
174+ candidates = append (candidates , legacy )
175+ }
176+
177+ // Modern V10: <subdir>/Data/Messages/ subdirectory.
178+ entries , err := os .ReadDir (mailboxPath )
179+ if err == nil {
180+ for _ , e := range entries {
181+ if ! e .IsDir () || e .Name () == "Messages" {
182+ continue
183+ }
184+ modern := filepath .Join (
185+ mailboxPath , e .Name (), "Data" , "Messages" ,
186+ )
187+ info , statErr := os .Stat (modern )
188+ if statErr == nil && info .IsDir () {
189+ candidates = append (candidates , modern )
190+ }
191+ }
192+ }
193+
194+ if len (candidates ) == 0 {
195+ return ""
196+ }
197+
198+ // Prefer the first candidate that has .emlx files.
199+ for _ , dir := range candidates {
200+ if hasEmlxFiles (dir ) {
201+ return dir
202+ }
203+ }
204+
205+ // No candidate has files; return first for isMailboxDir.
206+ return candidates [0 ]
207+ }
208+
209+ // hasEmlxFiles returns true if dir contains at least one
210+ // non-partial .emlx file.
211+ func hasEmlxFiles (dir string ) bool {
212+ entries , err := os .ReadDir (dir )
213+ if err != nil {
214+ return false
215+ }
216+ for _ , e := range entries {
217+ if e .IsDir () {
218+ continue
219+ }
220+ lower := strings .ToLower (e .Name ())
221+ if strings .HasSuffix (lower , ".emlx" ) &&
222+ ! strings .HasSuffix (lower , ".partial.emlx" ) {
223+ return true
224+ }
225+ }
226+ return false
227+ }
228+
229+ // isUUID returns true if s matches UUID format (8-4-4-4-12 hex).
230+ func isUUID (s string ) bool {
231+ if len (s ) != 36 {
232+ return false
233+ }
234+ for i , c := range s {
235+ switch i {
236+ case 8 , 13 , 18 , 23 :
237+ if c != '-' {
238+ return false
239+ }
240+ default :
241+ isHex := (c >= '0' && c <= '9' ) ||
242+ (c >= 'a' && c <= 'f' ) ||
243+ (c >= 'A' && c <= 'F' )
244+ if ! isHex {
245+ return false
246+ }
247+ }
248+ }
249+ return true
154250}
155251
156252func stripMailboxSuffix (name string ) string {
@@ -164,16 +260,23 @@ func stripMailboxSuffix(name string) string {
164260 return name
165261}
166262
167- // listEmlxFiles returns sorted .emlx filenames (not paths) within
168- // the Messages/ subdirectory of a mailbox, excluding .partial.emlx.
169- func listEmlxFiles (mailboxPath string ) ([]string , error ) {
170- msgDir := filepath .Join (mailboxPath , "Messages" )
263+ // listEmlxFiles returns the Messages directory path and sorted .emlx
264+ // filenames within it, excluding .partial.emlx. Returns ("", nil, nil)
265+ // if no Messages directory is found.
266+ func listEmlxFiles (
267+ mailboxPath string ,
268+ ) (string , []string , error ) {
269+ msgDir := findMessagesDir (mailboxPath )
270+ if msgDir == "" {
271+ return "" , nil , nil
272+ }
273+
171274 entries , err := os .ReadDir (msgDir )
172275 if err != nil {
173276 if os .IsNotExist (err ) {
174- return nil , nil
277+ return "" , nil , nil
175278 }
176- return nil , fmt .Errorf ("read Messages dir: %w" , err )
279+ return "" , nil , fmt .Errorf ("read Messages dir: %w" , err )
177280 }
178281
179282 var files []string
@@ -186,12 +289,14 @@ func listEmlxFiles(mailboxPath string) ([]string, error) {
186289 continue
187290 }
188291 // Skip .partial.emlx files (Apple Mail temp files).
189- if strings .HasSuffix (strings .ToLower (name ), ".partial.emlx" ) {
292+ if strings .HasSuffix (
293+ strings .ToLower (name ), ".partial.emlx" ,
294+ ) {
190295 continue
191296 }
192297 files = append (files , name )
193298 }
194299
195300 sort .Strings (files )
196- return files , nil
301+ return msgDir , files , nil
197302}
0 commit comments