// Copyright (c) 2021 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 transfer import ( "encoding/json" "fmt" "os" "path/filepath" "sort" "strings" "time" "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/pkg/errors" ) // transferRules maintains import rules, e.g. to which target mailbox should be // source mailbox imported or what time spans. type transferRules struct { filePath string // rules is map with key as hash of source mailbox to its rule. // Every source mailbox should have rule, at least disabled one. rules map[string]*Rule // globalMailbox is applied to every message in the import phase. // E.g., every message will be imported into this mailbox. globalMailbox *Mailbox // globalFromTime and globalToTime is applied to every rule right // before the transfer (propagateGlobalTime has to be called). globalFromTime int64 globalToTime int64 // skipEncryptedMessages determines whether message which cannot // be decrypted should be imported/exported or skipped. skipEncryptedMessages bool } // loadRules loads rules from `rulesPath` based on `ruleID`. func loadRules(rulesPath, ruleID string) transferRules { fileName := fmt.Sprintf("rules_%s.json", ruleID) filePath := filepath.Join(rulesPath, fileName) var rules map[string]*Rule f, err := os.Open(filePath) //nolint[gosec] if err != nil { log.WithError(err).Debug("Problem to read rules") } else { defer f.Close() //nolint[errcheck] if err := json.NewDecoder(f).Decode(&rules); err != nil { log.WithError(err).Warn("Problem to umarshal rules") } } if rules == nil { rules = map[string]*Rule{} } return transferRules{ filePath: filePath, rules: rules, } } func (r *transferRules) setSkipEncryptedMessages(skip bool) { r.skipEncryptedMessages = skip } func (r *transferRules) setGlobalMailbox(mailbox *Mailbox) { r.globalMailbox = mailbox } func (r *transferRules) setGlobalTimeLimit(fromTime, toTime int64) { r.globalFromTime = fromTime r.globalToTime = toTime } func (r *transferRules) propagateGlobalTime() { if r.globalFromTime == 0 && r.globalToTime == 0 { return } for _, rule := range r.rules { if !rule.HasTimeLimit() { rule.FromTime = r.globalFromTime rule.ToTime = r.globalToTime } } } func (r *transferRules) getRuleBySourceMailboxName(name string) (*Rule, error) { for _, rule := range r.rules { if rule.SourceMailbox.Name == name { return rule, nil } } return nil, fmt.Errorf("no rule for mailbox %s", name) } func (r *transferRules) iterateActiveRules() chan *Rule { ch := make(chan *Rule) go func() { for _, rule := range r.rules { if rule.Active { ch <- rule } } close(ch) }() return ch } // setDefaultRules iterates `sourceMailboxes` and sets missing rules with // matching mailboxes from `targetMailboxes`. In case no matching mailbox // is found, `defaultCallback` with a source mailbox as a parameter is used. func (r *transferRules) setDefaultRules(sourceMailboxes []Mailbox, targetMailboxes []Mailbox, defaultCallback func(Mailbox) []Mailbox) { for _, sourceMailbox := range sourceMailboxes { h := sourceMailbox.Hash() if _, ok := r.rules[h]; ok { continue } targetMailboxes := sourceMailbox.findMatchingMailboxes(targetMailboxes) if !containsExclusive(targetMailboxes) { targetMailboxes = append(targetMailboxes, defaultCallback(sourceMailbox)...) } active := true if len(targetMailboxes) == 0 { active = false } // For both import to or export from ProtonMail, spam and draft // mailboxes are by default deactivated. for _, mailbox := range append([]Mailbox{sourceMailbox}, targetMailboxes...) { if mailbox.ID == pmapi.SpamLabel || mailbox.ID == pmapi.DraftLabel || mailbox.ID == pmapi.TrashLabel { active = false break } } r.rules[h] = &Rule{ Active: active, SourceMailbox: sourceMailbox, TargetMailboxes: targetMailboxes, } } // There is no point showing rule which has no action (i.e., source mailbox // is not available). // A good reason to keep all rules and only deactivate them would be for // multiple imports from different sources with the same or similar enough // mailbox setup to reuse configuration. That is very minor feature which // can be implemented in more reasonable way by allowing users to save and // load configurations. for key, rule := range r.rules { found := false for _, sourceMailbox := range sourceMailboxes { if sourceMailbox.Name == rule.SourceMailbox.Name { found = true } } if !found { delete(r.rules, key) } } r.save() } // setRule sets messages from `sourceMailbox` between `fromData` and `toDate` // (if used) to be imported to all `targetMailboxes`. func (r *transferRules) setRule(sourceMailbox Mailbox, targetMailboxes []Mailbox, fromTime, toTime int64) error { numberOfExclusiveMailboxes := 0 for _, mailbox := range targetMailboxes { if mailbox.IsExclusive { numberOfExclusiveMailboxes++ } } if numberOfExclusiveMailboxes > 1 { return errors.New("rule can have only one exclusive target mailbox") } h := sourceMailbox.Hash() r.rules[h] = &Rule{ Active: true, SourceMailbox: sourceMailbox, TargetMailboxes: targetMailboxes, FromTime: fromTime, ToTime: toTime, } r.save() return nil } // unsetRule unsets messages from `sourceMailbox` to be exported. func (r *transferRules) unsetRule(sourceMailbox Mailbox) { h := sourceMailbox.Hash() if rule, ok := r.rules[h]; ok { rule.Active = false } else { r.rules[h] = &Rule{ Active: false, SourceMailbox: sourceMailbox, } } r.save() } // getRule returns rule for `sourceMailbox` or nil if it does not exist. func (r *transferRules) getRule(sourceMailbox Mailbox) *Rule { h := sourceMailbox.Hash() return r.rules[h] } // getSortedRules returns all set rules in order by `byRuleOrder`. func (r *transferRules) getSortedRules() []*Rule { rules := []*Rule{} for _, rule := range r.rules { rules = append(rules, rule) } sort.Sort(byRuleOrder(rules)) return rules } // reset wipes our all rules. func (r *transferRules) reset() { r.rules = map[string]*Rule{} r.save() } // save saves rules to file. func (r *transferRules) save() { f, err := os.Create(r.filePath) if err != nil { log.WithError(err).Warn("Problem to write rules") return } defer f.Close() //nolint[errcheck] if err := json.NewEncoder(f).Encode(r.rules); err != nil { log.WithError(err).Warn("Problem to marshal rules") } } // Rule is data holder of rule for one source mailbox used by `transferRules`. type Rule struct { Active bool `json:"active"` SourceMailbox Mailbox `json:"source"` TargetMailboxes []Mailbox `json:"targets"` FromTime int64 `json:"from"` ToTime int64 `json:"to"` } // String returns textual representation for log purposes. func (r *Rule) String() string { return fmt.Sprintf( "%s -> %s (%d - %d)", r.SourceMailbox.Name, strings.Join(r.TargetMailboxNames(), ", "), r.FromTime, r.ToTime, ) } func (r *Rule) isTimeInRange(t int64) bool { if !r.HasTimeLimit() { return true } return r.FromTime <= t && t <= r.ToTime } // HasTimeLimit returns whether rule defines time limit. func (r *Rule) HasTimeLimit() bool { return r.FromTime != 0 || r.ToTime != 0 } // FromDate returns time struct based on `FromTime`. func (r *Rule) FromDate() time.Time { return time.Unix(r.FromTime, 0) } // ToDate returns time struct based on `ToTime`. func (r *Rule) ToDate() time.Time { return time.Unix(r.ToTime, 0) } // TargetMailboxNames returns array of target mailbox names. func (r *Rule) TargetMailboxNames() (names []string) { for _, mailbox := range r.TargetMailboxes { names = append(names, mailbox.Name) } return } // byRuleOrder implements sort.Interface. Sort order: // * System folders first (as defined in getSystemMailboxes). // * Custom folders by name. // * Custom labels by name. type byRuleOrder []*Rule func (a byRuleOrder) Len() int { return len(a) } func (a byRuleOrder) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a byRuleOrder) Less(i, j int) bool { if a[i].SourceMailbox.IsExclusive && !a[j].SourceMailbox.IsExclusive { return true } if !a[i].SourceMailbox.IsExclusive && a[j].SourceMailbox.IsExclusive { return false } iSystemIndex := -1 jSystemIndex := -1 for index, systemFolders := range getSystemMailboxes(true) { if a[i].SourceMailbox.Name == systemFolders.Name { iSystemIndex = index } if a[j].SourceMailbox.Name == systemFolders.Name { jSystemIndex = index } } if iSystemIndex != -1 && jSystemIndex == -1 { return true } if iSystemIndex == -1 && jSystemIndex != -1 { return false } if iSystemIndex != -1 && jSystemIndex != -1 { return iSystemIndex < jSystemIndex } return a[i].SourceMailbox.Name < a[j].SourceMailbox.Name } // containsExclusive returns true if there is at least one exclusive mailbox. func containsExclusive(mailboxes []Mailbox) bool { for _, m := range mailboxes { if m.IsExclusive { return true } } return false }