diff --git a/src/internal/connector/exchange/exchange_data_collection.go b/src/internal/connector/exchange/exchange_data_collection.go index cfb9c4b31..4807f183e 100644 --- a/src/internal/connector/exchange/exchange_data_collection.go +++ b/src/internal/connector/exchange/exchange_data_collection.go @@ -32,10 +32,6 @@ var ( const ( collectionChannelBufferSize = 1000 numberOfRetries = 4 - // RestorePropertyTag defined: - // https://docs.microsoft.com/en-us/office/client-developer/outlook/mapi/pidtagmessageflags-canonical-property - RestorePropertyTag = "Integer 0x0E07" - RestoreCanonicalEnableValue = "4" ) // Collection implements the interface from data.Collection diff --git a/src/internal/connector/exchange/exchange_vars.go b/src/internal/connector/exchange/exchange_vars.go new file mode 100644 index 000000000..a3dcfd074 --- /dev/null +++ b/src/internal/connector/exchange/exchange_vars.go @@ -0,0 +1,29 @@ +package exchange + +// exchange_vars.go is package level collection of interfaces and +// constants that are used within the exchange. + +// Legacy Value Tags and constants are used to override certain values within +// M365 objects. +// Master Property Value Document: +// https://interoperability.blob.core.windows.net/files/MS-OXPROPS/%5bMS-OXPROPS%5d.pdf +const ( + // MailRestorePropertyTag inhibits exchange.Mail.Message from being "resent" through the server. + // DEFINED: Section 2.791 PidTagMessageFlags + MailRestorePropertyTag = "Integer 0x0E07" + + // RestoreCanonicalEnableValue marks message as sent via RopSubmitMessage + // Defined: https://interoperability.blob.core.windows.net/files/MS-OXCMSG/%5bMS-OXCMSG%5d.pdf + // Section: 2.2.1.6 PidTagMessageFlags Property + //nolint:lll + // Additional Information: https://docs.microsoft.com/en-us/office/client-developer/outlook/mapi/pidtagmessageflags-canonical-property + RestoreCanonicalEnableValue = "4" + + // MailSendTimeOverrideProperty allows for send time to be updated. + // Section: 2.635 PidTagClientSubmitTime + MailSendDateTimeOverrideProperty = "SystemTime 0x0039" + + // MailReceiveDateTimeOverrideProperty allows receive date time to be updated. + // Section: 2.789 PidTagMessageDeliveryTime + MailReceiveDateTimeOverriveProperty = "SystemTime 0x0E06" +) diff --git a/src/internal/connector/exchange/service_functions.go b/src/internal/connector/exchange/service_functions.go index a60ce2a1c..59a4ba108 100644 --- a/src/internal/connector/exchange/service_functions.go +++ b/src/internal/connector/exchange/service_functions.go @@ -1,7 +1,6 @@ package exchange import ( - "context" "fmt" "strings" @@ -11,13 +10,9 @@ import ( "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/pkg/errors" - "github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/support" - "github.com/alcionai/corso/src/internal/path" "github.com/alcionai/corso/src/pkg/account" - "github.com/alcionai/corso/src/pkg/control" - "github.com/alcionai/corso/src/pkg/logger" "github.com/alcionai/corso/src/pkg/selectors" ) @@ -374,221 +369,3 @@ func SetupExchangeCollectionVars(scope selectors.ExchangeScope) ( return nil, nil, nil, errors.New("exchange scope option not supported") } - -// GetRestoreContainer utility function to create -// an unique folder for the restore process -// @param category: input from fullPath()[2] -// that defines the application the folder is created in. -func GetRestoreContainer( - service graph.Service, - user string, - category path.CategoryType, -) (string, error) { - name := fmt.Sprintf("Corso_Restore_%s", common.FormatNow(common.SimpleDateTimeFormat)) - option := categoryToOptionIdentifier(category) - - folderID, err := GetContainerID(service, name, user, option) - if err == nil { - return *folderID, nil - } - // Experienced error other than folder does not exist - if !errors.Is(err, ErrFolderNotFound) { - return "", support.WrapAndAppend(user, err, err) - } - - switch option { - case messages: - fold, err := CreateMailFolder(service, user, name) - if err != nil { - return "", support.WrapAndAppend(user, err, err) - } - - return *fold.GetId(), nil - case contacts: - fold, err := CreateContactFolder(service, user, name) - if err != nil { - return "", support.WrapAndAppend(user, err, err) - } - - return *fold.GetId(), nil - case events: - calendar, err := CreateCalendar(service, user, name) - if err != nil { - return "", support.WrapAndAppend(user, err, err) - } - - return *calendar.GetId(), nil - default: - return "", fmt.Errorf("category: %s not supported for folder creation", option) - } -} - -func RestoreExchangeObject( - ctx context.Context, - bits []byte, - category path.CategoryType, - policy control.CollisionPolicy, - service graph.Service, - destination, user string, -) error { - if policy != control.Copy { - return fmt.Errorf("restore policy: %s not supported", policy) - } - - setting := categoryToOptionIdentifier(category) - - switch setting { - case messages: - return RestoreMailMessage(ctx, bits, service, control.Copy, destination, user) - case contacts: - return RestoreExchangeContact(ctx, bits, service, control.Copy, destination, user) - case events: - return RestoreExchangeEvent(ctx, bits, service, control.Copy, destination, user) - default: - return fmt.Errorf("type: %s not supported for exchange restore", category) - } -} - -// RestoreExchangeContact restores a contact to the @bits byte -// representation of M365 contact object. -// @destination M365 ID representing a M365 Contact_Folder -// Returns an error if the input bits do not parse into a models.Contactable object -// or if an error is encountered sending data to the M365 account. -// Post details: https://docs.microsoft.com/en-us/graph/api/user-post-contacts?view=graph-rest-1.0&tabs=go -func RestoreExchangeContact( - ctx context.Context, - bits []byte, - service graph.Service, - cp control.CollisionPolicy, - destination, user string, -) error { - contact, err := support.CreateContactFromBytes(bits) - if err != nil { - return err - } - - response, err := service.Client().UsersById(user).ContactFoldersById(destination).Contacts().Post(contact) - if err != nil { - return errors.Wrap(err, support.ConnectorStackErrorTrace(err)) - } - - if response == nil { - return errors.New("msgraph contact post fail: REST response not received") - } - - return nil -} - -func RestoreExchangeEvent( - ctx context.Context, - bits []byte, - service graph.Service, - cp control.CollisionPolicy, - destination, user string, -) error { - event, err := support.CreateEventFromBytes(bits) - if err != nil { - return err - } - - response, err := service.Client().UsersById(user).CalendarsById(destination).Events().Post(event) - if err != nil { - return errors.Wrap(err, support.ConnectorStackErrorTrace(err)) - } - - if response == nil { - return errors.New("msgraph event post fail: REST response not received") - } - - return nil -} - -// RestoreMailMessage utility function to place an exchange.Mail -// message into the user's M365 Exchange account. -// @param bits - byte array representation of exchange.Message from Corso backstore -// @param service - connector to M365 graph -// @param cp - collision policy that directs restore workflow -// @param destination - M365 Folder ID. Verified and sent by higher function. `copy` policy can use directly -func RestoreMailMessage( - ctx context.Context, - bits []byte, - service graph.Service, - cp control.CollisionPolicy, - destination, - user string, -) error { - // Creates messageable object from original bytes - originalMessage, err := support.CreateMessageFromBytes(bits) - if err != nil { - return errors.Wrapf(err, "restore mail message rcvd: %v", bits) - } - // Sets fields from original message from storage - clone := support.ToMessage(originalMessage) - valueID := RestorePropertyTag - enableValue := RestoreCanonicalEnableValue - - // Set Extended Properties: - // 1st: No transmission - // 2nd: Send Date - // 3rd: Recv Date - sv1 := models.NewSingleValueLegacyExtendedProperty() - sv1.SetId(&valueID) - sv1.SetValue(&enableValue) - - sv2 := models.NewSingleValueLegacyExtendedProperty() - sendPropertyValue := common.FormatLegacyTime(*clone.GetSentDateTime()) - sendPropertyTag := "SystemTime 0x0039" - sv2.SetId(&sendPropertyTag) - sv2.SetValue(&sendPropertyValue) - - sv3 := models.NewSingleValueLegacyExtendedProperty() - recvPropertyValue := common.FormatLegacyTime(*clone.GetReceivedDateTime()) - recvPropertyTag := "SystemTime 0x0E06" - sv3.SetId(&recvPropertyTag) - sv3.SetValue(&recvPropertyValue) - - svlep := []models.SingleValueLegacyExtendedPropertyable{sv1, sv2, sv3} - clone.SetSingleValueExtendedProperties(svlep) - - // Switch workflow based on collision policy - switch cp { - default: - logger.Ctx(ctx).DPanicw("unrecognized restore policy; defaulting to copy", - "policy", cp) - fallthrough - case control.Copy: - return SendMailToBackStore(service, user, destination, clone) - } -} - -// SendMailToBackStore function for transporting in-memory messageable item to M365 backstore -// @param user string represents M365 ID of user within the tenant -// @param destination represents M365 ID of a folder within the users's space -// @param message is a models.Messageable interface from "github.com/microsoftgraph/msgraph-sdk-go/models" -func SendMailToBackStore(service graph.Service, user, destination string, message models.Messageable) error { - var ( - sentMessage models.Messageable - err error - ) - - for count := 0; count < numberOfRetries; count++ { - sentMessage, err = service.Client().UsersById(user).MailFoldersById(destination).Messages().Post(message) - if err == nil && sentMessage != nil { - break - } - } - - if err != nil { - return support.WrapAndAppend(": "+support.ConnectorStackErrorTrace(err), err, nil) - } - - if sentMessage == nil { - return errors.New("message not Sent: blocked by server") - } - - if err != nil { - return support.WrapAndAppend(": "+support.ConnectorStackErrorTrace(err), err, nil) - } - - return nil -} diff --git a/src/internal/connector/exchange/service_restore.go b/src/internal/connector/exchange/service_restore.go new file mode 100644 index 000000000..cd583e1e5 --- /dev/null +++ b/src/internal/connector/exchange/service_restore.go @@ -0,0 +1,232 @@ +package exchange + +import ( + "context" + "fmt" + + "github.com/microsoftgraph/msgraph-sdk-go/models" + "github.com/pkg/errors" + + "github.com/alcionai/corso/src/internal/common" + "github.com/alcionai/corso/src/internal/connector/graph" + "github.com/alcionai/corso/src/internal/connector/support" + "github.com/alcionai/corso/src/internal/path" + "github.com/alcionai/corso/src/pkg/control" + "github.com/alcionai/corso/src/pkg/logger" +) + +// GetRestoreContainer utility function to create +// an unique folder for the restore process +// @param category: input from fullPath()[2] +// that defines the application the folder is created in. +func GetRestoreContainer( + service graph.Service, + user string, + category path.CategoryType, +) (string, error) { + name := fmt.Sprintf("Corso_Restore_%s", common.FormatNow(common.SimpleDateTimeFormat)) + option := categoryToOptionIdentifier(category) + + folderID, err := GetContainerID(service, name, user, option) + if err == nil { + return *folderID, nil + } + // Experienced error other than folder does not exist + if !errors.Is(err, ErrFolderNotFound) { + return "", support.WrapAndAppend(user, err, err) + } + + switch option { + case messages: + fold, err := CreateMailFolder(service, user, name) + if err != nil { + return "", support.WrapAndAppend(user, err, err) + } + + return *fold.GetId(), nil + case contacts: + fold, err := CreateContactFolder(service, user, name) + if err != nil { + return "", support.WrapAndAppend(user, err, err) + } + + return *fold.GetId(), nil + case events: + calendar, err := CreateCalendar(service, user, name) + if err != nil { + return "", support.WrapAndAppend(user, err, err) + } + + return *calendar.GetId(), nil + default: + return "", fmt.Errorf("category: %s not supported for folder creation", option) + } +} + +// RestoreExchangeObject directs restore pipeline towards restore function +// based on the path.CategoryType. All input params are necessary to perform +// the type-specific restore function. +func RestoreExchangeObject( + ctx context.Context, + bits []byte, + category path.CategoryType, + policy control.CollisionPolicy, + service graph.Service, + destination, user string, +) error { + if policy != control.Copy { + return fmt.Errorf("restore policy: %s not supported", policy) + } + + setting := categoryToOptionIdentifier(category) + + switch setting { + case messages: + return RestoreMailMessage(ctx, bits, service, control.Copy, destination, user) + case contacts: + return RestoreExchangeContact(ctx, bits, service, control.Copy, destination, user) + case events: + return RestoreExchangeEvent(ctx, bits, service, control.Copy, destination, user) + default: + return fmt.Errorf("type: %s not supported for exchange restore", category) + } +} + +// RestoreExchangeContact restores a contact to the @bits byte +// representation of M365 contact object. +// @destination M365 ID representing a M365 Contact_Folder +// Returns an error if the input bits do not parse into a models.Contactable object +// or if an error is encountered sending data to the M365 account. +// Post details: https://docs.microsoft.com/en-us/graph/api/user-post-contacts?view=graph-rest-1.0&tabs=go +func RestoreExchangeContact( + ctx context.Context, + bits []byte, + service graph.Service, + cp control.CollisionPolicy, + destination, user string, +) error { + contact, err := support.CreateContactFromBytes(bits) + if err != nil { + return err + } + + response, err := service.Client().UsersById(user).ContactFoldersById(destination).Contacts().Post(contact) + if err != nil { + return errors.Wrap(err, support.ConnectorStackErrorTrace(err)) + } + + if response == nil { + return errors.New("msgraph contact post fail: REST response not received") + } + + return nil +} + +// RestoreExchangeEvent restores a contact to the @bits byte +// representation of M365 event object. +// @param destination is the M365 ID representing Calendar that will receive the event. +// Returns an error if input byte array doesn't parse into models.Eventable object +// or if an error occurs during sending data to M365 account. +// Post details: https://docs.microsoft.com/en-us/graph/api/user-post-events?view=graph-rest-1.0&tabs=http +func RestoreExchangeEvent( + ctx context.Context, + bits []byte, + service graph.Service, + cp control.CollisionPolicy, + destination, user string, +) error { + event, err := support.CreateEventFromBytes(bits) + if err != nil { + return err + } + + response, err := service.Client().UsersById(user).CalendarsById(destination).Events().Post(event) + if err != nil { + return errors.Wrap(err, support.ConnectorStackErrorTrace(err)) + } + + if response == nil { + return errors.New("msgraph event post fail: REST response not received") + } + + return nil +} + +// RestoreMailMessage utility function to place an exchange.Mail +// message into the user's M365 Exchange account. +// @param bits - byte array representation of exchange.Message from Corso backstore +// @param service - connector to M365 graph +// @param cp - collision policy that directs restore workflow +// @param destination - M365 Folder ID. Verified and sent by higher function. `copy` policy can use directly +func RestoreMailMessage( + ctx context.Context, + bits []byte, + service graph.Service, + cp control.CollisionPolicy, + destination, + user string, +) error { + // Creates messageable object from original bytes + originalMessage, err := support.CreateMessageFromBytes(bits) + if err != nil { + return err + } + // Sets fields from original message from storage + clone := support.ToMessage(originalMessage) + valueID := MailRestorePropertyTag + enableValue := RestoreCanonicalEnableValue + + // Set Extended Properties: + // 1st: No transmission + // 2nd: Send Date + // 3rd: Recv Date + sv1 := models.NewSingleValueLegacyExtendedProperty() + sv1.SetId(&valueID) + sv1.SetValue(&enableValue) + + sv2 := models.NewSingleValueLegacyExtendedProperty() + sendPropertyValue := common.FormatLegacyTime(*clone.GetSentDateTime()) + sendPropertyTag := MailSendDateTimeOverrideProperty + sv2.SetId(&sendPropertyTag) + sv2.SetValue(&sendPropertyValue) + + sv3 := models.NewSingleValueLegacyExtendedProperty() + recvPropertyValue := common.FormatLegacyTime(*clone.GetReceivedDateTime()) + recvPropertyTag := MailReceiveDateTimeOverriveProperty + sv3.SetId(&recvPropertyTag) + sv3.SetValue(&recvPropertyValue) + + svlep := []models.SingleValueLegacyExtendedPropertyable{sv1, sv2, sv3} + clone.SetSingleValueExtendedProperties(svlep) + + // Switch workflow based on collision policy + switch cp { + default: + logger.Ctx(ctx).DPanicw("unrecognized restore policy; defaulting to copy", + "policy", cp) + fallthrough + case control.Copy: + return SendMailToBackStore(service, user, destination, clone) + } +} + +// SendMailToBackStore function for transporting in-memory messageable item to M365 backstore +// @param user string represents M365 ID of user within the tenant +// @param destination represents M365 ID of a folder within the users's space +// @param message is a models.Messageable interface from "github.com/microsoftgraph/msgraph-sdk-go/models" +func SendMailToBackStore(service graph.Service, user, destination string, message models.Messageable) error { + sentMessage, err := service.Client().UsersById(user).MailFoldersById(destination).Messages().Post(message) + if err != nil { + return support.WrapAndAppend(": "+support.ConnectorStackErrorTrace(err), err, nil) + } + + if sentMessage == nil { + return errors.New("message not Sent: blocked by server") + } + + if err != nil { + return support.WrapAndAppend(": "+support.ConnectorStackErrorTrace(err), err, nil) + } + + return nil +}