// Copyright (c) 2020 Proton Technologies AG // // This file is part of ProtonMail Bridge. // // ProtonMail Bridge is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // ProtonMail Bridge is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . package store import ( "fmt" "strings" "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/pkg/errors" ) // createMailbox creates the mailbox via the API. // The store mailbox is created later by processing an event. func (store *Store) createMailbox(name string) error { defer store.eventLoop.pollNow() log.WithField("name", name).Debug("Creating mailbox") if store.hasMailbox(name) { return fmt.Errorf("mailbox %v already exists", name) } color := store.leastUsedColor() var exclusive int switch { case strings.HasPrefix(name, UserLabelsPrefix): name = strings.TrimPrefix(name, UserLabelsPrefix) exclusive = 0 case strings.HasPrefix(name, UserFoldersPrefix): name = strings.TrimPrefix(name, UserFoldersPrefix) exclusive = 1 default: // Ideally we would throw an error here, but then Outlook for // macOS keeps trying to make an IMAP Drafts folder and popping // up the error to the user. store.log.WithField("name", name). Warn("Ignoring creation of new mailbox in IMAP root") return nil } _, err := store.client().CreateLabel(&pmapi.Label{ Name: name, Color: color, Exclusive: exclusive, Type: pmapi.LabelTypeMailbox, }) return err } // allAddressesHaveMailbox returns whether each address has a mailbox with the given labelID. func (store *Store) allAddressesHaveMailbox(labelID string) bool { store.lock.RLock() defer store.lock.RUnlock() for _, a := range store.addresses { addressHasMailbox := false for _, m := range a.mailboxes { if m.labelID == labelID { addressHasMailbox = true break } } if !addressHasMailbox { return false } } return true } // hasMailbox returns whether there is at least one address which has a mailbox with the given name. func (store *Store) hasMailbox(name string) bool { mailbox, _ := store.getMailbox(name) return mailbox != nil } // getMailbox returns the first mailbox with the given name. func (store *Store) getMailbox(name string) (*Mailbox, error) { store.lock.RLock() defer store.lock.RUnlock() for _, a := range store.addresses { for _, m := range a.mailboxes { if m.labelName == name { return m, nil } } } return nil, fmt.Errorf("mailbox %s does not exist", name) } // leastUsedColor returns the least used color to be used for a newly created folder or label. func (store *Store) leastUsedColor() string { store.lock.RLock() defer store.lock.RUnlock() usage := map[string]int{} for _, a := range store.addresses { for _, m := range a.mailboxes { if m.color != "" { usage[m.color]++ } } } leastUsed := pmapi.LabelColors[0] for _, color := range pmapi.LabelColors { if usage[leastUsed] > usage[color] { leastUsed = color } } return leastUsed } // updateMailbox updates the mailbox via the API. // The store mailbox is updated later by processing an event. func (store *Store) updateMailbox(labelID, newName, color string) error { defer store.eventLoop.pollNow() _, err := store.client().UpdateLabel(&pmapi.Label{ ID: labelID, Name: newName, Color: color, }) return err } // deleteMailbox deletes the mailbox via the API. // The store mailbox is deleted later by processing an event. func (store *Store) deleteMailbox(labelID, addressID string) error { defer store.eventLoop.pollNow() if pmapi.IsSystemLabel(labelID) { var err error switch labelID { case pmapi.SpamLabel: err = store.client().EmptyFolder(pmapi.SpamLabel, addressID) case pmapi.TrashLabel: err = store.client().EmptyFolder(pmapi.TrashLabel, addressID) default: err = fmt.Errorf("cannot empty mailbox %v", labelID) } return err } return store.client().DeleteLabel(labelID) } func (store *Store) createLabelsIfMissing(affectedLabelIDs map[string]bool) error { newLabelIDs := []string{} for labelID := range affectedLabelIDs { if pmapi.IsSystemLabel(labelID) || store.allAddressesHaveMailbox(labelID) { continue } newLabelIDs = append(newLabelIDs, labelID) } if len(newLabelIDs) == 0 { return nil } labels, err := store.client().ListLabels() if err != nil { return err } for _, newLabelID := range newLabelIDs { for _, label := range labels { if label.ID != newLabelID { continue } if err := store.createOrUpdateMailboxEvent(label); err != nil { return err } } } return nil } // createOrUpdateMailboxEvent creates or updates the mailbox in the store. // This is called from the event loop. func (store *Store) createOrUpdateMailboxEvent(label *pmapi.Label) error { store.lock.Lock() defer store.lock.Unlock() if label.Type != pmapi.LabelTypeMailbox { return nil } if err := store.createOrUpdateMailboxCountsBuckets([]*pmapi.Label{label}); err != nil { return errors.Wrap(err, "cannot update counts") } for _, a := range store.addresses { if err := a.createOrUpdateMailboxEvent(label); err != nil { return err } } return nil } // deleteMailboxEvent deletes the mailbox in the store. // This is called from the event loop. func (store *Store) deleteMailboxEvent(labelID string) error { store.lock.Lock() defer store.lock.Unlock() _ = store.removeMailboxCount(labelID) for _, a := range store.addresses { if err := a.deleteMailboxEvent(labelID); err != nil { return err } } return nil }