367 lines
9.6 KiB
Go
367 lines
9.6 KiB
Go
// 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 <https://www.gnu.org/licenses/>.
|
|
|
|
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
|
|
}
|