package selectors import ( "strings" "github.com/alcionai/corso/internal/common" "github.com/alcionai/corso/pkg/backup/details" ) // --------------------------------------------------------------------------- // Selectors // --------------------------------------------------------------------------- type ( // exchange provides an api for selecting // data scopes applicable to the Exchange service. exchange struct { Selector } // ExchangeBackup provides an api for selecting // data scopes applicable to the Exchange service, // plus backup-specific methods. ExchangeBackup struct { exchange } // ExchangeRestore provides an api for selecting // data scopes applicable to the Exchange service, // plus restore-specific methods. ExchangeRestore struct { exchange } ) // NewExchange produces a new Selector with the service set to ServiceExchange. func NewExchangeBackup() *ExchangeBackup { src := ExchangeBackup{ exchange{ newSelector(ServiceExchange), }, } return &src } // ToExchangeBackup transforms the generic selector into an ExchangeBackup. // Errors if the service defined by the selector is not ServiceExchange. func (s Selector) ToExchangeBackup() (*ExchangeBackup, error) { if s.Service != ServiceExchange { return nil, badCastErr(ServiceExchange, s.Service) } src := ExchangeBackup{exchange{s}} return &src, nil } // NewExchangeRestore produces a new Selector with the service set to ServiceExchange. func NewExchangeRestore() *ExchangeRestore { src := ExchangeRestore{ exchange{ newSelector(ServiceExchange), }, } return &src } // ToExchangeRestore transforms the generic selector into an ExchangeRestore. // Errors if the service defined by the selector is not ServiceExchange. func (s Selector) ToExchangeRestore() (*ExchangeRestore, error) { if s.Service != ServiceExchange { return nil, badCastErr(ServiceExchange, s.Service) } src := ExchangeRestore{exchange{s}} return &src, nil } // ------------------- // Exclude/Includes // Exclude appends the provided scopes to the selector's exclusion set. // Every Exclusion scope applies globally, affecting all inclusion scopes. // // All parts of the scope must match for data to be exclucded. // Ex: Mail(u1, f1, m1) => only excludes mail if it is owned by user u1, // located in folder f1, and ID'd as m1. MailSender(foo) => only excludes // mail whose sender is foo. Use selectors.Any() to wildcard a scope value. // No value will match if selectors.None() is provided. // // Group-level scopes will automatically apply the Any() wildcard to // child properties. // ex: User(u1) automatically cascades to all mail, events, and contacts, // therefore it is the same as selecting all of the following: // Mail(u1, Any(), Any()), Event(u1, Any()), Contacts(u1, Any(), Any()) func (s *exchange) Exclude(scopes ...[]ExchangeScope) { s.Excludes = appendScopes(s.Excludes, scopes...) } // Filter appends the provided scopes to the selector's filters set. // A selector with >0 filters and 0 inclusions will include any data // that passes all filters. // A selector with >0 filters and >0 inclusions will reduce the // inclusion set to only the data that passes all filters. // // All parts of the scope must match for data to pass the filter. // Ex: Mail(u1, f1, m1) => only passes mail that is owned by user u1, // located in folder f1, and ID'd as m1. MailSender(foo) => only passes // mail whose sender is foo. Use selectors.Any() to wildcard a scope value. // No value will match if selectors.None() is provided. // // Group-level scopes will automatically apply the Any() wildcard to // child properties. // ex: User(u1) automatically cascades to all mail, events, and contacts, // therefore it is the same as selecting all of the following: // Mail(u1, Any(), Any()), Event(u1, Any()), Contacts(u1, Any(), Any()) func (s *exchange) Filter(scopes ...[]ExchangeScope) { s.Filters = appendScopes(s.Filters, scopes...) } // Include appends the provided scopes to the selector's inclusion set. // Data is included if it matches ANY inclusion. // The inclusion set is later filtered (all included data must pass ALL // filters) and excluded (all included data must not match ANY exclusion). // // All parts of the scope must match for data to be included. // Ex: Mail(u1, f1, m1) => only includes mail if it is owned by user u1, // located in folder f1, and ID'd as m1. MailSender(foo) => only includes // mail whose sender is foo. Use selectors.Any() to wildcard a scope value. // No value will match if selectors.None() is provided. // // Group-level scopes will automatically apply the Any() wildcard to // child properties. // ex: User(u1) automatically cascades to all mail, events, and contacts, // therefore it is the same as selecting all of the following: // Mail(u1, Any(), Any()), Event(u1, Any()), Contacts(u1, Any(), Any()) func (s *exchange) Include(scopes ...[]ExchangeScope) { s.Includes = appendScopes(s.Includes, scopes...) } // Scopes retrieves the list of exchangeScopes in the selector. func (s *exchange) Scopes() []ExchangeScope { scopes := s.scopes() es := make([]ExchangeScope, len(scopes)) for i := range scopes { es[i] = ExchangeScope(scopes[i]) } return es } // ------------------- // Scope Factories func makeExchangeScope(granularity string, cat exchangeCategory, vs []string) ExchangeScope { return ExchangeScope{ scopeKeyGranularity: granularity, scopeKeyCategory: cat.String(), cat.String(): join(vs...), } } func makeExchangeUserScope(user, granularity string, cat exchangeCategory, vs []string) ExchangeScope { es := makeExchangeScope(granularity, cat, vs).set(ExchangeUser, user) es[scopeKeyResource] = user es[scopeKeyDataType] = cat.leafType().String() return es } // Produces one or more exchange contact scopes. // One scope is created per combination of users,folders,contacts. // If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice is empty, it defaults to [selectors.None] func (s *exchange) Contacts(users, folders, contacts []string) []ExchangeScope { users = normalize(users) folders = normalize(folders) contacts = normalize(contacts) scopes := []ExchangeScope{} for _, u := range users { for _, f := range folders { scopes = append( scopes, makeExchangeUserScope(u, Item, ExchangeContact, contacts).set(ExchangeContactFolder, f), ) } } return scopes } // Produces one or more exchange contact folder scopes. // One scope is created per combination of users,folders. // If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice is empty, it defaults to [selectors.None] func (s *exchange) ContactFolders(users, folders []string) []ExchangeScope { users = normalize(users) folders = normalize(folders) scopes := []ExchangeScope{} for _, u := range users { scopes = append( scopes, makeExchangeUserScope(u, Group, ExchangeContactFolder, folders), ) } return scopes } // Produces one or more exchange event scopes. // One scope is created per combination of users,events. // If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice is empty, it defaults to [selectors.None] func (s *exchange) Events(users, events []string) []ExchangeScope { users = normalize(users) events = normalize(events) scopes := []ExchangeScope{} for _, u := range users { scopes = append( scopes, makeExchangeUserScope(u, Item, ExchangeEvent, events), ) } return scopes } // Produces one or more mail scopes. // One scope is created per combination of users,folders,mails. // If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice is empty, it defaults to [selectors.None] func (s *exchange) Mails(users, folders, mails []string) []ExchangeScope { users = normalize(users) folders = normalize(folders) mails = normalize(mails) scopes := []ExchangeScope{} for _, u := range users { for _, f := range folders { scopes = append( scopes, makeExchangeUserScope(u, Item, ExchangeMail, mails).set(ExchangeMailFolder, f), ) } } return scopes } // Produces one or more exchange mail folder scopes. // One scope is created per combination of users,folders. // If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice is empty, it defaults to [selectors.None] func (s *exchange) MailFolders(users, folders []string) []ExchangeScope { users = normalize(users) folders = normalize(folders) scopes := []ExchangeScope{} for _, u := range users { scopes = append( scopes, makeExchangeUserScope(u, Group, ExchangeMailFolder, folders), ) } return scopes } // Produces one or more exchange contact user scopes. // One scope is created per user entry. // If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice is empty, it defaults to [selectors.None] func (s *exchange) Users(users []string) []ExchangeScope { users = normalize(users) scopes := []ExchangeScope{} for _, u := range users { scopes = append(scopes, makeExchangeUserScope(u, Group, ExchangeContactFolder, Any())) scopes = append(scopes, makeExchangeUserScope(u, Item, ExchangeEvent, Any())) scopes = append(scopes, makeExchangeUserScope(u, Group, ExchangeMailFolder, Any())) } return scopes } func makeExchangeFilterScope(cat, filterCat exchangeCategory, vs []string) ExchangeScope { return ExchangeScope{ scopeKeyGranularity: Filter, scopeKeyCategory: cat.String(), scopeKeyInfoFilter: filterCat.String(), scopeKeyResource: Filter, scopeKeyDataType: cat.leafType().String(), filterCat.String(): join(vs...), } } // Produces an exchange mail received-after filter scope. // Matches any mail which was received after the timestring. // If the input equals selectors.Any, the scope will match all times. // If the input is empty or selectors.None, the scope will always fail comparisons. func (sr *ExchangeRestore) MailReceivedAfter(timeStrings string) []ExchangeScope { return []ExchangeScope{ makeExchangeFilterScope(ExchangeMail, ExchangeInfoMailReceivedAfter, []string{timeStrings}), } } // Produces an exchange mail received-before filter scope. // Matches any mail which was received before the timestring. // If the input equals selectors.Any, the scope will match all times. // If the input is empty or selectors.None, the scope will always fail comparisons. func (sr *ExchangeRestore) MailReceivedBefore(timeStrings string) []ExchangeScope { return []ExchangeScope{ makeExchangeFilterScope(ExchangeMail, ExchangeInfoMailReceivedBefore, []string{timeStrings}), } } // Produces one or more exchange mail sender filter scopes. // Matches any mail whose mail sender equals one of the provided strings. // If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice is empty, it defaults to [selectors.None] func (sr *ExchangeRestore) MailSender(senderIDs []string) []ExchangeScope { return []ExchangeScope{ makeExchangeFilterScope(ExchangeMail, ExchangeInfoMailSender, senderIDs), } } // Produces one or more exchange mail subject line filter scopes. // Matches any mail whose mail subject contains one of the provided strings. // If any slice contains selectors.Any, that slice is reduced to [selectors.Any] // If any slice contains selectors.None, that slice is reduced to [selectors.None] // If any slice is empty, it defaults to [selectors.None] func (sr *ExchangeRestore) MailSubject(subjectSubstrings []string) []ExchangeScope { return []ExchangeScope{ makeExchangeFilterScope(ExchangeMail, ExchangeInfoMailSubject, subjectSubstrings), } } // --------------------------------------------------------------------------- // Destination // --------------------------------------------------------------------------- type ExchangeDestination Destination func NewExchangeDestination() ExchangeDestination { return ExchangeDestination{} } // GetOrDefault gets the destination of the provided category. If no // destination is set, returns the current value. func (d ExchangeDestination) GetOrDefault(cat exchangeCategory, current string) string { dest, ok := d[cat.String()] if !ok { return current } return dest } // Sets the destination value of the provided category. Returns an error // if a destination is already declared for that category. func (d ExchangeDestination) Set(cat exchangeCategory, dest string) error { if len(dest) == 0 { return nil } cs := cat.String() if curr, ok := d[cs]; ok { return existingDestinationErr(cs, curr) } d[cs] = dest return nil } // --------------------------------------------------------------------------- // Categories // --------------------------------------------------------------------------- // exchangeCategory enumerates the type of the lowest level // of data specified by the scope. type exchangeCategory int // interface compliance checks var _ categorizer = ExchangeCategoryUnknown //go:generate stringer -type=exchangeCategory const ( ExchangeCategoryUnknown exchangeCategory = iota // types of data identified by exchange ExchangeContact ExchangeContactFolder ExchangeEvent ExchangeMail ExchangeMailFolder ExchangeUser // filterable topics identified by exchange ExchangeInfoMailSender exchangeCategory = iota + 100 // offset to pad out future data additions ExchangeInfoMailSubject ExchangeInfoMailReceivedAfter ExchangeInfoMailReceivedBefore ) func exchangeCatAtoI(s string) exchangeCategory { switch s { // data types case ExchangeContact.String(): return ExchangeContact case ExchangeContactFolder.String(): return ExchangeContactFolder case ExchangeEvent.String(): return ExchangeEvent case ExchangeMail.String(): return ExchangeMail case ExchangeMailFolder.String(): return ExchangeMailFolder case ExchangeUser.String(): return ExchangeUser // filters case ExchangeInfoMailSender.String(): return ExchangeInfoMailSender case ExchangeInfoMailSubject.String(): return ExchangeInfoMailSubject case ExchangeInfoMailReceivedAfter.String(): return ExchangeInfoMailReceivedAfter case ExchangeInfoMailReceivedBefore.String(): return ExchangeInfoMailReceivedBefore default: return ExchangeCategoryUnknown } } // exchangePathSet describes the category type keys used in Exchange paths. // The order of each slice is important, and should match the order in which // these types appear in the canonical Path for each type. var categoryPathSet = map[categorizer][]categorizer{ ExchangeContact: {ExchangeUser, ExchangeContactFolder, ExchangeContact}, ExchangeEvent: {ExchangeUser, ExchangeEvent}, ExchangeMail: {ExchangeUser, ExchangeMailFolder, ExchangeMail}, ExchangeUser: {ExchangeUser}, // the root category must be represented } // leafType returns the leaf category of the receiver. // If the receiver category has multiple leaves (ex: User) or no leaves, // (ex: Unknown), the receiver itself is returned. // Ex: ExchangeContactFolder.leafType() => ExchangeContact // Ex: ExchangeEvent.leafType() => ExchangeEvent // Ex: ExchangeUser.leafType() => ExchangeUser func (ec exchangeCategory) leafType() exchangeCategory { switch ec { case ExchangeContact, ExchangeContactFolder: return ExchangeContact case ExchangeMail, ExchangeMailFolder: return ExchangeMail } return ec } // isType checks if either the receiver is a supertype of the parameter, // or if the parameter is a supertype of the receiver. // if either value is an unknown types, the comparison is always false. // if either value is the root type (user), the comparison is always true. func (ec exchangeCategory) isType(cat exchangeCategory) bool { if cat == ExchangeCategoryUnknown || ec == ExchangeCategoryUnknown { return false } if cat == ExchangeUser || ec == ExchangeUser { return true } return ec.leafType() == cat.leafType() } // includesType returns true if it matches the isType check for // the receiver's service category. func (ec exchangeCategory) includesType(cat categorizer) bool { c, ok := cat.(exchangeCategory) if !ok { return false } return ec.isType(c) } // transforms a path to a map of identified properties. // TODO: this should use service-specific funcs in the Paths pkg. Instead of // peeking at the path directly, the caller should compare against values like // path.UserID() and path.Folders(). // // Malformed (ie, short len) paths will return incomplete results. // Example: // [tenantID, userID, "mail", mailFolder, mailID] // => {exchUser: userID, exchMailFolder: mailFolder, exchMail: mailID} func (ec exchangeCategory) pathValues(path []string) map[categorizer]string { m := map[categorizer]string{} if len(path) < 4 { return m } m[ExchangeUser] = path[1] /* TODO/Notice: Mail and Contacts contain folder structures, identified in this code as being at index 3. This assumes a single folder, while in reality users can express subfolder hierarchies of arbirary depth. Subfolder handling is coming at a later time. */ switch ec { case ExchangeContact: if len(path) < 5 { return m } m[ExchangeContactFolder] = path[3] m[ExchangeContact] = path[4] case ExchangeEvent: if len(path) < 4 { return m } m[ExchangeEvent] = path[3] case ExchangeMail: if len(path) < 5 { return m } m[ExchangeMailFolder] = path[3] m[ExchangeMail] = path[4] } return m } // pathKeys returns the path keys recognized by the receiver's leaf type. func (ec exchangeCategory) pathKeys() []categorizer { return categoryPathSet[ec.leafType()] } // --------------------------------------------------------------------------- // Scopes // --------------------------------------------------------------------------- // ExchangeScope specifies the data available // when interfacing with the Exchange service. type ExchangeScope scope // interface compliance checks // var _ scoper = &ExchangeScope{} // Category describes the type of the data in scope. func (s ExchangeScope) Category() exchangeCategory { return exchangeCatAtoI(s[scopeKeyCategory]) } // categorizer type is a generic wrapper around Category. // Primarily used by scopes.go to for abstract comparisons. func (s ExchangeScope) categorizer() categorizer { return s.Category() } // Filer describes the specific filter, and its target values. func (s ExchangeScope) Filter() exchangeCategory { return exchangeCatAtoI(s[scopeKeyInfoFilter]) } // Granularity describes the granularity (directory || item) // of the data in scope. func (s ExchangeScope) Granularity() string { return s[scopeKeyGranularity] } // IncludeCategory checks whether the scope includes a // certain category of data. // Ex: to check if the scope includes mail data: // s.IncludesCategory(selector.ExchangeMail) func (s ExchangeScope) IncludesCategory(cat exchangeCategory) bool { return s.Category().isType(cat) } // Contains returns true if the category is included in the scope's // data type, and the target string is included in the scope. func (s ExchangeScope) Contains(cat exchangeCategory, target string) bool { return contains(s, cat, target) } // returns true if the category is included in the scope's data type, // and the value is set to Any(). func (s ExchangeScope) IsAny(cat exchangeCategory) bool { return isAnyTarget(s, cat) } // Get returns the data category in the scope. If the scope // contains all data types for a user, it'll return the // ExchangeUser category. func (s ExchangeScope) Get(cat exchangeCategory) []string { return getCatValue(s, cat) } // sets a value by category to the scope. Only intended for internal use. func (s ExchangeScope) set(cat exchangeCategory, v string) ExchangeScope { s[cat.String()] = v return s } // setDefaults ensures that contact folder, mail folder, and user category // scopes all express `AnyTgt` for their child category types. func (s ExchangeScope) setDefaults() { switch s.Category() { case ExchangeContactFolder: s[ExchangeContact.String()] = AnyTgt case ExchangeMailFolder: s[ExchangeMail.String()] = AnyTgt case ExchangeUser: s[ExchangeContactFolder.String()] = AnyTgt s[ExchangeContact.String()] = AnyTgt s[ExchangeEvent.String()] = AnyTgt s[ExchangeMailFolder.String()] = AnyTgt s[ExchangeMail.String()] = AnyTgt } } // --------------------------------------------------------------------------- // Backup Details Filtering // --------------------------------------------------------------------------- // matchesEntry returns true if either the path or the info in the exchangeEntry matches the scope details. func (s ExchangeScope) matchesEntry( cat categorizer, pathValues map[categorizer]string, entry details.DetailsEntry, ) bool { return false // TODO: uncomment when reducer is added. // return matchesPathValues(s, cat, pathValues) || s.matchesInfo(entry.Exchange) } // matches returns true if either the path or the info matches the scope details. func (s ExchangeScope) matches(cat exchangeCategory, path []string, info *details.ExchangeInfo) bool { return s.matchesPath(cat, path) || s.matchesInfo(cat, info) } // matchesInfo handles the standard behavior when comparing a scope and an exchangeInfo // returns true if the scope and info match for the provided category. func (s ExchangeScope) matchesInfo(cat exchangeCategory, info *details.ExchangeInfo) bool { // we need values to match against if info == nil { return false } // the scope must define targets to match on filterCat := s.Filter() targets := s.Get(filterCat) if len(targets) == 0 { return false } if targets[0] == AnyTgt { return true } if targets[0] == NoneTgt { return false } // any of the targets for a given info filter may succeed. for _, target := range targets { switch filterCat { case ExchangeInfoMailSender: if target == info.Sender { return true } case ExchangeInfoMailSubject: if strings.Contains(info.Subject, target) { return true } case ExchangeInfoMailReceivedAfter: if target < common.FormatTime(info.Received) { return true } case ExchangeInfoMailReceivedBefore: if target > common.FormatTime(info.Received) { return true } } } return false } // matchesPath handles the standard behavior when comparing a scope and a path // returns true if the scope and path match for the provided category. func (s ExchangeScope) matchesPath(cat exchangeCategory, path []string) bool { pathIDs := cat.pathValues(path) for _, c := range categoryPathSet[cat] { target := s.Get(c.(exchangeCategory)) // the scope must define the targets to match on if len(target) == 0 { return false } // None() fails all matches if target[0] == NoneTgt { return false } // the path must contain a value to match against id, ok := pathIDs[c] if !ok { return false } // all parts of the scope must match isAny := target[0] == AnyTgt if !isAny { if !common.ContainsString(target, id) { return false } } } return true } // --------------------------------------------------------------------------- // Restore Point Filtering // --------------------------------------------------------------------------- // Reduce reduces the entries in a backupDetails struct to only // those that match the inclusions, filters, and exclusions in the selector. func (sr *ExchangeRestore) Reduce(deets *details.Details) *details.Details { if deets == nil { return nil } entExcs := exchangeScopesByCategory(sr.Excludes) entFilt := exchangeScopesByCategory(sr.Filters) entIncs := exchangeScopesByCategory(sr.Includes) ents := []details.DetailsEntry{} for _, ent := range deets.Entries { // todo: use Path pkg for this path := strings.Split(ent.RepoRef, "/") // not all paths will be len=3. Most should be longer. // This just protects us from panicing four lines later. if len(path) < 3 { continue } var cat exchangeCategory switch path[2] { case "contact": cat = ExchangeContact case "event": cat = ExchangeEvent case "mail": cat = ExchangeMail } matched := matchExchangeEntry( cat, path, ent.Exchange, entExcs[cat.String()], entFilt[cat.String()], entIncs[cat.String()]) if matched { ents = append(ents, ent) } } deets.Entries = ents return deets } // groups each scope by its category of data (contact, event, or mail). // user-level scopes will duplicate to all three categories. func exchangeScopesByCategory(scopes []scope) map[string][]ExchangeScope { m := map[string][]ExchangeScope{ ExchangeContact.String(): {}, ExchangeEvent.String(): {}, ExchangeMail.String(): {}, } for _, msc := range scopes { sc := ExchangeScope(msc) if sc.IncludesCategory(ExchangeContact) { m[ExchangeContact.String()] = append(m[ExchangeContact.String()], sc) } if sc.IncludesCategory(ExchangeEvent) { m[ExchangeEvent.String()] = append(m[ExchangeEvent.String()], sc) } if sc.IncludesCategory(ExchangeMail) { m[ExchangeMail.String()] = append(m[ExchangeMail.String()], sc) } } return m } // compare each path to the included and excluded exchange scopes. Returns true // if the path is included, passes filters, and not excluded. func matchExchangeEntry( cat exchangeCategory, path []string, info *details.ExchangeInfo, excs, filts, incs []ExchangeScope, ) bool { // a passing match requires either a filter or an inclusion if len(incs)+len(filts) == 0 { return false } // skip this check if 0 inclusions were populated // since filters act as the inclusion check in that case if len(incs) > 0 { // at least one inclusion must apply. var included bool for _, inc := range incs { if inc.matches(cat, path, info) { included = true break } } if !included { return false } } // all filters must pass for _, filt := range filts { if !filt.matches(cat, path, info) { return false } } // any matching exclusion means failure for _, exc := range excs { if exc.matches(cat, path, info) { return false } } return true }