Keepers 45886e2ad9
allow eml export when attachments have no name (#5199)
#### Does this PR need a docs update or release note?

- [x]  Yes, it's included

#### Type of change

- [x] 🐛 Bugfix

#### Test Plan

- [x]  Unit test
- [x] 💚 E2E
2024-02-09 18:41:10 +00:00

484 lines
14 KiB
Go

package eml
import (
"bytes"
"regexp"
"strings"
"testing"
"time"
ical "github.com/arran4/golang-ical"
"github.com/jhillyerd/enmime"
kjson "github.com/microsoft/kiota-serialization-json-go"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/converters/eml/testdata"
"github.com/alcionai/corso/src/internal/converters/ics"
"github.com/alcionai/corso/src/internal/m365/collection/groups/metadata"
stub "github.com/alcionai/corso/src/internal/m365/service/groups/mock"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/services/m365/api"
)
type EMLUnitSuite struct {
tester.Suite
}
func TestEMLUnitSuite(t *testing.T) {
suite.Run(t, &EMLUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *EMLUnitSuite) TestFormatAddress() {
t := suite.T()
tests := []struct {
tname string
name string
email string
expected string
}{
{
tname: "different name and email",
name: "John Doe",
email: "johndoe@provider.com",
expected: `"John Doe" <johndoe@provider.com>`,
},
{
tname: "same name and email",
name: "johndoe@provider.com",
email: "johndoe@provider.com",
expected: "johndoe@provider.com",
},
{
tname: "only email",
name: "",
email: "johndoe@provider.com",
expected: "johndoe@provider.com",
},
{
tname: "only name",
name: "john doe",
email: "",
expected: `"john doe"`,
},
{
tname: "neither mail or name",
name: "",
email: "",
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.tname, func(t *testing.T) {
entity := models.NewEmailAddress()
if len(tt.name) != 0 {
entity.SetName(ptr.To(tt.name))
}
if len(tt.email) != 0 {
entity.SetAddress(ptr.To(tt.email))
}
assert.Equal(t, tt.expected, formatAddress(entity))
})
}
}
func (suite *EMLUnitSuite) TestConvert_messageble_to_eml() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
body := []byte(testdata.EmailWithAttachments)
out, err := FromJSON(ctx, body)
assert.NoError(t, err, "converting to eml")
msg, err := api.BytesToMessageable(body)
require.NoError(t, err, "creating message")
eml, err := enmime.ReadEnvelope(strings.NewReader(out))
require.NoError(t, err, "reading created eml")
assert.Equal(t, ptr.Val(msg.GetSubject()), eml.GetHeader("Subject"))
assert.Equal(t, msg.GetSentDateTime().Format(time.RFC1123Z), eml.GetHeader("Date"))
assert.Equal(t, formatAddress(msg.GetFrom().GetEmailAddress()), eml.GetHeader("From"))
ccs := strings.Split(eml.GetHeader("Cc"), ", ")
for _, cc := range msg.GetCcRecipients() {
assert.Contains(t, ccs, formatAddress(cc.GetEmailAddress()))
}
bccs := strings.Split(eml.GetHeader("Bcc"), ", ")
for _, bcc := range msg.GetBccRecipients() {
assert.Contains(t, bccs, formatAddress(bcc.GetEmailAddress()))
}
tos := strings.Split(eml.GetHeader("To"), ", ")
for _, to := range msg.GetToRecipients() {
assert.Contains(t, tos, formatAddress(to.GetEmailAddress()))
}
source := strings.ReplaceAll(eml.HTML, "\n", "")
target := strings.ReplaceAll(ptr.Val(msg.GetBody().GetContent()), "\n", "")
// replace the cid with a constant value to make the comparison
re := regexp.MustCompile(`src="cid:[^"]*"`)
source = re.ReplaceAllString(source, `src="cid:replaced"`)
target = re.ReplaceAllString(target, `src="cid:replaced"`)
assert.Equal(t, source, target)
}
func (suite *EMLUnitSuite) TestConvert_edge_cases() {
tests := []struct {
name string
transform func(models.Messageable)
}{
{
name: "just a name",
transform: func(msg models.Messageable) {
msg.GetFrom().GetEmailAddress().SetName(ptr.To("alphabob"))
msg.GetFrom().GetEmailAddress().SetAddress(nil)
},
},
{
name: "incorrect address",
transform: func(msg models.Messageable) {
msg.GetFrom().GetEmailAddress().SetAddress(ptr.To("invalid"))
},
},
{
name: "empty attachment",
transform: func(msg models.Messageable) {
attachments := msg.GetAttachments()
err := attachments[0].GetBackingStore().Set("contentBytes", []uint8{})
require.NoError(suite.T(), err, "setting attachment content")
},
},
{
name: "attachment without name",
transform: func(msg models.Messageable) {
attachments := msg.GetAttachments()
attachments[1].SetName(ptr.To(""))
// This test has to be run on a non inline attachment
// as inline attachments use contentID instead of name
// even when there is a name.
assert.False(suite.T(), ptr.Val(attachments[1].GetIsInline()))
},
},
{
name: "attachment with nil name",
transform: func(msg models.Messageable) {
attachments := msg.GetAttachments()
attachments[1].SetName(nil)
// This test has to be run on a non inline attachment
// as inline attachments use contentID instead of name
// even when there is a name.
assert.False(suite.T(), ptr.Val(attachments[1].GetIsInline()))
},
},
{
name: "multiple attachments without name",
transform: func(msg models.Messageable) {
attachments := msg.GetAttachments()
attachments[1].SetName(ptr.To(""))
attachments[2].SetName(ptr.To(""))
// This test has to be run on a non inline attachment
// as inline attachments use contentID instead of name
// even when there is a name.
assert.False(suite.T(), ptr.Val(attachments[1].GetIsInline()))
assert.False(suite.T(), ptr.Val(attachments[2].GetIsInline()))
},
},
}
for _, test := range tests {
suite.Run(test.name, func() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
body := []byte(testdata.EmailWithAttachments)
msg, err := api.BytesToMessageable(body)
require.NoError(t, err, "creating message")
test.transform(msg)
writer := kjson.NewJsonSerializationWriter()
defer writer.Close()
err = writer.WriteObjectValue("", msg)
require.NoError(t, err, "serializing message")
nbody, err := writer.GetSerializedContent()
require.NoError(t, err, "getting serialized content")
_, err = FromJSON(ctx, nbody)
assert.NoError(t, err, "converting to eml")
})
}
}
func (suite *EMLUnitSuite) TestConvert_eml_ics() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
body := []byte(testdata.EmailWithEventInfo)
out, err := FromJSON(ctx, body)
assert.NoError(t, err, "converting to eml")
rmsg, err := api.BytesToMessageable(body)
require.NoError(t, err, "creating message")
msg := rmsg.(*models.EventMessageRequest)
eml, err := enmime.ReadEnvelope(strings.NewReader(out))
require.NoError(t, err, "reading created eml")
require.NotNil(t, eml, "eml should not be nil")
require.Equal(t, 1, len(eml.OtherParts), "eml should have 1 attachment")
require.Equal(t, "text/calendar", eml.OtherParts[0].ContentType, "eml attachment should be a calendar")
catt := *eml.OtherParts[0]
cal, err := ical.ParseCalendar(bytes.NewReader(catt.Content))
require.NoError(t, err, "parsing calendar")
event := cal.Events()[0]
assert.Equal(t, ptr.Val(msg.GetId()), event.Id())
assert.Equal(t, ptr.Val(msg.GetSubject()), event.GetProperty(ical.ComponentPropertySummary).Value)
assert.Equal(
t,
msg.GetCreatedDateTime().Format(ics.ICalDateTimeFormat),
event.GetProperty(ical.ComponentPropertyCreated).Value)
assert.Equal(
t,
msg.GetLastModifiedDateTime().Format(ics.ICalDateTimeFormat),
event.GetProperty(ical.ComponentPropertyLastModified).Value)
st, err := ics.GetUTCTime(
ptr.Val(msg.GetStartDateTime().GetDateTime()),
ptr.Val(msg.GetStartDateTime().GetTimeZone()))
require.NoError(t, err, "getting start time")
et, err := ics.GetUTCTime(
ptr.Val(msg.GetEndDateTime().GetDateTime()),
ptr.Val(msg.GetEndDateTime().GetTimeZone()))
require.NoError(t, err, "getting end time")
assert.Equal(
t,
st.Format(ics.ICalDateTimeFormat),
event.GetProperty(ical.ComponentPropertyDtStart).Value)
assert.Equal(
t,
et.Format(ics.ICalDateTimeFormat),
event.GetProperty(ical.ComponentPropertyDtEnd).Value)
tos := msg.GetToRecipients()
ccs := msg.GetCcRecipients()
att := event.Attendees()
assert.Equal(t, len(tos)+len(ccs), len(att))
for _, to := range tos {
found := false
for _, attendee := range att {
if "mailto:"+ptr.Val(to.GetEmailAddress().GetAddress()) == attendee.Value {
found = true
assert.Equal(t, "REQ-PARTICIPANT", attendee.ICalParameters["ROLE"][0])
break
}
}
assert.True(t, found, "to recipient not found in attendees")
}
for _, cc := range ccs {
found := false
for _, attendee := range att {
if "mailto:"+ptr.Val(cc.GetEmailAddress().GetAddress()) == attendee.Value {
found = true
assert.Equal(t, "OPT-PARTICIPANT", attendee.ICalParameters["ROLE"][0])
break
}
}
assert.True(t, found, "cc recipient not found in attendees")
}
}
func (suite *EMLUnitSuite) TestConvert_eml_ics_from_event_obj() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
body := []byte(testdata.EmailWithEventObject)
out, err := FromJSON(ctx, body)
assert.NoError(t, err, "converting to eml")
rmsg, err := api.BytesToMessageable(body)
require.NoError(t, err, "creating message")
msg := rmsg.(*models.EventMessageRequest)
evt := msg.GetEvent()
eml, err := enmime.ReadEnvelope(strings.NewReader(out))
require.NoError(t, err, "reading created eml")
require.NotNil(t, eml, "eml should not be nil")
require.Equal(t, 1, len(eml.OtherParts), "eml should have 1 attachment")
require.Equal(t, "text/calendar", eml.OtherParts[0].ContentType, "eml attachment should be a calendar")
catt := *eml.OtherParts[0]
cal, err := ical.ParseCalendar(bytes.NewReader(catt.Content))
require.NoError(t, err, "parsing calendar")
event := cal.Events()[0]
assert.Equal(t, ptr.Val(evt.GetId()), event.Id())
assert.NotEqual(t, ptr.Val(msg.GetSubject()), event.GetProperty(ical.ComponentPropertySummary).Value)
assert.Equal(t, ptr.Val(evt.GetSubject()), event.GetProperty(ical.ComponentPropertySummary).Value)
}
//-------------------------------------------------------------
// Postable -> EML tests
//-------------------------------------------------------------
func (suite *EMLUnitSuite) TestConvert_postable_to_eml() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
body := []byte(stub.PostWithAttachments)
postMetadata := metadata.ConversationPostMetadata{
Recipients: []string{"group@example.com"},
Topic: "test subject",
}
out, err := FromJSONPostToEML(ctx, body, postMetadata)
assert.NoError(t, err, "converting to eml")
post, err := api.BytesToPostable(body)
require.NoError(t, err, "creating post")
eml, err := enmime.ReadEnvelope(strings.NewReader(out))
require.NoError(t, err, "reading created eml")
assert.Equal(t, postMetadata.Topic, eml.GetHeader("Subject"))
assert.Equal(t, post.GetCreatedDateTime().Format(time.RFC1123Z), eml.GetHeader("Date"))
assert.Equal(t, formatAddress(post.GetFrom().GetEmailAddress()), eml.GetHeader("From"))
// Test recipients. The post metadata should contain the group email address.
tos := strings.Split(eml.GetHeader("To"), ", ")
for _, sourceTo := range postMetadata.Recipients {
assert.Contains(t, tos, sourceTo)
}
// Assert cc, bcc to be empty since they are not supported for posts right now.
assert.Equal(t, "", eml.GetHeader("Cc"))
assert.Equal(t, "", eml.GetHeader("Bcc"))
// Test attachments using PostWithAttachments data as a reference.
// This data has 1 direct attachment and 1 inline attachment.
assert.Equal(t, 1, len(eml.Attachments), "direct attachment count")
assert.Equal(t, 1, len(eml.Inlines), "inline attachment count")
for _, sourceAttachment := range post.GetAttachments() {
targetContent := eml.Attachments[0].Content
if ptr.Val(sourceAttachment.GetIsInline()) {
targetContent = eml.Inlines[0].Content
}
sourceContent, err := sourceAttachment.GetBackingStore().Get("contentBytes")
assert.NoError(t, err, "getting source attachment content")
assert.Equal(t, sourceContent, targetContent)
}
// Test body
source := strings.ReplaceAll(eml.HTML, "\n", "")
target := strings.ReplaceAll(ptr.Val(post.GetBody().GetContent()), "\n", "")
// replace the cid with a constant value to make the comparison
re := regexp.MustCompile(`(?:src|originalSrc)="cid:[^"]*"`)
source = re.ReplaceAllString(source, `src="cid:replaced"`)
target = re.ReplaceAllString(target, `src="cid:replaced"`)
assert.Equal(t, source, target)
}
// Tests an ics within an eml within another eml
func (suite *EMLUnitSuite) TestConvert_message_in_messageble_to_eml() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
body := []byte(testdata.EmailWithinEmail)
out, err := FromJSON(ctx, body)
assert.NoError(t, err, "converting to eml")
msg, err := api.BytesToMessageable(body)
require.NoError(t, err, "creating message")
eml, err := enmime.ReadEnvelope(strings.NewReader(out))
require.NoError(t, err, "reading created eml")
assert.Equal(t, ptr.Val(msg.GetSubject()), eml.GetHeader("Subject"))
assert.Equal(t, msg.GetSentDateTime().Format(time.RFC1123Z), eml.GetHeader("Date"))
assert.Equal(t, formatAddress(msg.GetFrom().GetEmailAddress()), eml.GetHeader("From"))
attachments := eml.Attachments
assert.Equal(t, 1, len(attachments), "attachment count in parent email")
ieml, err := enmime.ReadEnvelope(strings.NewReader(string(attachments[0].Content)))
require.NoError(t, err, "reading created eml")
itm, err := msg.GetAttachments()[0].GetBackingStore().Get("item")
require.NoError(t, err, "getting item from message")
imsg := itm.(*models.Message)
assert.Equal(t, ptr.Val(imsg.GetSubject()), ieml.GetHeader("Subject"))
assert.Equal(t, imsg.GetSentDateTime().Format(time.RFC1123Z), ieml.GetHeader("Date"))
assert.Equal(t, formatAddress(imsg.GetFrom().GetEmailAddress()), ieml.GetHeader("From"))
iattachments := ieml.Attachments
assert.Equal(t, 1, len(iattachments), "attachment count in child email")
// Known from testdata
assert.Contains(t, string(iattachments[0].Content), "X-LIC-LOCATION:Africa/Abidjan")
}