sanitizes replyTo emailAddresses (#5221)

sanitizes replyTo emailAddresses based on:
- valid email address format
- valid DN format

#### Does this PR need a docs update or release note?

- [ ]  Yes, it's included
- [x] 🕐 Yes, but in a later PR
- [ ]  No

#### Type of change

<!--- Please check the type of change your PR introduces: --->
- [ ] 🌻 Feature
- [x] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)
INC-43

#### Test Plan

<!-- How will this be tested prior to merging.-->
- [x] 💪 Manual
- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
Hitesh Pattanayak 2024-02-14 00:22:11 +05:30 committed by GitHub
parent 28aba60cc5
commit 4b56754546
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 178 additions and 0 deletions

View File

@ -3,6 +3,7 @@ package exchange
import (
"context"
"errors"
"regexp"
"github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models"
@ -147,6 +148,8 @@ func restoreMail(
msg = setMessageSVEPs(toMessage(msg))
setReplyTos(msg)
attachments := msg.GetAttachments()
// Item.Attachments --> HasAttachments doesn't always have a value populated when deserialized
msg.SetAttachments([]models.Attachmentable{})
@ -229,6 +232,38 @@ func setMessageSVEPs(msg models.Messageable) models.Messageable {
return msg
}
func setReplyTos(msg models.Messageable) {
var (
replyTos = msg.GetReplyTo()
emailAddress models.EmailAddressable
name, address string
sanitizedReplyTos = make([]models.Recipientable, 0)
)
if len(replyTos) == 0 {
return
}
for _, replyTo := range replyTos {
emailAddress = replyTo.GetEmailAddress()
address = ptr.Val(emailAddress.GetAddress())
name = ptr.Val(emailAddress.GetName())
if isValidEmail(address) || isValidDN(address) {
newEmailAddress := models.NewEmailAddress()
newEmailAddress.SetAddress(ptr.To(address))
newEmailAddress.SetName(ptr.To(name))
sanitizedReplyTo := models.NewRecipient()
sanitizedReplyTo.SetEmailAddress(newEmailAddress)
sanitizedReplyTos = append(sanitizedReplyTos, sanitizedReplyTo)
}
}
msg.SetReplyTo(sanitizedReplyTos)
}
func (h mailRestoreHandler) GetItemsInContainerByCollisionKey(
ctx context.Context,
userID, containerID string,
@ -240,3 +275,24 @@ func (h mailRestoreHandler) GetItemsInContainerByCollisionKey(
return m, nil
}
// [TODO]relocate to a common place
func isValidEmail(email string) bool {
emailRegex := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
r := regexp.MustCompile(emailRegex)
return r.MatchString(email)
}
// isValidDN check if given string's format matches that of a MSFT Distinguished Name
// This regular expression matches strings that start with /o=,
// followed by any characters except /,
// then /ou=, followed by any characters except /,
// then /cn=, followed by any characters except /,
// then /cn= followed by a 32-character hexadecimal string followed by - and any additional characters.
func isValidDN(dn string) bool {
dnRegex := `^/o=[^/]+/ou=[^/]+/cn=[^/]+/cn=[a-fA-F0-9]{32}-[a-zA-Z0-9-]+$`
r := regexp.MustCompile(dnRegex)
return r.MatchString(dn)
}

View File

@ -11,6 +11,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/m365/service/exchange/mock"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/its"
@ -24,6 +25,127 @@ import (
"github.com/alcionai/corso/src/pkg/services/m365/api"
)
//nolint:lll
const TestDN = "/o=ExchangeLabs/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=4eca0d46a2324036b0b326dc58cfc802-user"
type RestoreMailUnitSuite struct {
tester.Suite
}
func TestRestoreMailUnitSuite(t *testing.T) {
suite.Run(t, &RestoreMailUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *RestoreMailUnitSuite) TestIsValidEmail() {
table := []struct {
name string
email string
check assert.BoolAssertionFunc
}{
{
name: "valid email",
email: "foo@bar.com",
check: assert.True,
},
{
name: "invalid email, missing domain",
email: "foo.com",
check: assert.False,
},
{
name: "invalid email, random uuid",
email: "12345678-abcd-90ef-88f8-2d95ef12fb66",
check: assert.False,
},
{
name: "empty email",
email: "",
check: assert.False,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
result := isValidEmail(test.email)
test.check(t, result)
})
}
}
func (suite *RestoreMailUnitSuite) TestIsValidDN() {
table := []struct {
name string
dn string
check assert.BoolAssertionFunc
}{
{
name: "valid DN",
dn: TestDN,
check: assert.True,
},
{
name: "invalid DN",
dn: "random string",
check: assert.False,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
result := isValidDN(test.dn)
test.check(t, result)
})
}
}
func (suite *RestoreMailUnitSuite) TestSetReplyTos() {
t := suite.T()
replyTos := make([]models.Recipientable, 0)
emailAddresses := map[string]string{
"foo.bar": "foo@bar.com",
"foo.com": "foo.com",
"empty": "",
"dn": TestDN,
}
validEmailAddresses := map[string]string{
"foo.bar": "foo@bar.com",
"dn": TestDN,
}
for k, v := range emailAddresses {
emailAddress := models.NewEmailAddress()
emailAddress.SetAddress(ptr.To(v))
emailAddress.SetName(ptr.To(k))
replyTo := models.NewRecipient()
replyTo.SetEmailAddress(emailAddress)
replyTos = append(replyTos, replyTo)
}
mailMessage := models.NewMessage()
mailMessage.SetReplyTo(replyTos)
setReplyTos(mailMessage)
sanitizedReplyTos := mailMessage.GetReplyTo()
require.Len(t, sanitizedReplyTos, len(validEmailAddresses))
for _, sanitizedReplyTo := range sanitizedReplyTos {
emailAddress := sanitizedReplyTo.GetEmailAddress()
assert.Contains(t, validEmailAddresses, ptr.Val(emailAddress.GetName()))
assert.Equal(t, validEmailAddresses[ptr.Val(emailAddress.GetName())], ptr.Val(emailAddress.GetAddress()))
}
}
var _ mailRestorer = &mailRestoreMock{}
type mailRestoreMock struct {