// package uidlist implements Dovecot's `dovecot-uidlist` file format for // Maildir directories // // Reference: https://wiki.dovecot.org/MailboxFormat/Maildir package uidlist import ( "bufio" "fmt" "log" "os" "path/filepath" "sync" ) const ( DefaultFilename = "dovecot-uidlist" ) type List struct { filename string header Header entries []Entry mutex *sync.Mutex } func Create(dir string) (*List, error) { return CreateFile(filepath.Join(dir, DefaultFilename)) } // FIXME: on startup, we need to scan `cur/` in case any problems arose func Open(dir string) (*List, error) { return OpenFile(filepath.Join(dir, DefaultFilename)) } func CreateFile(filename string) (*List, error) { list, err := handleFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC) if err != nil { return nil, err } hdr, err := GenerateHeader() if err != nil { return nil, err } list.header = hdr // TODO: allow the dovecot-uidlist to be populated from scratch // FIXME: in particular, NextUID and entries are wrong here if err := list.save(); err != nil { return nil, err } return list, nil } func OpenFile(filename string) (*List, error) { list, err := handleFile(filename, os.O_RDWR|os.O_CREATE) if err != nil { return nil, err } if err := list.parse(); err != nil { return nil, err } return list, nil } func handleFile(filename string, flags int) (*List, error) { file, err := os.OpenFile(filename, flags, 0600) if err != nil { return nil, err } file.Close() return &List{ filename: filename, mutex: &sync.Mutex{}, }, nil } func (l *List) NextUID() uint64 { l.mutex.Lock() defer l.mutex.Unlock() return l.header.NextUID } func (l *List) UIDValidity() uint64 { return l.header.UIDValidity } func (l *List) GUID() string { return l.header.GUID } func (l *List) Entries() []Entry { l.mutex.Lock() defer l.mutex.Unlock() return l.entries } func (l *List) EntriesInRangeBySeqNum(start, end uint64) []Entry { l.mutex.Lock() defer l.mutex.Unlock() // IMAP SeqNums are indexed from 1. So 0 should be invalid, right? if start == 0 { return nil } start-- if int(start) > len(l.entries) { return nil } if end == 0 { return l.entries[start:] } if int(end) > len(l.entries) { end = uint64(len(l.entries)) } return l.entries[start:end] } func (l *List) EntriesInRangeByUID(start, end uint64) []Entry { l.mutex.Lock() defer l.mutex.Unlock() // FIXME: this shouldn't need a full scan var out []Entry log.Printf("Checking range %d to %d in %#v", start, end, l.entries) for _, e := range l.entries { if e.UID >= start && (end == 0 || e.UID <= end) { out = append(out, e) } } return out } func (l *List) Entry(key string) *Entry { l.mutex.Lock() defer l.mutex.Unlock() return nil } func (l *List) Count() uint64 { l.mutex.Lock() defer l.mutex.Unlock() return uint64(len(l.entries)) } func (l *List) Append(filenames ...string) error { l.mutex.Lock() defer l.mutex.Unlock() file, err := os.OpenFile(l.filename, os.O_APPEND|os.O_RDWR, 0600) if err != nil { return err } defer file.Close() for _, filename := range filenames { entry := Entry{ UID: l.header.NextUID, LastKnownFilename: filename, } log.Printf("Adding entry: %#v", entry) l.entries = append(l.entries, entry) log.Printf("NextUID: %v", l.header.NextUID) l.header.NextUID++ log.Printf("NextUID: %v", l.header.NextUID) // Try to write them all, at least // FIXME: we don't bubble up errors when we should if _, err := fmt.Fprintln(file, entry.String()); err != nil { log.Printf("failed to write UID %d to %s: %v", entry.UID, l.filename, err) } } log.Printf("Appended %d entries", len(filenames)) return nil } func (l *List) save() (err error) { l.mutex.Lock() defer l.mutex.Unlock() // FIXME: When rewriting the file, we're meant to: // * Take the dovecot-uidlist.lock file // * write to a temporary file then atomically rename onto -uidlist // Unsure if the temporary file and the lock file are the same thing or not file, err := os.OpenFile(l.filename, os.O_TRUNC|os.O_RDWR, 0600) if err != nil { return } defer func() { err = file.Close() }() _, err = fmt.Fprintln(file, l.header.String()) if err != nil { return } for _, e := range l.entries { _, err = fmt.Fprintln(file, e.String()) if err != nil { return } } return } func (l *List) parse() error { l.mutex.Lock() defer l.mutex.Unlock() log.Printf("Parsing %v", l.filename) defer func() { log.Printf("Finished parsing %v", l.filename) }() file, err := os.Open(l.filename) if err != nil { return err } defer file.Close() r := bufio.NewScanner(file) // Get the header if present, which is the first line if !r.Scan() { return r.Err() } header, err := ParseHeader(r.Text()) if err != nil { return err } // Now parse each additional line entries := []Entry{} for r.Scan() { entry, err := ParseEntry(r.Text()) if err != nil { return err } entries = append(entries, entry) // If NextUID is out of date in the file, ignore it. We never want to // re-use a seen UID log.Printf("l.header.NextUID: %v", l.header.NextUID) if entry.UID >= l.header.NextUID { log.Printf("Incrementing as entry with UID %v is greater", entry.UID) l.header.NextUID = entry.UID + 1 } log.Printf("l.header.NextUID: %v", l.header.NextUID) } l.header = header l.entries = entries log.Printf("list: %#v", l) return nil }